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
选项,则使用复制编解码器的二进制文件。检查
使用 ffprobe
、mp4trackdump
或 mp4file --dump
的缺少帧的文件表明,如果其采样时间恰好是
与编辑列表的末尾相同。当我制作一个没有掉帧的文件时,它
仍然有两个编辑列表:唯一的区别是编辑的结束时间
列表超出了没有丢帧的文件中的所有示例。虽然这
这不是一个公平的比较,如果我为每一帧制作一个 .png
然后生成
.mp4
和 ffmpeg
使用 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)...)
编辑:一些我已经尝试过的东西
- 将流持续时间明确设置为与我添加的帧数相同的数字,或者多一些
- 将流开始时间明确设置为零,而第一帧的 PTS 为 1
- 使用编码器参数,以及
gop_size
,使用 B 帧等
- 为 mov/mp4 多路复用器设置私有数据以设置 movflag
negative_cts_offsets
- 改变帧率
- 尝试了不同的像素格式,例如AV_PIX_FMT_YUV420P
另外要明确的是,我可以忽略编辑列表将文件转移到另一个文件来解决这个问题,我希望首先不要制作损坏的 mp4 文件。
我有一个类似的问题,最后一帧丢失,这导致计算出的 FPS 与我预期的不同。
您似乎没有设置 AVPacket 的持续时间字段。我发现依靠自动持续时间(将该字段保留为 0)显示了您描述的问题。
如果你有恒定的帧率,你可以计算持续时间应该是多少,例如对于 25 FPS 的 12800 时基(= 1/25 秒),将其设置为 512。希望对您有所帮助。
我正在尝试使用 libavformat 创建 .mp4
视频
使用单个 h.264 视频流,但结果文件中的最后一帧
通常持续时间为零,并且有效地从视频中删除。
奇怪的是,最后一帧掉不掉取决于有多少
我尝试添加到文件中的帧。我在下面概述的一些简单测试使
我认为我以某种方式错误配置了 AVFormatContext
或
h.264 编码器,导致两个编辑列表有时会切断最终的
框架。我还将 post 我正在使用的代码的简化版本,以防我
犯了一些明显的错误。任何帮助将不胜感激:我一直
这几天一直在为这个问题苦苦挣扎,但进展甚微。
我可以通过使用 ffmpeg
创建一个新的 mp4 容器来恢复丢失的帧
如果我使用 -ignore_editlist
选项,则使用复制编解码器的二进制文件。检查
使用 ffprobe
、mp4trackdump
或 mp4file --dump
的缺少帧的文件表明,如果其采样时间恰好是
与编辑列表的末尾相同。当我制作一个没有掉帧的文件时,它
仍然有两个编辑列表:唯一的区别是编辑的结束时间
列表超出了没有丢帧的文件中的所有示例。虽然这
这不是一个公平的比较,如果我为每一帧制作一个 .png
然后生成
.mp4
和 ffmpeg
使用 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)...)
编辑:一些我已经尝试过的东西
- 将流持续时间明确设置为与我添加的帧数相同的数字,或者多一些
- 将流开始时间明确设置为零,而第一帧的 PTS 为 1
- 使用编码器参数,以及
gop_size
,使用 B 帧等 - 为 mov/mp4 多路复用器设置私有数据以设置 movflag
negative_cts_offsets
- 改变帧率
- 尝试了不同的像素格式,例如AV_PIX_FMT_YUV420P
另外要明确的是,我可以忽略编辑列表将文件转移到另一个文件来解决这个问题,我希望首先不要制作损坏的 mp4 文件。
我有一个类似的问题,最后一帧丢失,这导致计算出的 FPS 与我预期的不同。
您似乎没有设置 AVPacket 的持续时间字段。我发现依靠自动持续时间(将该字段保留为 0)显示了您描述的问题。 如果你有恒定的帧率,你可以计算持续时间应该是多少,例如对于 25 FPS 的 12800 时基(= 1/25 秒),将其设置为 512。希望对您有所帮助。