FFmpeg学习之开发Mac播放器(六):FFmpeg与Mac编解码器混合使用

Mac和iOS支持使用VideoToolBox硬件编解码H264和H265的视频流,这次使用FFmpeg解封装使用VideoToolBox解码器解码,还有从Mac采集的数据用FFmpeg编码封装。

FFmpeg解封装+VideoToolBox解码
FFmpeg中AVPacket对应Mac中的CMBlockBufferRef

//用于解析AVCodecContext->extradata中的sps和pps
static void parseH264SequenceHeader(uint8_t * extra_data, uint8_t ** sps, size_t * sps_size, uint8_t ** pps, size_t * pps_size) {
    int spsSize = (extra_data[6] << 8) + extra_data[7];
    *sps_size = spsSize;
    *sps = &extra_data[8];
    int ppsSize = (extra_data[8 + spsSize + 1] << 8) + extra_data[8 + spsSize + 2];
    *pps = &extra_data[8 + spsSize + 3];
    *pps_size = ppsSize;
}

- (void)main {
    ...
    //初始化VideoToolBox解码器
    parseH264SequenceHeader(codec_ctx->extradata, &sps, &sps_size, &pps, &pps_size);
    uint8_t * parameterSetPointers[2] = {sps, pps};
    size_t parameterSetSizes[2] = {sps_size, pps_size};
    OSStatus status = CMVideoFormatDescriptionCreateFromH264ParameterSets(kCFAllocatorDefault, 2, (const uint8_t * const *)parameterSetPointers, parameterSetSizes, 4, &formatDescRef);
    if (status != noErr) {
        NSLog(@"Create video description failed...");
    }
    VTDecompressionOutputCallbackRecord callback;
    callback.decompressionOutputCallback = didDecompress;
    callback.decompressionOutputRefCon = (__bridge void *)self;
    NSDictionary * destinationImageBufferAttributes = @{(id)kCVPixelBufferPixelFormatTypeKey: @(kCVPixelFormatType_420YpCbCr8Planar)};
    status = VTDecompressionSessionCreate(kCFAllocatorDefault, formatDescRef, NULL, (__bridge CFDictionaryRef)destinationImageBufferAttributes, &callback, &sessionRef);
    if (status != noErr) {
        NSLog(@"Create decompression session failed status = %@", @(status));
    }
    ...
    AVPacket * pkt = av_packet_alloc();
    while (av_read_frame(format_ctx, pkt) >= 0) {
        if (pkt->stream_index == video_index) {
            CMBlockBufferRef blockBuffer = NULL;
            //使用AVPacket中的数据直接创建BlockBuffer,AVPacket中的nalu数据需要AVCC格式,如果是Annex B格式要转换成AVCC格式
            OSStatus status = CMBlockBufferCreateWithMemoryBlock(kCFAllocatorDefault, pkt->data, pkt->size, kCFAllocatorNull, NULL, 0, pkt->size, 0, &blockBuffer);
            if (status != kCMBlockBufferNoErr) {
                NSLog(@"Create BlockBuffer failed status = %@", @(status));
            }
            const size_t sampleSize = pkt->size;
            CMSampleBufferRef sampleBuffer = NULL;
            status = CMSampleBufferCreate(kCFAllocatorDefault, blockBuffer, true, NULL, NULL, formatDescRef, 1, 0, NULL, 1, &sampleSize, &sampleBuffer);
            if (status != noErr) {
                NSLog(@"SampleBuffer create failed");
            }
            
            VTDecodeFrameFlags flags = kVTDecodeFrame_EnableAsynchronousDecompression;
            VTDecodeInfoFlags flagOut;
            status = VTDecompressionSessionDecodeFrame(sessionRef, sampleBuffer, flags, &sampleBuffer, &flagOut);
            if (status == noErr) {
                VTDecompressionSessionWaitForAsynchronousFrames(sessionRef);
            }
            CFRelease(blockBuffer);
            CFRelease(sampleBuffer);
        }
    }
}
//解码回调方法
void didDecompress( void *decompressionOutputRefCon, void *sourceFrameRefCon, OSStatus status, VTDecodeInfoFlags infoFlags, CVImageBufferRef imageBuffer, CMTime presentationTimeStamp, CMTime presentationDuration ) {
  if (status == noError && imageBuffer) {
    for (int i = 0; i < CVPixelBufferGetPlaneCount(imageBuffer); i++) {
      void * baseAddress = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, i);
      size_t bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, i);
      size_t height = CVPixelBufferGetHeightOfPlane(imageBuffer, i);
      fwrite(baseAddress, bytesPerRow * height, 1, file);
      //这里直接写入yuv文件会出现问题,VideoToolBox解码得到的数据是按照DTS顺序,需要按照PTS排序然后再写入yuv文件
    }
  }
}
7D30E197-D064-4763-9D20-F734B7BBBD37.png
Mac采集+FFmpeg编码

- (void)initEncoder {
  format_ctx = avformat_alloc_context();
  if (avformat_alloc_output_context2(&format_ctx, NULL, NULL, outputString.UTF8String) < 0) {
    NSLog(@"Open output path failed");
  }
  codec = avcodec_find_encoder(AV_CODEC_ID_H264);
  codec_ctx = avcodec_alloc_context3(codec);
  codec_ctx->bit_rate = 5000000;
  codec_ctx->width = 1280;  //使用AVFoundation设置摄像头采集视频的宽和高
  codec_ctx->height = 720;
  codec_ctx->time_base = (AVRational){1, 24};  //视频为24帧
  codec_ctx->framerate = (AVRational){24, 1};
  codec_ctx->gop_size = 10;
  codec_ctx->max_b_frames = 1;
  codec_ctx->pix_fmt = AV_PIX_FMT_NV12;  //Mac和iPhone摄像头采集的pixel format为NV12
  codec_ctx->color_range = AVCOL_RANGE_JPEG;
  av_opt_set(codec_ctx->priv_data, "present", "slow", 0);

  if (format_ctx->oformat->flags & AVFMT_GLOBALHEADER) {
    codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; //填充codec_ctx中extradata
  }

  AVStream * stream = avformat_new_stream(format_ctx, NULL); //创建视频流
  ret = avcodec_parameters_from_context(stream->codecpar, codec_ctx);
  if (ret < 0) {
    NSLog(@"Failed to copy encoder parameters to output stream 0");
  }
  stream->time_base = codec_ctx->time_base;

  av_dump_format(format_ctx, 0, outputString.UTF8String, 1);
  if (!(format_ctx->oformat->flags & AVFMT_NOFILE)) {
    ret = avio_open(&format_ctx->pb, outputString.UTF8String, AVIO_FLAG_WRITE);
    if (ret < 0) {
      NSLog(@"Could not open output file");
    }
  }
    
  ret = avformat_write_header(format_ctx, NULL);
  if (ret < 0) {
    NSLog(@"Error occurred when opening output file");
  }
}
//摄像头采集回调方法
- (void)captureOutput:(AVCaptureOutput *)output didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
  CVImageBufferRef imageBuffer = CMSampleBufferGetImageBuffer(sampleBuffer);
  CMTime duration = CMSampleBufferGetDuration(sampleBuffer);
  
  AVFrame * frame = av_frame_alloc(); //创建AVFrame存储像素数据
  frame->height = (int)CVPixelBufferGetHeight(imageBuffer);
  frame->width = (int)CVPixelBufferGetWidth(imageBuffer);
  frame->format = AV_PIX_FMT_NV12;
  frame->color_range = AVCOL_RANGE_JPEG;
  av_frame_get_buffer(frame, 0); //为frame中存储像素数据的data分配空间,调用这个方法之前要设置pixel format(视频)或者sample format(音频),视频的宽高,音频的nb_samples和channel_layout

  CVPixelBufferLockBaseAddress(imageBuffer, 0);
  void * baseAddress = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 0);
  size_t bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 0);
  size_t height = CVPixelBufferGetHeightOfPlane(imageBuffer, 0);
  frame->linesize[0] = (int)bytesPerRow;
  memcpy(frame->data[0], baseAddress, bytesPerRow * height);
    
  baseAddress = CVPixelBufferGetBaseAddressOfPlane(imageBuffer, 1);
  bytesPerRow = CVPixelBufferGetBytesPerRowOfPlane(imageBuffer, 1);
  height = CVPixelBufferGetHeightOfPlane(imageBuffer, 1);
  frame->linesize[1] = (int)bytesPerRow;
  memcpy(frame->data[1], baseAddress, bytesPerRow * height);
  CVPixelBufferUnlockBaseAddress(imageBuffer, 0);

  int ret = avcodec_send_frame(codec_ctx, frame);
  AVPacket * pkt = av_packet_alloc();
  while (ret >= 0) {
    ret = avcodec_receive_packet(codec_ctx, pkt);
    if (ret == AVERROR(EAGAIN)) {
      NSLog(@"Output is not available in the current state");
      break;
    } else if (ret == AVERROR_EOF) {
      NSLog(@"The encoder has been fully flushed, and there will be no more output packets");
      break;
    } else if (ret < 0) {
      NSLog(@"Error during encoding");
      break;
    }
        
    pkt->stream_index = 0;
    pkt->pts = _pts;
    pkt->dts = pkt->pts;
    pkt->duration = duration.value;
        
    _pts++;
        
    av_packet_rescale_ts(pkt, codec_ctx->time_base, format_ctx->streams[0]->time_base);
    if (av_write_frame(format_ctx, pkt) >= 0) {
      NSLog(@"Write success");
    }
  }
  av_packet_free(&pkt);
  av_frame_free(&frame);
}

- (void)EndRecord {
  [_captureSession stopRunning];

  int ret = avcodec_send_frame(codec_ctx, NULL); //flush data
  AVPacket * pkt = av_packet_alloc();
  while (ret >= 0) {
    ret = avcodec_receive_packet(codec_ctx, pkt);
    if (ret == AVERROR(EAGAIN)) {
      NSLog(@"Output is not available in the current state");
      break;
    } else if (ret == AVERROR_EOF) {
      NSLog(@"The encoder has been fully flushed, and there will be no more output packets");
      break;
    } else if (ret < 0) {
      NSLog(@"Error during encoding");
      break;
    }
    pkt->stream_index = 0;
    pkt->pts = _pts;
    pkt->dts = pkt->pts;
    pkt->duration = duration.value;
    av_packet_rescale_ts(pkt, codec_ctx->time_base, format_ctx->streams[0]->time_base);
    if (av_write_frame(format_ctx, pkt) >= 0) {
      NSLog(@"Write success");
    }
    _pts++;
  }
  avcodec_close(codec_ctx);
  av_write_trailer(format_ctx);
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,290评论 6 491
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,107评论 2 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 156,872评论 0 347
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,415评论 1 283
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,453评论 6 385
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,784评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,927评论 3 406
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,691评论 0 266
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,137评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,472评论 2 326
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,622评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,289评论 4 329
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,887评论 3 312
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,741评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,977评论 1 265
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,316评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,490评论 2 348