iOS-视屏编码

编码方式

  • 在iOS中编码方式有两种

    • 硬编码: 在iOS8.0之后,使用原生框架VideoToolBox&AudioToolbox对视屏和音频进行硬编码.
    • 软编码: 使用CPU进行编码,通常使用的框架为ffmpeg+x264.
      • ffmpeg:是一套开源的框架, 用于对音视频进行编码&解码&转化计算机程序
      • x264x264是一种免费的、开源的、具有更优秀算法的H.264/MPEG-4 AVC视频压缩编码方式.
  • 编码方式对比:

    • 硬编码性能高于软编码,对CPU无要求和压力,对硬件要求较高(如GPU
    • 软编码对CPU的负载较高,容易造成手机发热,但其实现简单,编码自由度高(可以自由调整想要的参数)

硬编码

编码流程:采集视屏信息--> 获取到视频帧--> 对视频帧进行编码 --> 获取到视频帧信息 --> 将编码后的数据以NALU方式写入到文件

  • 采集视屏信息

    - (void)startCapture:(UIView *)preview{
       // 1.创建捕捉会话
       AVCaptureSession *session = [[AVCaptureSession alloc] init];
       session.sessionPreset = AVCaptureSessionPreset1280x720;
       self.captureSession = session;
       
       // 2.设置输入设备
       AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
       NSError *error = nil;
       AVCaptureDeviceInput *input = [[AVCaptureDeviceInput alloc] initWithDevice:device error:&error];
       [session addInput:input];
       
       // 3.添加输出设备
       AVCaptureVideoDataOutput *output = [[AVCaptureVideoDataOutput alloc] init];
       self.captureQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
       [output setSampleBufferDelegate:self queue:self.captureQueue];
       [session addOutput:output];
       
       // 设置录制视频的方向
       AVCaptureConnection *connection = [output connectionWithMediaType:AVMediaTypeVideo];
       [connection setVideoOrientation:AVCaptureVideoOrientationPortrait];
       
       // 4.添加预览图层
       AVCaptureVideoPreviewLayer *previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
       previewLayer.frame = preview.bounds;
       [preview.layer insertSublayer:previewLayer atIndex:0];
       self.previewLayer = previewLayer;
       
       // 5.开始捕捉
       [self.captureSession startRunning];
    }
    
  • 获取到视屏帧

     - (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
        // 对sampleBuffer进行硬编码处理
       [self.encoder encodeSampleBuffer:sampleBuffer];
    }   
    
  • 对视屏帧进行硬编码
    (1). 初始化写文件对象,用于保存编码完的视屏信息

    - (void)setupFileHandle {
        // 1.获取沙盒路径
        NSString *file = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"video.h264"];
        
        // 2.如果原来有文件,则删除
        [[NSFileManager defaultManager] removeItemAtPath:file error:nil];
        [[NSFileManager defaultManager] createFileAtPath:file contents:nil attributes:nil];
        
        // 3.创建对象
        self.fileHandle = [NSFileHandle fileHandleForWritingAtPath:file];
    }
    

    (2). 初始化编码会话

    通过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);
      }
    

    (3). 开始编码

      - (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");
       }
    }
    

    (4). 编码回调,对视屏做真正的编码处理

    1. 编码成功后会回调之前输入的函数didCompressH264.
    • 1> 先判断是否是关键帧:

    如果是关键帧,则需要在写入关键帧之前,先写入PPS、SPS的NALU
    取出PPS、SPS数据,并且封装成NALU单元写入文件

    • 2> 将I帧、P帧、B帧分别封装成NALU单元写入文件
    1. 写入后,数据存储方式:


      image8.png
    // 编码完成回调
    void didCompressH264(void *outputCallbackRefCon, void *sourceFrameRefCon, OSStatus status, VTEncodeInfoFlags infoFlags, CMSampleBufferRef sampleBuffer) {
    
    // 1.判断状态是否等于没有错误
    if (status != noErr) {
        return;
    }
    
    // 2.根据传入的参数获取对象
    VideoEncode* encoder = (__bridge VideoEncode*)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;
        }
      }
    }
    

    (5). 码流存储

     // sps和pps存储
     - (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];
             }
     }
    
  • 结束编码

    - (void)endEncode{
      VTCompressionSessionCompleteFrames(self.compressionSession, kCMTimeInvalid);
       VTCompressionSessionInvalidate(self.compressionSession);
       CFRelease(self.compressionSession);
       self.compressionSession = NULL;
    }
    

软编码

安装 FFmpeg

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

编译FFmpeg-iOS

  • 下载编译FFmpeg所需要的脚本文件gas-preprocessor.pl

如果/usr/local没有权限Operation not permitted
按如下操作:

1、关闭 Rootless

  • 重启MAC电脑
  • 开机时(听到开机启动声音),按下Command+RCommand+Option+R, 进入恢复模式
  • 在上面的菜单实用工具中找到并打开Terminal终端, 输入如下命令$ csrutil disable
  • 重启电脑.

2、给/usr/local增加读写权限

  • 进入终端输入$ sudo chown -R $(whoami) /usr/local

3、开启 Rootless

  • 为了系统安全,重新开启Rootless
  • 重复步骤1,开启Rootless: $ csrutil enable
  • 下载脚本FFmpeg-iOS脚本

    编译过程中有可能需要安装yasmnasm,如果需要,直接安装即可
    $ brew install yasm$ brew install nasm

编译X264

集成FFmpeg

  • 将执行脚本./build-ffmpeg.sh./build-x264.sh生成的文件FFmpeg-iOSx264-iOS拖入到工程中。

  • 添加依赖库:VideoToolbox.frameworkCoreMedia.frameworkAVFoundation.frameworklibiconv.tbdlibbz2.tbdlibz.tbd

  • 采集视屏信息

- (void)startCapture:(UIView *)preview
{
    // 1.获取沙盒路径
    NSString *file = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByAppendingPathComponent:@"video.h264"];
    
    // 2.开始编码
    [self.encoder setFileSavedPath:file];
    // 特别注意: 宽度&高度
    [self.encoder setX264ResourceWithVideoWidth:480 height:640 bitrate:1500000];
    
    // 1.创建捕捉会话
    AVCaptureSession *session = [[AVCaptureSession alloc] init];
    session.sessionPreset = AVCaptureSessionPreset640x480;
    self.captureSession = session;
    
    // 2.设置输入设备
    AVCaptureDevice *device = [AVCaptureDevice defaultDeviceWithMediaType:AVMediaTypeVideo];
    NSError *error = nil;
    AVCaptureDeviceInput *input = [[AVCaptureDeviceInput alloc] initWithDevice:device error:&error];
    [session addInput:input];
    
    
    // 3.添加预览图层
    AVCaptureVideoPreviewLayer *previewLayer = [[AVCaptureVideoPreviewLayer alloc] initWithSession:session];
    previewLayer.frame = preview.bounds;
    [preview.layer insertSublayer:previewLayer atIndex:0];
    self.previewLayer = previewLayer;
    
    // 4.添加输出设备
    AVCaptureVideoDataOutput *output = [[AVCaptureVideoDataOutput alloc] init];
    self.captureQueue = dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0);
    [output setSampleBufferDelegate:self queue:self.captureQueue];
    NSDictionary *settings = [[NSDictionary alloc] initWithObjectsAndKeys:
                              [NSNumber numberWithUnsignedInt:kCVPixelFormatType_420YpCbCr8BiPlanarFullRange],
                              kCVPixelBufferPixelFormatTypeKey,
                              nil];
    
    output.videoSettings = settings;
    output.alwaysDiscardsLateVideoFrames = YES;
    [session addOutput:output];
    
    // 5.设置录制视频的方向
    AVCaptureConnection *connection = [output connectionWithMediaType:AVMediaTypeVideo];
    [connection setVideoOrientation:previewLayer.connection.videoOrientation];
    
    // 6.开始捕捉
    [self.captureSession startRunning];
}
  • 获取到视屏帧
- (void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
    [self.encoder encoderToH264:sampleBuffer];
}
  • 对视屏帧进行软编码
    (1). 初始化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 = AV_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;
    }   
    

    (2). 对视屏帧进行编码

    - (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);
        
        // 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 = AV_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);
    }
    

    (3). 结束采集后,对资源释放

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

推荐阅读更多精彩内容