ios直播基础篇三关于推流
一:推流需要的三方库和一些常用格式和协议介绍
1.rtmp协议 :实时消息传输协议,Adobe Systems公司为Flash播放器和服务器之间音频、视频和数据传输开发的开 放协议,因为是开放协议所以都可以使用了。RTMP协议用于对象、视频、音频的传输。这个协议建立在TCP协议或者轮询HTTP协议之上。RTMP协议就像一个用来装数据包的容器,这些数据可以是FLV中的视音频数据。一个单一的连接可以通过不同的通道传输多路网络流,这些通道中的包都是按照固定大小的包传输的
2.nginx:免费开源web服务器,常用来配置流媒体服务器。(后面会写一篇介绍如何在mac上搭建Nginx服务器)
3.常用直播协议介绍与对比
HLS:由Apple公司定义的用于实时流传输的协议,HLS基于HTTP协议实现,传输内容包括两部分,一是M3U8描述文件,二是TS媒体文件。可实现流媒体的直播和点播,主要应用在iOS系统
HLS是以点播的技术方式来实现直播
HLS是自适应码率流播,客户端会根据网络状况自动选择不同码率的视频流,条件允许的情况下使用高码率,网络繁忙的时候使用低码率,并且自动在二者间随意切换。这对移动设备网络状况不稳定的情况下保障流畅播放非常有帮助。
实现方法是服务器端提供多码率视频流,并且在列表文件中注明,播放器根据播放进度和下载速度自动调整。
HLS与RTMP对比:HLS主要是延时比较大,RTMP主要优势在于延时低
HLS协议的小切片方式会生成大量的文件,存储或处理这些文件会造成大量资源浪费
相比使用RTSP协议的好处在于,一旦切分完成,之后的分发过程完全不需要额外使用任何专门软件,普通的网络服务器即可,大大降低了CDN边缘服务器的配置要求,可以使用任何现成的CDN,而一般服务器很少支持RTSP。
HTTP-FLV:基于HTTP协议流式的传输媒体内容。
相对于RTMP,HTTP更简单和广为人知,内容延迟同样可以做到1~3秒,打开速度更快,因为HTTP本身没有复杂的状态交互。所以从延迟角度来看,HTTP-FLV要优于RTMP。
RTSP:实时流传输协议,定义了一对多应用程序如何有效地通过IP网络传送多媒体数据.
RTP:实时传输协议,RTP是建立在UDP协议上的,常与RTCP一起使用,其本身并没有提供按时发送机制或其它服务质量(QoS)保证,它依赖于低层服务去实现这一过程。
RTCP:RTP的配套协议,主要功能是为RTP所提供的服务质量(QoS)提供反馈,收集相关媒体连接的统计信息,例如传输字节数,传输分组数,丢失分组数,单向和双向网络延迟等等
关于协议的选择方面:即时性要求较高或有互动需求的可以采用RTMP,RTSP;对于有回放或跨平台需求的,推荐使用HLS
4.视频封装格式:
TS: 一种流媒体封装格式,流媒体封装有一个好处,就是不需要加载索引再播放,大大减少了首次载入的延迟,如果片子比较长,mp4文件的索引相当大,影响用户体验
为什么要用TS:这是因为两个TS片段可以无缝拼接,播放器能连续播放
FLV: 一种流媒体封装格式,由于它形成的文件极小、加载速度极快,使得网络观看视频文件成为可能,因此FLV格式成为了当今主流视频格式
5 需要的库文件
librtmp:这是一个C++的开源工程。主要作用是下载RTMP流媒体
libfaac :将获取到的音频数据编码成acc格式以及将aac数据合成flv格式
libx264:把视频原数据YUV编码压缩成H.264格式
libyuv:将获取到的视频转化为yuv(NV12)格式
二:推流流程
关于推流流程我会主要用代码截图来展示
1 :获取视频音频流 此处主要用不带美颜效果的系统获取方法
(1):初始化视频设备
(2)创建输入输出管道
(3)创建会话
(4)创建预览
(5)在前面几步实现后我们就可以来用系统方法获取视频音频流了,这个方法是AVCaptureAudioDataOutputSampleBufferDelegate的代理方法,由于系统返回没有区分是视频数据还是音频数据 所以我们需要自己代码判断如下图:
2.视频编码及推流
(1)将视频流变成yuvdata数据
-(NSData*) convertVideoSmapleBufferToYuvData:(CMSampleBufferRef) videoSample{
//获取yuv数据
//通过CMSampleBufferGetImageBuffer方法,获得CVImageBufferRef。
//这里面就包含了yuv420数据的指针
CVImageBufferRefpixelBuffer =CMSampleBufferGetImageBuffer(videoSample);
//表示开始操作数据
CVPixelBufferLockBaseAddress(pixelBuffer,0);
//图像宽度(像素)
size_tpixelWidth =CVPixelBufferGetWidth(pixelBuffer);
//图像高度(像素)
size_tpixelHeight =CVPixelBufferGetHeight(pixelBuffer);
//yuv中的y所占字节数
size_ty_size = pixelWidth * pixelHeight;
//yuv中的u和v分别所占的字节数
size_tuv_size = y_size /4;
uint8_t*yuv_frame =aw_alloc(uv_size *2+ y_size);
//获取CVImageBufferRef中的y数据
uint8_t*y_frame =CVPixelBufferGetBaseAddressOfPlane(pixelBuffer,0);
memcpy(yuv_frame, y_frame, y_size);
//获取CMVImageBufferRef中的uv数据
uint8_t*uv_frame =CVPixelBufferGetBaseAddressOfPlane(pixelBuffer,1);
memcpy(yuv_frame + y_size, uv_frame, uv_size *2);
CVPixelBufferUnlockBaseAddress(pixelBuffer,0);
NSData*nv12Data = [NSDatadataWithBytesNoCopy:yuv_framelength:y_size + uv_size *2];
//旋转
return[selfrotateNV12Data:nv12Data];
}
(2)yuv格式---->nv12格式
-(NSData*)rotateNV12Data:(NSData*)nv12Data{
intdegree =0;
switch(self.videoConfig.orientation) {
caseUIInterfaceOrientationLandscapeLeft:
degree =90;
break;
caseUIInterfaceOrientationLandscapeRight:
degree =270;
break;
default:
//do nothing
break;
}
if(degree !=0) {
uint8_t*src_nv12_bytes = (uint8_t*)nv12Data.bytes;
uint32_twidth = (uint32_t)self.videoConfig.width;
uint32_theight = (uint32_t)self.videoConfig.height;
uint32_tw_x_h = (uint32_t)(self.videoConfig.width*self.videoConfig.height);
uint8_t*rotatedI420Bytes =aw_alloc(nv12Data.length);
NV12ToI420Rotate(src_nv12_bytes, width,
src_nv12_bytes + w_x_h, width,
rotatedI420Bytes, height,
rotatedI420Bytes + w_x_h, height /2,
rotatedI420Bytes + w_x_h + w_x_h /4, height /2,
width, height, (RotationModeEnum)degree);
I420ToNV12(rotatedI420Bytes, height,
rotatedI420Bytes + w_x_h, height /2,
rotatedI420Bytes + w_x_h + w_x_h /4, height /2,
src_nv12_bytes, height, src_nv12_bytes + w_x_h, height,
height, width);
aw_free(rotatedI420Bytes);
}
returnnv12Data;
}
(3)nv12格式数据合成flv格式
-(aw_flv_video_tag*)encodeYUVDataToFlvTag:(NSData*)yuvData{
if(!_vEnSession) {
returnNULL;
}
//yuv变成转CVPixelBufferRef
OSStatusstatus =noErr;
//视频宽度
size_tpixelWidth =self.videoConfig.pushStreamWidth;
//视频高度
size_tpixelHeight =self.videoConfig.pushStreamHeight;
//现在要把NV12数据放入CVPixelBufferRef中,因为硬编码主要调用VTCompressionSessionEncodeFrame函数,此函数不接受yuv数据,但是接受CVPixelBufferRef类型。
CVPixelBufferRefpixelBuf =NULL;
//初始化pixelBuf,数据类型是kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,此类型数据格式同NV12格式相同。
CVPixelBufferCreate(NULL, pixelWidth, pixelHeight,kCVPixelFormatType_420YpCbCr8BiPlanarVideoRange,NULL, &pixelBuf);
// Lock address,锁定数据,应该是多线程防止重入操作。
if(CVPixelBufferLockBaseAddress(pixelBuf,0) !=kCVReturnSuccess){
[selfonErrorWithCode:AWEncoderErrorCodeLockSampleBaseAddressFaileddes:@"encode video lock base address failed"];
returnNULL;
}
//将yuv数据填充到CVPixelBufferRef中
size_ty_size =aw_stride(pixelWidth) * pixelHeight;
size_tuv_size = y_size /4;
uint8_t*yuv_frame = (uint8_t*)yuvData.bytes;
//处理y frame
uint8_t*y_frame =CVPixelBufferGetBaseAddressOfPlane(pixelBuf,0);
memcpy(y_frame, yuv_frame, y_size);
uint8_t*uv_frame =CVPixelBufferGetBaseAddressOfPlane(pixelBuf,1);
memcpy(uv_frame, yuv_frame + y_size, uv_size *2);
//硬编码CmSampleBufRef
//时间戳
uint32_tptsMs =self.manager.timestamp+1;//self.vFrameCount++ * 1000.f / self.videoConfig.fps;
CMTimepts =CMTimeMake(ptsMs,1000);
//硬编码主要其实就这一句。将携带NV12数据的PixelBuf送到硬编码器中,进行编码。
status =VTCompressionSessionEncodeFrame(_vEnSession, pixelBuf, pts,kCMTimeInvalid,NULL, pixelBuf,NULL);
if(status ==noErr) {
dispatch_semaphore_wait(self.vSemaphore,DISPATCH_TIME_FOREVER);
if(_naluData) {
//此处硬编码成功,_naluData内的数据即为h264视频帧。
//我们是推流,所以获取帧长度,转成大端字节序,放到数据的最前面
uint32_tnaluLen = (uint32_t)_naluData.length;
//小端转大端。计算机内一般都是小端,而网络和文件中一般都是大端。大端转小端和小端转大端算法一样,就是字节序反转就行了。
uint8_tnaluLenArr[4] = {naluLen >>24&0xff, naluLen >>16&0xff, naluLen >>8&0xff, naluLen &0xff};
//将数据拼在一起
NSMutableData*mutableData = [NSMutableDatadataWithBytes:naluLenArrlength:4];
[mutableDataappendData:_naluData];
//将h264数据合成flv tag,合成flvtag之后就可以直接发送到服务端了。后续会介绍
aw_flv_video_tag*video_tag =aw_encoder_create_video_tag((int8_t*)mutableData.bytes, mutableData.length, ptsMs,0,self.isKeyFrame);
//到此,编码工作完成,清除状态。
_naluData=nil;
_isKeyFrame=NO;
CVPixelBufferUnlockBaseAddress(pixelBuf,0);
CFRelease(pixelBuf);
returnvideo_tag;
}
}else{
[selfonErrorWithCode:AWEncoderErrorCodeEncodeVideoFrameFaileddes:@"encode video frame error"];
}
CVPixelBufferUnlockBaseAddress(pixelBuf,0);
CFRelease(pixelBuf);
returnNULL;
}
(4)发送视频flv到rtmp服务器
3 音频数据编码和推流
(1)将音频流转换成data数据
-(NSData*) convertAudioSmapleBufferToPcmData:(CMSampleBufferRef) audioSample{
//获取pcm数据大小
NSIntegeraudioDataSize =CMSampleBufferGetTotalSampleSize(audioSample);
//分配空间
int8_t*audio_data =aw_alloc((int32_t)audioDataSize);
//获取CMBlockBufferRef
//这个结构里面就保存了PCM数据
CMBlockBufferRefdataBuffer =CMSampleBufferGetDataBuffer(audioSample);
//直接将数据copy至我们自己分配的内存中
CMBlockBufferCopyDataBytes(dataBuffer,0, audioDataSize, audio_data);
//返回数据
return[NSDatadataWithBytesNoCopy:audio_datalength:audioDataSize];
}
(2)将音频data数据编码成acc格式并合成为flv
-(aw_flv_audio_tag*)encodePCMDataToFlvTag:(NSData*)pcmData{
self.curFramePcmData= pcmData;
AudioBufferListoutAudioBufferList = {0};
outAudioBufferList.mNumberBuffers=1;
outAudioBufferList.mBuffers[0].mNumberChannels= (uint32_t)self.audioConfig.channelCount;
outAudioBufferList.mBuffers[0].mDataByteSize=self.aMaxOutputFrameSize;
outAudioBufferList.mBuffers[0].mData=malloc(self.aMaxOutputFrameSize);
uint32_toutputDataPacketSize =1;
OSStatusstatus =AudioConverterFillComplexBuffer(_aConverter,aacEncodeInputDataProc, (__bridgevoid*_Nullable)(self), &outputDataPacketSize, &outAudioBufferList,NULL);
if(status ==noErr) {
NSData*rawAAC = [NSDatadataWithBytesNoCopy: outAudioBufferList.mBuffers[0].mDatalength:outAudioBufferList.mBuffers[0].mDataByteSize];
self.manager.timestamp+=1024*1000/self.audioConfig.sampleRate;
returnaw_encoder_create_audio_tag((int8_t*)rawAAC.bytes, rawAAC.length, (uint32_t)self.manager.timestamp, &_faacConfig);
}else{
[selfonErrorWithCode:AWEncoderErrorCodeAudioEncoderFaileddes:@"aac编码错误"];
}
returnNULL;
}
(3)发送音频flv到rtmp服务器
至此 我们就把flv格式的音视频数据发送到了rtmp服务器,服务器通过cdn分发后我们用ijkplayer打开就可以播了
三 需要注意的部分
1:获取完视频退出是要记得销毁会话
2 编(解)码分硬(解)编码和软编(解)码
软编码:使用CPU进行编码,性能高,低码率下通常质量低于硬编码器,但部分产品在GPU硬件平台移植了优秀的软编码算法(如X264)的,质量基本等同于软编码。
硬编码:使用非CPU进行编码,如显卡GPU、专用的DSP、FPGA、ASIC芯片等,实现直接、简单,参数调整方便,升级易,但CPU负载重,性能较硬编码低,低码率下质量通常比硬编码要好一点。