zhec.moe


MiniDV 磁带采集笔记

省流:最终采集方案

硬件

  1. Sony DCR-TRV40E 摄像机

  2. MiniDV 磁带 10 盒

  3. ThinkPad T410

  4. 1394 4-pin to 4-pin 线

  5. Sony MiniDV 新磁带 2 盒(2DVM60R3,非必须)

  6. Sony MiniDV 干式清洁带(DVM4CLD2,非必须)

软件

  1. T410 安装操作系统:Ubuntu 24.04 LTS

  2. dvgrab:直接从 apt 安装,源代码在 https://github.com/ddennedy/dvgrab

一、将摄像机连接到电脑

2022 年 1 月 29 日,我在家里“重新发现”了一台旧的索尼摄像机(DCR-TRV40E)和一台照相机(DSC-W1)。另有 10 盒 MiniDV 磁带和 3 条 Memory Stick 保存在一个红色的布盒子里,Memory Stick 的容量分别是 8 MB、128 MB 和 1 GB。我在索尼的产品支持页面找到了 DCR-TRV40E 摄像机的使用说明书

第二天,我(好像是用读卡器,记不清了)把 Memory Stick 中的全部文件复制到电脑上,除了一些系统文件外都是照片和视频。至于 MiniDV 磁带,虽然可以在摄像机的屏幕上观看,但我不知道如何将摄像机连接到电脑,也就无法将视频导入电脑中。好消息是,MiniDV 磁带上的数据已经是数字的了,所以不需要像 VHS 等更老的磁带格式一样进行数字化转换,只需要考虑怎么将它无损传输到电脑。

失败和错误的尝试

1. Sony PlayMemories Home

索尼在 DCR-TRV40E 的支持页面列出了这个软件,支持 Windows 和 Mac。我在安装之后发现没有什么作用。它里面提供了一些图片、视频管理和剪辑的功能,但在底层仍然依赖操作系统识别摄像机硬件。本来识别不到的硬件并不会因为安装了它就能识别出来了。

2. 使用摄像机上的 USB 接口

说明书第 174 页

DCR-TRV40E 摄像机上有一个 USB Mini B 接口,可以通过一根 Mini B 到 Type A 的线连接到电脑上。但是,用这种方法连接之前必须先安装驱动程序。

说明书第 177 页

说明书第 14 页提到了这张 CD-ROM(SPVD-008 USB 驱动程序),但我在摄像机附带的一堆东西里没有找到,可能是存放时放到别的地方或者扔了。我在 Google 上搜索,找到几个驱动下载站(solodrivers[dot]com, driverfix[dot]com,现在看都是广告引流网站),下载了几个 .exe 格式的驱动安装程序。说明书提到驱动支持的操作系统最高到 Windows XP,所以我在 VMWare 里开了一个虚拟机,逐个尝试安装之后连接摄像机。结果自然是失败了,识别不到设备。

2023 年 2 月 19 日,我尝试在 archive.org 搜索,找到并下载了 CD-ROM 的镜像文件它的说明书,但是可能是后面忘记了,好像没有重新安装并测试连接。

2026 年 2 月 4 日写这篇文章时我又试了一下,在 Windows XP 虚拟机里加载光盘镜像,可以正常安装驱动和 PIXELA Image Mixer 软件。考虑到虚拟机的宿主系统没有这个驱动、可能识别不到,直接用 Windows XP 物理机连接摄像机,成功的概率应该更大。另外,我在其中一盒磁带里也看到当时刚买摄像机、测试连接电脑时录制的视频,视频中电脑屏幕上确实是 Image Mixer 这个软件界面,说明当时用 CD-ROM 是可以正常安装驱动、连接电脑的。

不过,这个软件的发布时间有点太久远了,开发公司早已经不知所终。我在说明书中看到它的设置支持使用 AVI 格式存储视频,但是看不到源代码,不知道这个 AVI 选项保存的是原始的 DV 数据,还是做了什么转换。综合来看,这恐怕不是最好的采集方法。

imx1.0_sony_en.pdf 第 61 页

3. 使用摄像机上的 IEEE1394 接口和 IEEE1394-USB 转接线

看上去这样连接是最方便的,但这两个接口无法转换,原因是协议完全不同:1394 接口传输 DV 数据流这种操作 USB 标准根本就不支持。所有声称能转换的线或者转接头都是假的。

(为什么上文中摄像机又可以通过 USB-USB 线连接电脑?我推测可能是摄像机内部先有一个数据转换,把 DV 数据转换成能在 USB 协议上传输的格式,再从 USB 传到电脑上。)

4. 使用摄像机上的 AUDIO/VIDEO 接口和线(在线的另一端有黄白红三个插头,又称为“莲花头”)

输出是模拟的,摄像机会先做一遍数模转换,采集时又要模数转换,画质损失较大。

5. 使用摄像机上的 S VIDEO(S 端子)接口和线

输出图像的质量比 A/V 接口稍高,但传输的仍然是模拟信号,画质有损失。

正确方法:使用摄像机上的 IEEE1394 接口和 1394 线,连接到电脑上的 1394 接口

经过详细调查,我确认在摄像机上的这么多接口中,只有 1394 接口支持 MiniDV 磁带数据的无损传输。

说明书 88 页

(i.LINK 是 Sony 对 1394 接口的别称,苹果称它为 FireWire。)

说明书 225 页

摄像机上有一个 1394 4-pin 的接口,因此需要连接一台有相同接口的电脑。

说明书 226 页

一般有这样几种连接方案:

  1. 用一张 1394 采集卡,插在台式机主板的 PCIe 插槽里。需要:有空闲 PCIe 槽的台式机。
  2. 用苹果的两个转换器拼起来:Thunderbolt 3 (USB-C) 转 Thunderbolt 2Thunderbolt 2 转 FireWire 800,再连一根 FireWire 800 转 FireWire 400(即 1394 4-pin)的线,这样可以将摄像机连到支持 Thunderbolt 3/4 的 USB-C 接口上。需要:买两个转换器,¥500多(这就是苹果价格);另外这个方案里电脑可能只能用 Mac,不一定支持带 Thunderbolt 接口的 Windows PC。
  3. 用一台带有 1394 接口的旧笔记本(或者有 ExpressCard 卡槽的旧笔记本加一张提供 1394 接口的 ExpressCard)。需要:有 1394 接口的笔记本。

由于我没有台式机,而方案 2 的钱又足够买到一台有 1394 接口的笔记本,所以我选择了方案 3。在闲鱼上蹲了几周之后,我在 2024 年 2 月 28 日买了一台 ThinkPad T410(¥180)。这台笔记本于 2010 年生产,配有 i5-540M CPU、4 GB DDR3 内存、NVIDIA NVS 3100M 显卡和 320GB 磁盘,最重要的是在右侧靠近屏幕转轴的位置有一个 IEEE 1394 接口。

ThinkPad T410i T410s T410si T510i T510 W510用户使用手册

2024 年 3 月 14 日,我又买了一根 1394 4-pin to 4-pin 的线(¥40)。第二天我在日本亚马逊上买了两盒同款的新磁带(874 日元)用于测试、一盒干式清洁带(584 日元)用于在采集的视频质量较差时清洗摄像机的磁头。(我买 1394 线时专门在闲鱼上挑了同城的买家,要求发顺丰同城,说明当时很急,但为什么收到后没有立即采集而要买磁带?是否当时就采集了,但画面质量有问题?我现在也记不清楚了)这样,采集所需的硬件就准备齐全了。

二、从摄像机采集 DV

由于买的磁带需要蹭我在任你购上买日本音乐 CD 大概每 3 个月发回国一次的包裹,直到 2024 年 5 月 8 日我才收到。由于各种记不清了的原因,这件事又搁置下去了,正式开始采集 MiniDV 磁带要等到 2025 年 1 月 19 日。即使之前已经准备了大量设备和工具,也做了些前期调研 [5][6],采集时仍然遇到了诸多问题。

软件选择

第一个问题就是选择哪个软件采集。之前在 Hacker News [1] 和 r/DataHoarder [2][3][4] 上搜索一番后,我发现了这几个选项:dvgrab、WinDV、ScenalyzerLive、DVRescue(及同一组织开发的DV Analyzer)。我给 T410 安装了 Windows 7 和 Ubuntu 16.04 的双系统,逐个测试。

Windows 上的两个软件——WinDV 和 Scenalyzer ,前者的界面太简陋,后者的界面太古老,而且最后发布的时间分别是 2003 年和 2006 年,之后就没有再维护了,我不敢信任它们俩。

Hacker News 讨论里还有人推荐 DVRescueDV Analyzer。但是我在 Windows 7 上用 DVRescue GUI 连接摄像机后识别不到设备。当时没读明白文档,也可能是受到 vrecord 的影响,误以为 DVRescue 的采集功能必须要连接它支持的 deck,看了一下巨贵。(后面在 Ubuntu 24 上使用 DVRescue 的 CLI 版本就连接成功了,发现文档也新加了一段,注明 Windows/Linux 的 GUI 版本暂不支持采集。)这两个软件功能很多,看得人眼花缭乱;文档内容又比较繁杂,读着费劲,不好上手。

最后还是从第一性原理分析。我现在手头上有的采集设备只有这台 DV 摄像机,而且这些磁带就是用它录的,读取效果肯定比其它 DV 设备好;买一个 deck 又太贵了。所以最终选择用 dvgrab 采集。这个软件包目前还在维护,而且 Ubuntu/Debian 上可以直接用 apt 安装:

sudo apt install dvgrab
dvgrab --help

至于 DVRescue 和 DV Analyzer 的一堆分析功能,它们只是分析视频哪里有错误,而没法修复错误——唯有 DVRescue 的 merge 功能除外。

第一次尝试

我先将所有 10 卷磁带快退回起点,并贴上便签,标注编号(tape01 到 tape10)和开头部分画面的内容。测试播放时画面条纹(术语叫 dropout)很多,用清洁带播放了 10 秒后似乎好一些了。

将摄像机用 1394 线连接到电脑上,然后运行 dvgrab,试着采集一盒 MiniDV 磁带:

dvgrab -autosplit -rewind -showstatus -size 0 -srt -timecode -timestamp tape01-pass1-

这些选项的含义分别是:

最终生成的文件名类似tape01-pass1-2005.07.23_16-45-22--00:12:25:22.dv-format选项使用默认的raw就很好,不需要专门指明。

摄像机播放磁带的同时,终端上显示的文件大小、帧数也滚动增加。大约 1 小时后,采集顺利完成。录像日期的 SRT 文件没什么用,我就删掉了。文件名中时间码的格式是hh:mm:ss:frame,而 Windows 系统不支持包含冒号:的文件名,所以最好不用这个选项。

修理摄像机故障

观看采集下来的视频时,我发现总是出现周期性的 dropout。播放磁带时把摄像机的外放声音调小,机器内部也有“哒、哒、哒”的响声,并且每次发出“哒”声时(摄像机屏幕上的)画面就会或多或少地 dropout。又采集了三卷磁带,问题相同。我开始怀疑是摄像机的问题。

我尝试用新磁带录一段播放看看,排除旧磁带老化的问题,但是发现录完播放出的画面是全黑,估计摄像机的镜头部分已经坏了。

在闲鱼上搜索同型号的 DV 摄像机:便宜的三四百,贵的七八百。于是我转而在小红书和闲鱼上找 DV 维修的师傅,找到一位评价很好的。1 月 19 日晚上询问了一下,尝试了把磁带快进快退,没有用。师傅判断是带仓故障,因为播放清洁带的时候也有响声。

将摄像机寄给师傅,师傅发现是机芯问题:主导轮的皮带有一处“毛”了,每转一圈转到这里就会卡一下。由于现在已经没法单独买到皮带,所以师傅把机芯整个换掉了。

维修中

拆下来的机芯(或者是准备换上去的?)

维修费用是¥320,我在 2025 年 2 月 26 日收到师傅寄回来的摄像机。

采集

再次将摄像机连接到电脑,运行 dvgrab:

dvgrab -autosplit -rewind -showstatus -size 0 -timestamp tape01-pass1-

检查这一次采集的视频文件,播放过程中(除了在两端视频切换处)画面上没有条纹,声音也正常。于是我继续采集其它的磁带。tape02 到 tape09 都没什么问题。tape10 的后半部分似乎有点问题,录制时间显示 2067.02.15_22-26-25 不动。由于每个片段开始时 dvgrab 会新创建文件,而新文件和旧文件同名,所以所有前面的片段都被覆盖掉了,只剩下最后的一个片段。无奈之下,我只能先加上 -timecode 选项:

dvgrab -autosplit -rewind -showstatus -size 0 -timestamp -timecode tape10-pass2-

采集完再把文件名中的所有:改成_

由于 MiniDV 磁带记录的本来就是数字数据,而从摄像机到电脑的数据传输也是数字的,因此这样采集的视频文件就是磁带内容的精确复制。DCR-TRV40E 使用 PAL 制式,每一个 DV 帧的大小固定为 144000 字节,每秒 25 帧,1 小时的视频大约占 13 GB 的存储空间。在 2000 年这还是一块硬盘存不下的大小,现在就稀松平常了。

三、修复数据错误

采集工作虽然完成了,但是还有一件事让我一直惦记着,就是 DVRescue 的 merge 功能,它能从对同一盒磁带的多次采集中,以帧和更小的数据块(Block)为单位,提取各个输入中的最佳选择并合并为一份最完整的数据。在播放采集到的视频文件时,我注意到还有少许画面有轻微的条纹,有一处两个镜头切换时比较严重。如果采集多次再用 DVRescue 合并就能获得错误更少的文件,那自然最好。

安装 DVRescue

2026 年 1 月 2 日,我把这一套采集设备重新拿出来、组装好,完整地再次阅读了 DVRescue 的文档。文档中添加了不少步骤和例子,比之前读时更完善一些。一个重要发现是它还支持 rewind 功能,在有错误的帧自动倒带重读,这应该能省下好几次完整采集的时间。

然而在 Windows 7 上,GUI 版本的窗口太大,T410 的小屏幕显示不完整;CLI 版本缺少 dll 无法运行。至于 Linux,官方 apt 源只支持这几年的 Ubuntu 版本(别的 distro 应该也一样),我之前装的 Ubuntu 16 太老了。

所以,我先把 Ubuntu 24 装在之前 16 的分区,中间由于 WiFi 时断时续失败了一次;然后添加 apt 源

wget https://mediaarea.net/repo/deb/repo-mediaarea-snapshots_1.0-26_all.deb
sudo dpkg -i repo-mediaarea-snapshots_1.0-26_all.deb
sudo apt-get update
sudo apt list | grep dvrescue

安装 dvrescue、dvrescue-gui 和 ffmpeg:

sudo apt install dvrescue 
sudo apt install dvrescue-gui
sudo apt install ffmpeg

中途 Ubuntu 还死机了两三次,Gemini 分析是显卡驱动问题,我只好禁用硬件图形加速。

使用 DVRescue 采集

将摄像机开机、用 1394 线连接到电脑,查看 DVRescue 能否识别到设备:

dvrescue --list_devices

DVRescue 看着很专业,实际用起来 bug 不少。我首先遇到的问题是采集时能控制摄像机播放,但读取不到画面,必须用 sudo 执行才能解决。

sudo dvrescue device://0x8004601023e402f --rewind-count 3 --merge tape01-pass01.dv --xml-output tape01-pass01.xml --merge-log tape01-pass01.log --verbosity 7 --capture

这些选项的含义分别是:

因为没有采集前倒带的选项,需要先在摄像机上按 REW⏪ 键将磁带倒回开头。采集开始后,终端也会滚动更新当前的总帧数和时间码。DVRescue 也没有 dvgrab 那样的自动切片选项,而是会把所有数据输出到一个完整的 .dv 文件,我推测这也是为了后续的分析、merge 方便。如果遇到需要 rewind 的帧,DVRescue 会将各次读取的结果输出到单独的 .dv 文件,文件名的格式是 tape01-pass01.dv.dvrescue.take0.frames33-33.dv

输出的 XML 我读不太懂,应该不是给人类看的。日志文件如下:

DVRescue v.24.07.20251227 (MediaInfoLib v.25.10.20251228) by MIPoPS

File 0: device://0x8004601023e402f
File 1: auto-rewind
File 2: auto-rewind
File 3: auto-rewind

     #|Abst  |HH:MM:SS:FF|U|St  |Comments
     0    216 00:00:00:18 0      (     7 frames)
     7        00:00:00:25 M      (     5 frames)
    12    300 00:00:01:00 0      (    21 frames)
    33    552 00:00:01:21 X P    1601                block picks &  127 remaining block errors

Rewind to frame 33
    34    564 00:00:01:22 0
    33    552 00:00:01:21 1 P   
    34    564 00:00:01:22 0  P  
    35    576 00:00:01:23 0      (     2 frames)
    37        00:00:01:25 M      (     5 frames)
    42    600 00:00:02:00 0      (    25 frames)
    67        00:00:02:25 M      (     5 frames)
    72    900 00:00:03:00 0      (    25 frames)
    97        00:00:03:25 M      (     5 frames)
   102   1200 00:00:04:00 0      (    25 frames)
   127        00:00:04:25 M      (     5 frames)
   132   1500 00:00:05:00 0      (    25 frames)
   157        00:00:05:25 M      (     5 frames)
   162   1800 00:00:06:00 0      (    25 frames)
   187        00:00:06:25 M      (     5 frames)
   192   2100 00:00:07:00 0      (    25 frames)
   217        00:00:07:25 M      (     5 frames)
   222   2400 00:00:08:00 0      (    25 frames)
   247        00:00:08:25 M      (     5 frames)
   252   2700 00:00:09:00 0      (    25 frames)

(...中间内容省略...)

 28212 282300 00:15:41:00 0      (    25 frames)
 28237        00:15:41:25 M      (     5 frames)
 28242 282600 00:15:42:00 0      (    25 frames)
 28267        00:15:42:25 M      (     5 frames)
 28272 282900 00:15:43:00 0      (    25 frames)
 28297        00:15:43:25 M      (     5 frames)
 28302 283200 00:15:44:00 0      (    25 frames)
 28327        00:15:44:25 M      (     5 frames)
 28332 283500 00:15:45:00 0      (    25 frames)
 28357        00:15:45:25 M      (     5 frames)
 28362 283800 00:15:46:00 0
 28363 283812 00:15:46:01 X P    1635                block picks &   93 remaining block errors
 28364 283825 00:15:46:02 X P    1025                block picks &  703 remaining block errors
 28365 283836 00:15:46:03 X P    1635                block picks &   93 remaining block errors

Rewind to frame 28363
 28366 283848 00:15:46:04 0
 28363 283812 00:15:46:01 X PP      0 1696           block picks &   32 remaining block errors
 28364 ?????? 00:15:46:02 X PP      0 1692           block picks &   36 remaining block errors, abst 283825 283824
 28365 283836 00:15:46:03 1 P   

Rewind to frame 28363
 28366 283848 00:15:46:04 0
 28363 283812 00:15:46:01 X PPP     0    1 1717      block picks &   10 remaining block errors
 28364 ?????? 00:15:46:02 X PPP     0    2 1715      block picks &   11 remaining block errors, abst 283825 283824 283824
 28365 283836 00:15:46:03 1 P   

Rewind to frame 28363
 28366 283848 00:15:46:04 0
 28363 283812 00:15:46:01 X PPPP    0    1 1717    0 block picks &   10 remaining block errors
 28364 ?????? 00:15:46:02 X PPPP    0    2 1715    0 block picks &   11 remaining block errors, abst 283825 283824 283824 283824
 28365 283836 00:15:46:03 1 P   
 28366 283848 00:15:46:04 0      (    21 frames)
 28387        00:15:46:25 M      (     5 frames)
 28392 284100 00:15:47:00 0      (    15 frames)
 28407 284286 00:15:47:15 X P    1149                block picks &  579 remaining block errors
 28408 284292 00:15:47:16 X P    1219                block picks &  509 remaining block errors
 28409 284304 00:15:47:17 X P    1715                block picks &   13 remaining block errors

Rewind to frame 28407
 28410 284316 00:15:47:18 0
 28407 284286 00:15:47:15 X PP   1149  182           block picks &  397 remaining block errors
 28408 ?????? 00:15:47:16 X PP   1219    0           block picks &  509 remaining block errors, abst 284292 284299
 28409 284304 00:15:47:17 X PP   1715    7           block picks &    6 remaining block errors
 28410 284316 00:15:47:18 0  P  
Rewind to frame 28407
 28407 ?????? 00:15:47:15 X PPP    54   20 1328      block picks &  326 remaining block errors, abst 284286 284286 284287
 28408 ?????? 00:15:47:16 X PPP  1219    0   69      block picks &  440 remaining block errors, abst 284292 284299 284293
 28409 ?????? 00:15:47:17 X PPP  1715    7    0      block picks &    6 remaining block errors, abst 284304 284304 284305
 28410 ?????? 00:15:47:18 0  PP  abst 284316 284316 284317
Rewind to frame 28407
 28407 ?????? 00:15:47:15 X PPPP   54   18 1328   46 block picks &  282 remaining block errors, abst 284286 284286 284287 284286
 28408 ?????? 00:15:47:16 X PPPP 1219    0   69   95 block picks &  345 remaining block errors, abst 284292 284299 284293 284292
 28409 ?????? 00:15:47:17 X PPPP 1715    2    0    7 block picks &    4 remaining block errors, abst 284304 284304 284305 284304
 28410 ?????? 00:15:47:18 0  PPP abst 284316 284316 284317 284316
 28411 284328 00:15:47:19 0      (     6 frames)
 28417        00:15:47:25 M      (     5 frames)
 28422 284400 00:15:48:00 0      (    25 frames)
 28447        00:15:48:25 M      (     5 frames)
 28452 284700 00:15:49:00 0      (    25 frames)
 28477        00:15:49:25 M      (     5 frames)
 28482 285000 00:15:50:00 0      (    25 frames)
 28507        00:15:50:25 M      (     5 frames)
 28512 285300 00:15:51:00 0      (    25 frames)
 28537        00:15:51:25 M      (     5 frames)
 28542 285600 00:15:52:00 0      (    25 frames)
 28567        00:15:52:25 M      (     5 frames)

(...中间内容省略...)

 59647        00:33:08:25 M      (     5 frames)
 59652 596700 00:33:09:00 0      (    25 frames)
 59677        00:33:09:25 M      (     5 frames)
 59682 597000 00:33:10:00 0      (    25 frames)
 59707        00:33:10:25 M      (     5 frames)
 59712 597300 00:33:11:00 0      (    25 frames)
 59737        00:33:11:25 M      (     5 frames)
 59742 597600 00:33:12:00 0      (    25 frames)
 59767        00:33:12:25 M      (     5 frames)
 59772 597900 00:33:13:00 0      (    25 frames)
 59797        00:33:13:25 M      (     5 frames)
 59802 598200 00:33:14:00 0      (    25 frames)
 59827        00:33:14:25 M      (     5 frames)
 59832 598500 00:33:15:00 0      (     4 frames)
 59836        ??:??:??:?? X P       0                block picks & 1728 remaining block errors
 59837        ??:??:??:?? X P       0                block picks & 1728 remaining block errors
 59838        ??:??:??:?? X P       0                block picks & 1728 remaining block errors
 59839        ??:??:??:?? X P       0                block picks & 1728 remaining block errors
 59840        ??:??:??:?? X P       0                block picks & 1728 remaining block errors
 59841        ??:??:??:?? X P       0                block picks & 1728 remaining block errors
 59842        ??:??:??:?? X P       0                block picks & 1728 remaining block errors
 59843        ??:??:??:?? X P       0                block picks & 1728 remaining block errors

 59844 frames in total.

How frames are merged:
 49854 =  83.3066% of frames are recovered from full frames.
    13 =   0.0217% of frames remain with errors.
                     (in these frames, errors reduced from      2582 =   3.6781% of blocks bad in file 3
                                                        to     14476 =  20.6211% of blocks bad).
  9977 =  16.6717% of frames are still missing.

Usage of input files:
 89756753 =  83.3247% of blocks from file 0     used.
    21826 =   0.0203% of blocks from file 1     used.
     8873 =   0.0082% of blocks from file 2     used.
      148 =   0.0001% of blocks from file 3     used.

Input files summary:
File 0:  31237485 =  28.9990% of blocks have errors, in  17480 =  29.2093% of frames
Rew  1:      3784 =   0.0035% of blocks have errors, in      7 =   0.0117% of frames
Rew  2:      4521 =   0.0042% of blocks have errors, in      6 =   0.0100% of frames
Rew  3:      2611 =   0.0024% of blocks have errors, in      6 =   0.0100% of frames

Result:
         17973076 =  16.6851% of blocks have errors, in   9990 =  16.6934% of frames
                  (- 12.3139)                                  (- 12.5159)

日志中各列分别表示:

日志中有大段大段的0 ( 25 frames)M ( 5 frames)连续交替的行,我推测是将 25 fps 的 PAL 输入错误识别为了 30 fps 的 NTSC,因此每秒都会“缺”最后 5 帧,实际是不缺的。

Debug

采集 tape01 两遍之后,在检查视频文件时,我才发现一个严重的问题。这两遍输出的文件不完整,都只有 33 分钟长,而这盒磁带是基本录满的,应该有 1 个小时;和 dvgrab 采集的文件比较,前半部分的片段内容一致,但在 33 分钟处的一个镜头结束后,后面的片段就缺失了。我在摄像机上播放这一段时看到这个片段后紧接着的就是一段无内容、摄像机屏幕显示蓝屏的间隔,猜测可能是 DVRescue 碰到这个就以为视频结束了,没有接着读;但是采集时磁带却是播放到了结尾才停,很令人困惑。

我又用 dvgrab 采集了一次,视频内容完整,说明并非磁带或摄像机的问题。1 月 18 日,我 clone 了 DVRescue 的源码,尝试使用 Claude Code debug。Claude 先分析出可能是采集结束条件的判断有问题,改完源代码重新编译、把可执行文件拷到 T410 上、再采集,发现采集出的文件还是只有 33 分钟,bug 没修好。

重新拷打 Claude,在经过若干次拉扯和反复检查之后,它终于分析出了错误原因:在两个片段交界处,DVRescue 先遇到一段错误帧、触发 rewind 逻辑;紧接着下一个好帧的 timecode 归零,需要建立并切换为新的 Segment。在一个遍历 Inputs 数组、检查是否需要建立新 Segment 的 for 循环中,错误地没有跳过 DoNotUseFile flag 为 True 的 auto-rewind 输入,导致切换 Segment 的检查失败。之后采集的所有帧都被加入新的 Segment 中,但是由于永远不会切换到这个新 Segment,也就不会向文件输出。

在检查所有遍历 Inputs 的循环后,Claude Code 进行了下列修改:

diff --git a/Source/Common/Merge.cpp b/Source/Common/Merge.cpp
index 998911d..8d8eb8e 100644
--- a/Source/Common/Merge.cpp
+++ b/Source/Common/Merge.cpp
@@ -722,6 +722,8 @@ bool dv_merge_private::Process(float Speed)
         auto Segment_Pos1 = Segment_Pos + 1;
         for (const auto Input : Inputs)
         {
+            if (Input->DoNotUseFile)
+                continue;
             if (Segment_Pos1 >= Input->Segments.size())
                 return true;
         }
@@ -993,6 +995,8 @@ bool dv_merge_private::Process(float Speed)
                 for (size_t i = 0; i < Input_Count; i++)
                 {
                     auto& Input = Inputs[i];
+                    if (Input->DoNotUseFile)
+                        continue;
                     auto& Frames = Input->Segments[Segment_Pos].Frames;
                     auto& Frame = Frames[Frame_Pos];
                     if (!Frame.Status[Status_FrameMissing])
@@ -1382,6 +1386,11 @@ bool dv_merge_private::Process(float Speed)
                 {
                     Log_Line << Log_Separator;
                     auto& Input = Inputs[i];
+                    if (Input->DoNotUseFile)
+                    {
+                        Log_Line << "[DoNotUseF]";  // 11 characters, matching timecode width
+                        continue;
+                    }
                     auto& Frames = Input->Segments[Segment_Pos].Frames;
                     auto& Frame = Frames[Frame_Pos];
                     Log_Line << (Frame.Status[Status_TimeCodeIssue] ? "??:??:??:??" : TC_String(Frame.TC));

再重新编译、采集,修改后的 DVRescue 在 33 分钟的视频片段切换处,遇到后一片段的第一帧后马上开始向前倒带(我推测是 auto-rewind 生效了,想要重新读取这一段),但即使磁带位置已经回到蓝屏区域之前、前一片段的完好帧,也没有停止倒带。我等了一分钟,感觉倒带肯定不会结束了,只能强行终止了进程。这次 Claude 似乎也分析不出来具体是什么原因,分析了几次都是错的,我只能大致猜测是结束倒带的逻辑判断有问题。到这里我已经 debug 了一个晚上,有点累了,DVRescue 的代码又很长、没什么注释,很难看懂。我找到一个 2024 年的 open issue 非常类似,但没有下文。最终,我决定放弃继续 debug,转而用 dvgrab 采集多次后,再用 DVRescue merge。

dvgrab + DVRescue

如果用这种方法,一个新的问题就是 2025 年用 dvgrab autosplit 选项采集的视频应该怎么 merge。如果新采集时也用 autosplit、也输出十几个文件、每个文件分别 merge,是非常麻烦的。幸好 dvgrab 也有源代码,Claude Code 分析源码之后确定 raw DV 文件就是一帧帧数据直接拼起来的,所以只要把同一条磁带上的所有视频片段按(文件系统上的)最后修改时间 cat 起来,就相当于当时不用 autosplit 所输出的完整文件。

ls -1t tape01-pass1/*.dv | tac | xargs cat > tape01-pass1-merged.dv

需要注意的是,不能按文件名排序,因为文件名中的录制时间和时间码在磁带上不一定单调递增。1 月 25 至 28 日,我用 dvgrab 把所有磁带重新采集了一遍。

1 月 29 日,我又让 Claude 分析了一下 DVRescue 的 merge 算法,发现也不是特别严谨,我不敢完全相信。DVRescue 在不同输入源之间对齐帧主要依赖于 timecode,对于没有 timecode 的帧就按上一帧的 timecode + 1 来推断。但这样问题就来了,如果在两个视频片段的交界处,前一片段的末尾和后一片段的开头都有若干无 timecode 的帧,合并时就会把后一片段开头的帧误认为是前一片段末尾的。如果摄像机由于物理原因在读取交界处时多了或少了一两帧,后一片段的开头就对不齐,merge 的结果中反而会引入新的错误,这是我不能接受的。(之后我尝试 merge 过一次,确实在画面条纹较多的片段交界处出现了同一段音频的重复,说明的确有这个问题。)可是磁带上的元数据就这么多,还有什么更好的 merge 算法吗?似乎也想不到。

柳暗花明

事已至此,转念一想,我那几盒磁带里有这么多数据错误,值得费这么大劲去修复吗?(我还是太爱折腾了,一想到 Data Hoarding 就发了狠了。)我又注意到用 DVRescue 采集的几个日志的 summary 中有错误的 block 和 frame 比例都是 16.6% 左右,这差不多就是 $\frac{1}{6}=\frac{5}{25 + 5}$。上文提到,日志中每隔 25 帧就缺失 5 帧,这么高的错误率是否是因为帧率错误算出来的呢?

我让 Claude Code 再次分析源代码,的确如此:代码中有 5 处 30 /*TEMP*/,明显是开发时先临时硬编码了 NTSC 的帧率,但后续没有适配 PAL。由于我只处理 PAL 磁带,可以把它们全部改成 25

diff --git a/Source/Common/Merge.cpp b/Source/Common/Merge.cpp
index 998911d..95f27ad 100644
--- a/Source/Common/Merge.cpp
+++ b/Source/Common/Merge.cpp
@@ -505,7 +505,7 @@ bool dv_merge_private::AppendFrameToList(size_t InputPos, const MediaInfo_Event_
     // Time code jumps - after first frame
     timecode TC_Temp(FrameData);
     if (TC_Temp.HasValue())
-        CurrentFrame.TC = TimeCode(TC_Temp.TimeInSeconds() / 3600, (TC_Temp.TimeInSeconds() / 60) % 60, TC_Temp.TimeInSeconds() % 60, TC_Temp.Frames(), 30 /*TEMP*/, TC_Temp.DropFrame());
+        CurrentFrame.TC = TimeCode(TC_Temp.TimeInSeconds() / 3600, (TC_Temp.TimeInSeconds() / 60) % 60, TC_Temp.TimeInSeconds() % 60, TC_Temp.Frames(), 25 /*PAL*/, TC_Temp.DropFrame());
     if (!Frames.empty() && Frames.back().TC.HasValue())
     {
         TimeCode TC_Previous(Frames.back().TC);
diff --git a/Source/Common/ProcessFile.cpp b/Source/Common/ProcessFile.cpp
index 608454b..078821e 100644
--- a/Source/Common/ProcessFile.cpp
+++ b/Source/Common/ProcessFile.cpp
@@ -678,7 +678,7 @@ void file::AddFrameAnalysis(const MediaInfo_Event_DvDif_Analysis_Frame_1* FrameD
         timecode TC_Temp(FrameData);
         if (TC_Temp.HasValue())
         {
-            TimeCode TC(TC_Temp.TimeInSeconds() / 3600, (TC_Temp.TimeInSeconds() / 60) % 60, TC_Temp.TimeInSeconds() % 60, TC_Temp.Frames(), 30 /*TEMP*/, TC_Temp.DropFrame());
+            TimeCode TC(TC_Temp.TimeInSeconds() / 3600, (TC_Temp.TimeInSeconds() / 60) % 60, TC_Temp.TimeInSeconds() % 60, TC_Temp.Frames(), 25 /*PAL*/, TC_Temp.DropFrame());
             if (Verbosity == 10)
             {
                 cerr << "Rewind ";
@@ -706,7 +706,7 @@ void file::AddFrameAnalysis(const MediaInfo_Event_DvDif_Analysis_Frame_1* FrameD
         timecode TC_Temp(FrameData);
         if (TC_Temp.HasValue())
         {
-            TimeCode TC(TC_Temp.TimeInSeconds() / 3600, (TC_Temp.TimeInSeconds() / 60) % 60, TC_Temp.TimeInSeconds() % 60, TC_Temp.Frames(), 30 /*TEMP*/, TC_Temp.DropFrame());
+            TimeCode TC(TC_Temp.TimeInSeconds() / 3600, (TC_Temp.TimeInSeconds() / 60) % 60, TC_Temp.TimeInSeconds() % 60, TC_Temp.Frames(), 25 /*PAL*/, TC_Temp.DropFrame());
             if (RewindMode==Rewind_Mode_TimeCode && TC.ToFrames()<RewindTo_TC.ToFrames())
             {
                 if (Verbosity == 10)
@@ -791,7 +791,7 @@ void file::AddFrameAnalysis(const MediaInfo_Event_DvDif_Analysis_Frame_1* FrameD
         timecode TC_Temp(FrameData);
         if (TC_Temp.HasValue())
         {
-            TimeCode TC(TC_Temp.TimeInSeconds() / 3600, (TC_Temp.TimeInSeconds() / 60) % 60, TC_Temp.TimeInSeconds() % 60, TC_Temp.Frames(), 30 /*TEMP*/, TC_Temp.DropFrame());
+            TimeCode TC(TC_Temp.TimeInSeconds() / 3600, (TC_Temp.TimeInSeconds() / 60) % 60, TC_Temp.TimeInSeconds() % 60, TC_Temp.Frames(), 25 /*PAL*/, TC_Temp.DropFrame());
             if (TC.ToFrames()<RewindTo_TC.ToFrames())
                 return;
             RewindTo_TC = TimeCode();
@@ -884,7 +884,7 @@ void file::AddFrameAnalysis(const MediaInfo_Event_DvDif_Analysis_Frame_1* FrameD
         timecode TC_Temp(FrameData);
         if (TC_Temp.HasValue())
         {
-            TimeCode TC(TC_Temp.TimeInSeconds() / 3600, (TC_Temp.TimeInSeconds() / 60) % 60, TC_Temp.TimeInSeconds() % 60, TC_Temp.Frames(), 30 /*TEMP*/, TC_Temp.DropFrame());
+            TimeCode TC(TC_Temp.TimeInSeconds() / 3600, (TC_Temp.TimeInSeconds() / 60) % 60, TC_Temp.TimeInSeconds() % 60, TC_Temp.Frames(), 25 /*PAL*/, TC_Temp.DropFrame());
             Text += ' ';
             Text += TC.ToString();
         }

重新编译,丢弃 merge 的输出文件,只保留日志,以此分析已用 dvgrab 采集的视频:

dvrescue-patched-v3-PAL tape01-pass03001.dv --merge /dev/null --merge-log tape01-pass03001.log --xml-output tape01-pass03001.xml --verbosity 7

输出日志如下:

DVRescue v.24.07 (MediaInfoLib v.25.10.20260120) by MIPoPS

File 0: tape01-pass03001.dv

     #|Abst  |HH:MM:SS:FF|U|S|Comments
     0    216 00:00:00:18 0   ( 23257 frames)
 23257 279300 00:15:31:00 X P 1710 block picks &   18 remaining block errors
 23258 279312 00:15:31:01 0   (   375 frames)
 23633 283812 00:15:46:01 X P 1705 block picks &   23 remaining block errors
 23634 283824 00:15:46:02 X P 1716 block picks &   12 remaining block errors
 23635 283836 00:15:46:03 0   (    37 frames)
 23672 284287 00:15:47:15 X P 1653 block picks &   75 remaining block errors
 23673 284292 00:15:47:16 X P  744 block picks &  984 remaining block errors
 23674 284304 00:15:47:17 X P  864 block picks &  864 remaining block errors
 23675 284316 00:15:47:18 X P  864 block picks &  864 remaining block errors
 23676 284328 00:15:47:19 X P  864 block picks &  864 remaining block errors
 23677 284340 00:15:47:20 X P  864 block picks &  864 remaining block errors
 23678 284352 00:15:47:21 X P  864 block picks &  864 remaining block errors
 23679 284364 00:15:47:22 X P  864 block picks &  864 remaining block errors
 23680 284376 00:15:47:23 X P  864 block picks &  864 remaining block errors
 23681 284388 00:15:47:24 X P  864 block picks &  864 remaining block errors
 23682 284400 00:15:48:00 X P  864 block picks &  864 remaining block errors
 23683 284412 00:15:48:01 X P  864 block picks &  864 remaining block errors

(...中间内容省略...)

 71177 874093 00:00:03:16 X P  864 block picks &  864 remaining block errors
 71178 874105 00:00:03:17 X P 1486 block picks &  242 remaining block errors
 71179 874116 00:00:03:18 0   ( 15113 frames)
 86292        ??:??:??:?? X P    0 block picks & 1728 remaining block errors
 86293        ??:??:??:?? X P    0 block picks & 1728 remaining block errors
 86294        ??:??:??:?? X P    0 block picks & 1728 remaining block errors
 86295        ??:??:??:?? X P    0 block picks & 1728 remaining block errors
 86296        ??:??:??:?? X P    0 block picks & 1728 remaining block errors
 86297        ??:??:??:?? X P    0 block picks & 1728 remaining block errors

 86298 frames in total.

How frames are merged:
 86001 =  99.6558% of frames are recovered from full frames.
   297 =   0.3442% of frames remain with errors.

Usage of input files:
155336400 = 100.0000% of blocks from file 0     used.

Input files summary:
File 0:    285950 =   0.1841% of blocks have errors, in    297 =   0.3442% of frames

Result:
           285950 =   0.1841% of blocks have errors, in    297 =   0.3442% of frames

现在日志就短了很多,因为大量(可能是成千上万行的)连续的正常帧被压缩为一行日志。我像这样分析了之前所有用 dvgrab 采集的视频文件,所有文件的帧错误率都在 1% 以下,块错误率更低,说明这些磁带状况很好,其实没有必要进行额外的修复。由于我希望只保留一份原始数据,现在问题就变成了:我应该保留哪次采集的视频文件?

错误率比较

既然现在已经能计算出每次采集的实际错误率,自然应该保留每盒磁带的错误率最低的那次采集。当我分别对比每盒磁带在 2025 年 2 月和 2026 年 1 月采集的分析日志时,惊讶地发现它们的帧错误率和块错误率基本都升高了,只有 tape07 例外。

采集时间 2025-02 2026-01
tape01 86305 frames in total. 243491 = 0.1567% of blocks have errors, in 268 = 0.3105% of frames 86298 frames in total. 285950 = 0.1841% of blocks have errors, in 297 = 0.3442% of frames
tape02 91278 frames in total. 181300 = 0.1103% of blocks have errors, in 247 = 0.2706% of frames 91278 frames in total. 182945 = 0.1113% of blocks have errors, in 265 = 0.2903% of frames
tape03 27111 frames in total. 24448 = 0.0501% of blocks have errors, in 23 = 0.0848% of frames 27110 frames in total. 25180 = 0.0516% of blocks have errors, in 20 = 0.0738% of frames
tape04 86931 frames in total. 149609 = 0.0956% of blocks have errors, in 100 = 0.1150% of frames 86932 frames in total. 151848 = 0.0970% of blocks have errors, in 104 = 0.1196% of frames
tape05 84372 frames in total. 82470 = 0.0543% of blocks have errors, in 96 = 0.1138% of frames 84372 frames in total. 87591 = 0.0577% of blocks have errors, in 114 = 0.1351% of frames
tape06 29232 frames in total. 35582 = 0.0676% of blocks have errors, in 53 = 0.1813% of frames 29231 frames in total. 44650 = 0.0849% of blocks have errors, in 70 = 0.2395% of frames
tape07 26908 frames in total. 54657 = 0.1128% of blocks have errors, in 43 = 0.1598% of frames 26906 frames in total. 51154 = 0.1056% of blocks have errors, in 37 = 0.1375% of frames
tape08 35771 frames in total. 10368 = 0.0161% of blocks have errors, in 6 = 0.0168% of frames 35773 frames in total. 12199 = 0.0189% of blocks have errors, in 11 = 0.0307% of frames
tape09 79363 frames in total. 78548 = 0.0550% of blocks have errors, in 55 = 0.0693% of frames 79365 frames in total. 82671 = 0.0579% of blocks have errors, in 58 = 0.0731% of frames
tape10 87762 frames in total. 45276 = 0.0287% of blocks have errors, in 43 = 0.0490% of frames 87762 frames in total. 52375 = 0.0332% of blocks have errors, in 46 = 0.0524% of frames

我让 Claude 画了一张图,直观地展示两种错误率随时间的变化:

磁带错误率比较

tape01 和 tape06 的升高最为明显,但是在 2025 年的采集之前它们就已经保存了二十多年,如果每年磁带都以这种速率损坏的话,数据错误率应该远高于当前水平。思来想去,我回忆起在2022 年最初发现它们之前,这些磁带是放在完全避光的柜子里的;而 2025 年第一次采集后,我就把它们装在一个塑料袋里,和摄像机、T410 笔记本一起放在柜子外面。虽然温度、湿度也基本稳定,但是能见光。如果是这个原因,那说明光照对磁带的损害还是比较大的。

幸好,我在 2025 年已经把所有磁带用可靠的方法采集下来了。最终我决定保留所有 2025 年 2 月采集的文件,也包括后来错误率降低的 tape07。(多错的这几帧数据已经不值得投入更多精力折腾了,错就错吧。)到此,这个横跨五年的超长项目就圆满结束了。

参考

下面是一些我在调查时保存到浏览器书签栏的讨论和文章,按添加时间排序。

[1] Digitize MiniDV Tapes with Linux | Hacker News - 2022-11-29 添加
[2] Found a bunch of my parents’ MiniDVs… any advice on how I can digitize these? : DataHoarder - 2024-03-03 添加
[3] The "How do I digitize/transfer/capture video tapes" quick info thread. VHS VHSC HI8 Video8 Digital8 miniDV : DataHoarder - 2024-03-03 添加
[4] How I archive miniDV tapes - a short guide : DataHoarder - 2024-03-03 添加
[5] The challenges (and rewards!) of preserving video from MiniDV tapes – University of Bristol Theatre Collection Blog - 2025-01-15 添加
[6] Gather: Digitize MiniDVs - Elgrito - 2025-01-15 添加