网页端视频fps帧率非标准24、25、30、50、60fps音画不同步问题 时间: 2026-05-20 16:39 分类: JAVA ###前言 最近在网上看一些视频,发现老是出现播着播着就音画不同步的问题,但是下载下来使用potplayer播放却又没有问题。 于是搜索了很多资料,但最终都未果,虽然没有直接找到解决方案,但是也根据资料了解了不少知识,最终根据自己的理解找到了解决办法。 ###分析 一开始搜索出来的结果都是说什么关闭浏览器硬件加速啥的,结果根本没有效果。后面发现,出现这种问题的视频大多都是`fps`为小数,可能都是动态帧率的视频。 `24.69 fps, 25 tbr, 90k tbn`这是网页上音画不同步视频的相关信息,并且都是播着播着画面就跟不上声音,所以猜测:将视频播放加快,将视频帧率改为`25`就可以了,但这就又出现了一个问题:如何在不重编码的情况下修改`fps`,尝试过`ffmpeg`来修改,但未果,最终发现使用`mkvmerge --default-duration`可以成功实现帧率修改,本以为这就可以了,但是帧率修改成功,意味着每秒播放帧数变多,视频时长也会相应缩短,而通过`ffprobe`查看原始视频文件音视频信息,发现音频时长没变,还是音画不同步。 到了这里,真想破口大骂那个网站哪找的视频,明明是25帧率的视频,非要去转为24.69帧率的视频,而网页端浏览器又对于这种帧率的视频处理有BUG。 既然单纯改了视频帧率不行,那么要想将音频时长与之对应该怎么办,音频没有帧率一说,所以只能通过`加速快进`来调整,好在`mkvmerge`够强大,最开始一直想着`ffmpeg`来修改,但根本没有我要的效果。 最终加速音频的命令如下: > mkvmerge --sync 1:0,倍速值 帧率变为25的视频时长缩短了,要想音画同步,那就得将24.69帧率对应的音频时长调整为与25帧率对应的时长,简单粗暴的解决办法就是直接用`25帧率视频时长/24.69帧率视频时长`,我最后写了个脚本用来矫正在网页上音画不同步的视频: ``` #!/bin/bash # 用法: ./fix_audio_duration.sh set -e # 遇到错误立即退出 INPUT="$1" # 检查输入参数 if [ -z "$INPUT" ]; then echo "用法: $0 " exit 1 fi # 检查文件是否存在 if [ ! -f "$INPUT" ]; then echo "错误: 文件不存在 - $INPUT" exit 1 fi # 检查必要命令是否存在 for cmd in ffprobe mkvmerge; do if ! command -v "$cmd" &> /dev/null; then echo "错误: 未找到命令 $cmd,请先安装" exit 1 fi done echo "正在分析文件: $INPUT" # 函数:从 Metadata 中读取 DURATION # 参数: $1 = 轨道类型 (v:0 或 a:0) # 输出: 时长(秒) get_duration_from_metadata() { local stream="$1" # 尝试从流的 Metadata 中读取 DURATION duration=$(ffprobe -v error \ -select_streams "$stream" \ -show_entries stream_tags=DURATION \ -of default=noprint_wrappers=1:nokey=1 \ "$INPUT" 2>/dev/null) # 如果找到了 DURATION(格式通常是 "HH:MM:SS.ms" 或秒数) if [ -n "$duration" ] && [ "$duration" != "N/A" ]; then # 判断格式:如果包含冒号,则是时间格式 HH:MM:SS.ms if [[ "$duration" == *":"* ]]; then # 将 HH:MM:SS.ms 转换为秒 echo "$duration" | awk -F: '{ print $1*3600 + $2*60 + $3 }' else # 直接是秒数 echo "$duration" fi else echo "" fi } # 函数:从 format metadata 中获取 DURATION(备选方案) get_format_duration() { duration=$(ffprobe -v error \ -show_entries format_tags=DURATION \ -of default=noprint_wrappers=1:nokey=1 \ "$INPUT" 2>/dev/null) if [ -n "$duration" ] && [ "$duration" != "N/A" ]; then if [[ "$duration" == *":"* ]]; then echo "$duration" | awk -F: '{ print $1*3600 + $2*60 + $3 }' else echo "$duration" fi else echo "" fi } fps_fraction=$(ffprobe -v error -select_streams v:0 -show_entries stream=avg_frame_rate -of default=noprint_wrappers=1:nokey=1 "$INPUT" 2>/dev/null) # 将分数转为小数 if [[ "$fps_fraction" == *"/"* ]]; then num=$(echo "$fps_fraction" | cut -d'/' -f1) den=$(echo "$fps_fraction" | cut -d'/' -f2) if [ "$den" -ne 0 ]; then fps=$(awk "BEGIN {printf \"%.6f\", $num / $den}") else fps=0 fi else fps="$fps_fraction" fi # 四舍五入 fps_rounded=$(awk "BEGIN {printf \"%.0f\", $fps}") #先进行一次转换 mkvmerge -o tmp_output.mkv --default-duration 0:${fps_rounded}fps "$INPUT" mv tmp_output.mkv "$INPUT" # 获取视频时长(优先从 metadata 读取) echo "获取视频时长..." VIDEO_DURATION=$(get_duration_from_metadata "v:0") # 如果视频 metadata 中没有,尝试从 format metadata 获取 if [ -z "$VIDEO_DURATION" ]; then echo "视频 metadata 中未找到 DURATION,尝试从 format 获取..." VIDEO_DURATION=$(get_format_duration) fi # 如果还是没有,回退到 stream.duration if [ -z "$VIDEO_DURATION" ]; then echo "回退到 stream.duration..." VIDEO_DURATION=$(ffprobe -v error \ -select_streams v:0 \ -show_entries stream=duration \ -of default=noprint_wrappers=1:nokey=1 \ -read_intervals "%+#INF" \ "$INPUT" 2>/dev/null) fi # 获取音频时长(优先从 metadata 读取) echo "获取音频时长..." AUDIO_DURATION=$(get_duration_from_metadata "a:0") # 如果音频 metadata 中没有,尝试从 format metadata 获取 if [ -z "$AUDIO_DURATION" ]; then echo "音频 metadata 中未找到 DURATION,尝试从 format 获取..." AUDIO_DURATION=$(get_format_duration) fi # 如果还是没有,回退到 stream.duration if [ -z "$AUDIO_DURATION" ]; then echo "回退到 stream.duration..." AUDIO_DURATION=$(ffprobe -v error \ -select_streams a:0 \ -show_entries stream=duration \ -of default=noprint_wrappers=1:nokey=1 \ -read_intervals "%+#INF" \ "$INPUT" 2>/dev/null) fi # 检查是否成功获取时长 if [ -z "$VIDEO_DURATION" ] || [ "$VIDEO_DURATION" = "N/A" ]; then echo "错误: 无法获取视频时长" echo "请尝试手动查看 metadata: ffprobe -v error -show_entries stream_tags=DURATION -select_streams v:0 \"$INPUT\"" exit 1 fi if [ -z "$AUDIO_DURATION" ] || [ "$AUDIO_DURATION" = "N/A" ]; then echo "错误: 无法获取音频时长" echo "请尝试手动查看 metadata: ffprobe -v error -show_entries stream_tags=DURATION -select_streams a:0 \"$INPUT\"" exit 1 fi echo "视频时长: $VIDEO_DURATION 秒" echo "音频时长: $AUDIO_DURATION 秒" # 计算差值(秒) DIFF=$(echo "$AUDIO_DURATION - $VIDEO_DURATION" | bc) echo "差值: $DIFF 秒" # 判断是否需要调整(允许 0.1 秒的误差) THRESHOLD=0.1 if (( $(echo "$DIFF > -$THRESHOLD && $DIFF < $THRESHOLD" | bc -l) )); then echo "音视频时长基本一致(差值 < ${THRESHOLD}秒),无需调整" exit 0 fi # 计算比例因子 = 视频时长 / 音频时长 RATIO=$(echo "scale=10; $VIDEO_DURATION / $AUDIO_DURATION" | bc) echo "比例因子: $RATIO (视频时长 / 音频时长)" # 生成输出文件名 OUTPUT="${INPUT%.*}_fixed.mkv" TEMP_MKV="${INPUT%.*}_temp.mkv" # 转换为毫秒(整数,避免浮点精度问题) VIDEO_MS=$(echo "$VIDEO_DURATION * 1000" | bc | cut -d. -f1) AUDIO_MS=$(echo "$AUDIO_DURATION * 1000" | bc | cut -d. -f1) echo "正在使用 mkvmerge 进行音频拉伸..." echo "命令: mkvmerge -o \"$TEMP_MKV\" --sync 1:0,${VIDEO_MS}/${AUDIO_MS} \"$INPUT\"" echo "命令: mkvmerge -o \"$TEMP_MKV\" --sync 1:0,${VIDEO_MS}/${AUDIO_MS} \"$INPUT\"" # 执行 mkvmerge if mkvmerge -o "$TEMP_MKV" \ --sync 1:0,${VIDEO_MS}/${AUDIO_MS} \ "$INPUT" 2>&1; then echo "✓ 音频拉伸完成: $TEMP_MKV" # 验证修复后的文件 echo "验证修复结果..." NEW_VIDEO=$(ffprobe -v error \ -show_entries stream_tags=DURATION \ -select_streams v:0 \ -of default=noprint_wrappers=1:nokey=1 \ "$TEMP_MKV" 2>/dev/null) NEW_AUDIO=$(ffprobe -v error \ -show_entries stream_tags=DURATION \ -select_streams a:0 \ -of default=noprint_wrappers=1:nokey=1 \ "$TEMP_MKV" 2>/dev/null) echo "修复后视频 DURATION: $NEW_VIDEO" echo "修复后音频 DURATION: $NEW_AUDIO" # 重命名最终文件 mv "$TEMP_MKV" "$OUTPUT" mv "$OUTPUT" "${INPUT}" echo "最终输出文件: $OUTPUT" else echo "✗ mkvmerge 执行失败" exit 1 fi echo "完成!" ``` 最终测试,完美解决此类网页端音画不同步的视频。 标签: 无