从 h264 帧解析数据包时 PyAV 不一致

PyAV inconsistency when parsing packets from h264 frames

当生成 H.264 帧并使用 pyAV 对其进行解码时,只有在两次调用 parse 方法时才会从帧中解析数据包。

考虑以下测试 H.264 输入,使用以下方法创建:

ffmpeg -f lavfi -i testsrc=duration=10:size=1280x720:rate=30 -f image2 -vcodec libx264 -bsf h264_mp4toannexb -force_key_frames source -x264-params keyint=1:scenecut=0 "frame-%4d.h264"

现在,使用pyAV解析第一帧:

import av
codec = av.CodecContext.create('h264', 'r')
with open('/path/to/frame-0001.h264', 'rb') as file_handler:
    chunk = file_handler.read()
    packets = codec.parse(chunk) # This line needs to be invoked twice to parse packets

数据包保持为空,除非再次调用最后一行 (packets = codec.parse(chunk))

此外,对于我无法描述的不同现实生活示例,似乎从数据包中解码帧也需要多次解码调用:

packet = packets[0]
frames = codec.decode(packet) # This line needs to be invoked 2-3 times to actually receive frames.

有人知道 pyAV 这种不一致的行为吗?

(在 macOS Monterey 12.3.1、ffmpeg 4.4.1、pyAV 9.0.2 上使用 Python 3.8.12)

这是预期的 PyAV 行为。不仅如此,它是底层 libav 的预期行为。一个数据包并不能保证一帧,可能需要多个数据包才能产生一帧。这在 FFmpeg's video decoder example:

中很明显
    while (ret >= 0) {
        ret = avcodec_receive_frame(dec_ctx, frame);
        if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
            return;

如果它需要更多的数据包来形成一个帧,它会抛出 EAGAIN 错误。

[编辑]

实际上,上面的例子并不是一个很好的例子,因为它只是在 EAGAIN 退出。要检索帧,它应该 continue on EAGAIN:

    while (ret >= 0) {
        ret = avcodec_receive_frame(dec_ctx, frame);
        if (AVERROR(EAGAIN))
            continue;
        if (ret == AVERROR_EOF)
            return;

[编辑]

pyav的codec.parse()

解码有时需要额外的调用是一个相当well-known的事实,但需要刷新的解析器并不常见。下面是 PyAV 和 FFmpeg 的区别:

PyAV 像这样 av_parser_parse2() 解析输入数据 [ref]:


        while True:

            with nogil:
                consumed = lib.av_parser_parse2(
                    self.parser,
                    self.ptr,
                    &out_data, &out_size,
                    in_data, in_size,
                    lib.AV_NOPTS_VALUE, lib.AV_NOPTS_VALUE,
                    0
                )
            err_check(consumed)

            # ...snip...

            if not in_size:
                # This was a flush. Only one packet should ever be returned.
                break

            in_data += consumed
            in_size -= consumed

            if not in_size:
                # Aaaand now we're done.
                break

所以它一直读取直到输入数据被 100% 消耗,并注意它不会在缓冲区末尾调用 av_parser_parse2(这是有道理的,因为输入数据可能只是流数据的一部分。

相比之下,FFmpeg 不直接调用 av_parser_parse2 而是使用 parse_packet,你可以看到它是如何处理类似情况的:

while (size > 0 || (flush && got_output)) {
   int64_t next_pts = pkt->pts;
   int64_t next_dts = pkt->dts;
   int len;

   len = av_parser_parse2(sti->parser, sti->avctx,
                          &out_pkt->data, &out_pkt->size, data, size,
                          pkt->pts, pkt->dts, pkt->pos);

在输入数据流耗尽后,它也会调用av_parser_parse2刷新流。因此,您需要在 PyAV 中执行相同的操作:读取所有帧后,最后一次调用 codec.parse() 以刷新最后一个数据包。