五、IOS-FFmpeg解封装、解码、编码、封装

结合前面的知识,这次把FFmpeg的解封装、解码、编码和封装整合到一块。
代码仓库:https://github.com/wulang150/FFmpegTest.git
代码文件:MuxVideoViewController.m

一、解封装(MP4-->H264)

跟之前讨论的差不多,就是从MP4文件中解出:
1、视频流H264
2、音频流AAC
都存于AVPacket结构体中。

二、解码(H264-->YUV)

这里跟前面讲的不太一样。前面讲的是基于Annex-B格式的H264流的解码。这次是从Mp4解封装得到了,所以是AVCC格式的H264。
它们区别有两点:一个是参数集(SPS, PPS)组织格式,一个是分隔。

  • Annex-B:使用start code分隔NAL(start code为三字节或四字节,0x000001或0x00000001,一般是四字节);SPS和PPS按流的方式写在头部。
  • AVCC:使用NALU长度(固定字节,通常为4字节)分隔NAL;在头部包含extradata(或sequence header)的结构体。

所以,对于Annex-B格式,因为有特定的分隔符,那么不需要其他信息,我就可以得到每个NAL,包括SPS和PPS等信息。但对于AVCC格式的H264流,如果不借助其他信息,我们根本无法得到每个NAL(因为存储NAL的长度不一定是4个字节)、SPS和PPS等信息。这时候就必须借助extradata头部信息。
下面是H264的extradata格式:


屏幕快照 2019-06-01 下午2.34.34.png

其中有几个比较重要的信息:
1、lengthSizeMinusOne:代表用几个字节来存NAL的长度。计算方法是 1 + (lengthSizeMinusOne & 3)
2、sequenceParameterSetNALUnits:SPS信息
3、pictureParameterSetNALUnits:PPS信息

所以,要解码AVCC格式的H264,就必须拿到extradata。去哪里拿呢?
方法一:直接通过in_stream->codecpar的拷贝给解码上下文de_CodecCtx,里面包含了extradata信息。简单粗暴。

avcodec_parameters_to_context(de_CodecCtx, in_stream->codecpar)

方法二:你也可以只拷贝extradata信息

AVStream *in_stream = ifmt_ctx->streams[in_stream_video];
de_CodecCtx->pix_fmt = STREAM_PIX_FMT;
//这个extradata才是最重要的,从这里才可以拿到解码相关的信息,其实就是怎么拿到每一个nal
de_CodecCtx->extradata_size = in_stream->codecpar->extradata_size;
de_CodecCtx->extradata = malloc(de_CodecCtx->extradata_size);
memcpy(de_CodecCtx->extradata, in_stream->codecpar->extradata, de_CodecCtx->extradata_size);

后面解码的代码跟之前差不多。我修改了timeBase,为了使AVCodeCtx里面的timeBase不同于AVStream里面的,当然不修改也是可以的。修改只是显示更直观,方面后面的操作。

//修改解码后的参数,转换为我们常见的pts是0 1 2,ctb
de_frame->pts = av_rescale_q(de_frame->pts, ifmt_ctx->streams[in_stream_video]->time_base, Base_TB);
de_frame->pkt_duration = av_rescale_q(de_frame->pkt_duration, ifmt_ctx->streams[in_stream_video]->time_base, Base_TB);
de_frame->pkt_dts = av_rescale_q(de_frame->pkt_dts, ifmt_ctx->streams[in_stream_video]->time_base, Base_TB);

三、编码(YUV-->H264)

上面的解码好像不需要配置什么参数,比如帧率、width、height,就可以解码成功了。为什么呢?编码其实就是压缩,压缩后的文件,解压后都只对应一种原文件。不会因为配置某些参数,就可以解压出更佳的文件,所以配置参数也没用。比如说我有一个200MB的低清Mp4文件,难道我配置某个参数就可以得到高清的原文件了?不可能。
但是,编码就不一样,我可以配置参数,比如压缩率10%、20%,这些我是可选择的。

en_CodecCtx->bit_rate = in_stream->codecpar->bit_rate;
/* resolution must be a multiple of two */
en_CodecCtx->width = width;
en_CodecCtx->height = height;
/* frames per second */
en_CodecCtx->time_base = Base_TB;

1、配置了width、height。对应YUV的分辨率。那我可以不按YUV的分辨率配置吗?
可以的,只是每一帧都不完整。比如,原分辨率:960x540,如果我设置为一半:480x270,那么每一帧只有原来的1/4。你喜欢这样,也是可以的。
2、那么,我可以改变分辨率吗?
可以的,需进行转分辨率。对应里面scaleVideo函数的内容:

//创建转换器
if(!sws_scale_ctx){
    sws_scale_ctx = sws_getContext(src_w, src_h, frame->format,
                                       dst_w, dst_h, frame->format,
                                       SWS_BILINEAR, NULL, NULL, NULL);
}
//进行转换
sws_scale(sws_scale_ctx, (const uint8_t * const*)frame->data,
              frame->linesize, 0, src_h, dstFrame->data, dstFrame->linesize);

3、还有就是配置了码流和timeBase。为了控制编码后的大小,前面有解释过。

四、封装(H264-->MP4)

1、配置对应的AVCodecContext,可以使用上面编码使用的AVCodecContext,但注意的是,必须在avcodec_open2前加入:

//mp4一定要配这个,并且得在avcodec_open2前添加,要不然,就只有黑屏
/* Some formats want stream headers to be separate. */
if (ofmt_ctx->oformat->flags & AVFMT_GLOBALHEADER)
      muxEnCodecCtx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;

要不然就只有黑屏。

2、创建视频流AVStream
创建AVStream后,需要配置它time_base和codecpar参数,具体就需要用到前面提到的AVCodecContext了:

videoStream->time_base = codecCtx->time_base;
avcodec_parameters_from_context(videoStream->codecpar, codecCtx);

3、写入头部信息:

avformat_write_header(ofmt_ctx, NULL);

执行这个之后,videoStream->time_base会自动修改为正常的值,小于15360,都会改为15360。

4、写入具体的pkt
在写入pkt之前,需进行timebase的转换,因为前面进行了转换。如果前面没进行转换,这里就不需要转换:

av_packet_rescale_ts(pkt, muxEnCodecCtx->time_base, videoStream->time_base);

videoStream->time_base很重要,控制视频总时间,播放速度,就靠它了。
最后,再写入pkt

av_interleaved_write_frame(ofmt_ctx, pkt);

五、后话

(一)前面我进行了timebase的转换,后面我想修改帧率就比较直观了。
1、比如设置帧率为15:
time_base = (AVRational){1, 15};
2、以两倍速率播放
time_base = (AVRational){1, 30};

(二)如果你需要合成mp4,需要自己构建extraData的时候。分两种情况。
1、如果流是Annex-B,那么就这样构建extraData

        extradata[0]=0x0;
        extradata[1]=0x0;
        extradata[2]=0x0;
        extradata[3]=0x01;
        memcpy(extradata+4, m_Sps, m_spsLen);
        endIndex = m_spsLen+4;
        
        extradata[endIndex + 0]=0x0;
        extradata[endIndex + 1]=0x0;
        extradata[endIndex + 2]=0x0;
        extradata[endIndex + 3]=0x01;
        memcpy(extradata+endIndex + 4, m_Pps, m_ppsLen);
        endIndex = endIndex + m_ppsLen + 4;

    formatSt->codec->extradata=extradata;
    formatSt->codec->extradata_size=endIndex;

2、如果流是AVCC,就得按标准格式构建

// Extradata contains PPS & SPS for AVCC format
        int extradata_len = 8 + spsFrameLen - 4 + 1 + 2 + ppsFrameLen - 4;
        c->extradata = (uint8_t*) av_mallocz(extradata_len);
        c->extradata_size = extradata_len;
        c->extradata[0] = 0x01;
        c->extradata[1] = spsFrame[4 + 1];
        c->extradata[2] = spsFrame[4 + 2];
        c->extradata[3] = spsFrame[4 + 3];
        c->extradata[4] = 0xFC | 3;
        c->extradata[5] = 0xE0 | 1;
        int tmp = spsFrameLen - 4;
        c->extradata[6] = (tmp >> 8) & 0x00ff;
        c->extradata[7] = tmp & 0x00ff;
        int i = 0;
        for (i = 0; i < tmp; i++)
            c->extradata[8 + i] = spsFrame[4 + i];
        c->extradata[8 + tmp] = 0x01;
        int tmp2 = ppsFrameLen - 4;
        c->extradata[8 + tmp + 1] = (tmp2 >> 8) & 0x00ff;
        c->extradata[8 + tmp + 2] = tmp2 & 0x00ff;
        for (i = 0; i < tmp2; i++)
            c->extradata[8 + tmp + 3 + i] = ppsFrame[4 + i];

构造AVCC的extradata还是比较麻烦的,有一种曲线救国的方法,先构造Annex-B格式的extradata,再通过ffmpeg自带的转换函数,转成AVCC的extradata:

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

推荐阅读更多精彩内容