实现音视频在Android与IOS平台的采集与编码之Android视频(1)

一、前言

前面我们已经讲解了视频的编码、解码、网络传输的相关基础知识,相信认真阅读多的朋友,应该熟悉了,有人会问,这些知识能够帮我们做什么呢?本篇文章就来说说具体能做那些项目。由于时间和篇幅的关系,先来说说音视频的采集、编码、推流(网络传输),这种应用场景大多在直播,拍摄视频上传服务器等场景。比如通过手机摄像头拍摄了一段很精彩的视频,发送到朋友圈,这个过程就是本文所要详细描述的音视频采集、编码、推流。这里我们就以Android和ios平台讲解,硬件平台就以各类主流手机。
实现音视频在Android与IOS平台的采集与编码之Android视频(1)_第1张图片
二、视频采集

2.1.Android视频采集
实现音视频在Android与IOS平台的采集与编码之Android视频(1)_第2张图片
首先打开手机摄像头需要获取系统权限,目前主要是用Cmaera和Camera2这两个类来实现视频的采集,需要打开如下的权限。在Android7.1平台上还需要动态获取权限,所以需要有用户去确认,是否给定权限。动态获取权限的核心代码如下所示。

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />

指明一个需要获取权限的数组,比如需要获取打开摄像头权限、网络权限、存储卡的读写权限、窗口权限等。

private static String[] REQUEST_PERMISSIONS_ARRAY = {    

"android.permission.READ_EXTERNAL_STORAGE", 
"android.permission.WRITE_EXTERNAL_STORAGE",    
 "android.permission.INTERNET",        
 "android.permission.ACCESS_NETWORK_STATE",  
 "android.permission.CAMERA",        
"android.hardware.camera",        
"android.hardware.camera.autofocus",        
"android.permission.SYSTEM_ALERT_WINDOW"}; 
private static final int REQUEST_PERMISSIONS = 1;

再完成检查权限是否拥有和申请权限

public static void verifyPermissions(Activity activity) 
{    
try {       
 //检测是否有写的权限        
 int permission = ActivityCompat.checkSelfPermission(activity,                "android.permission.CAMERA");        
 if (permission != PackageManager.PERMISSION_GRANTED) 
 {            
 // 没有写的权限,去申请写的权限,会弹出对话框
 ActivityCompat.requestPermissions(activity,REQUEST_PERMISSIONS_ARRAY,REQUEST_PERMISSIONS);        
  }    
     } catch (Exception e) {        
             e.printStackTrace();    
             }
}

最后用onCreate方法去实现调用,如下图所示:

@Overrideprotected void onCreate(Bundle savedInstanceState)
 {    
 super.onCreate(savedInstanceState); 
 verifyPermissions(MainActivity.this);
 }

2.2Android摄像头的打开、预览、预览缓冲设置

通过设置ID获取摄像头属性,并打开摄像头。这里的摄像头ID包括前置和后置摄像头,前置摄像ID是Camera.CameraInfo.CAMERA_FACING_FRONT,后置摄像头ID是Camera.CameraInfo.CAMERA_FACING_BACK。VParam是自己封装的视频相关参数设置。这里的尺寸暂时就设定为1920X1080,你也可以设置其它预览大小。

打开摄像头后,就可以获取摄像头相关参数,并设置参数,如预览像素格式,预览图片大小,预览缓冲区的大小等。注意这里一定要设置预览大小,否则图片会被拉伸,图片会被放大,这里是一个坑

//获取摄像头
IDmcamera = Camera.open(VParam.getCameraId());
//获取参数            
Camera.Parameters Cpara = mcamera.getParameters();            
//这里是设置预览像素格式,如果预览格式与编码的像素格式不一致,就要     //像素格式转换           
//关于像素格式的文章,后面会有文章详细介绍            Cpara.setPreviewFormat(ImageFormat.NV21);            
//获取系统支持的预览图片大小,后面会有方法SelectOptionSize介绍怎样匹配最合适的图片大小            
List<Camera.Size> SupportPreSize = parameters.getSupportedPreviewSizes();                       Cpara.setPictureSize(SelectOptionSize(SupportPreSize,VParam.getWidth(),VParam.getHeight()).width,SelectOptionSize(SupportPreSize,VParam.getWidth(),VParam.getHeight()).height);           
//注意这里一定要设置预览大小,否则图片会被拉伸,图片会被放大,这里/是一个**坑**            Cpara.setPreviewSize(SelectOptionSize(SupportPreSize,VParam.getWidth(),VParam.getHeight()).width,SelectOptionSize(SupportPreSize,VParam.getWidth(),VParam.getHeight()).height);            
//给camera类配置参数           
 mcamera.setParameters(Cpara);           
 //这里是设置横竖屏,暂停设定为横屏           
 //竖屏是90°            
 mcamera.setDisplayOrientation(0);           
 //预览的图片或视频,需要在surfaceview上显示,这个surface就是由外面创建,然后传递过来            mcamera.setPreviewDisplay(surfaceHolder);            
 //分配回调摄像头空间大小,这个空间就是用来缓存预览视频大小           //这个大小根据视频的长、宽和像素格式来计算            
 prevBuffer = new byte[videoParam.getWidth() * videoParam.getHeight()*3];           
 //把prevBuffer 设置为CallbackBuffer            mcamera.addCallbackBuffer(prevBuffer );           
 //这个方法表示与上面配套使用,表示当前对象使用这个buffer            mcamera.setPreviewCallbackWithBuffer(this);            
 //开启预览            
 mcamera.startPreview();

接下来就说说方法SelectOptionSize,如何去匹配合适的预览图片大小。

private static Camera.Size SelectOptionSize( List<Camera.Size> sizes, int w, int h) {    
final double ASPECT_TOLERANCE = 0.1;  
//这里适合横屏,竖屏预览,这里可能需要修改    
double targetRatio = (double) w / h;    
Camera.Size OptionSize = null;    
double minDiff = Double.MAX_VALUE;    
int targetHeight = h;   
//遍历系统支持的大小,找出最合适    
for (Camera.Size size : sizes) {        
double ratio = (double) size.width / size.height;        
if (Math.abs(ratio - targetRatio) > ASPECT_TOLERANCE) 
continue;        
if (Math.abs(size.height - targetHeight) < minDiff) {            OptionSize = size;           
 minDiff = Math.abs(size.height - targetHeight);       
  }    
  }   
  //是否是第一次匹配    
  if (OptionSize == null) {        
  minDiff = Double.MAX_VALUE;        
  for (Camera.Size size : sizes) {            
  if (Math.abs(size.height - targetHeight) < minDiff) {                OptionSize = size;                
  minDiff = Math.abs(size.height - targetHeight);           
   }       
     }    
       }    
    return OptionSize;
        }

预览方法startPreview()需要在surface被创建的时候就调用,就能够立即渲染出来。

@Overridepublic void surfaceCreated(SurfaceHolder holder) {    
startPreview();}

那现在采集和渲染视频都有了,怎样把采集的视频数据送入编码器呢?这里是一个关键的步骤。需要调用这个方法。这里有个,就是不能有耗时操作去阻塞,最好是再开一个线程去做耗时操作,否则可能只会回调一次,导致重复编码一帧数据。比如,我这里就开了一个线程去做底层编码推流的耗时操作。

@Override    public void onPreviewFrame(byte[] data, Camera camera) {       
 mcamera.addCallbackBuffer(prevBuffer);            
 }
@Overridepublic void startPush() {    
isPushing = true;    
new Thread(new Runnable() {        
@Override        
public void run() {            
CameraUtil.pushCmeraData(previewBuffer);        
}    
}).start();
}

2.3预览时通过什么方式?

这里的预览是通过OpenGL Es,目前使用Android提供的接口,如果需要优化或者适配更多场景,需要从底层设计,后面也会有文章介绍。首先图像的原始YUV像素格式要转换为RGB格式,再把RGB格式的数据渲染到一个纹理,最后才能渲染到屏幕上。整个过程是包括开始预览、刷新、结束。

先讲讲开始预览的流程,如图所示:先准备一个SurfaceView控件来渲染显示,实际的工作还是由EGL和OpenGL ES构造一个渲染线程用于渲染图像。并生成一个纹理ID传回到JAVA层,JAVA层会利用这个ID生成一个Surface-Texture或Surface,这个Surface-Texture或Surface就是作为Camera预览控件。这样就可以把采集到的视频渲染到显示器上。
实现音视频在Android与IOS平台的采集与编码之Android视频(1)_第3张图片
再讲讲更新预览的流程,如图所示。当camera开始预览,如何流畅重复预览?当JAVA层调用updateTexture方法,就表示新的视频帧已经传到Native层的纹理ID上,Native层的RenderThread就可以把该纹理ID重新渲染到Activity的界面。就这样实现了更新图像的过程,用户就可以流畅的观看。
实现音视频在Android与IOS平台的采集与编码之Android视频(1)_第4张图片
最后讲讲结束预览的流程。当Surface被销毁时,调用surfaceDestroyed方法,就可以调用stopPreview()方法渲染。如图所示:

@Overridepublic void surfaceDestroyed(SurfaceHolder holder) {    
stopPreview();
}

stopPreview()方法的具体实现如下:

private void stopPreview() {    
if(mcamera != null){      
//camera停止预览        
mcamera.stopPreview();      
//camera预览回调释放缓冲区        
mcamera.setPreviewCallback(null);      
//camera释放资源        
mcamera.release();        
mcamera = null;    
}
}

实现音视频在Android与IOS平台的采集与编码之Android视频(1)_第5张图片
三、如何编码?

3.1.像素格式转换

前文讲解了Camera采集视频的像素格式是NV21,底层的编码的像素格式为YUV420SP,这里就涉及到像素格式转换。具体代码实现如下文所示,具体原理以后的文章会讲解。注意:像素格式转换模块的代码最好在Native层去实现,在JAVA层会带来性能开销较大。

void NV21TOYUV420SP(const unsigned char *Yuv_Src,const unsigned char *Yuv_Dst,int yData){  
int UvData = 0;  
int uData = 0;   
unsigned char *Nv = NULL;  
unsigned char *Yuv = NULL;  
 int change_count=0;  
 //Y像素直接拷贝   
 memcpy(Yuv_Dst,Yuv_Src,yData); 
  //U、V像素位置   
  UvData = yData>>1;   
  uData = UvData>>1;  
  //U、V像素拷贝   
  memcpy(Yuv_Dst+yData,Yuv_Src+yData+1,UvData-1);   
  Nv  = Yuv_Src+yData;   Yuv = Yuv_Dst+yData+1;  
  //交换YUV420SP与NV21的UV地址   
  while(count<uData)   {      
  (*Yuv)=(*Nv);     
   Yuv+=2;     
    Nv+=2;      
    ++change_count;   
    }
  }

3.2.X264编码

为了适应更多的平台,这里先用X264实现软件编码,关于X264具体的接口和源码分析介绍,以后会有文章详解。虽然Android系统提供的MediaCode也可以实现硬编码,实现跨平台,对于应用开发的人来说,无法看清底层的实现原理。以后会有文章来讲解如何用MediaCode实现硬编码或用其它硬编码器实现硬编码。

3.2.1.编码参数设置及打开编码器

//X264输入和输出图像
x264_picture_t picture_in;
x264_picture_t picture_out;
//编码器句柄
x264_t *video_encode_handle;  
x264_param_t Vpara; yData = width * height; 
Uv_Data = yData/4;      
//默认设置    
x264_param_default_preset(&Vpara, "ultrafast", "zerolatency");
//YUV420SP,底层设置像素格式   
 Vpara.i_csp = X264_CSP_NV12;   
 //设置视频的宽和高,这个可以开放接口给JAVA层去设置   
 //设置的宽高和编码的视频宽高需要一致,否则可能会出现问题,比如底部有绿边的问题    
 Vpara.i_width = width;    
 Vpara.i_height = height;   
 //码率通过fps控制    
 Vpara.b_vfr_input = 0;   
 //还记得前面有文章讲解的SPS和PPS,如果忘记,可以去看前面的文章    //每帧写sps和pps,也可以只在头部写。    
 Vpara.b_repeat_headers = 1;    
 //每帧传入sps和pps,提高视频纠错能力    
 Vpara.i_level_idc = 51;   
  //控制恒定码率 CRF:恒定码率;CQP:恒定质量;ABR:平均码率    
  Vpara.rc.i_rc_method = X264_RC_CRF;
 //码率   
 Vpara.rc.i_bitrate = bitRate;
   //瞬间最大码率
   Vpara.rc.i_vbv_max_bitrate = (int) (bitRate * 1.2);
      
   Vpara.i_fps_num = (uint32_t) frameRate;
   //帧率分子    
   Vpara.i_fps_den = 1;
   //帧率分母    
   Vpara.i_timebase_num = param.i_fps_den;
   //时间基分子    
   Vpara.i_timebase_den = param.i_fps_num;
   //时间基分母    
   Vpara.i_threads = 1;
   //编码线程数    
   //设置profile档次,"baseline"代表没有B帧    x264_param_apply_profile(&Vpara, "baseline");    
   //初始化图像    
   x264_picture_alloc(&picture_in, Vpara.i_csp, Vpara.i_width, Vpara.i_height);    
   //打开编码器    
   video_encode_handle = x264_encoder_open(&Vpara);    if(video_encode_handle){    
   } else{       
    throw_error_to_java(OPEN_VIDEOENCODER_FAILED);    
    }

前面把像素格式转换后,需要把数据拷贝到Frmae_in,拷贝完了,就可以送入编码器。

memcpy(picture_in, Yuv_Dst,  Y_Data);
//还记得前面讲的h264编码的单元吧
x264_nal_t *h264_nal = NULL;
//单元个数
int h264_nal _num = -1;
//正式开始编码,这里的h264_nal就是NAL单元,前面要文章详解,所以学习理论知识还是很有用
//Frame_out就是编码后的图像
//这个接口虽然简单,但是有很多做了工作,以后会有很多文章介绍
x264_encoder_encode(V_encode_handle, &h264_nal , &h264_nal _num, &Frame_in, & Frame_out)

实现音视频在Android与IOS平台的采集与编码之Android视频(1)_第6张图片
四、如何推流?

目前流媒体广泛应用的协议有RTMP、RTSP、RTP、HTTP等,本文主要是讲解RTMP,暂时不做详细的理论介绍,讲解在工程中如何使用。使用RTMP推流。关于更多网络传输知识,请阅读这篇文章。

[https://mp.weixin.qq.com/s?__biz=MzU2MDU4OTk3Mw==&mid=2247483805&idx=1&sn=20588e2e3a0be0e739b7f1f1e741e7eb&chksm=fc04fc67cb737571ea5cf05e0e5abbc661796b52e5226ddb540b8b66f01dd1aa8332918d3232&scene=21#wechat_redirect]
4.1.RTMP总流程

首先给IDR帧添加sps和pps,放在IDR帧头部。把头部发送出去,再发送具体的包数据。

//在IDR帧加上SPS和PPS
int Sps_Len = 0, Pps_Len = 0;
unsigned char Sps[100];
unsigned char Pps[100];
memset(Sps, 0, 100);
memset(Pps, 0, 100);
for (i = 0; i < nal_num; ++i) {    
if(nal[i].i_type == NAL_SPS){        
//sps         
Sps_Len = nal[i].i_payload - 4;        
memcpy(Sps, nal[i].p_payload + 4, Sps_Len);    
} else if(nal[i].i_type == NAL_PPS){     
 //pps        
 Pps_Len = nal[i].i_payload - 4;       
  memcpy(Pps, nal[i].p_payload + 4,Pps_Len);        
  Send_x264_header(Sps, Pps, Sps_Len, Pps_Len);   
   } else{       
    Send_x264_Packet(nal[i].p_payload, nal[i].i_payload);    
    }
    }

4.2.添加包头的SPS和PPS信息

//头部数据的长度
int Packet_size = 16 + Sps_Len + Pps_Len;
//给packet分配内存和初始化
RTMPPacket *packet = malloc(sizeof(RTMPPacket));
RTMPPacket_Alloc(packet, Packet_size);
RTMPPacket_Reset(packet);
unsigned char* body = (unsigned char *) packet->m_body;
int i = 0;
//每个字段的含义
//VideoHeadType 0-3:
//FrameType(KeyFrame=1);
//4-7:CodecId(AVC=7)
body[i++] = 0x17;
////AVC 包类型
body[i++] = 0x00;
body[i++] = 0x00;
body[i++] = 0x00;
body[i++] = 0x00;
//AVC 解码器配置
body[i++] = 0x01;
body[i++] = sps[1];
body[i++] = sps[2];
body[i++] = sps[3];
body[i++] = 0xFF;
//sps信息
body[i++] = 0xE1;
body[i++] = (unsigned char) ((sps_len >> 8) & 0xFF);
body[i++] = (unsigned char) (sps_len & 0xFF);
memcpy(&body[i], sps, (size_t) sps_len);
i += sps_len;
//pps信息
body[i++] = 0x01;
body[i++] = (unsigned char) ((pps_len >> 8) & 0xFF);
body[i++] = (unsigned char) (pps_len & 0xFF);
memcpy(&body[i], pps, (size_t) pps_len);
i += pps_len;
//Video包
packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
packet->m_nBodySize = (uint32_t) body_size;packet->m_nTimeStamp =0;
packet->m_hasAbsTimestamp = 0;packet->m_nChannel = 0x04;
packet->m_headerType = RTMP_PACKET_SIZE_MEDIUM;
//添加到RTMP包到队列中
Send_Rtmp_Packet_TO_Quenue(packet);

4.3.添加包体

这个就是真正的包体数据,这个发送到服务器了。

//是否是起始帧识别,前面的文章也分析过
if(buf[2] == 0x01){
//00 00 01    
buf += 3;    
len -= 3;
} else if (buf[3] == 0x01){
//00 00 00 01   
 buf += 4;    
 len -= 4;
 }int body_size = len + 9;
 RTMPPacket *packet = malloc(sizeof(RTMPPacket));
 RTMPPacket_Alloc(packet, body_size);
 RTMPPacket_Reset(packet);
 unsigned char *body = (unsigned char *) packet->m_body;
 int type = buf[0] & 0x1F;
 //如果是IDR帧
 if(type == NAL_SLICE_IDR){    
 body[0] = 0x17;
 } else{    
 body[0] = 0x27;
 }
 //配置包体信息
 body[1] = 0x01;
 body[2] = 0x00;
 body[3] = 0x00;
 body[4] = 0x00;
 body[5] = (unsigned char) ((len >> 24) & 0xFF);
 body[6] = (unsigned char) ((len >> 16) & 0xFF);
 body[7] = (unsigned char) ((len >> 8) & 0xFF);
 body[8] = (unsigned char) (len & 0xFF);
 memcpy(&body[9], buf, (size_t) len);
 packet->m_headerType = RTMP_PACKET_SIZE_LARGE;
 packet->m_hasAbsTimestamp = 0;
 packet->m_nBodySize = (uint32_t) body_size;
 packet->m_nChannel = 0x04;
 //包类型
 packet->m_packetType = RTMP_PACKET_TYPE_VIDEO;
 //时间戳
 packet->m_nTimeStamp = RTMP_GetTime() - start_time;
 //添加发送包体到队列中
 Send_Rtmp_Packet_To_Quenue(packet);

4.4发送PACKET到队列中

把头部和包体数据发送到队列中,实现流控式的发送,防止覆盖数据。

void Send_Rtmp_Packet_To_Quenue(RTMPPacket *pPacket) { 
   mux.lock();    
   if(Is_Publish){       
    queue_append_last(pPacket);    
    }    
    mux.unlock();
    }

五、开启RTMP推流线程

这一步是非常重要,开启推流线程,就是采集,编码,推流的最后一部分。详细解释如下:
完成这部分,java端就可以实现调用Native完成整个项目的工作了。

//推流线程
void *Send_Stream_Thread(void * args){  
bool isPushing = false;    
//RTMP分配空间    
RTMP* pRtmp = RTMP_Alloc();    
if(!pRtmp){        
goto end;    
}  
//初始化rtmp    
RTMP_Init(pRtmp);  
//设置需要推流的地址,并与服务器连接    
RTMP_SetupURL(pRtmp, url_path);   
//使能RTMP推流    
RTMP_EnableWrite(pRtmp);  
//设置超时时间,就是如果到了超时时间没有连接上,就断开   
 rtmp->Link.timeout = 500000;   
  if(!RTMP_Connect(pRtmp, NULL)){          
  Throw_Error_To_Java(RTMP_CONNECT_ERROR);       
   goto end;    
   }        
   if(!RTMP_ConnectStream(pRtmp, 0)){    
             Throw_Error_To_Java(RTMP_CONNECT_STREAM_ERROR);    
                 goto end;    
                 }       
                 //初始时间,开始计时    
                 start_time = RTMP_GetTime();   
                 isPushing = TRUE;        
                 //推流真正开始   
                  while( isPushing) {        
                  mux.lock()        
                  //从队列中取出第一个RTMP包        
                  RTMPPacket *pPacket = queue_get_first();  
                        if(pPacket){          
                        //从队列中删除第一个包            
                        queue_delete_first();            
                        //发送rtmp包,true表示rtmp空间有缓存            
                        
                     int ret = RTMP_SendPacket(rtmp, packet, TRUE);        
                           
                     if(!ret){              
                     //如果失败 释放内存 
                    
                  RTMPPacket_Free(packet);
                   
                  mux.unlock();               
                  //向java抛出异常
                                  Throw_Error_To_Java(SEND_RTMP_PACKAT_ERROR);                
                                  goto end;            
                                  }          
                                  //防止内存泄漏  
                                            RTMPPacket_Free(packet);        
                                            }        
                                            mux.unlock();    
                                            }   
                                             end:        
                                             RTMP_Close(pRtmp);    
                                             free(pRtmp);    
                                             free(url_path);    
                                             return 0;}

实现音视频在Android与IOS平台的采集与编码之Android视频(1)_第7张图片
5.1编码器资源释放

当编码器退出或者整个应用退出,需要释放空间,清理编码器的资源,防止内存泄漏,导致第二次打开失败。

//清除x264的picture缓存x264_picture_clean(&Frame_in);x264_picture_clean(&Frame_out);
//关闭视频编码器   
 x264_encoder_close(V_encode_handle);
 //删除全局引用
 (*env)->DeleteGlobalRef(env, jobject_error);
 (*javaVM)->DestroyJavaVM(javaVM);
 //退出线程
 pthread_exit(0);

六、总结

到此为止,整个Adnroid平台的视频采集,编码、推流等底层实现代码就分析完毕,详细你阅读完一定对底层有了新的认识,当涉及到编码参数设置,比如有些字段不了解,可以看看前面的文章。我会在后面的文章写出音频采集,编码,推流,敬请关注。欢迎大家关注,并转发给身边的朋友,谢谢。
实现音视频在Android与IOS平台的采集与编码之Android视频(1)_第8张图片
欢迎扫描二维码关注,同步更新,后面会有源码链接,欢迎交流学习。
实现音视频在Android与IOS平台的采集与编码之Android视频(1)_第9张图片
扫扫,加关注
实现音视频在Android与IOS平台的采集与编码之Android视频(1)_第10张图片

你可能感兴趣的:(流媒体开发)