iOS上对采集的视频流数据进行硬编码

前言

前段时间忙着找工作所以没有进行更新,现在抽空更新一下吧,目前的进度由于离开了前公司,而现在的公司任务又比较多,所以更新的速度会慢一些吧.目前写到了RTP分包了,打算春节期间把C语言和TCP传输再好好看一下吧.后面还会把ffmpeg+opengl补完的.本文学习自链接,大家可以去看一下.

苹果提供了一个硬编码的框架VideoToolBox,这个框架iOS8以后开发者可以去使用,这里是VideoToolBox提供的编码类型:

支持类型

demo地址

初始化VideoToolBox

这里要提的一点是码率的设置,这里给一个公式吧,方便大家查看(原文链接),关于码率的理解我可以给大家举一个形象的例子.有钱的人可以过好一点的生活,没钱的人可以过差一点的生活,但也不至于饿死,码率大了的话就非常清晰,但同时文件也会比较大,码率小了的话,图像有时会糊,但也是勉强能看的,这里尽量给一个合适的码率吧.

码率公式,仅供参考
/*
     1、-initVideoToolBox中调用VTCompressionSessionCreate创建编码session,然后调用VTSessionSetProperty设置参数,最后调用VTCompressionSessionPrepareToEncodeFrames开始编码;
     2、开始视频录制,获取到摄像头的视频帧,传入-encode:,调用VTCompressionSessionEncodeFrame传入需要编码的视频帧,如果返回失败,调用VTCompressionSessionInvalidate销毁session,然后释放session;
     3、每一帧视频编码完成后会调用预先设置的编码函数didCompressH264,如果是关键帧需要用CMSampleBufferGetFormatDescription获取CMFormatDescriptionRef,然后用
     CMVideoFormatDescriptionGetH264ParameterSetAtIndex取得PPS和SPS;
     最后把每一帧的所有NALU数据前四个字节变成0x00 00 00 01之后再写入文件;
     4、调用VTCompressionSessionCompleteFrames完成编码,然后销毁session:VTCompressionSessionInvalidate,释放session。
     
     */


    /**
     初始化videoToolBox
     */
    -(void)initVideoToolBox{
        

        
        //同步
        dispatch_sync(_encodeQueue, ^{
           
            _frameID = 0;
            
            //给定宽高,过高的话会编码失败
            int width = 640 , height = 480;
            
            /**
             创建编码会话

             @param allocator#> 会话的分配器,传入NULL默认 description#>
             @param width#> 帧宽 description#>
             @param height#> 帧高 description#>
             @param codecType#> 编码器类型 description#>
             @param encoderSpecification#> 指定必须使用的特定视频编码器。通过空来让视频工具箱选择一个编码器。 description#>
             @param sourceImageBufferAttributes#> 像素缓存池源帧 description#>
             @param compressedDataAllocator#> 压缩数据分配器,默认为空 description#>
             @param outputCallback#> 回调函数,图像编码成功后调用 description#>
             @param outputCallbackRefCon#> 客户端定义的输出回调的参考值。 description#>
             @param compressionSessionOut#> 指向一个变量,以接收新的压缩会话 description#>
             @return <#return value description#>
             */
            OSStatus status = VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void *)(self), &_encodeingSession);
            
            NSLog(@"H264状态:VTCompressionSessionCreate %d",(int)status);
            
            if (status != 0) {
                
                NSLog(@"H264会话创建失败");
                return ;
            }
            
            //设置实时编码输出(避免延迟)
            VTSessionSetProperty(_encodeingSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
            VTSessionSetProperty(_encodeingSession, kVTCompressionPropertyKey_ProfileLevel, kVTProfileLevel_H264_Baseline_AutoLevel);
            
            // 设置关键帧(GOPsize)间隔,gop太小的话有时候图像会糊
            int frameInterval = 10;
            CFNumberRef  frameIntervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
            VTSessionSetProperty(_encodeingSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRef);
            
            // 设置期望帧率,不是实际帧率
            int fps = 10;
            CFNumberRef fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
            VTSessionSetProperty(_encodeingSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);

            //设置码率,上限,单位是bps
            int bitRate = width * height * 3 * 4 * 8 ;
            CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
            VTSessionSetProperty(_encodeingSession, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);
            
            // 设置码率,均值,单位是byte
            int bitRateLimit = width * height * 3 * 4 ;
            CFNumberRef bitRateLimitRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRateLimit);
            NSLog(@"码率%@",bitRateLimitRef);
            VTSessionSetProperty(_encodeingSession, kVTCompressionPropertyKey_DataRateLimits, bitRateLimitRef);
            
            //可以开始编码
            VTCompressionSessionPrepareToEncodeFrames(_encodeingSession);
            
        });
        
        
    }

根据代理方法判断是音频数据还是视频数据

-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection{

            if ([self.videoDataOutput isEqual:captureOutput]) {
                //捕获到视频数据
                NSLog(@"视频");
                
                //将视频数据转换成YUV420数据
    //            NSData *yuv420Data = [self convertVideoSampleToYUV420:sampleBuffer];
                
                dispatch_sync(_encodeQueue, ^{
                    
                    //开始硬编码
                    _isStartHardEncoding = 1;
                    
                    // 摄像头采集后的图像是未编码的CMSampleBuffer形式,
                    [self videoEncode:sampleBuffer];
                    
                });
                
    //            [self sendVideoSampleBuffer:sampleBuffer];
            }else if([self.audioDataOutput isEqual:captureOutput]){
                //捕获到音频数据
                NSLog(@"音频");

                //AudioToolBox PCM->AAC硬编码
                dispatch_sync(_encodeQueue, ^{
                    
                    [self.aacEncode encodeSampleBuffer:sampleBuffer completionBlock:^(NSData *encodedData, NSError *error) {
                        [_audioFileHandle writeData:encodedData];
                        NSLog(@"%@",_audioFileHandle);
                        
                    }];
                });
                
                //音频数据转PCM
    //            NSData *pcmData = [self convertAudioSampleToYUV420:sampleBuffer];
                
            }
        
    }

编码完成后的回调

/**
     *  h.264硬编码完成后回调 VTCompressionOutputCallback
     *  将硬编码成功的CMSampleBuffer转换成H264码流,通过网络传播
     *  解析出参数集SPS和PPS,加上开始码后组装成NALU。提取出视频数据,将长度码转换成开始码,组长成NALU。将NALU发送出去。
     */

    //编码完成后回调
    void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer){
        
    //    NSLog(@"didCompressH264 called with status %d infoFlags %d", (int)status, (int)infoFlags);
        //状态错误
        if (status != 0) {
            return;
        }
        
        //没准备好
        if (!CMSampleBufferDataIsReady(sampleBuffer)) {
            
            NSLog(@"didCompressH264 data is not ready ");
            return;
        }
        
        VideoEncodeVC * encoder = (__bridge VideoEncodeVC*)outputCallbackRefCon;
        
        bool keyframe = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
        
        // 判断当前帧是否为关键帧 获取sps & pps 数据
        // 解析出参数集SPS和PPS,加上开始码后组装成NALU。提取出视频数据,将长度码转换成开始码,组长成NALU。将NALU发送出去。
        if (keyframe) {
            
            // CMVideoFormatDescription:图像存储方式,编解码器等格式描述
            CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
            // sps
            size_t sparameterSetSize, sparameterSetCount;
            const uint8_t *sparameterSet;
            OSStatus statusSPS = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0);
            if (statusSPS == noErr) {
                
                // Found sps and now check for pps
                // pps
                size_t pparameterSetSize, pparameterSetCount;
                const uint8_t *pparameterSet;
                OSStatus statusPPS = CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0);
                if (statusPPS == noErr) {
                    
                    // found sps pps
                    NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
                    NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
                    if (encoder) {
                        
                        [encoder gotSPS:sps withPPS:pps];
                    }
                }
            }
        }
        
        // 编码后的图像,以CMBlockBuffe方式存储
        CMBlockBufferRef dataBuffer = CMSampleBufferGetDataBuffer(sampleBuffer);
        size_t length, totalLength;
        char *dataPointer;
        OSStatus statusCodeRet = CMBlockBufferGetDataPointer(dataBuffer, 0, &length, &totalLength, &dataPointer);
        if (statusCodeRet == noErr) {
            
            size_t bufferOffSet = 0;
            // 返回的nalu数据前四个字节不是0001的startcode,而是大端模式的帧长度length
            static const int AVCCHeaderLength = 4;
            
            // 循环获取nalu数据
            while (bufferOffSet < totalLength - AVCCHeaderLength) {
                
                uint32_t NALUUnitLength = 0;
                // Read the NAL unit length
                memcpy(&NALUUnitLength, dataPointer + bufferOffSet, AVCCHeaderLength);
                // 从大端转系统端
                NALUUnitLength = CFSwapInt32BigToHost(NALUUnitLength);
                NSData *data = [[NSData alloc] initWithBytes:(dataPointer + bufferOffSet + AVCCHeaderLength) length:NALUUnitLength];
                [encoder gotEncodedData:data isKeyFrame:keyframe];
                
                // Move to the next NAL unit in the block buffer
                bufferOffSet += AVCCHeaderLength + NALUUnitLength;
            }
        }
    }

h264视频编码

/**
     视频编码

     @param videoSample <#videoSample description#>
     */
    -(void)videoEncode:(CMSampleBufferRef)videoSampleBuffer{
        
        // CVPixelBufferRef 编码前图像数据结构
        // 利用给定的接口函数CMSampleBufferGetImageBuffer从中提取出CVPixelBufferRef
        CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(videoSampleBuffer);
        
        // 帧时间, 如果不设置会导致时间轴过长
        CMTime presentationTimeStamp = CMTimeMake(_frameID++, 1000);
        VTEncodeInfoFlags flags;
        
        // 使用硬编码接口VTCompressionSessionEncodeFrame来对该帧进行硬编码
        // 编码成功后,会自动调用session初始化时设置的回调函数
        OSStatus statusCode = VTCompressionSessionEncodeFrame(_encodeingSession, imageBuffer, presentationTimeStamp, kCMTimeInvalid, NULL, NULL, &flags);
        
        if (statusCode != noErr) {
            NSLog(@"H264: VTCompressionSessionEncodeFrame failed with %d", (int)statusCode);
            VTCompressionSessionInvalidate(_encodeingSession);
            CFRelease(_encodeingSession);
            _encodeingSession = NULL;
            return;
        }
        
    //    NSLog(@"H264: VTCompressionSessionEncodeFrame Success : %d", (int)statusCode);
    }

写入沙盒

//传入PPS和SPS,写入到文件
    - (void)gotSPS:(NSData *)sps withPPS:(NSData *)pps{
        
    //    NSLog(@"gotSPSAndPPS %d withPPS %d", (int)[sps length], (int)[pps length]);
        const char bytes[] = "\x00\x00\x00\x01";
        size_t length = (sizeof bytes) - 1;
        NSData *byteHeader = [NSData dataWithBytes:bytes length:length];
        [_fileHandle writeData:byteHeader];
        [_fileHandle writeData:sps];
        [_fileHandle writeData:byteHeader];
        [_fileHandle writeData:pps];
    }

    - (void)gotEncodedData:(NSData *)data isKeyFrame:(BOOL)isKeyFrame {
        
    //    NSLog(@"gotEncodedData %d", (int)[data length]);
        if (_fileHandle != NULL) {
            
            const char bytes[]= "\x00\x00\x00\x01";
            size_t lenght = (sizeof bytes) - 1;
            NSData *byteHeader = [NSData dataWithBytes:bytes length:lenght];
            [_fileHandle writeData:byteHeader];
            [_fileHandle writeData:data];
        }
    }

结束编码

/**
     结束编码
     */
    - (void)endVideoToolBox{
        
        VTCompressionSessionCompleteFrames(_encodeingSession, kCMTimeInvalid);
        VTCompressionSessionInvalidate(_encodeingSession);
        CFRelease(_encodeingSession);
        _encodeingSession = NULL;
    }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 151,688评论 1 330
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 64,559评论 1 273
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 101,749评论 0 226
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 42,581评论 0 191
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 50,741评论 3 271
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 39,684评论 1 192
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,122评论 2 292
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 29,847评论 0 182
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 33,441评论 0 228
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 29,939评论 2 232
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,333评论 1 242
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 27,783评论 2 236
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,275评论 3 220
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 25,830评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,444评论 0 180
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 34,553评论 2 249
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 34,618评论 2 249

推荐阅读更多精彩内容

  • 硬件编码相关知识(H264,H265) 阅读人群:研究硬件编码器应用于iOS开发中,从0研究关于硬件编解码,码流中...
    小东邪啊阅读 12,514评论 0 18
  • 0 概述 FFmpeg是一套领先的音视频多媒体处理开源框架,采用LGPL或GPL许可证。它提供了对音视频的采集、编...
    但行耕者阅读 6,659评论 0 19
  • "在吗?亲爱的." "嗯嗯" "我今天碰到她了,嗯....." "你前任啊!" "你怎么知道?" "我看见你俩在一...
    被遺忘的角落i阅读 263评论 0 0
  • 当面对孤独与空虚似家常便饭时,你已经成长了,不需要依赖别人,一个人也能好好的
    李星谕阅读 487评论 0 51
  • 今天看见了很对对于大学生的忠告,第一个就是在大学当中应该怎么去学习,第二个就是在大学当中应不应该着急去赚钱,第三个...
    我的成长路阅读 102评论 0 0