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

Gal*_*nch 4 mp4 ffmpeg h.264 libav libavformat

我尝试使用 libavformat 创建.mp4具有单个 h.264 视频流的视频,但生成的文件中的最后一帧的持续时间通常为零,并且实际上从视频中删除。奇怪的是,最终帧是否被丢弃取决于我尝试添加到文件中的帧数。我在下面概述的一些简单测试让我认为我在某种程度上错误配置了AVFormatContexth.264 编码器,导致两个编辑列表有时会截断最终帧。我还将发布我正在使用的代码的简化版本,以防我犯一些明显的错误。任何帮助将不胜感激:过去几天我一直在努力解决这个问题,但进展甚微。

ffmpeg 如果我使用该选项,我可以通过使用带有复制编解码器的二进制文件创建一个新的 mp4 容器来恢复丢失的帧-ignore_editlistffprobe使用、mp4trackdump、 或来检查丢失帧的文件mp4file --dump表明,如果最终帧的采样时间与编辑列表末尾完全相同,则最终帧将被丢弃。当我制作一个没有丢帧的文件时,它仍然有两个编辑列表:唯一的区别是编辑列表的结束时间超出了没有丢帧的文件中的所有样本。虽然这不是一个公平的比较,但如果我.png为每个帧创建一个,然后使用编解码器和类似的 h.264 设置生成一个.mp4ffmpegimage2会制作一部包含所有帧的电影,只有一个编辑列表,以及与我的类似的 PTS 时间带有两个编辑列表的损坏的电影。在这种情况下,编辑列表始终在最后一帧/采样时间之后结束。

我使用此命令来确定结果流中的帧数,尽管我也通过其他实用程序获得相同的数字:

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
Run Code Online (Sandbox Code Playgroud)

使用 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
Run Code Online (Sandbox Code Playgroud)

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

$ 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
...
Run Code Online (Sandbox Code Playgroud)

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

$ 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
Run Code Online (Sandbox Code Playgroud)

我生成的未损坏的视频具有类似的结构,正如您在该视频中看到的那样,它有 99 个输入帧,所有这些都在输出中可见。即使 stss 框中的样本之一的sample_duration 设置为零,它也不会从帧计数中删除,也不会在使用 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
...
Run Code Online (Sandbox Code Playgroud)
$ 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
Run Code Online (Sandbox Code Playgroud)

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

下面是我用来生成这些文件的代码,它是我正在修改的库函数的 MWE 脚本版本。它是用 Julia 编写的,我认为这在这里并不重要,并且调用 FFMPEG 库版本 4.3.1。它或多或少是FFMPEG 混合演示的直接翻译,尽管这里的编解码器上下文是在格式上下文之前创建的。我首先展示与 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)
Run Code Online (Sandbox Code Playgroud)

下面是帮助程序代码,它使得在 Julia 中访问嵌套 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)...)
Run Code Online (Sandbox Code Playgroud)

编辑:我已经尝试过的一些事情

  • 明确将流持续时间设置为与我添加的帧数相同的数字,或者比该数字多一些
  • 将流开始时间显式设置为零,而第一帧的 PTS 为 1
  • 尝试使用编码器参数以及gop_size使用 B 帧等。
  • 设置mov/mp4复用器的私有数据以设置movflagnegative_cts_offsets
  • 更改帧速率
  • 尝试了不同的像素格式,例如AV_PIX_FMT_YUV420P

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

小智 5

我遇到了类似的问题,最终帧丢失,这导致计算出的 FPS 与我的预期不同。

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