问题描述
我正在尝试使用libavformat创建一个.mp4
视频
带有单个h.264视频流,但最终文件中的最后一帧
通常持续时间为零,并且实际上已从视频中删除。
奇怪的是,最后一帧是否丢失取决于多少帧
我尝试添加到文件中的帧。我在下面概述的一些简单测试
我认为我在某种程度上错误地配置了AVFormatContext
或
h.264编码器,导致两个编辑列表有时会切断最终的
帧。我还将发布我正在使用的代码的简化版本,以防万一
犯一些明显的错误。任何帮助将不胜感激:我一直
过去几天在此问题上苦苦挣扎,进展甚微。
我可以通过使用ffmpeg
创建一个新的mp4容器来恢复丢失的帧
如果使用-ignore_editlist
选项,则使用复制编解码器进行二进制处理。检查中
使用ffprobe
,mp4trackdump
或mp4file --dump
丢失帧的文件显示,如果最后一个帧的采样时间恰好是
在编辑列表的末尾。当我制作没有掉帧的文件时,
仍然有两个编辑列表:唯一的区别是编辑的结束时间
该列表超出了没有丢帧的文件中的所有样本。虽然这
如果我为每个帧制作一个.png
然后生成
.mp4
和ffmpeg
使用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。希望有帮助。