iOS实现直播详解

直播流媒体介绍

直播,音乐播放demo
https://github.com/AndreHu88/iOS_Live
视频流传输使用的是RTMP协议(类似于socket,基于TCP)
RTMP是Real Time Messaging Protocol(实时消息传输协议)的首字母缩写。该协议基于TCP

流媒体开发:网络层(socket或st)负责传输,协议层(rtmp或hls)负责网络打包,封装层(flv、ts)负责编解码数据的封装,编码层(h.264和aac)负责图像,音频压缩。
用于对象,视频,音频的传输.这个协议建立在TCP协议或者轮询HTTP协议之上.

HLS:由Apple公司定义的用于实时流传输的协议,HLS基于HTTP协议实现,传输内容包括两部分,一是M3U8描述文件,二是TS媒体文件。可实现流媒体的直播和点播,主要应用在iOS系统
HLS与RTMP对比:HLS主要是延时比较大,RTMP主要优势在于延时低

下图是直播的完整图解


image

播放网络视频需要以下几步(依赖FFmpeg框架)

  • 将数据解协议
  • 解封装
  • 解码音视频
  • 音视频同步

播放本地视频不需要解协议,直接解封装

  • 解协议
    解协议就是将流媒体协议上的数据解析为相应的封装格式数据,流媒体一般是RTMP协议传输,这些协议在传输音视频数据的同时也可以传输一些指令数据(播放,停止,暂停,网络状态的描述) ,解协议会去掉信令数据,只保留音视频数据。采用RTMP协议通过解协议后,输入FLV的流

    FFMpeg会根据相关协议的特性,本机与服务器建立连接,获取流数据

  • 解封装

    将封装的视频数据分离成音频和视频编码数据,常见的封装的格式有MP4,MKV, RMVB, FLV, AVI等。它的作用就是将已压缩的视频数据和音频数据按照一定的格式放在一起。FLV格式经过解封装后,可以得到H.264的视频编码数据和aac的音频编码数据,一般称为“packet”

  • 解码音视频

    解码就是将音视频压缩编码数据解码成非压缩的音视频的原始数据,解码是最复杂最重要的一个环节,通过解码压缩的视频数据被输出成非压缩的颜色数据。目前常用的音频编码方式是aac,mp3,视频编码格式是H.264,H.265。分析源数据的音视频信息,分别设置对应的音频解码器,视频编码器。对packet分别进行解码后,音频解码获得的数据是PCM(Pulse Code Modulation,脉冲编码调制)采样数据,一般称为“sample”。视频解码获得的数据是一幅YUV或RGB图像数据,一般称为“picture”

  • 音视频同步

    音视频解码是两个独立的线程,获取到的音视频是分开的。理想情况下,音视频按照自己的固有频率渲染输出能达到音视频同步的效果,但是在现实中,断网、弱网、丢帧、缓冲、音视频不同的解码耗时等情况都会妨碍实现同步,很难达到预期效果。 通过音视频同步调整后,将同步解码出来的音频,视频数据,同步给显卡和声卡播放出来。

VideoToolbox.framework(硬编码)

videoToolbox是苹果的一个硬解码的框架,提供实现压缩,解压缩服务,并存储在缓冲区corevideo像素栅格图像格式之中。这些服务以会话对象的形式提供(压缩、解压,和像素传输),应用程序不需要直接访问硬件编码器和解码器相关内容,硬件编解码这块的质量有一定保证,可以优先使用硬编解码,和软解码FFmpeg可以互补

编码H.264

1.初始化VideoToolbox

- (void)setupVideoToolbox{
    
    dispatch_sync(_encodeQueue, ^{
        
        [self setupFileHandle];
        
        int width = 720, height = 1280;
        OSStatus status = VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, encodingComplectionCallback, (__bridge void *)(self), &_encodingSession);
        DLog(@"status code is %d",(int)status);
        if (status != 0) {
            DLog(@"create H264 session error");
            return ;
        }
        
        //设置实时编码,避免延迟
        VTSessionSetProperty(_encodingSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
        VTSessionSetProperty(_encodingSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
        
        //设置关键帧间隔()关键字间隔越小越清晰,数值越大压缩率越高
        int frameInterval = 1;
        CFNumberRef frameIntervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
        VTSessionSetProperty(_encodingSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRef);
        
        //设置期望帧率
        int fps = 30;
        CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
        VTSessionSetProperty(_encodingSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
        
        //设置码率,均值,单位是byte
        int bitRate = width * height * 3 * 4 * 8;
        CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
        VTSessionSetProperty(_encodingSession, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);
        
        //设置码率上限,单位是bps,如果不设置默认会以很低的码率编码,导致编码出来的视频很模糊
        int bitRateMax = width * height * 3 * 4;
        CFNumberRef bitRateMaxRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRateMax);
        VTSessionSetProperty(_encodingSession, kVTCompressionPropertyKey_DataRateLimits, bitRateMaxRef);
        
        //准备编码
        VTCompressionSessionPrepareToEncodeFrames(_encodingSession);
        
    });
}

- (void)setupFileHandle{
    
    //创建文件,初始化fileHandle;
    NSString *file = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"test.h264"];
    [[NSFileManager defaultManager] removeItemAtPath:file error:nil];
    [[NSFileManager defaultManager] createFileAtPath:file contents:nil attributes:nil];
    _fileHandle = [NSFileHandle fileHandleForWritingAtPath:file];
}

2.sampleBuffer回调处理

- (void)videoEncodeWithSampleBuffer:(CMSampleBufferRef)sampleBuffer{
    
    dispatch_sync(_encodeQueue, ^{
        
        // CVPixelBufferRef 编码前图像数据结构
        // 利用给定的接口函数CMSampleBufferGetImageBuffer从中提取出CVPixelBufferRef
        CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
        // 帧时间 如果不设置导致时间轴过长
        CMTime presentationTimeStamp = CMTimeMake(_frameID++, 1000);
        //flags 0 表示同步解码
        VTEncodeInfoFlags flags;
        OSStatus status = VTCompressionSessionEncodeFrame(_encodingSession, imageBuffer, presentationTimeStamp, kCMTimeInvalid, NULL, NULL, &flags);
        DLog(@"status code is %d",(int)status);
        if (status == noErr) {
            DLog(@"H264 VTCompressionSessionEncodeFrame success");
        }
        else{
            DLog(@"H264: VTCompressionSessionEncodeFrame failed with %d", (int)status);
            if (!_encodingSession) return;
            VTCompressionSessionInvalidate(_encodingSession);
            //释放资源
            CFRelease(_encodingSession);
            _encodingSession = NULL;
        }
    });
    
}

3.对VideoToolbox的编码回调

//每压缩一次都异步的调用此方法
void encodingComplectionCallback(void * CM_NULLABLE outputCallbackRefCon,
                               void * CM_NULLABLE sourceFrameRefCon,
                               OSStatus status,
                               VTEncodeInfoFlags infoFlags,
                               CM_NULLABLE CMSampleBufferRef sampleBuffer ){

}
音视频同步详解

音视频采集的数据分别来自于麦克风与摄像头,而摄像头与麦克风其实是两个独立的硬件,而音视频同步的原理是相信摄像头与麦克风采集数据是实时的,并在采集到数据时给他们一个时间戳来标明数据所属的时间,而编码封装模块只要不改动音视频时间的相对关系就能保证音频与视频在时间上的对应。如此封装好数据之后,播放端就能够根据音视频的时间戳来播放对应的音视频,从实现音视频同步的效果

时间戳参考标准
  • 取格林威治时间做为对比标准,即音视频时间戳都为采集时间点相对于格林威治标准时间的时间差
  • 取系统开机时间做为对比标准,即音视频时间戳都是采集时间点相对于手机开机时间的时间差。目前iOS上AVCaptureSession这套API就是参考这个时间标准给的时间戳
  • 其它时间戳标准
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,214评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,307评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,543评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,221评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,224评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,007评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,313评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,956评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,441评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,925评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,018评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,685评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,234评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,240评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,464评论 1 261
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,467评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,762评论 2 345

推荐阅读更多精彩内容