Libavformat/FFMPEG:使用 AVFormatContext 混合到 mp4 中会丢弃最后一帧,具体取决于帧数

Libavformat/FFMPEG: Muxing into mp4 with AVFormatContext drops the final frame, depending on the number of frames

我正在尝试使用 libavformat 创建 .mp4 视频 使用单个 h.264 视频流,但结果文件中的最后一帧 通常持续时间为零,并且有效地从视频中删除。 奇怪的是,最后一帧掉不掉取决于有多少 我尝试添加到文件中的帧。我在下面概述的一些简单测试使 我认为我以某种方式错误配置了 AVFormatContext 或 h.264 编码器,导致两个编辑列表有时会切断最终的 框架。我还将 post 我正在使用的代码的简化版本,以防我 犯了一些明显的错误。任何帮助将不胜感激:我一直 这几天一直在为这个问题苦苦挣扎,但进展甚微。

我可以通过使用 ffmpeg 创建一个新的 mp4 容器来恢复丢失的帧 如果我使用 -ignore_editlist 选项,则使用复制编解码器的二进制文件。检查 使用 ffprobemp4trackdumpmp4file --dump 的缺少帧的文件表明,如果其采样时间恰好是 与编辑列表的末尾相同。当我制作一个没有掉帧的文件时,它 仍然有两个编辑列表:唯一的区别是编辑的结束时间 列表超出了没有丢帧的文件中的所有示例。虽然这 这不是一个公平的比较,如果我为每一帧制作一个 .png 然后生成 .mp4ffmpeg 使用 image2 编解码器和类似的 h.264 设置,我 制作一部所有帧都存在、只有一个编辑列表和类似 PTS 的电影 次因为我的电影有两个编辑列表。在这种情况下,编辑列表 总是在最后 frame/sample 次之后结束。

我正在使用此命令来确定结果流中的帧数, 虽然我也得到了与其他实用程序相同的数字:

ffprobe -v error -count_frames -select_streams v:0 -show_entries stream=nb_read_frames -of default=nokey=1:noprint_wrappers=1 video_file_name.mp4

使用 ffprobe 对文件进行的简单检查显示没有明显的警告迹象 我,除了帧率受到丢失帧的影响(目标是 24):

$ ffprobe -hide_banner testing.mp4
Input #0, mov,mp4,m4a,3gp,3g2,mj2, from 'testing.mp4':
  Metadata:
    major_brand     : isom
    minor_version   : 512
    compatible_brands: isomiso2avc1mp41
    encoder         : Lavf58.45.100
  Duration: 00:00:04.13, start: 0.041016, bitrate: 724 kb/s
    Stream #0:0(und): Video: h264 (High) (avc1 / 0x31637661), yuv420p, 100x100, 722 kb/s, 24.24 fps, 24 tbr, 12288 tbn, 48 tbc (default)
    Metadata:
      handler_name    : VideoHandler

我以编程方式生成的文件总是有两个编辑列表,其中之一 这是很短的。在有和没有丢失帧的文件中, 其中一帧的持续时间为 0,而所有其他帧的持续时间相同 (512)。您可以在我尝试放入的文件的 ffmpeg 输出中看到这一点 100 帧,虽然只有 99 是可见的,尽管文件包含所有 100 样品。

$ ffmpeg -hide_banner -y -v 9 -loglevel 99 -i testing.mp4  
...
<edited to remove the class printing>
type:'edts' parent:'trak' sz: 48 100 948
type:'elst' parent:'edts' sz: 40 8 40
track[0].edit_count = 2
duration=41 time=-1 rate=1.000000
duration=4125 time=0 rate=1.000000
type:'mdia' parent:'trak' sz: 808 148 948
type:'mdhd' parent:'mdia' sz: 32 8 800
type:'hdlr' parent:'mdia' sz: 45 40 800
ctype=[0][0][0][0]
stype=vide
type:'minf' parent:'mdia' sz: 723 85 800
type:'vmhd' parent:'minf' sz: 20 8 715
type:'dinf' parent:'minf' sz: 36 28 715
type:'dref' parent:'dinf' sz: 28 8 28
Unknown dref type 0x206c7275 size 12
type:'stbl' parent:'minf' sz: 659 64 715
type:'stsd' parent:'stbl' sz: 151 8 651
size=135 4CC=avc1 codec_type=0
type:'avcC' parent:'stsd' sz: 49 8 49
type:'stts' parent:'stbl' sz: 32 159 651
track[0].stts.entries = 2
sample_count=99, sample_duration=512
sample_count=1, sample_duration=0
...
AVIndex stream 0, sample 99, offset 5a0ed, dts 50688, size 3707, distance 0, keyframe 1
Processing st: 0, edit list 0 - media time: -1, duration: 504
Processing st: 0, edit list 1 - media time: 0, duration: 50688
type:'udta' parent:'moov' sz: 98 1072 1162
...

最后一帧的持续时间为零:

$ mp4trackdump -v testing.mp4
...
mp4file testing.mp4, track 1, samples 100, timescale 12288
sampleId      1, size  6943 duration      512 time        0 00:00:00.000 S
sampleId      2, size  3671 duration      512 time      512 00:00:00.041 S
...
sampleId     99, size  3687 duration      512 time    50176 00:00:04.083 S
sampleId    100, size  3707 duration        0 time    50688 00:00:04.125 S

我生成的未损坏视频具有相似的结构,如您在 这个视频有 99 个输入帧,所有这些都在输出中可见。 即使 sample_duration 中的一个样本设置为零 stss 框,它不会从帧计数中删除或在读回帧时 在 ffmpeg 中。

$ ffmpeg -hide_banner -y -v 9 -loglevel 99 -i testing_99.mp4  
...
type:'elst' parent:'edts' sz: 40 8 40
track[0].edit_count = 2
duration=41 time=-1 rate=1.000000
duration=4084 time=0 rate=1.000000
...
track[0].stts.entries = 2
sample_count=98, sample_duration=512
sample_count=1, sample_duration=0
...
AVIndex stream 0, sample 98, offset 5d599, dts 50176, size 3833, distance 0, keyframe 1
Processing st: 0, edit list 0 - media time: -1, duration: 504
Processing st: 0, edit list 1 - media time: 0, duration: 50184
...
$ mp4trackdump -v testing_99.mp4
...
sampleId     98, size  3814 duration      512 time    49664 00:00:04.041 S
sampleId     99, size  3833 duration        0 time    50176 00:00:04.083 S

我突然想到的一个区别是损坏文件的第二个编辑列表 在时间 50688 结束,与最后一个样本重合,而未损坏的 文件的编辑列表结束于 50184,这是在最后一个样本的时间之后 在50176。正如我之前提到的,最后一帧是否被剪裁取决于 我编码并复用到容器中的帧数:100 个输入帧 导致 1 个丢帧,99 个导致 0 个,98 个导致 0 个,97 个导致 1 个,等等...

这是我用来生成这些文件的代码,它是一个 MWE 脚本 我正在修改的库函数的版本。它是用朱莉娅写的, 我认为这在这里不重要,并调用 FFMPEG 库版本 4.3.1.它或多或少是 FFMPEG muxing 的直接翻译 demo,虽然编解码器 这里的上下文是在格式上下文之前创建的。我正在展示的代码 首先与 ffmpeg 交互,尽管它依赖于我将要使用的一些辅助代码 放在下面。

辅助代码只是让在 Julia 中使用嵌套的 C 结构变得更容易,并且 允许使用 Julia 中的 . 语法代替 C 的箭头 (->) 运算符 结构指针的字段访问。 Libav 结构如 AVFrame 显示为 薄包装类型 AVFramePtr,类似地 AVStream 显示为 AVStreamPtr 等等......这些就像单指针或双指针一样 函数调用的次数,具体取决于函数的类型签名。希望它会 如果您熟悉在 C 中使用 libav,请足够清楚地理解, 如果您不这样做,我认为没有必要查看帮助程序代码 想要运行代码。

# Function to transfer array to AVPicture/AVFrame
function transfer_img_buf_to_frame!(frame, img)
    img_pointer = pointer(img)
    data_pointer = frame.data[1] # Base-1 indexing, get pointer to first data buffer in frame
    for h = 1:frame.height
        data_line_pointer = data_pointer + (h-1) * frame.linesize[1] # base-1 indexing
        img_line_pointer = img_pointer + (h-1) * frame.width
        unsafe_copyto!(data_line_pointer, img_line_pointer, frame.width) # base-1 indexing
    end
end

# Function to transfer AVFrame to AVCodecContext, and AVPacket to AVFormatContext
function encode_mux!(packet, format_context, frame, codec_context; flush = false)
    if flush
        fret = avcodec_send_frame(codec_context, C_NULL)
    else
        fret = avcodec_send_frame(codec_context, frame)
    end
    if fret < 0 && !in(fret, [-Libc.EAGAIN, VIO_AVERROR_EOF])
        error("Error $fret sending a frame for encoding")
    end

    pret = Cint(0)
    while pret >= 0
        pret = avcodec_receive_packet(codec_context, packet)
        if pret == -Libc.EAGAIN || pret == VIO_AVERROR_EOF
             break
        elseif pret < 0
            error("Error $pret during encoding")
        end
        stream = format_context.streams[1] # Base-1 indexing
        av_packet_rescale_ts(packet, codec_context.time_base, stream.time_base)
        packet.stream_index = 0
        ret = av_interleaved_write_frame(format_context, packet)
        ret < 0 && error("Error muxing packet: $ret")
    end
    if !flush && fret == -Libc.EAGAIN && pret != VIO_AVERROR_EOF
        fret = avcodec_send_frame(codec_context, frame)
        if fret < 0 && fret != VIO_AVERROR_EOF
            error("Error $fret sending a frame for encoding")
        end
    end
    return pret
end

# Set parameters of test movie
nframe = 100
width, height = 100, 100
framerate = 24
gop = 0
codec_name = "libx264"
filename = "testing.mp4"

((width % 2 !=0) || (height % 2 !=0)) && error("Encoding error: Image dims must be a multiple of two")

# Make test images
imgstack = map(x->rand(UInt8,width,height),1:nframe);

pix_fmt = AV_PIX_FMT_GRAY8
framerate_rat = Rational(framerate)

codec = avcodec_find_encoder_by_name(codec_name)
codec == C_NULL && error("Codec '$codec_name' not found")

# Allocate AVCodecContext
codec_context_p = avcodec_alloc_context3(codec) # raw pointer
codec_context_p == C_NULL && error("Could not allocate AVCodecContext")
# Easier to work with pointer that acts like a c struct pointer, type defined below
codec_context = AVCodecContextPtr(codec_context_p)

codec_context.width = width
codec_context.height = height
codec_context.time_base = AVRational(1/framerate_rat)
codec_context.framerate = AVRational(framerate_rat)
codec_context.pix_fmt = pix_fmt
codec_context.gop_size = gop

ret = avcodec_open2(codec_context, codec, C_NULL)
ret < 0 && error("Could not open codec: Return code $(ret)")

# Allocate AVFrame and wrap it in a Julia convenience type
frame_p = av_frame_alloc()
frame_p == C_NULL && error("Could not allocate AVFrame")
frame = AVFramePtr(frame_p)

frame.format = pix_fmt
frame.width = width
frame.height = height

# Allocate picture buffers for frame
ret = av_frame_get_buffer(frame, 0)
ret < 0 && error("Could not allocate the video frame data")

# Allocate AVPacket and wrap it in a Julia convenience type
packet_p = av_packet_alloc()
packet_p == C_NULL && error("Could not allocate AVPacket")
packet = AVPacketPtr(packet_p)

# Allocate AVFormatContext and wrap it in a Julia convenience type
format_context_dp = Ref(Ptr{AVFormatContext}()) # double pointer
ret = avformat_alloc_output_context2(format_context_dp, C_NULL, C_NULL, filename)
if ret != 0 || format_context_dp[] == C_NULL
    error("Could not allocate AVFormatContext")
end
format_context = AVFormatContextPtr(format_context_dp)

# Add video stream to AVFormatContext and configure it to use the encoder made above
stream_p = avformat_new_stream(format_context, C_NULL)
stream_p == C_NULL && error("Could not allocate output stream")
stream = AVStreamPtr(stream_p) # Wrap this pointer in a convenience type

stream.time_base = codec_context.time_base
stream.avg_frame_rate = 1 / convert(Rational, stream.time_base)
ret = avcodec_parameters_from_context(stream.codecpar, codec_context)
ret < 0 && error("Could not set parameters of stream")

# Open the AVIOContext
pb_ptr = field_ptr(format_context, :pb)
# This following is just a call to avio_open, with a bit of extra protection
# so the Julia garbage collector does not destroy format_context during the call
ret = GC.@preserve format_context avio_open(pb_ptr, filename, AVIO_FLAG_WRITE)
ret < 0 && error("Could not open file $filename for writing")

# Write the header
ret = avformat_write_header(format_context, C_NULL)
ret < 0 && error("Could not write header")

# Encode and mux each frame
for i in 1:nframe # iterate from 1 to nframe
    img = imgstack[i] # base-1 indexing
    ret = av_frame_make_writable(frame)
    ret < 0 && error("Could not make frame writable")
    transfer_img_buf_to_frame!(frame, img)
    frame.pts = i
    encode_mux!(packet, format_context, frame, codec_context)
end

# Flush the encoder
encode_mux!(packet, format_context, frame, codec_context; flush = true)

# Write the trailer
av_write_trailer(format_context)

# Close the AVIOContext
pb_ptr = field_ptr(format_context, :pb) # get pointer to format_context.pb
ret = GC.@preserve format_context avio_closep(pb_ptr) # simply a call to avio_closep
ret < 0 && error("Could not free AVIOContext")

# Deallocation
avcodec_free_context(codec_context)
av_frame_free(frame)
av_packet_free(packet)
avformat_free_context(format_context)

下面是帮助程序代码,它使访问嵌套 c 结构的指针不是 朱莉娅完全痛苦。如果您自己尝试 运行 代码,请将其输入 在上面显示的代码逻辑之前。这个需要 VideoIO.jl,一个 libav 的 Julia 包装器。

# Convenience type and methods to make the above code look more like C
using Base: RefValue, fieldindex

import Base: unsafe_convert, getproperty, setproperty!, getindex, setindex!,
    unsafe_wrap, propertynames

# VideoIO is a Julia wrapper to libav
#
# Bring bindings to libav library functions into namespace
using VideoIO: AVCodecContext, AVFrame, AVPacket, AVFormatContext, AVRational,
    AVStream, AV_PIX_FMT_GRAY8, AVIO_FLAG_WRITE, AVFMT_NOFILE,
    avformat_alloc_output_context2, avformat_free_context, avformat_new_stream,
    av_dump_format, avio_open, avformat_write_header,
    avcodec_parameters_from_context, av_frame_make_writable, avcodec_send_frame,
    avcodec_receive_packet, av_packet_rescale_ts, av_interleaved_write_frame,
    avformat_query_codec, avcodec_find_encoder_by_name, avcodec_alloc_context3,
    avcodec_open2, av_frame_alloc, av_frame_get_buffer, av_packet_alloc,
    avio_closep, av_write_trailer, avcodec_free_context, av_frame_free,
    av_packet_free

# Submodule of VideoIO
using VideoIO: AVCodecs

# Need to import this function from Julia's Base to add more methods
import Base: convert

const VIO_AVERROR_EOF = -541478725 # AVERROR_EOF

# Methods to convert between AVRational and Julia's Rational type, because it's
# hard to access the AV rational macros with Julia's C interface
convert(::Type{Rational{T}}, r::AVRational) where T = Rational{T}(r.num, r.den)
convert(::Type{Rational}, r::AVRational) = Rational(r.num, r.den)
convert(::Type{AVRational}, r::Rational) = AVRational(numerator(r), denominator(r))

"""
    mutable struct NestedCStruct{T}

Wraps a pointer to a C struct, and acts like a double pointer to that memory.
The methods below will automatically convert it to a single pointer if needed
for a function call, and make interacting with it in Julia look (more) similar
to interacting with it in C, except '->' in C is replaced by '.' in Julia.
"""
mutable struct NestedCStruct{T}
    data::RefValue{Ptr{T}}
end
NestedCStruct{T}(a::Ptr) where T = NestedCStruct{T}(Ref(a))
NestedCStruct(a::Ptr{T}) where T = NestedCStruct{T}(a)

const AVCodecContextPtr = NestedCStruct{AVCodecContext}
const AVFramePtr = NestedCStruct{AVFrame}
const AVPacketPtr = NestedCStruct{AVPacket}
const AVFormatContextPtr = NestedCStruct{AVFormatContext}
const AVStreamPtr = NestedCStruct{AVStream}

function field_ptr(::Type{S}, struct_pointer::Ptr{T}, field::Symbol,
                           index::Integer = 1) where {S,T}
    fieldpos = fieldindex(T, field)
    field_pointer = convert(Ptr{S}, struct_pointer) +
        fieldoffset(T, fieldpos) + (index - 1) * sizeof(S)
    return field_pointer
end

field_ptr(a::Ptr{T}, field::Symbol, args...) where T =
    field_ptr(fieldtype(T, field), a, field, args...)

function check_ptr_valid(p::Ptr, err::Bool = true)
    valid = p != C_NULL
    err && !valid && error("Invalid pointer")
    valid
end

unsafe_convert(::Type{Ptr{T}}, ap::NestedCStruct{T}) where T =
    getfield(ap, :data)[]
unsafe_convert(::Type{Ptr{Ptr{T}}}, ap::NestedCStruct{T}) where T =
    unsafe_convert(Ptr{Ptr{T}}, getfield(ap, :data))

function check_ptr_valid(a::NestedCStruct{T}, args...) where T
    p = unsafe_convert(Ptr{T}, a)
    GC.@preserve a check_ptr_valid(p, args...)
end

nested_wrap(x::Ptr{T}) where T = NestedCStruct(x)
nested_wrap(x) = x

function getproperty(ap::NestedCStruct{T}, s::Symbol) where T
    check_ptr_valid(ap)
    p = unsafe_convert(Ptr{T}, ap)
    res = GC.@preserve ap unsafe_load(field_ptr(p, s))
    nested_wrap(res)
end

function setproperty!(ap::NestedCStruct{T}, s::Symbol, x) where T
    check_ptr_valid(ap)
    p = unsafe_convert(Ptr{T}, ap)
    fp = field_ptr(p, s)
    GC.@preserve ap unsafe_store!(fp, x)
end

function getindex(ap::NestedCStruct{T}, i::Integer) where T
    check_ptr_valid(ap)
    p = unsafe_convert(Ptr{T}, ap)
    res = GC.@preserve ap unsafe_load(p, i)
    nested_wrap(res)
end

function setindex!(ap::NestedCStruct{T}, i::Integer, x) where T
    check_ptr_valid(ap)
    p = unsafe_convert(Ptr{T}, ap)
    GC.@preserve ap unsafe_store!(p, x, i)
end

function unsafe_wrap(::Type{T}, ap::NestedCStruct{S}, i) where {S, T}
    check_ptr_valid(ap)
    p = unsafe_convert(Ptr{S}, ap)
    GC.@preserve ap unsafe_wrap(T, p, i)
end

function field_ptr(::Type{S}, a::NestedCStruct{T}, field::Symbol,
                           args...) where {S, T}
    check_ptr_valid(a)
    p = unsafe_convert(Ptr{T}, a)
    GC.@preserve a field_ptr(S, p, field, args...)
end

field_ptr(a::NestedCStruct{T}, field::Symbol, args...) where T =
    field_ptr(fieldtype(T, field), a, field, args...)

propertynames(ap::T) where {S, T<:NestedCStruct{S}} = (fieldnames(S)...,
                                                       fieldnames(T)...)

编辑:一些我已经尝试过的东西

另外要明确的是,我可以忽略编辑列表将文件转移到另一个文件来解决这个问题,我希望首先不要制作损坏的 mp4 文件。

我有一个类似的问题,最后一帧丢失,这导致计算出的 FPS 与我预期的不同。

您似乎没有设置 AVPacket 的持续时间字段。我发现依靠自动持续时间(将该字段保留为 0)显示了您描述的问题。 如果你有恒定的帧率,你可以计算持续时间应该是多少,例如对于 25 FPS 的 12800 时基(= 1/25 秒),将其设置为 512。希望对您有所帮助。