我们可以使用修改时间戳PyAV使用称为“重新复用”的过程。
“重新复用”是用于替换流的容器而不重新编码流的术语。
例如,假设视频文件类型是MP4,并且该文件包括用H.264视频编解码器编码的单个视频流。
我们可以替换或修改 MP4 容器,而不对视频进行解码和编码。
由于时间戳是容器的一部分,我们可以使用重新复用过程来修改时间戳(与重新编码相比,主要优点是视频质量可以完美保留)。
重新复用解决方案基于以下代码示例PyAV 文档
为了进行测试,我们可以首先使用 FFmpeg CLI 创建一个简短的合成 MP4 视频文件(10 帧,1fps):
ffmpeg -y -f lavfi -i testsrc=size=192x108:rate=1:duration=10 -vcodec libx264 -crf 10 -pix_fmt yuv444p input.mp4
我们可以使用 FFprobe 查看时间戳:
ffprobe -show_packets input.mp4
Output:
pts=0
pts_time=0.000000
dts=-32768
dts_time=-2.000000
duration=16384
duration_time=1.000000
...
pts=65536
pts_time=4.000000
dts=-16384
dts_time=-1.000000
duration=16384
duration_time=1.000000
...
pts=32768
pts_time=2.000000
dts=0
dts_time=0.000000
duration=16384
duration_time=1.000000
...
pts=16384
pts_time=1.000000
dts=16384
dts_time=1.000000
duration=16384
duration_time=1.000000
...
pts_time=3.000000
可以看到,DTS时间戳是依次增加的,但是从-2
.
PTS 时间戳计数 0, 4, 2, 1, 3...(非单调计数的原因是使用B-Frames)
我们还可以看到原始时间戳以每秒 16384 个刻度为单位(1/16384 是 MP4 容器使用的时基)。
所有帧的持续时间均为 1 秒(16384 个刻度)。
为了进行测试,我们将使用包含 10 个时间戳的列表(以秒为单位):
new_pts_list = [0.0, 1.0, 5.0, 7.0, 8.0, 18.0, 19.0, 20.0, 21.0, 22.0]
大间隙用于测试。
计算列表中时间戳的索引:
由于 PTS 时间戳不是连续的(并且 DTS 不是从 0 开始),因此我们必须计算应用时间戳的帧的索引。
当我们有了索引后,我们可以从列表中获取新的时间戳。
通过数据包的PTS计算索引:
index_of_old_pts = int(np.round(float(packet.pts) / float(packet.duration)))
通过数据包的DTS计算索引:
index_of_old_dts = int(np.round(float(packet.dts) / float(packet.duration)))
从列表中获取更新的时间戳:
new_pts = new_pts_list[index_of_old_pts]
new_dts = new_pts_list[index_of_old_dts]
注意:索引计算仅适用于恒定帧速率输入视频。
获得更新的时间戳后,我们必须将它们从秒转换为时基刻度单位,并修改数据包的时间戳:
new_pts_in_timebase_units = int(np.round(new_pts / packet.time_base))
new_dts_in_timebase_units = int(np.round(new_dts / packet.time_base))
packet.pts = new_pts_in_timebase_units
packet.dts = new_dts_in_timebase_units
为了提高时间戳的准确性,我决定不依赖输入的时基,并将输出的时基设置为 1/1000000 秒。
out_video_stream.time_base = Fraction(1, 1000000)
代码示例:
import av
import numpy as np
from fractions import Fraction
# Build 1 fps input file using FFmpeg CLI (for testing):
# ffmpeg -y -f lavfi -i testsrc=size=192x108:rate=1:duration=10 -vcodec libx264 -crf 10 -pix_fmt yuv444p input.mp4
input_video_file = 'input.mp4'
output_video_file = 'output.mp4'
# List of new PTS in unit of seconds (set long gaps for testing)
new_pts_list = [0.0, 1.0, 5.0, 7.0, 8.0, 18.0, 19.0, 20.0, 21.0, 22.0]
average_out_frame_period = np.mean(np.diff(np.array(new_pts_list))) # Compute the average frame period (used for negative DTS).
# https://pyav.org/docs/develop/cookbook/basics.html#remuxing
with av.open(input_video_file, 'r') as inp:
inp_video_stream = inp.streams.video[0] # Get video stream - assume the input has only one stream, and that stream is a video stream.
with av.open(output_video_file, 'w', format="mp4") as out: # Open output file, set format to mp4
out_video_stream = out.add_stream(template=inp_video_stream) # Add the input stream to the output container.
out_video_stream.time_base = Fraction(1, 1000000) # Set the timebase to 1/1000000 for improving accuracy.
for i, packet in enumerate(inp.demux(inp_video_stream)): # Demux the input video stream
if packet.dts is None:
continue # When DTS = None, marks that the packet should be ignored.
packet.stream = out_video_stream
old_timebase = packet.time_base
packet.time_base = out_video_stream.time_base # Set the timebase to 1/1000000 for improving accuracy.
index_of_old_pts = int(np.round(float(packet.pts) / float(packet.duration))) # Convert the PTS from time-base ticks to frame index of that PTS.
if index_of_old_pts < 0 or index_of_old_pts >= len(new_pts_list):
new_pts = index_of_old_pts*average_out_frame_period # Negative PTS (not supposed to exist) - use average frame period for setting the new PTS.
else:
new_pts = new_pts_list[index_of_old_pts] # Get the value by the index of PTS in new_pts_list.
new_pts_in_timebase_units = int(np.round(new_pts / packet.time_base)) # Convert from second to units of time base
packet.pts = new_pts_in_timebase_units # Update the PTS of the packet
index_of_old_dts = int(np.round(float(packet.dts) / float(packet.duration))) # Convert the DTS from time-base ticks to frame index of that DTS.
if index_of_old_dts < 0 or index_of_old_dts >= len(new_pts_list):
new_dts = index_of_old_dts*average_out_frame_period # Negative DTS - use average frame period for setting the new DTS.
else:
new_dts = new_pts_list[index_of_old_dts] # Get the value by the index of PTS in new_pts_list.
new_dts_in_timebase_units = int(np.round(new_dts / packet.time_base)) # Convert from second to units of time base
packet.dts = new_dts_in_timebase_units # Update the DTS of the packet
#packet.duration = new_packet_duration_in_timebase_units #attribute 'duration' of 'av.packet.Packet' objects is not writable
out.mux(packet)
使用 FFprobe 测试输出:
ffprobe -show_packets output.mp4
Output:
pts=0
pts_time=0.000000
dts=-4666667
dts_time=-4.666667
duration=1000000
duration_time=1.000000
...
pts=7000000
pts_time=7.000000
dts=-2333333
dts_time=-2.333333
duration=1000000
duration_time=1.000000
...
pts=5000000
pts_time=5.000000
dts=0
dts_time=0.000000
duration=1000000
duration_time=1.000000
...
pts=1000000
pts_time=1.000000
dts=1000000
dts_time=1.000000
duration=1000000
duration_time=1.000000
...
pts=6000000
pts_time=6.000000
dts=5000000
dts_time=5.000000
duration=1000000
duration_time=1.000000
...
pts=17000000
pts_time=17.000000
dts=6000000
dts_time=6.000000
duration=1000000
duration_time=1.000000
...
如您所见,时间戳与给定列表匹配。
请注意,我们无法修改duration
数据包的长度,因为持续时间存储在 H.264 视频流中,而不是 MP4 容器中。
PyAV 不支持修改帧持续时间而不重新编码(但使用现代视频播放器播放视频时应该不成问题)。
动画 GIF 输出示例: