video-scrub
動画をクリーンなファイルに再構築します:映像と音声だけを持ち越し、メタデータは1バイトも残しません。
{baseDir} = このファイルがあるディレクトリ。スクリプト {baseDir}/scripts/video-scrub.mjs、ゼロ依存、node で直接実行可能。
原理:ホワイトリスト方式、ブラックリスト方式ではない
メタデータ除去には2つのアプローチがあり、その違いは死活問題です:
| 方法 | 問題点 | |
|---|---|---|
| ブラックリスト方式 | 削除するフィールドを列挙して一つずつ削除(exiftoolのような) | 知らないものは削除できない——ベンダー固有のudta、GoProのテレメトリトラック、コードストリーム内のSEI、一つ漏れるとアウト |
| ホワイトリスト方式 | 削除するものを指定せず、持ち越すものだけを指定する:映像1ストリーム、音声1ストリーム、それ以外は一切不要 | —— |
ホワイトリスト方式では、ソース動画のメタデータが入り込む経路がありません。それは「削除された」のではなく、「そもそも運ばれなかった」のです。
しかし、ffmpegのデフォルト動作はこの構造を2度裏切るため、しっかり抑え込む必要があります:
- デフォルトでソースのコンテナタグを出力にコピーする →
-map_metadata -1 - デフォルトで自動的にストリームを選択するため、GoProの
gpmdテレメトリトラックも一緒に連れて行かれる →-map 0:v:0 -map 0:a:0?
2番目はGoPro / DJIなどの動画に対する唯一の防御線です:それらのGPSはタグにあるのではなく、独立したタイムコードメタデータトラックに存在します。タグを消しても除去できず、「2つのストリームだけを選択する」ことでしか防げません。
本当の難所:見えない層たち
上記2つを抑えればタグレベルはクリーンになり、ffprobe は何も表示しなくなります。しかし、ファイル内にはまだ3つの識別子が潜んでいます:
| 隠れ場所 | 内容 | ffprobeで見えるか |
|---|---|---|
| H.264 の SEI | x264の完全なパラメータ文字列 cabac=1 ref=3 … crf=23.0 | ✗ |
| AAC コードストリームの DSE | Lavc62.28.102 | ✗ |
| avc1 の compressorname | Lavc libx264 | ✗ |
5つの隠れ場所とそれぞれの対策方法は {baseDir}/references/residue-map.md にあります——それぞれに実測記録があり、「効きそうで効かなかった」いくつかの落とし穴を含みます(-flags:v +bitexact や -x264-params info=0 ではSEIを止められない)。
2つのモード
| モード | 映像 | 音声 | 速度 | さらに何を除去するか |
|---|---|---|---|---|
copy(デフォルト) | バイトレベルでそのままコピー | 再エンコード | 53秒の動画が1.3秒 | —— |
encode | 再エンコード | 再エンコード | 非常に遅い | コードストリーム領域の透かし、脆弱なステガノグラフィー |
音声は両方のモードで再エンコードされます——AACのバージョン番号はDSEに書き込まれており、それはコードストリーム内部のため -c:a copy では除去できません。再エンコードはコストが低く、音声トラックをクリーンにします。映像はコピーモードでは本当に1ビットも変更されていません(cmp で検証済み、除去されたSEIを除く)。
透かしについて:encode モードが copy モードより多く除去するのは、コードストリーム領域の透かしと脆弱なステガノグラフィーであり、これらは実際の素材ではまれです。本当に厄介なロバストピクセル透かし(SynthID、フォレンジック透かし)は、再エンコードに耐えるように設計されており、どちらのモードでも除去できません。そのため、モードの選択は画質と速度に基づいて行い、再エンコードで透かしが除去できると期待してはいけません。
4つのプロファイル
拭き取った後に書き戻す層:
| プロファイル | 書き込む内容 |
|---|---|
obs(デフォルト) | OBS Studio の外観。OBS自体がffmpegでラッピングされているため、これは偽造ではなく、同種のツールの正常な外観を保持します |
quicktime | macOS QuickTime 画面収録:Core Media Video / Core Media Audio |
screencapture | ScreenCaptureKit 画面収録 |
bare | 何も書き込まない。最もプライバシー強度が高い——検証可能な宣言を一切残さない |
一つだけできないことがあるので、試す手間を省くために書いておきます:コンテナの encoder フィールドはmuxer自身が占有しており、-metadata encoder=Lavf60.16.100 では上書きできず、空にしようとしても消せません(実測済み)。-fflags +bitexact だけがそれを完全に消去できます。そのため obs プロファイルはバージョン番号を偽造せず、ffmpegに実際のバージョン番号を書き込ませます。
Step 0 — まずソース動画が何を持っているか確認する
node {baseDir}/scripts/video-scrub.mjs inspect <video>
一画面にすべてを表示:コンテナタグ、各ストリームのタグとサイドデータ、チャプター、そして ffprobe では見えないコードストリーム内のいくつかの識別子。最後に、検証時に何本の針でスキャンする必要があるか、そしてSEIを除去するかどうかが通知されます。
この表示を見てから作業を始めてください。 特に以下の2点に注意:
data/timecodeストリームがあるか——あればソース動画がテレメトリトラック(GoPro、DJI)を持っている可能性がある- HDR かどうか——HDRの場合はSEIは一切削除しない(理由は後述)
Step 1 — 除去
node {baseDir}/scripts/video-scrub.mjs scrub <video> -o out.mp4
デフォルトは --mode copy --profile obs。よく使うパラメータ:
--mode encode # 完全再エンコード
--profile bare # 何も書き込まない、最もプライバシー強度が高い
--crf 18 # encode モードでのみ意味がある
--date 2026-01-01T00:00:00Z # 作成時刻を指定、指定しない場合は現在時刻を使用
Step 2 — 検証 ⛔ 省略禁止
node {baseDir}/scripts/video-scrub.mjs verify <source> <output>
または一発で実行(日常的にはこれを使う):
node {baseDir}/scripts/video-scrub.mjs run <video> -o out.mp4
検証の核心は一言で言えば:ここまではすべて「正しいパラメータを設定しました」という主張に過ぎません。このステップは「スキャンして、本当に存在しないことを証明する」ものです。
方法は、ソース動画のすべてのメタデータ文字列を「針」として抽出します——タグのキーと値、ハンドラー名、チャプタータイトル——そして出力ファイル内をバイトレベルでスキャンし、一つでも一致すれば赤で表示します。
実装上の2つのこだわり:
- 針は5バイト以上でなければならない。より短い文字列は圧縮データ内でランダムに偽陽性を引き起こす可能性があります。スキップされた針はプロンプトにリストされます。
- 汎用的な文字列は針にしない。
VideoHandlerはどこにでも存在し、情報量はゼロです。Lavf58.45.100は特定のバージョンに固定されており、それは指紋です。線引きは「範囲を特定の人物、特定のデバイス、特定のエクスポートに絞り込めるかどうか」です。
12のドア
| ドア | 防ぐもの |
|---|---|
| バイトレベルの残骸 | ソース動画のメタデータ文字列が出力にヒットする |
| タグレベルの残骸 | 出力のタグ値がソース動画由来である |
| 位置情報 | GPS / location フィールド |
| デバイス・ソフトウェア識別子 | make / model / software / artist / comment など |
| ソース動画のタイムスタンプ | 出力の creation_time がソース動画と同じ |
| 回転行列 | encode モードで回転が映像に焼き込まれていない(copy モードはスキップ——削除すると映像が歪む) |
| ストリーム構成 | data / timecode / 添付ストリームが混入している |
| チャプター | チャプターが引き継がれている |
| コードストリームの識別子 | SEI / DSE / compressorname にまだエンコーダ名が残っている |
| プロファイルの一致 | 残ったタグが宣言されたプロファイルと一致しない |
| 再生時間 | 出力が密かにカットされている |
| デコード可能性 | 開けない壊れたファイルが生成されている |
HDR と埋め込み字幕:SEI を絶対に削除してはいけない場合
ffmpeg は NAL レベル の粒度しか提供しません——filter_units は NAL タイプで削除し、h264_metadata にも「特定のSEIペイロードのみ削除」するオプションはありません。そのため、remove_types=6 は すべての SEI をまとめて削除 します。そして、SEI には x264 のゴミ以外にも以下のものが含まれています:
- HDR 静的メタデータ(mastering display / content light level)——削除すると色が変わり、トーンマッピングが崩れる
- CEA-608/708 埋め込み字幕——削除すると字幕が消える
そのため、スクリプトの戦略は次の通りです:実際に識別子が見つかった場合のみSEIを削除し、HDRソース動画は決して削除しない(削除しなかったこととその理由を明示します)。この判断は seiPlan() が行うため、ユーザーが気にする必要はありませんが、その仕組みが存在することは知っておくべきです。
境界(やらないこと)
透かしの除去はしない(ロバストピクセル透かしは再エンコードでも除去できないので期待しない)、映像コンテンツの変更はしない(切り抜き、リサイズ、回転はしない)、デバイスモデルやGPSの偽造はしない(プロファイルはツール情報のみを書き込み、撮影デバイスや位置は偽造しない)、バッチディレクトリスキャンはしない(一度に1ファイルずつ処理)、mp4/mov 以外のコンテナは処理しない。
セルフテスト
node {baseDir}/scripts/selftest.mjs
117 項目のアサーション。ffmpeg や実際のファイルには依存しません。12のドアすべてに突破テストがあります——それが実際にブロックすることを証明します。スクリプトを変更したら、まずこれを実行してください。