解决Moviepy剪辑视频画面卡帧,但有声的问题
编辑1. 起因
最近在做视频批量化处理,调研要么用moviepy,要么用ffmpeg。后面选择用moviepy,因为上手更简单一点。
但是在根据逻辑(删除视频的前两秒和后两秒,只保留中间部分)进行裁剪时,发现只要经过裁剪的视频,都会出现一个问题: 前1s左右的画面能而动,但是后面的视频画面不动,但是声音继续播放
这是非常致命的,这相当于就是把整个视频毁了,因此需要排查解决。
2. 思考过程
先简单看一下我写的代码
# 根据逻辑裁剪视频
if duration >= 6:
logger.info(f"视频 {video_file} 大于6秒,前后裁剪2秒。")
# 计算裁剪的起始和结束时间
start_time = 2
end_time = duration - 2
new_clip = clip.subclip(start_time, end_time)
# 处理裁剪后还是大于10s的视频
if new_clip.duration >= 10:
print(f"视频 {video_file} 大于10秒,加速1.2倍。")
new_clip = speedx(new_clip, factor=1.2)
else:
# 小于6秒的视频不进行裁剪
new_clip = clip
# 保存裁剪后的视频
new_clip.write_videofile(video_file, remove_temp=True, fps=clip.fps)
可以看到 有两个地方会涉及到视频的处理:
视频时长
duration >= 6
大于等于6s裁剪后的时长
new.clip.duration >= 10
还是大于等于10s,那么就对视频进行加速处理
2.1 思考一
因为出现“卡帧”的情况最早是挺随机的,只在后面加速视频的时候出现,我就先删除这部分逻辑,也就仅保留
# 根据逻辑裁剪视频
if duration >= 6:
logger.info(f"视频 {video_file} 大于6秒,前后裁剪2秒。")
# 计算裁剪的起始和结束时间
start_time = 2
end_time = duration - 2
new_clip = clip.subclip(start_time, end_time)
else:
# 小于6秒的视频不进行裁剪
new_clip = clip
# 保存裁剪后的视频
new_clip.write_videofile(video_file, remove_temp=True, fps=clip.fps)
但是发现,我这么改了以后,之前裁剪的视频,也一样会出现“卡帧”的情况。
因此,这部分肯定不是问题所在。
2.2 思考二
后面我就在想,是不是moviepy这个库的的问题,于是我就想,直接换成ffmpeg来处理
cmd = [
"ffmpeg",
"-y", # 自动确认覆盖输出文件
"-i",
input_file,
"-ss",
str(2), # 起始时间
"-t",
str(duration - 4), # 持续时间
"-c:v",
"-c:a",
"-strict",
output_file,
]
此时,我以为解决了,满怀信心的运行,发现出来的视频还是一样的卡帧= =
至此,陷入了一个死循环,本身我是知道moviepy底层其实调用的也是ffmpeg,我以为是moviepy封装ffmpeg的时候导致的一些问题,但是现在看来不是,因为我直接使用ffmpeg,还是一样的问题。因此,可以断定,问题出在ffmpeg上面。
开始寻找解决方案,功夫不负有心人,终于看到了一篇blog!并且其附带了一个视频~ 因此,在看了blog和视频后,终于终于解决方案出来了!
3.解决方案
重点如下:
因为I帧的关系,视频解码时从I帧开始的,如果你的开始时间点不是I帧,则只先解码音频,等到下一个I帧时间点时,开始播放视频,之前卡的那个画面也是下一个I帧。可以用aegisub看I帧,知道前后I帧的位置,决定从哪个I帧开始截取,或者把-ss写在-i前面,但此时不能用-to,只能用-t。
讲这么多,其实我也不懂前半段句是什么意思。后面简单看了看文档,总结概括如下:
视频帧类型:
在视频压缩中,帧被分为不同类型,主要有三种:I帧(关键帧)、P帧(预测帧)和B帧(双向预测帧)。
I帧是自包含的帧,不依赖于其他帧的信息,是视频解码的起点和参考点。P帧和B帧则依赖于其他帧的信息。
视频解码的开始:
当解码器开始解码视频流时,它必须从一个I帧开始。如果你指定的起始时间点不是一个I帧,解码器会等待直到下一个I帧出现才开始解码视频部分。
在等待期间,音频部分会继续解码和播放,这会导致画面停滞在上一个I帧的图像。
简单来说 I帧非常重要 裁剪必须是他起手,但是在我这种设定下,很明显 不好实现。
再看后半句,总结了一下:
要避免卡帧,需要确保 -ss 参数指定的时间点正好是一个I帧的位置。
可以通过使用 aegisub 或者查阅视频信息来确定合适的起始时间点。
如果无法确保,可以通过调整 -ss 和 -t 参数的组合来控制视频裁剪的时长,以确保视频从一个完整的I帧开始解码。
我并不想去确认关键帧的位置,因为他相对来说 比较麻烦。
一开始的指令就是 把 -ss 放在 -t 前面 发现其实并不管用,因此 这个方法只能算放弃了= =
但是但是,在这个方法的启发下,我突然意识到,如果保存的时候,我重新编码,是不是就能解决!
因此,最终的解决方案如下:
# 导出处理后的视频(重新编码)
new_clip.write_videofile(
output_file,
codec="libx264",
audio_codec="aac",
temp_audiofile="temp-audio.m4a",
remove_temp=True,
fps=clip.fps,
preset="medium", # 可以根据需要调整,如 "fast", "slow" 等
ffmpeg_params=["-crf", "0"], # 控制质量,值越低质量越高,0表示完整保留视频画质,不压缩
)
终于终于,解决了!!!!
其实核心在
crf
是ffmpeg中控制视频编码质量的参数,0 表示无损编码。当设置为0时,每一帧都会被认为是关键帧(I帧),因为无损编码要求每一帧都能独立解码,不依赖于其他帧的信息。当调用
write_videofile
并指定codec="libx264"
和audio_codec="aac"
时,moviepy
将使用libx264
编码器重新对视频进行编码,同时将音频编码为 AAC 格式。在重新编码过程中,ffmpeg会生成新的关键帧序列,确保新输出的视频从一个I帧开始,避免了旧视频中可能存在的不完整的关键帧问题。
到这里所有问题都解决啦,但是其中还有需要注意点
虽然这种方法解决了卡帧问题,但重新编码可能会导致一定的质量损失,并且会消耗更多的处理时间和计算资源。因此,建议在选择重新编码作为解决方案时,权衡好输出质量和性能需求。
- 0
- 0
-
分享