美文网首页iOS之视频处理
iOS直播技术学习笔记-硬编码&软编码实现(五)

iOS直播技术学习笔记-硬编码&软编码实现(五)

作者: Tate_code | 来源:发表于2017-09-08 23:42 被阅读327次

    iOS硬编码实现

    前言

    • 在上一篇中,我们已经知道iOS编码的一些概念知识,从现在开始,我们可以正式对采集到的视频进行编码
    • 这里我们重点介绍硬编码的使用方式,也就是VideoToolBox框架的使用
    • 编码的流程:采集--> 获取到视频帧--> 对视频帧进行编码 --> 获取到视频帧信息 --> 将编码后的数据以NALU方式写入到文件

    视频采集

    • 视频采集我们已经在前面进行了介绍和学习,所有这里就直接贴代码,只是我对采集过程进行了一些简单的封装
    视频采集.png

    视频硬件编码

    • 初始化压缩编码会话(VTCompressionSessionRef)
      • 在VideoToolbox框架的使用过程中,基本都是C语言函数
    • 初始化后通过VTSessionSetProperty设置对象属性
      • 编码方式:H.264编码
      • 帧率:每秒钟多少帧画面
      • 码率:单位时间内保存的数据量
      • 关键帧(GOPsize)间隔:多少帧为一个GOP
    • 准备编码
    • 代码如下:
    - (void)setupVideoSession {
        // 1.用于记录当前是第几帧数据(画面帧数非常多)
        self.frameID = 0;
    
        // 2.录制视频的宽度&高度
        int width = [UIScreen mainScreen].bounds.size.width;
        int height = [UIScreen mainScreen].bounds.size.height;
    
        // 3.创建CompressionSession对象,该对象用于对画面进行编码
        // kCMVideoCodecType_H264 : 表示使用h.264进行编码
        // didCompressH264 : 当一次编码结束会在该函数进行回调,可以在该函数中将数据,写入文件中
        VTCompressionSessionCreate(NULL, width, height, kCMVideoCodecType_H264, NULL, NULL, NULL, didCompressH264, (__bridge void *)(self),  &_compressionSession);
    
        // 4.设置实时编码输出(直播必然是实时输出,否则会有延迟)
        VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_RealTime, kCFBooleanTrue);
    
        // 5.设置期望帧率(每秒多少帧,如果帧率过低,会造成画面卡顿)
        int fps = 30;
        CFNumberRef  fpsRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &fps);
        VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_ExpectedFrameRate, fpsRef);
    
        // 6.设置码率(码率: 编码效率, 码率越高,则画面越清晰, 如果码率较低会引起马赛克 --> 码率高有利于还原原始画面,但是也不利于传输)
        int bitRate = 800*1024;
        CFNumberRef bitRateRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberSInt32Type, &bitRate);
        VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_AverageBitRate, bitRateRef);
        NSArray *limit = @[@(bitRate * 1.5/8), @(1)];
        VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_DataRateLimits, (__bridge CFArrayRef)limit);
    
        // 7.设置关键帧(GOPsize)间隔
        int frameInterval = 30;
        CFNumberRef  frameIntervalRef = CFNumberCreate(kCFAllocatorDefault, kCFNumberIntType, &frameInterval);
        VTSessionSetProperty(self.compressionSession, kVTCompressionPropertyKey_MaxKeyFrameInterval, frameIntervalRef);
    
        // 8.基本设置结束, 准备进行编码
        VTCompressionSessionPrepareToEncodeFrames(self.compressionSession);
    }
    
    
    • 将输入的帧进行编码
      • 将CMSampleBufferRef转成CVImageBufferRef
      • 开始对CVImageBufferRef进行编码
    - (void)encodeSampleBuffer:(CMSampleBufferRef)sampleBuffer {
        // 1.将sampleBuffer转成imageBuffer
        CVImageBufferRef imageBuffer = (CVImageBufferRef)CMSampleBufferGetImageBuffer(sampleBuffer);
    
        // 2.根据当前的帧数,创建CMTime的时间
        CMTime presentationTimeStamp = CMTimeMake(self.frameID++, 1000);
        VTEncodeInfoFlags flags;
    
        // 3.开始编码该帧数据
        OSStatus statusCode = VTCompressionSessionEncodeFrame(self.compressionSession,
                                                              imageBuffer,
                                                              presentationTimeStamp,
                                                              kCMTimeInvalid,
                                                              NULL, (__bridge void * _Nullable)(self), &flags);
        if (statusCode == noErr) {
            NSLog(@"H264: VTCompressionSessionEncodeFrame Success");
        }
    }
    
    
    • 当编码成功后,将编码后的码流写入文件
      • 编码成功后会回调之前输入的函数
      • 1> 先判断是否是关键帧:
        • 如果是关键帧,则需要在写入关键帧之前,先写入PPS、SPS的NALU
        • 取出PPS、SPS数据,并且封装成NALU单元,写入文件
      • 2> 将I帧、P帧、B帧分别封装成NALU单元写入文件
      • 写入后,数据存储方式:
    数据存储方式.png
    • 代码如下:
    // 编码完成回调
    void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) {
    
        // 1.判断状态是否等于没有错误
        if (status != noErr) {
            return;
        }
    
        // 2.根据传入的参数获取对象
        VideoEncoder* encoder = (__bridge VideoEncoder*)outputCallbackRefCon;
    
        // 3.判断是否是关键帧
        bool isKeyframe = !CFDictionaryContainsKey( (CFArrayGetValueAtIndex(CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, true), 0)), kCMSampleAttachmentKey_NotSync);
        // 判断当前帧是否为关键帧
        // 获取sps & pps数据
        if (isKeyframe)
        {
            // 获取编码后的信息(存储于CMFormatDescriptionRef中)
            CMFormatDescriptionRef format = CMSampleBufferGetFormatDescription(sampleBuffer);
    
            // 获取SPS信息
            size_t sparameterSetSize, sparameterSetCount;
            const uint8_t *sparameterSet;
            CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 0, &sparameterSet, &sparameterSetSize, &sparameterSetCount, 0 );
    
            // 获取PPS信息
            size_t pparameterSetSize, pparameterSetCount;
            const uint8_t *pparameterSet;
            CMVideoFormatDescriptionGetH264ParameterSetAtIndex(format, 1, &pparameterSet, &pparameterSetSize, &pparameterSetCount, 0 );
    
            // 装sps/pps转成NSData,以方便写入文件
            NSData *sps = [NSData dataWithBytes:sparameterSet length:sparameterSetSize];
            NSData *pps = [NSData dataWithBytes:pparameterSet length:pparameterSetSize];
    
            // 写入文件
            [encoder gotSpsPps:sps pps:pps];
        }
    
        // 获取数据块
        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;
            static const int AVCCHeaderLength = 4; // 返回的nalu数据前四个字节不是0001的startcode,而是大端模式的帧长度length
    
            // 循环获取nalu数据
            while (bufferOffset < totalLength - AVCCHeaderLength) {
                uint32_t NALUnitLength = 0;
                // Read the NAL unit length
                memcpy(&NALUnitLength, dataPointer + bufferOffset, AVCCHeaderLength);
    
                // 从大端转系统端
                NALUnitLength = CFSwapInt32BigToHost(NALUnitLength);
    
                NSData* data = [[NSData alloc] initWithBytes:(dataPointer + bufferOffset + AVCCHeaderLength) length:NALUnitLength];
                [encoder gotEncodedData:data isKeyFrame:isKeyframe];
    
                // 移动到写一个块,转成NALU单元
                // Move to the next NAL unit in the block buffer
                bufferOffset += AVCCHeaderLength + NALUnitLength;
            }
        }
    }
    
    - (void)gotSpsPps:(NSData*)sps pps:(NSData*)pps
    {
        // 1.拼接NALU的header
        const char bytes[] = "\x00\x00\x00\x01";
        size_t length = (sizeof bytes) - 1;
        NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
    
        // 2.将NALU的头&NALU的体写入文件
        [self.fileHandle writeData:ByteHeader];
        [self.fileHandle writeData:sps];
        [self.fileHandle writeData:ByteHeader];
        [self.fileHandle writeData:pps];
    
    }
    - (void)gotEncodedData:(NSData*)data isKeyFrame:(BOOL)isKeyFrame
    {
        NSLog(@"gotEncodedData %d", (int)[data length]);
        if (self.fileHandle != NULL)
        {
            const char bytes[] = "\x00\x00\x00\x01";
            size_t length = (sizeof bytes) - 1; //string literals have implicit trailing '\0'
            NSData *ByteHeader = [NSData dataWithBytes:bytes length:length];
            [self.fileHandle writeData:ByteHeader];
            [self.fileHandle writeData:data];
        }
    }
    
    

    iOS视频软编码

    软编码介绍

    • 软编码主要是利用CPU进行编码的过程, 具体的编码通常会用FFmpeg+x264
    • FFmpeg
      • FFmpeg是一个非常强大的音视频处理库,包括视频采集功能、视频格式转换、视频抓图、给视频加水印等。
      • FFmpeg在Linux平台下开发,但它同样也可以在其它操作系统环境中编译运行,包括Windows、Mac OS X等。
    • X264
      • H.264是ITU制定的视频编码标准
      • 而x264是一个开源的H.264/MPEG-4 AVC视频编码函数库[1] ,是最好的有损视频编码器,里面集成了非常多优秀的算法用于视频编码.
    • 关于软编码推荐博客(雷霄骅)

    Mac安装/使用FFmpeg

    • 安装
      • ruby -e "$(curl -fsSkL raw.github.com/mxcl/homebrew/go)"
      • brew install ffmpeg
    • 简单使用
      • 转化格式: ffmpeg -i story.webm story.mp4
      • 分离视频: ffmpeg -i story.mp4 -vcodec copy -an demo.mp4
      • 分离音频: ffmpeg -i story.mp4 -acodec copy -vn demo.aac

    编译FFmpeg(iOS)

    编译X264

    • 下载x264
    • 下载gas-preprocessor(FFmpeg编译时已经下载过)
    • 下载x264 build shell
    • 修改权限/执行脚本
      • sudo chmod u+x build-x264.sh
      • sudo ./build-x264.sh

    iOS项目中集成FFmpeg

    • 将编译好的文件夹拖入到工程中
    • 添加依赖库: libiconv.dylib/libz.dylib/libbz2.dylib/CoreMedia.framework/AVFoundation.framework
    • 初始化编码器
    /*
     *  设置X264
     */
    - (int)setX264ResourceWithVideoWidth:(int)width height:(int)height bitrate:(int)bitrate
    {
        // 1.默认从第0帧开始(记录当前的帧数)
        framecnt = 0;
    
        // 2.记录传入的宽度&高度
        encoder_h264_frame_width = width;
        encoder_h264_frame_height = height;
    
        // 3.注册FFmpeg所有编解码器(无论编码还是解码都需要该步骤)
        av_register_all();
    
        // 4.初始化AVFormatContext: 用作之后写入视频帧并编码成 h264,贯穿整个工程当中(释放资源时需要销毁)
        pFormatCtx = avformat_alloc_context();
    
        // 5.设置输出文件的路径
        fmt = av_guess_format(NULL, out_file, NULL);
        pFormatCtx->oformat = fmt;
    
        // 6.打开文件的缓冲区输入输出,flags 标识为  AVIO_FLAG_READ_WRITE ,可读写
        if (avio_open(&pFormatCtx->pb, out_file, AVIO_FLAG_READ_WRITE) < 0){
            printf("Failed to open output file! \n");
            return -1;
        }
    
        // 7.创建新的输出流, 用于写入文件
        video_st = avformat_new_stream(pFormatCtx, 0);
    
        // 8.设置 20 帧每秒 ,也就是 fps 为 20
        video_st->time_base.num = 1;
        video_st->time_base.den = 25;
    
        if (video_st==NULL){
            return -1;
        }
    
        // 9.pCodecCtx 用户存储编码所需的参数格式等等
        // 9.1.从媒体流中获取到编码结构体,他们是一一对应的关系,一个 AVStream 对应一个  AVCodecContext
        pCodecCtx = video_st->codec;
    
        // 9.2.设置编码器的编码格式(是一个id),每一个编码器都对应着自己的 id,例如 h264 的编码 id 就是 AV_CODEC_ID_H264
        pCodecCtx->codec_id = fmt->video_codec;
    
        // 9.3.设置编码类型为 视频编码
        pCodecCtx->codec_type = AVMEDIA_TYPE_VIDEO;
    
        // 9.4.设置像素格式为 yuv 格式
        pCodecCtx->pix_fmt = PIX_FMT_YUV420P;
    
        // 9.5.设置视频的宽高
        pCodecCtx->width = encoder_h264_frame_width;
        pCodecCtx->height = encoder_h264_frame_height;
    
        // 9.6.设置帧率
        pCodecCtx->time_base.num = 1;
        pCodecCtx->time_base.den = 15;
    
        // 9.7.设置码率(比特率)
        pCodecCtx->bit_rate = bitrate;
    
        // 9.8.视频质量度量标准(常见qmin=10, qmax=51)
        pCodecCtx->qmin = 10;
        pCodecCtx->qmax = 51;
    
        // 9.9.设置图像组层的大小(GOP-->两个I帧之间的间隔)
        pCodecCtx->gop_size = 250;
    
        // 9.10.设置 B 帧最大的数量,B帧为视频图片空间的前后预测帧, B 帧相对于 I、P 帧来说,压缩率比较大,也就是说相同码率的情况下,
        // 越多 B 帧的视频,越清晰,现在很多打视频网站的高清视频,就是采用多编码 B 帧去提高清晰度,
        // 但同时对于编解码的复杂度比较高,比较消耗性能与时间
        pCodecCtx->max_b_frames = 5;
    
        // 10.可选设置
        AVDictionary *param = 0;
        // H.264
        if(pCodecCtx->codec_id == AV_CODEC_ID_H264) {
            // 通过--preset的参数调节编码速度和质量的平衡。
            av_dict_set(&param, "preset", "slow", 0);
    
            // 通过--tune的参数值指定片子的类型,是和视觉优化的参数,或有特别的情况。
            // zerolatency: 零延迟,用在需要非常低的延迟的情况下,比如视频直播的编码
            av_dict_set(&param, "tune", "zerolatency", 0);
        }
    
        // 11.输出打印信息,内部是通过printf函数输出(不需要输出可以注释掉该局)
        av_dump_format(pFormatCtx, 0, out_file, 1);
    
        // 12.通过 codec_id 找到对应的编码器
        pCodec = avcodec_find_encoder(pCodecCtx->codec_id);
        if (!pCodec) {
            printf("Can not find encoder! \n");
            return -1;
        }
    
        // 13.打开编码器,并设置参数 param
        if (avcodec_open2(pCodecCtx, pCodec,&param) < 0) {
            printf("Failed to open encoder! \n");
            return -1;
        }
    
        // 13.初始化原始数据对象: AVFrame
        pFrame = av_frame_alloc();
    
        // 14.通过像素格式(这里为 YUV)获取图片的真实大小,例如将 480 * 720 转换成 int 类型
        avpicture_fill((AVPicture *)pFrame, picture_buf, pCodecCtx->pix_fmt, pCodecCtx->width, pCodecCtx->height);
    
        // 15.h264 封装格式的文件头部,基本上每种编码都有着自己的格式的头部,想看具体实现的同学可以看看 h264 的具体实现
        avformat_write_header(pFormatCtx, NULL);
    
        // 16.创建编码后的数据 AVPacket 结构体来存储 AVFrame 编码后生成的数据
        av_new_packet(&pkt, picture_size);
    
        // 17.设置 yuv 数据中 y 图的宽高
        y_size = pCodecCtx->width * pCodecCtx->height;
    
        return 0;
    }
    
    
    • 编码每一帧数据
    /*
     * 将CMSampleBufferRef格式的数据编码成h264并写入文件
     *
     */
    - (void)encoderToH264:(CMSampleBufferRef)sampleBuffer
    {
        // 1.通过CMSampleBufferRef对象获取CVPixelBufferRef对象
        CVPixelBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
    
        // 2.锁定imageBuffer内存地址开始进行编码
        if (CVPixelBufferLockBaseAddress(imageBuffer, 0) == kCVReturnSuccess) {
            // 3.从CVPixelBufferRef读取YUV的值
            // NV12和NV21属于YUV格式,是一种two-plane模式,即Y和UV分为两个Plane,但是UV(CbCr)为交错存储,而不是分为三个plane
            // 3.1.获取Y分量的地址
            UInt8 *bufferPtr = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,0);
            // 3.2.获取UV分量的地址
            UInt8 *bufferPtr1 = (UInt8 *)CVPixelBufferGetBaseAddressOfPlane(imageBuffer,1);
    
            // 3.3.根据像素获取图片的真实宽度&高度
            size_t width = CVPixelBufferGetWidth(imageBuffer);
            size_t height = CVPixelBufferGetHeight(imageBuffer);
            // 获取Y分量长度
            size_t bytesrow0 = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,0);
            size_t bytesrow1  = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer,1);
            UInt8 *yuv420_data = (UInt8 *)malloc(width * height *3/2);
    
            /* convert NV12 data to YUV420*/
            // 3.4.将NV12数据转成YUV420数据
            UInt8 *pY = bufferPtr ;
            UInt8 *pUV = bufferPtr1;
            UInt8 *pU = yuv420_data + width*height;
            UInt8 *pV = pU + width*height/4;
            for(int i =0;i<height;i++)
            {
                memcpy(yuv420_data+i*width,pY+i*bytesrow0,width);
            }
            for(int j = 0;j<height/2;j++)
            {
                for(int i =0;i<width/2;i++)
                {
                    *(pU++) = pUV[i<<1];
                    *(pV++) = pUV[(i<<1) + 1];
                }
                pUV+=bytesrow1;
            }
    
            // 3.5.分别读取YUV的数据
            picture_buf = yuv420_data;
            pFrame->data[0] = picture_buf;              // Y
            pFrame->data[1] = picture_buf+ y_size;      // U
            pFrame->data[2] = picture_buf+ y_size*5/4;  // V
    
            // 4.设置当前帧
            pFrame->pts = framecnt;
            int got_picture = 0;
    
            // 4.设置宽度高度以及YUV各式
            pFrame->width = encoder_h264_frame_width;
            pFrame->height = encoder_h264_frame_height;
            pFrame->format = PIX_FMT_YUV420P;
    
            // 5.对编码前的原始数据(AVFormat)利用编码器进行编码,将 pFrame 编码后的数据传入pkt 中
            int ret = avcodec_encode_video2(pCodecCtx, &pkt, pFrame, &got_picture);
            if(ret < 0) {
                printf("Failed to encode! \n");
    
            }
    
            // 6.编码成功后写入 AVPacket 到 输入输出数据操作着 pFormatCtx 中,当然,记得释放内存
            if (got_picture==1) {
                framecnt++;
                pkt.stream_index = video_st->index;
                ret = av_write_frame(pFormatCtx, &pkt);
                av_free_packet(&pkt);
            }
    
            // 7.释放yuv数据
            free(yuv420_data);
        }
    
        CVPixelBufferUnlockBaseAddress(imageBuffer, 0);
    }
    
    
    • 释放资源
    /*
     * 释放资源
     */
    - (void)freeX264Resource
    {
        // 1.释放AVFormatContext
        int ret = flush_encoder(pFormatCtx,0);
        if (ret < 0) {
            printf("Flushing encoder failed\n");
        }
    
        // 2.将还未输出的AVPacket输出出来
        av_write_trailer(pFormatCtx);
    
        // 3.关闭资源
        if (video_st){
            avcodec_close(video_st->codec);
            av_free(pFrame);
        }
        avio_close(pFormatCtx->pb);
        avformat_free_context(pFormatCtx);
    }
    
    int flush_encoder(AVFormatContext *fmt_ctx,unsigned int stream_index)
    {
        int ret;
        int got_frame;
        AVPacket enc_pkt;
        if (!(fmt_ctx->streams[stream_index]->codec->codec->capabilities &
              CODEC_CAP_DELAY))
            return 0;
    
        while (1) {
            enc_pkt.data = NULL;
            enc_pkt.size = 0;
            av_init_packet(&enc_pkt);
            ret = avcodec_encode_video2 (fmt_ctx->streams[stream_index]->codec, &enc_pkt,
                                         NULL, &got_frame);
            av_frame_free(NULL);
            if (ret < 0)
                break;
            if (!got_frame){
                ret=0;
                break;
            }
            ret = av_write_frame(fmt_ctx, &enc_pkt);
            if (ret < 0)
                break;
        }
        return ret;
    }
    
    

    相关文章

      网友评论

        本文标题:iOS直播技术学习笔记-硬编码&软编码实现(五)

        本文链接:https://www.haomeiwen.com/subject/ukdvjxtx.html