ffmpeg6.0从demux_decode.c源码探索解封装流程

在播放器的播放视频、音视频媒体文件的推流等实际应用中,解封装(demux)这个操作是不可避免的,也是最基础的操作。

拿播放器播放MP4来说,如果想实现播放器视频画面的播放和音频声音的播放都需要经过这个解封装的步骤。因为MP4是一种媒体文件格式,是一种封装格式,MP4还可以存放音频流、视频流、字幕流;音频流还可以是MP3、AAC、G711等格式,视频流还可以存放H264,H265等格式。

解封装流程分析

所以解封装解的是什么?

答案:相当于你从一个zip文件中解压出来zip文件中的所需要的东西。

从宏观上来讲,就如下图所示的流程大致一样:

可以看出针对媒体文件和一个zip文件的解封装和解压缩,大致意思都是相似的,都是把封装到内部的数据解压出来,从而得到封装前的数据,既:还原出来封装前的数据。

ffmpeg6.0从demux_decode.c源码探索解封装流程

在ffmpeg中解封装是对媒体文件拆分成一个一个的数据包,也就是AVPacket,AVPacket中包含一帧或多帧数据。

在FFmpeg的lib库中对一系列的操作API接口做了很简单的封装。一般使用FFmpeg库的API来解码一个媒体文件的过程是:

  • (1)首先使用avformat_open_input函数打开一个媒体文件;
  • (2)因为每个封装格式不同,所以解码器当然也需要使用不同的类型,不过在ffmpeg当中操作都是相同的;然后对解码器进行配置和初始化,然后使用avcodec_open2打开解码器;
  • (3)然后循环使用av_read_frame函数读取一个一个的AVPacket数据;
  • (4)然后调用avcodec_send_packet函数把数据发送到解码器中;
  • (5)然后循环调用avcodec_receive_frame函数把解码器中的解码出来的一帧一帧的数据AVFrame拿到;
  • (6)然后就可以使用AVFrame去做需要做的事情了:
  • a. 例如保存H264数据,保存AAC数据;
  • b. 例如重新封装成FLV等;
  • c. 例如转成其他格式等;
  • d. 例如做推流等;
  • (7)然后就是一系列清理资源的操作。

如下图所示就是整理出来的一个流程图,我想其实可以分为八个阶段,还可以在阶段六后面增加一个阶段,用来做一些自己需要做的事情,例如:

  • 1.音视频的推流直播;
  • 2.音视频的播放和渲染;
  • 3.音视频的转码再封装;
ffmpeg6.0从demux_decode.c源码探索解封装流程

解封装源码解析

注:以下代码在ffmpeg 6.0的examples中的demux_decode.c代码文件中找到。

执行效果

demux_decode.c编译后得到demux_decode可执行程序。

demux_decode 参数格式:

usage: %s  input_file video_output_file audio_output_file
  • input_file:输入文件,就是需要解码的文件,例如MP4文件
  • video_output_file:video rawdata数据存储位置;
  • audio_output_file:audio rawdata数据存储位置;

执行后:

 1 zhenghui@zh-pc:examples$ ./demux_decode /home/zhenghui/视频/1080P.mp4 /data/project/VSCProject/avall/avall/avserver/source/tests/test_ffmpeg/ff_01_decoder/output/out.h264 /data/project/VSCProject/avall/avall/avserver/source/tests/test_ffmpeg/ff_01_decoder/output/out.aa
 2 ...
 3 ...
 4 ...
 5 audio_frame n:9753 nb_samples:1024 pts:226.464
 6 audio_frame n:9754 nb_samples:1024 pts:226.487
 7 audio_frame n:9755 nb_samples:1024 pts:226.51
 8 video_frame n:6789
 9 video_frame n:6790
10 Demuxing succeeded.
11 Play the output video file with the command:
12 ffplay -f rawvideo -pix_fmt yuv420p -video_size 1920x1080 /data/project/VSCProject/avall/avall/avserver/source/tests/test_ffmpeg/ff_01_decoder/output/out.h264
13 Warning: the sample format the decoder produced is planar (fltp). This example will output the first channel only.
14 Play the output audio file with the command:
15 ffplay -f f32le -ac 1 -ar 44100 /data/project/VSCProject/avall/avall/avserver/source/tests/test_ffmpeg/ff_01_decoder/output/out.aac
16 zhenghui@zh-pc:examples$ 

执行后会得如何播放的提示:

因为转之后的数据,例如AAC是没有ADTS头的,所以无法直接播放,需要加一些参数才可以。

Play the output video file with the command:
ffplay -f rawvideo -pix_fmt yuv420p -video_size 1920x1080 /data/project/VSCProject/avall/avall/avserver/source/tests/test_ffmpeg/ff_01_decoder/output/out.h264
Warning: the sample format the decoder produced is planar (fltp). This example will output the first channel only.
Play the output audio file with the command:
ffplay -f f32le -ac 1 -ar 44100 /data/project/VSCProject/avall/avall/avserver/source/tests/test_ffmpeg/ff_01_decoder/output/out.aac

重点源码解析

源码的行数也挺多的,这里就着重来介绍部分代码。

1、使用avformat_open_input打开输入文件,得到AVFormatContext 上下文结构体

1 static AVFormatContext *fmt_ctx = NULL;
2 static const char *src_filename = NULL; // /data/1080P.mp4
3
4 /* open input file, and allocate format context */
5 if (avformat_open_input(&fmt_ctx, src_filename, NULL, NULL) < 0) {
6    fprintf(stderr, "Could not open source file %s\n", src_filename);
7    exit(1);
8 }

如上面这段代码所示,调用avformat_open_input时传递了2个实际用到的参数,一个是AVFormatContext二级指针,和输入文件url。

可以看到传递是一个空的AVFormatContext 指针,在avformat_open_input函数源码中是会进行检测的,如果传递了一个为空的AVFormatContext指针,内部会调用avformat_alloc_context为期分配一个AVFormatContext空间。

无论你输入的是媒体流,还是本地文件,在avformat_open_input函数中都会统一调用io_open来处理这一切。内部会检查输入文件的后缀,判断输入url是什么,然后统一再细化做处理,例如输入是rtmp://xxx那么就会调用rtmp流的处理流程,如果是:file://xx就会调用file的处理流程。最终会把这个阶段分析的结果封装到AVFormatContext中。

关于avformat_open_input函数,之后会单独再出一篇详细深入的,这里先以简单的解封装流程为准。

2、使用avformat_find_stream_info分析数据

有些媒体文件的头信息,是得不到很全的信息的。例如FLV文件,就必须使用avformat_find_stream_info函数来分析上一段流数据,从而确定媒体流比较全的基本信息。

1 /* retrieve stream information */
2 if (avformat_find_stream_info(fmt_ctx, NULL) < 0) {
3    fprintf(stderr, "Could not find stream information\n");
4    exit(1);
5 }

3、解码器的初始化

open_codec_context是被再封装的一个内部函数,该函数实现了所有解码器初始化的操作:

 1 static int open_codec_context(int *stream_idx,
 2                              AVCodecContext **dec_ctx, AVFormatContext *fmt_ctx, enum AVMediaType type)
 3 {
 4    int ret, stream_index;
 5    AVStream *st;
 6    const AVCodec *dec = NULL;
 7
 8    ret = av_find_best_stream(fmt_ctx, type, -1, -1, NULL, 0);
 9    if (ret < 0) {
10        fprintf(stderr, "Could not find %s stream in input file '%s'\n",
11                av_get_media_type_string(type), src_filename);
12        return ret;
13    } else {
14        stream_index = ret;
15        st = fmt_ctx->streams[stream_index];
16
17        /* find decoder for the stream */
18        dec = avcodec_find_decoder(st->codecpar->codec_id);
19        if (!dec) {
20            fprintf(stderr, "Failed to find %s codec\n",
21                    av_get_media_type_string(type));
22            return AVERROR(EINVAL);
23        }
24
25        /* Allocate a codec context for the decoder */
26        *dec_ctx = avcodec_alloc_context3(dec);
27        if (!*dec_ctx) {
28            fprintf(stderr, "Failed to allocate the %s codec context\n",
29                    av_get_media_type_string(type));
30            return AVERROR(ENOMEM);
31        }
32
33        /* Copy codec parameters from input stream to output codec context */
34        if ((ret = avcodec_parameters_to_context(*dec_ctx, st->codecpar)) < 0) {
35            fprintf(stderr, "Failed to copy %s codec parameters to decoder context\n",
36                    av_get_media_type_string(type));
37            return ret;
38        }
39
40        /* Init the decoders */
41        if ((ret = avcodec_open2(*dec_ctx, dec, NULL)) < 0) {
42            fprintf(stderr, "Failed to open %s codec\n",
43                    av_get_media_type_string(type));
44            return ret;
45        }
46        *stream_idx = stream_index;
47    }
48
49    return 0;
50 }

使用av_find_best_stream得到解码器,流索引等信息

不确定你是否对下面这段代码熟悉:

 1 for(int i = 0;i<ctx->nb_streams;i++){
 2    AVStream *stream = ctx->streams[i];
 3    if(stream->codecpar->codec_type == AVMEDIA_TYPE_AUDIO){
 4        printf("audio index=%d \n", stream->index);
 5        audio_stream_index = stream->index;
 6    }else if(stream->codecpar->codec_type == AVMEDIA_TYPE_VIDEO){
 7        printf("video index=%d \n", stream->index);
 8        video_stream_index = stream->index;
 9    }
10 }

如果使用了av_find_best_stream函数就不必使用上面这段手写的for循环了,在av_find_best_stream函数的内部,已经实现了给你返回一个你要查找的流索引,还有解码器等信息。

可以这么玩:

1 const AVCodec *audioDecoder = NULL;
2 const AVCodec *videoDecoder = NULL;
3 //通过av_find_best_stream获取流索引和AVCodec
4 video_stream_index = av_find_best_stream(ctx, AVMEDIA_TYPE_VIDEO, -1, -1, &videoDecoder, 0);
5 audio_stream_index = av_find_best_stream(ctx, AVMEDIA_TYPE_AUDIO, -1, -1, &audioDecoder, 0);

4、循环调用av_read_frame读取数据

把数据从fmt_ctx中读取数据,然后数据会封装在一个一个的AVPacket中返回。
decode_packet是一个内部的函数,decode_packet传入相应的解码器,然后需要解码的pkt数据。

 1 /* read frames from the file */
 2 while (av_read_frame(fmt_ctx, pkt) >= 0) {
 3    // check if the packet belongs to a stream we are interested in, otherwise
 4    // skip it
 5    if (pkt->stream_index == video_stream_idx)
 6        ret = decode_packet(video_dec_ctx, pkt);
 7    else if (pkt->stream_index == audio_stream_idx)
 8        ret = decode_packet(audio_dec_ctx, pkt);
 9    av_packet_unref(pkt);
10    if (ret < 0)
11        break;
12 }

5、调用avcodec_send_packet发送解码数据到解码器

 1 static int decode_packet(AVCodecContext *dec, const AVPacket *pkt)
 2 {
 3    int ret = 0;
 4
 5    // submit the packet to the decoder
 6    ret = avcodec_send_packet(dec, pkt);
 7    if (ret < 0) {
 8        fprintf(stderr, "Error submitting a packet for decoding (%s)\n", av_err2str(ret));
 9        return ret;
10    }
11
12    ...
13 }

6、遍历avcodec_receive_frame接收解码后的数据

decode_packet:

 1 static int decode_packet(AVCodecContext *dec, const AVPacket *pkt)
 2 {
 3    int ret = 0;
 4
 5    // submit the packet to the decoder
 6    ret = avcodec_send_packet(dec, pkt);
 7    if (ret < 0) {
 8        fprintf(stderr, "Error submitting a packet for decoding (%s)\n", av_err2str(ret));
 9        return ret;
10    }
11
12    // get all the available frames from the decoder
13    while (ret >= 0) {
14        ret = avcodec_receive_frame(dec, frame);
15        if (ret < 0) {
16            // those two return values are special and mean there is no output
17            // frame available, but there were no errors during decoding
18            if (ret == AVERROR_EOF || ret == AVERROR(EAGAIN))
19                return 0;
20
21            fprintf(stderr, "Error during decoding (%s)\n", av_err2str(ret));
22            return ret;
23        }
24
25        // write the frame data to output file
26        if (dec->codec->type == AVMEDIA_TYPE_VIDEO)
27            ret = output_video_frame(frame);
28        else
29            ret = output_audio_frame(frame);
30
31        av_frame_unref(frame);
32        if (ret < 0)
33            return ret;
34    }
35
36    return 0;
37 }

7、写数据

这里只看一下output_audio_frame函数:

output_audio_frame函数只有fwrite一个函数的调用,可以看到output_audio_frame函数的作用是把从avcodec_receive_frame解码器中拿到的AVFrame数据写入到本地文件中。把AVFrame中的extended_data写入到本地文件。

1 static int output_audio_frame(AVFrame *frame)
2 {
3    size_t unpadded_linesize = frame->nb_samples * av_get_bytes_per_sample(frame->format);
4    fwrite(frame->extended_data[0], 1, unpadded_linesize, audio_dst_file);
5    return 0;
6 }

对于AAC就是这么简单,但是是没有ADTS头的AAC数据,所以在播放AAC的时候,是需要指定声音声道个数,采样率等参数,否则无法播放。

作者:郑晖
来源:手撕代码八百里
原文:https://mp.weixin.qq.com/s/nurefZcwEK6Enj6vbpCP2Q

版权声明:本文内容转自互联网,本文观点仅代表作者本人。本站仅提供信息存储空间服务,所有权归原作者所有。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至1393616908@qq.com 举报,一经查实,本站将立刻删除。

(0)

相关推荐

发表回复

登录后才能评论