MoWorld 生成结果保存为视频文件的格式转换方案

文章导读
MoWorld 生成结果如果直接保存为 .bin 或自定义扩展名文件,通常不是标准视频容器,播放器无法直接识别。需要先确认文件内部是原始像素帧、带时间戳的帧序列,还是图像序列打包,再决定用 ffmpeg 重新封装还是先提取再转码。下面给出针对常见原始帧输出的识别、提取和封装流程。
📋 目录
  1. 用 file 命令识别输出文件类型
  2. 提取原始帧数据与时间戳
  3. 使用 ffmpeg 封装为 mp4
  4. 检查音画同步与帧率标签
  5. 写一个自动转换脚本示例
A A

MoWorld 生成结果如果直接保存为 .bin 或自定义扩展名文件,通常不是标准视频容器,播放器无法直接识别。需要先确认文件内部是原始像素帧、带时间戳的帧序列,还是图像序列打包,再决定用 ffmpeg 重新封装还是先提取再转码。下面给出针对常见原始帧输出的识别、提取和封装流程。

适用场景:拿到 MoWorld 输出的非标准视频文件,希望转为电脑和手机可播放的 MP4。操作方向:先用 file 判断类型,再根据宽高、像素格式和帧率用 ffmpeg 封装;若文件带自定义帧头,则需要先用 Python 提取裸帧和时间戳。验证方式:封装后使用 ffprobe 查看 avg_frame_rate 与 nb_frames,播放确认音画同步。风险边界:MoWorld 输出格式不确定时,盲目指定 -s、-pix_fmt 和 -r 会导致花屏或时长错误,必须先确认参数。

用 file 命令识别输出文件类型

在转换前,先对 MoWorld 生成的文件执行 file output.bin,查看输出的文件类型说明。该命令会提示文件是“data”还是带有特定编码头的流。如果输出为 raw videovideo data,说明是裸帧数据;如果输出为一堆图片文件,则是图像序列;如果显示为 MP4、MOV 等容器名称,说明已经有封装,只是扩展名不对。

这个步骤能避免盲目用 ffmpeg 去猜格式。比如文件实际是 YUV420p 的裸帧,却按 RGB24 解析,画面颜色会完全错乱。建议同时查看文件大小,结合分辨率和帧数估算是否符合裸帧体积(宽×高×像素字节数×帧数)。这一步不需要精确计算,主要是建立判断方向。

提取原始帧数据与时间戳

MoWorld 某些输出会在帧头写入自定义标记或时间信息,直接交给 ffmpeg 会把它当成像素数据。这时需要用 Python 读取二进制帧,记录宽高和帧标记,导出时间戳序列。以下是一个通用读取骨架,需要根据实际帧头结构修改偏移量:

import struct, json

width, height = 640, 360
frame_size = width * height * 3  # 假设 RGB24
pix_fmt = 'rgb24'

timestamps = []
raw_frames = b''

with open('output.bin', 'rb') as f:
    while True:
        header = f.read(8)
        if len(header) < 8:
            break
        # 假设前 4 字节是帧号,后 4 字节是时间戳(整数毫秒)
        frame_no, ts_ms = struct.unpack('<II', header)
        frame_data = f.read(frame_size)
        if len(frame_data) < frame_size:
            break
        raw_frames += frame_data
        timestamps.append({'frame': frame_no, 'timestamp_ms': ts_ms})

with open('extracted_frames.rgb', 'wb') as out:
    out.write(raw_frames)

with open('timestamps.json', 'w') as jf:
    json.dump(timestamps, jf, indent=2)

执行后得到 extracted_frames.rgbtimestamps.json。时间戳文件可以用于后续检查帧间隔是否均匀,也能作为设置 ffmpeg 帧率的依据。如果帧数据直接是连续裸流,没有自定义头,这一步可以跳过。

MoWorld 生成结果保存为视频文件的格式转换方案

使用 ffmpeg 封装为 mp4

对于已经确认是裸 RGB 数据的文件,可以直接用 ffmpeg 封装。注意参数中的分辨率、像素格式和帧率必须与 MoWorld 输出一致。示例如下:

ffmpeg -f rawvideo -pix_fmt rgb24 -s 640x360 -r 30 -i input.bin -c:v libx264 out.mp4

如果文件是从 extracted_frames.rgb 提取出来的,就把 input.bin 替换成该文件名。若原始像素格式是 yuv420p,则将 -pix_fmt rgb24 改为 -pix_fmt yuv420p。这里没有指定码率,libx264 会根据内容自动选择,通常足够播放。封装后建议用播放器打开检查是否出现绿屏、花屏或时长异常。

如果 MoWorld 输出的是图像序列(如 frame_0001.png),则改用 ffmpeg -framerate 30 -i frame_%04d.png -c:v libx264 out.mp4。总之,核心原则是把原始数据转换为标准编码流后再放入 MP4 容器。

检查音画同步与帧率标签

封装完成后,用 ffprobe 查看输出文件的帧率标签和帧数:

MoWorld 生成结果保存为视频文件的格式转换方案
ffprobe -v error -select_streams v:0 -show_entries stream=avg_frame_rate,nb_frames -of default=noprint_wrappers=1 out.mp4

检查 avg_frame_rate 是否为期望值(如 30/1),nb_frames 是否与原始帧数一致。如果 ffprobe 显示的帧率是 0/0 或者实际播放卡顿,可以在封装时加 fps 滤镜强制统一帧率:

ffmpeg -f rawvideo -pix_fmt rgb24 -s 640x360 -r 30 -i input.bin -vf "fps=30" -c:v libx264 out.mp4

注意:fps 滤镜会复制或丢弃帧来达到目标帧率,并非无损操作。如果 MoWorld 输出本身带有不均匀时间戳,建议先分析 timestamps.json 中的间隔,再决定是否需要调整帧率。若源数据没有音频轨道,ffprobe 不会显示音频信息,这属于正常现象。

写一个自动转换脚本示例

批量处理多个 MoWorld 输出文件时,可以用 Python 的 subprocess 调用 ffmpeg,遍历目录并保存转换日志。以下脚本扫描当前目录下所有 .bin 文件,假设它们都是 640x360 RGB24 30fps 的裸流:

import subprocess, pathlib, logging

logging.basicConfig(filename='convert.log', level=logging.INFO,
                    format='%(asctime)s - %(message)s')

for bin_file in pathlib.Path('.').glob('*.bin'):
    out_file = bin_file.with_suffix('.mp4')
    cmd = [
        'ffmpeg', '-y',
        '-f', 'rawvideo',
        '-pix_fmt', 'rgb24',
        '-s', '640x360',
        '-r', '30',
        '-i', str(bin_file),
        '-c:v', 'libx264',
        str(out_file)
    ]
    result = subprocess.run(cmd, capture_output=True, text=True)
    if result.returncode == 0:
        logging.info(f'OK: {bin_file.name} -> {out_file.name}')
    else:
        logging.error(f'FAIL: {bin_file.name}: {result.stderr}')

print('转换结束,详情见 convert.log')

脚本会把成功和失败的记录写入 convert.log。如果实际文件的分辨率、像素格式或帧率不同,需要先修改参数,或者先通过 file 命令和 ffprobe 验证结果。对于带有自定义帧头的文件,脚本中需要加入提取逻辑,不能直接用原始文件喂给 ffmpeg。这样批量转换时就不用每个文件手动敲命令了。