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

问题描述

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

我可以通过使用ffmpeg创建一个新的mp4容器来恢复丢失的帧 如果使用-ignore_editlist选项,则使用复制编解码器进行二进制处理。检查中 使用ffprobemp4trackdumpmp4file --dump丢失帧的文件显示,如果最后一个帧的采样时间恰好是 在编辑列表的末尾。当我制作没有掉帧的文件时, 仍然有两个编辑列表:唯一的区别是编辑的结束时间 该列表超出了没有丢帧的文件中的所有样本。虽然这 如果我为每个帧制作一个.png然后生成 .mp4ffmpeg使用image2编解码器和类似的h.264设置,我 制作一部电影,其中包含所有帧,只有一个编辑列表和类似的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

使用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帧,尽管文件包含全部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
...

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

$ 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 98,offset 5d599,dts 50176,size 3833,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的箭头(->)运算符 结构指针的字段访问。诸如AVFrame之类的Libav结构以 薄包装器类型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,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,codec_context)
end

# Flush the encoder
encode_mux!(packet,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},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,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},args...) where {S,T}
    check_ptr_valid(a)
    p = unsafe_convert(Ptr{T},a)
    GC.@preserve a field_ptr(S,args...)
end

field_ptr(a::nestedCStruct{T},args...)

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

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

  • 将流持续时间明确设置为与我添加的帧数相同的数字,或者将其设置为更多
  • 将第一帧的PTS明确设置为流开始时间为零,
  • 使用B帧等来播放编码器参数以及gop_size
  • 设置mov / mp4混合器的私有数据以设置movflag negative_cts_offsets
  • 更改帧速率
  • 尝试了不同的像素格式,例如AV_PIX_FMT_YUV420P

还要清楚一点,尽管我可以将文件转移到另一个文件中,而忽略编辑列表来解决此问题,但我希望首先不要制作损坏的mp4文件

解决方法

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

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

相关问答

Selenium Web驱动程序和Java。元素在(x,y)点处不可单击。其...
Python-如何使用点“。” 访问字典成员?
Java 字符串是不可变的。到底是什么意思?
Java中的“ final”关键字如何工作?(我仍然可以修改对象。...
“loop:”在Java代码中。这是什么,为什么要编译?
java.lang.ClassNotFoundException:sun.jdbc.odbc.JdbcOdbc...