专栏首页进击的多媒体开发FFmpeg 实现视频 封装 与 解封装

FFmpeg 实现视频 封装 与 解封装

FFmpeg 封装实现

本例子实现的是将视频数据和音频数据,按照一定的格式封装为特定的容器,比如FLV、MKV、MP4、AVI等等。

实现的过程,可以大致用如下图表示:

从图中可以大致看出视频封装的流程:

  • 首先要有编码好的视频、音频数据。
  • 其次要根据想要封装的格式选择特定的封装器。
  • 最后利用封装器进行封装。

根据流程可以推倒出大致的代码实现:

  • 利用给定的YUV数据编码得到某种 CODEC 格式的编码视频(可以参见上面提到的编码实现),同样的方法得到音频数据。
  • 获取输出文件格式。获取输出文件格式可以直接指定文件格式,比如FLV/MKV/MP4/AVI等,也可以通过输出文件的后缀名来确定,或者也可以选择默认的输出格式。根据得到的文件格式,其中可能有视频、音频等,为此我们需要为格式添加视频、音频、并对格式中的一些信息进行设置(比如头)。
  • 利用设置好的音频、视频、头信息等,开始封装。

对于由 YUV 数据得到编码的视频数据部分,不再重复。

直接看与 Muxer 相关的部分,与特定的 Muxer 相关的信息,FFMpeg 提供了一个 AVFormatContext 的结构体描述,并用avformat_alloc_output_context2()函数来分配它。

该函数的声明如下:

int avformat_alloc_output_context2(AVFormatContext **ctx, AVOutputFormat *oformat,
                                   const char *format_name, const char *filename);

其中:

  • ctx:输出到 AVFormatContext 结构的指针,如果函数失败则返回给该指针为 NULL。
  • oformat:指定输出的 AVOutputFormat 类型,如果设为 NULL,则根据 format_name 和 filename 生成。
  • format_name:输出格式的名称,如果设为 NULL,则使用 filename 默认格式。
  • filename:目标文件名,如果不使用,可以设为 NULL。
  • 返回值:>=0 则成功,否则失败。

代码如下:

AVOutputFormat *fmt;
AVFormatContext *oc;

/* allocate the output media context */
avformat_alloc_output_context2(&oc, NULL, NULL, filename);
if (!oc) {
    printf("Could not deduce output format from file extension: using MPEG.\n");
    avformat_alloc_output_context2(&oc, NULL, "mpeg", filename);
}
if (!oc)
    return 1;

fmt = oc->oformat;

有了表示媒体文件格式的 AVFormatContext 结构后,就需要根据媒体格式来判断是否需要往媒体文件中添加视频流、音频流(有的媒体文件,这两种流并不是必须的);

以 MP4 格式的媒体文件为例,我们需要一路视频流、一路音频流。

因此需要创建一路流,FFMpeg 提供的创建流的函数为avformat_new_stream(),该函数完成向 AVFormatContext 结构体中所代码的媒体文件中添加数据流,函数声明如下:

AVStream *avformat_new_stream(AVFormatContext *s, const AVCodec *c);

其中:

  • s:AVFormatContext 结构,表示要封装生成的视频文件。
  • c:视频或音频流的编码器的指针。
  • 返回值:指向生成的 stream 对象的指针;失败则返回 NULL。

注意:对于 Muxer,该函数必须在调用avformat_write_header()前调用。

使用完成后,需要调用avcodec_close()和avformat_free_context()来清理由它分配的内容。

该函数调用完成后,一个新的 AVStream 便已经加入到输出文件中,下面就需要设置 stream 的 id 和 codec 等参数。

以视频流为例,代码如下:

OutputStream *ost;
AVFormatContext *oc;
AVCodec **codec;
AVCodecContext *c;
AVStream *st;

st = avformat_new_stream(oc, *codec);
if(!st){
    fprintf(stderr, "Could not allocate stream\n");
    exit(1);
}
st->id = oc->nb_streams-1;
c = st->codec;

参数设置完成后,就可以打开编码器并为编码器分配必要的内存。

步骤跟之前的类似,以视频为例,示例代码如下:

//open the codec
ret = avcodec_open(c, codec, &opt);
if(ret < 0){
    fprintf(stderr, "Could not open video codec: %s\n", av_err2str(ret));
    exit(1);
}
//allocate and init a re-usable frame
ost->frame = alloc_picture(c->pix_fmt, c->width, c->height);

接下来进行真正的封装:首先,为媒体文件添加头部信息,FFMpeg 为此提供的函数为avformat_write_header()。

其次,将编码好的音视频 AVPacket 包添加到媒体文件中去,FFMpeg 为此提供的函数为av_interleaved_write_frame()。

最后,写入文件尾的数据,FFMpeg 为此提供的函数为av_write_trailer()。

封装的大致流程已经完成了,剩余的是一些收尾工作,比如释放分配的内存、结构体等等。

FFMpeg 解封装实现

本例子实现的是将音视频分离,例如将封装格式为 FLV、MKV、MP4、AVI 等封装格式的文件,将音频、视频分离开来。

实现的过程,可以大致用如下图表示:

从图中可以看出大致的节封装流程:

  • 首先要对解复用器进行初始化。
  • 其次将输入的封装格式文件给到解复用器内。
  • 最后利用解封装对 Container 进行解封装。

根据流程可以推到出大致的代码流程:

  • 首先对输入文件(Container 文件)、输出文件(Video/Audio 进行处理),方便后面的使用;
  • 其次打开输入文件,并分配 Format Context,从输入文件中得到流信息
  • 之后打开视频、音频编码器 Context,针对视频数据,分配图像 image。
  • 分配 frame 结构,初始化 packet,从输入文件中读取 frame 信息,并之后进行解码 packet。
  • 最后释放各种分配的数据信息。

在音视频分离后,需要将分离出的音视频分别放到不同的输出文件中,为此,需要打开文件以备后用。

static const char *video_dst_filename = NULL;
static const char *audio_dst_filename = NULL;
static FILE *video_dst_file = NULL;
static FILE *audio_dst_file = NULL;
video_dst_filename = argv[2];
audio_dst_filename = argv[3];
video_dst_file = fopen(video_dst_filename, "wb+");
audio_dst_file = fopen(audio_dst_filename, "wb+");

对于给定的需要 AV 分离的输入文件,使用avformat_open_input打开输入文件,并分配AVFormatContext结构。

该函数的声明如下:

int avformat_open_input(AVFormatContext **ps, const char *filename, AVInputFormat *fmt, AVDictionary **options);

其中:

  • ps:指向由用户提供的AVFormatContext结构体,该结构体通过avformat_alloc_context分配,如果它是一个 NULL,该结构在此函数内分配并负值给 ps。
  • filename:指向需要打开的流的名称。
  • fmt:如果是 non-NULL,该参数指定输入的文件格式,否则输入文件的格式自动根据文件本身自动获取。
  • options:此处可以为 NULL。
  • 返回值:成功返回0,否则返回 AVERROR。

实现代码如下:

//open input file, and allocate format context
if(avformat_open_input(&fmt_ctx, src_filename, NULL, NULL) < 0){
    fprintf(stderr, "Could not open source file %s\n", src_filename);
    exit(1);
}

//retrive stream information
if(avformat_find_stream_info(fmt_ctx, NULL) < 0){
    fprintf(stderr, "Could not find stream information\n");
    exit(1);
}

通过输入文件分配好AVFormatContext后,需要找到里面的音频流和视频流,此处需要用到的函数为av_find_best_stream;

之后要根据找到的不同的流(如H264流、HEVC流等)找到特定的编解码器,此处使用avcodec_find_decoder;

找到了解码器后, 就需要打开解码器,此处使用avcodec_open2函数完成。

下面分别介绍这几个函数的使用:

av_find_best_stream函数定义如下:

int av_find_best_stream(AVFormatContext *ic, enum AVMediaType type, int wanted_stream_nb, int related_stream, AVCodec **decoder_ret, int flags);

其中:

  • ic:媒体文件句柄。
  • type:媒体类型,视频、音频、文本等。
  • wanted_stream_nb:用户请求的流,-1 代表自动选择。
  • related_stream:尝试找到相关流,如果没有就设为-1。
  • decoder_ret:如果是non-NULL,返回选定的流的解码器。
  • flags:此处定位0。
  • 返回值:成功返回非负值,如果找不到指定的请求类型的流,就返回AVERROR_STREAM_NOT_FOUND;如果找到了流,但没找到对应的解码器,就返回AVERROR_DECODER_NOT_FOUND。

avcodec_find_decoder函数定义如下:

AVCodec *avcodec_find_decoder(enum AVCodecID id);

该函数参数为AVCodecID指定了请求的解码器,成功返回解码器,否则返回 NULL。

avcodec_open2函数定义如下:

int avcodec_open2(AVCodecContext *avctx, const AVCodec *codec, AVDictionary **options);

其中:

  • avctx:即将初始化的AVCodecContext结构体。
  • codec:打开的解码器,如果它是non-NULL codec,并在之前传递给了avcodec_alloc_context3或avcodec_get_context_defaults3,该参数必须为 NULL 或之前传递的 CODEC。
  • Options:此处我们设置为 NULL。
  • 返回值:成功返回0,出错返回一个负值。

该函数的主要作用是根据给定的AVCodec初始化AVCodecContext,在使用该函数之前,待初始化的AVCodecContext结构需要先使用avcodec_alloc_context3分配好。

其中的参数 AVCodec可以通过avcodec_find_decoder_by_nameavcodec_find_encoder_by_nameavcodec_find_decoder或avcodec_find_endcoder来获取。

在进行真正的解码之前,必须调用该函数。

下面给出使用的示例:

avcodec_register_all();
av_dict_set(&opts, "b", "2.5M", 0);
codec = avcodec_find_decoder(AV_CODEC_ID_H264);
if(!codec)
    exit(1);

context = avcodec_alloc_context3(codec);
if(avcodec_open2(context, codec, opts) < 0)
    exit(1);

对于上面分析的部分,我们将其封装在一个函数里,代码如下:

static int open_codec_context(int *stream_idx,
                              AVFormatContext *fmt_ctx,
                              enum AVMediaType type)
{
    int ret, stream_index;
    AVStream *pStream;
    AVCodecContext *codec_ctx = NULL;
    AVCodec *codec;

    ret = av_find_best_stream(fmt_ctx, type, -1, -1, NULL, 0);
    if(ret < 0){
        fprintf(stderr, "Could not find %s stream in input file '%s'\n",
                av_get_media_type_string(type), src_filename);
    }else{
        stream_index = ret;
        pStream = fmt_ctx->streams[stream_index];

        //find decoder for the stream
        codec_ctx = pStream->codec;
        codec = avcodec_find_decoder(codec_ctx->codec_id);
        if(!codec){
            fprintf(stderr, "Failed to find %s codec\n",
                    av_get_media_type_string(type));
            return AVERROR(EINVAL);
        }

        //open the decoder
        if((ret = avcodec_open2(codec_ctx, codec, NULL))< 0){
            fprintf(stderr, "Failed to open %s codec\n",
                    av_get_media_type_string(type));
            return ret;
        }
    }
    *stream_idx = stream_index;
}

针对音频、视频,分别调用该函数,示例代码如下:

    if(open_codec_context(&video_stream_idx, fmt_ctx, AVMEDIA_TYPE_VIDEO) >= 0){
        video_stream    = fmt_ctx->streams[video_stream_idx];
        video_codec_ctx = video_stream->codec;

        //allocate image where the decoded image will be put
        width   = video_codec_ctx->width;
        height  = video_codec_ctx->height;
        pix_fmt = video_codec_ctx->pix_fmt;
        ret = av_image_alloc(video_dst_data, video_dst_linesize,
                             width, height, pix_fmt, 1);
        if(ret < 0){
            fprintf(stderr, "Could not allocate raw video buffer\n");
            exit(1);
        }
        video_dst_bufsize = ret;
    }

    if(open_codec_context(&audio_stream_idx, fmt_ctx, AVMEDIA_TYPE_AUDIO) >= 0){
        audio_stream = fmt_ctx->streams[audio_stream_idx];
        audio_codec_ctx = audio_stream->codec;
    }

上面的一些准备工作完成后,就需要从输入文件中一帧一帧读取数据,并进行解码了。

从这里可以看出,需要找到一个 一帧视频存放的地方,为此需要使用av_init_packet初始化一个AVPacket。

之后就可以使用av_read_frame来从输入 文件中读取一个 frame。

示例代码如下:

static int decode_packet(int *got_frame, int cached)
{
    int ret = 0;
    int decoded = pkt.size;
    *got_frame = 0;

    if(pkt.stream_index == video_stream_idx){
        //decode video frame
        ret = avcodec_decode_video2(video_codec_ctx, frame, got_frame, &pkt);
        if(ret < 0){
            fprintf(stderr, "Error decoding video frame (%s) \n",
                    av_err2str(ret));
            return ret;
        }

        printf("num %d got_frame %d\n", num++, *got_frame);
        if(*got_frame){
            av_image_copy(video_dst_data, video_dst_linesize,
                          (const uint8_t **)(frame->data), frame->linesize,
                          pix_fmt, width, height);

            //write to raw video file
            fwrite(video_dst_data[0], 1, video_dst_bufsize, video_dst_file);
        }
    }else if(pkt.stream_index == audio_stream_idx){
        //decode audio frame
        ret = avcodec_decode_audio4(audio_codec_ctx, frame, got_frame, &pkt);
        if(ret < 0){
            fprintf(stderr, "Error decoding audio frame (%s)\n", av_err2str(ret));
            return ret;
        }

        if(*got_frame){
            size_t unpadded_linesize = frame->nb_samples * av_get_bytes_per_sample(frame->format);
            fwrite(frame->extended_data[0], 1, unpadded_linesize, audio_dst_file);
        }
    }

    return FFMIN(ret, pkt.size);
}

//allocate frame 
frame = av_frame_alloc();
if(!frame){
    fprintf(stderr, "Could not allocate frame\n");
    exit(1);
}

av_init_packet(&pkt);
pkt.data = NULL;
pkt.size = 0;

//read frames from the file
int got_frame;
while(av_read_frame(fmt_ctx, &pkt) >= 0){
    AVPacket orig_pkt = pkt;

    do{
        ret = decode_packet(&got_frame, 0);
        if(ret < 0)
            break;
        pkt.data += ret;
        pkt.size -= ret;
    }while(pkt.size > 0);
    av_free_packet(&orig_pkt);
}

解封装大致流程已经完成了,剩余的是一些收尾工作,例如释放刚刚分配的内存等。

作者:赖人李冰

http://lazybing.github.io/blog/2017/01/01/ffmpeg-sdk-learning/

本文分享自微信公众号 - 音视频开发进阶(glumes_blog)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2020-07-23

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Gradle初探

    task:构建脚本定义一个任务,并取名。doLast:构建脚本给任务添加一个动作Action,在task执行完毕后,要回调doLast这部分闭包的业务代码逻辑或...

    glumes
  • 【每周一记-002】

    话说工作至今快四年了,从实习开始就一直用 Mac 开发,见证了办公设备从信仰灯到 TouchBar 的升级换代。

    glumes
  • 在 iOS 上用 Shader 实现 图片 转 字符画 效果~~

    那天在朋友圈问了一下如何通过 OpenGL Shader 实现同样效果,没想到引来了大神的关注。

    glumes
  • MADlib——基于SQL的数据挖掘解决方案(16)——回归之弹性网络回归

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.n...

    用户1148526
  • Android Gradle实用技巧(三) | 批量控制生成的APK文件名

    在讲批量修改APK文件名之前,我们讲一下关于Android Gradle插件集成的Task任务问题。普通的Java工程比较简单,因为它有一个有限的任务集合,而且...

    飞雪无情
  • [答题赛21轮]

     B) 观察某位退休但仍有影响力的官员坐火车回家过春节时随从和铁路部门人员的行为。

    用户6288414
  • 祥云无界联合国际公益链基金会爱心捐赠活动走进兴山县黄粮小学

    2018年6月1日上午,祥云无界部分员工和国际公益链基金会员一行30余人在湖北祥云无界电子商务有限公司创始人&CEO熊刚先生的带领下,来到湖北省宜昌市兴山县黄粮...

    D13
  • Merit价值与成果管理系统——2、首页以及按时段统计

    版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/hotqin888/article/det...

    hotqin888
  • TPatch动态补丁系统(iOS)

    对于每一个开发,从写Hello World开始,到使用各种语言,可能都会遇到各种BUG。有的BUG能快速解决,比如Web侧的,发个JS或者Html即可。但是在终...

    QQ空间开发团队
  • 【Python】五分钟画一条动态心形曲线~

    网上有很多关于心形曲线的资料,各种各样的形状,但是我比较倾心于桃心形。先在Matlab里简单看一下是什么样的。

    树枝990

扫码关注云+社区

领取腾讯云代金券