基于字节对齐、宏块宽高等因素,导致一个宽 w 高 h 的视频其实际参与编码的某一帧的宽高并不一定等于 w 和 h,而是有一个 ffmpeg 称之为 coded_width 及 coded_height 的编码宽高。出于另一些原因,例如数据送出与读取、数据加载到纹理等需求,不仅需要知道 w h,还需要知道其 coded_width 及 coded_height(以下简称 cw ch),那么该如何尽可能早地获取到这两个值呢?
解码之前获取不到 cw ch
参考着 FFmpeg 源码中的示例编写的编解码库,使用 avformat_open_input(&format_context, filename, nullptr, nullptr)
打开文件,然后用 avformat_find_stream_info(format_context, nullptr)
查找文件的流信息,再用 int stream_id = av_find_best_stream(format_context, AVMEDIA_TYPE_VIDEO, -1, -1, nullptr, false)
获取到视频流的 id,由此即可获取到该文件的视频流信息,其大部分主要信息存储于 format_context->streams[stream_id]->codecpar
这个参数中。包括宽、高、颜色格式、比特率、编码格式、采样宽高比等;而另一些诸如时长、帧数、帧率、时间基等信息也存在于上一层的 format_context->streams[stream_id]
这个 AVStream
结构体变量中。
而以上这些信息里,并没有 coded_width 及 coded_height 这两个信息。通过查 FFmpeg 源码可知,这两个变量是存储于结构体 AVCodecContext
中的,早一些的版本里,AVStream
结构体貌似还有个 AVCodecContext
变量,但 5.0 之后的版本已经去除。那该怎么获取呢?
如上图所示该视频宽为 1080 但参与编码的宽为 1088,忽然想到,使用命令行 ffprobe -show_streams -show_format
查看文件信息的时候是有这个参数打印的,那么,看一下 ffprobe 的源码不就行了?
查源码
其实全局搜索 coded_width
的时候,出现在比较靠前的搜索结果,或者说第一个存在于 c 文件中的搜索结果就来自 ffprobe.c 这个文件,点过去看,对比其代码上下文及上图 ffprobe 的输出信息,看起来就是打印上图信息的代码位置。
而这个 dec_ctx
就是个 AVCodecContext *
,那它是怎么获取到的呢?找到上图这段代码的函数起始发现,dec_ctx
来自参数 InputStream *ist
static int show_stream(WriterContext *w, AVFormatContext *fmt_ctx, int stream_idx, InputStream *ist, int in_program)
{
AVStream *stream = ist->st;
AVCodecParameters *par;
AVCodecContext *dec_ctx;
char val_str[128];
const char *s;
AVRational sar, dar;
AVBPrint pbuf;
const AVCodecDescriptor *cd;
int ret = 0;
const char *profile = NULL;
av_bprint_init(&pbuf, 1, AV_BPRINT_SIZE_UNLIMITED);
writer_print_section_header(w, in_program ? SECTION_ID_PROGRAM_STREAM : SECTION_ID_STREAM);
print_int("index", stream->index);
par = stream->codecpar;
dec_ctx = ist->dec_ctx;
// ...
再找 show_stream
的调用处发现这个 InputStream * ist
来自另一个非 API 标准的自定义结构体 InputFile
。
static int show_streams(WriterContext *w, InputFile *ifile)
{
AVFormatContext *fmt_ctx = ifile->fmt_ctx;
int i, ret = 0;
writer_print_section_header(w, SECTION_ID_STREAMS);
for (i = 0; i < ifile->nb_streams; i++)
if (selected_streams[i]) {
ret = show_stream(w, fmt_ctx, i, &ifile->streams[i], 0);
if (ret < 0)
break;
}
writer_print_section_footer(w);
return ret;
}
再找 show_streams
的调用处,看看这个 InputFile->InputStream->AVCodecContext
是如何获取的即可。
static int probe_file(WriterContext *wctx, const char *filename,
const char *print_filename)
{
InputFile ifile = { 0 };
int ret, i;
int section_id;
do_read_frames = do_show_frames || do_count_frames;
do_read_packets = do_show_packets || do_count_packets;
ret = open_input_file(&ifile, filename, print_filename);
// ...
if (do_show_streams) {
ret = show_streams(wctx, &ifile);
CHECK_END;
}
是个自定义方法 open_input_file
,由于这个方法太长,就不贴代码了。第一遍看到这个方法的时候,感觉没啥特别的,就是它使用 const AVCodec *codec = avcodec_find_decoder(stream->codecpar->codec_id)
获取到了文件对应的 AVCodec *
,再用 avcodec_alloc_context3
申请了一个 AVCodecContext
,然后用 avcodec_parameters_to_context(ctx, stream->codecpar)
把 codecpar
中存储的信息拷贝到了 AVCodecContext
中去了。于是,我也比着葫芦画瓢地写到此处,结果发现 ctx->coded_width
和 ctx->coded_height
都是 0,并没有被赋值。
想来也是,结构体 AVCodecParameters
中根本就没有 cw ch 这两个参数,怎么能拷贝到 AVCodecContext
结构体中去呢。于是猜测,是否跟 ffprobe 源码中调用了 avcodec_open2
打开了解码器有关?于是再把代码写到这一句,发现值是有了,但跟正常宽高是一样的,并不是参与编码的宽高。
再细看 open_input_file
这个方法,里面一堆 AVDictionary
相关的操作看得很迷,只能祈祷不是这相关的操作给 cw ch 这两个参数赋值的,虽然迷,仍然艰难地看了下,应该不是。那是哪里呢?
重新看了下 全局搜索 coded_width
的结果,发现大部分语句都是对其值的使用,而对其进行赋值的语句,大约存在于类似 vc1_parser.c vp8_parser.c 等文件里。所以怀疑,cw ch 是各解码器自己的实现中对其进行了赋值,那这,可能就得解码一帧才行吧?
没有头绪,看了半天,又看回最初的 probe_file
这个方法,发现在 open_input_file
之后,还有两个比较可疑的方法调用,一个是avformat_match_stream_specifier
,不过这应该是个标准 API,而且参数中也不涉及 InputFile->InputStream->AVCodecContext
这个参数,所以,往后稍稍吧。还有一个就是 read_packets()
这个方法了,这的确是个自定义方法。
找进去发现主要调用了另一个 read_interval_packets
方法,由于这个方法的参数里有 InputFile *
,仍然是有可能修改到 InputFile->InputStream->AVCodecContext
这个参数的,因此继续看进去,再分析能修改到那个参数的,就只有一个新的自定义方法 process_frame()
了。看完之后果然是调用了送包和收帧这两大 api,那么也就是说,在解码之前获取不到 cw ch 咯?
照猫画虎
那就仿照这个方法,写吧。写一个读到视频包,然后送进解码器,然后收取解码后的帧的一段程序吧。很快写完之后调试发现
在代码执行完 avcodec_parameters_to_context()
方法之后,width 和 height 有了 1080 1920 的值,但 cw ch 是 0,在执行完 avcodec_open2
方法之后,cw ch 有值了,但值是 1080 和 1920。继续单步执行下去,仅仅执行完 74 行 avcodec_send_packet
之后,cw ch 的值就变成 1088 1920 了!!!这下,就可以把 75 到 78 行删掉了。最后,不要忘了调用释放 packet 关闭 codec 以及释放 acodec context 的相关代码哟。由于这两张图只是调试用的,并非最终结果,因此并没写释放相关的代码。
最终代码:
const AVCodec *codec = avcodec_find_decoder(stream_->codecpar->codec_id);
AVCodecContext *ctx = avcodec_alloc_context3(codec);
avcodec_parameters_to_context(ctx, stream_->codecpar);
avcodec_open2(ctx, codec, nullptr);
AVPacket *packet = av_packet_alloc();
av_init_packet(packet);
while (!av_read_frame(format_context, packet)) {
if (packet->stream_index == stream_id_) break; // stream_id_ 是视频的视频流 id
}
avcodec_send_packet(ctx, packet);
av_packet_unref(packet);
av_packet_free(&packet); // 好像有这一句,上一句 unref 就可以不用写了。不过,写了也没啥开销。
avcodec_close(ctx);
avcodec_free_context(&ctx);