ガイド

ブラウザでのファイル修復:IntactFile の仕組み

「壊れた」メディアファイルのほとんどは、実はまったく壊れていません — データは無傷で、ただそれを対応づけるインデックスが失われているだけです。IntactFile はそのインデックスをローカルで再構築します:ファイルはディスクから読み込まれ、WebAssembly によってタブ内で修復され、書き戻されます。アップロードされるのは0バイトで、それは Network タブで証明できます。これがその仕組みであり、そして正直にどこで機能しなくなるか、です。

死んでいないのに死んで見えるファイル

誰かがカメラのランプがまだ点いているうちに SD カードを抜く。ドローンが 水面に突っ込む。3 時間の節目で OBS がクラッシュする。スマートフォンが 熱で処理を絞られ、書き込みの途中で録画アプリが終了させられる。こうした ケースのどれでも、結果は同じです:どのプレーヤーも開くことを拒む、 数ギガバイトの動画ファイル。ffmpegmoov atom not found と表示する。OS はそれを削除しようと 申し出る。外から見えるあらゆる兆候からして、それは死んでいるように 見えます。

たいていの場合、そうではありません。MP4 や MOV ファイルの大部分は、 圧縮されたフレームの巨大なひとかたまり — mdat ボックス — であり、その末尾に moov アトムと呼ばれる小さなインデックスが 付いています。moov は地図です:各フレームがどこから始まるか、 その大きさはどれくらいか、いつ表示されるべきか、そしてデコーダーが 自らを構成するために必要なコーデック設定を記録しています。プレーヤーは まずインデックスを読み、それを使ってデータの中を移動します。それが なければ、プレーヤーは 1 つのフレームがどこから始まるのかすらわからず、 ファイルの「大半」を再生する代わりに、完全にあきらめてしまいます。

設計上の残酷な点はここです:moov アトムは録画が ファイナライズされる瞬間に、最後に、一度のパスで書き込まれます。 それより早くは書き込めません。なぜなら、フレームがまだ存在しないうちは そのバイトオフセットがわからず、カメラはあなたが 10 秒で止めるのか 2 時間続けるのかを知る由もないからです。そのため録画機はフレームを エンコードしながら mdat へ流し込み、インデックスは あなたが停止を押したときにだけ書き込みます。そのファイナライズを 中断すると — バッテリー切れ、サーマルシャットダウン、カードの引き抜き、 クラッシュ — 本物の、無傷の映像で満たされていながらインデックスが まったくないファイルが手元に残ります。バイトはそこにあります。 地図だけが書き込まれなかったのです。

IntactFile は、まさにこの「データは生き残った」と「ファイルが開く」の 間の隙間のために存在します。moov が欠けた MP4 や MOV なら、 インデックスを再構築します。他の多くのフォーマットについては、失われた 同等の構造が何であれ、それを再構築します。そしてそのすべてを、ファイルが タブから出ることなく、あなたのマシン上で行います。

なぜ修復はアップロードを意味するのか — そして私たちがそうしなかった理由

ファイル修復ツールを検索すると、ほぼすべての結果が同じ方法で動きます: あなたがファイルをサーバーにアップロードし、そのサーバー上のプロセスが 実際の修復を実行し、あなたが結果をダウンロードする。これには単純な 理由があります。修復のロジックはたいていネイティブのバイナリ —ffmpeg のようなライブラリを中心に組まれた C や C++ の プログラム — であり、ネイティブバイナリを動かすのに手っ取り早いのは 自分が管理するマシンだからです。ブラウザは長年、その種のコードを動かすには 不向きな場所でした。

私たちはそういう作り方をしたくありませんでした。哲学的である以前に、 実際的な理由からです。修復が必要とされるファイルは、まさにアップロードが 苦痛なファイルそのものです:数ギガバイトの録画、しばしば遅い回線、 しかもかけがえのないものの唯一のコピーであることも多い。6 GB の 結婚式の動画を見知らぬ人のサーバーにアップロードし、転送を二度分 待たされ、そのうえで元がその後削除されると信じてくれ、と頼むのは、 より悪いプライバシーの物語を包んだ、ひどい体験です。ファイルの中身は 私たちの関知するところではなく、それを尊重する最も単純な方法は、 私たちがそれを見ることを不可能にすることです。

そこで私たちが設けた制約は無骨なものでした:ファイルはあなたのデバイスから 決して出ない。「転送中は暗号化」でも「処理後に削除」でもなく — そもそも 転送しない。これにより問題全体が一つのエンジニアリング上の問いに 変わりました:修復そのものをブラウザで動かせるか? WebAssembly の おかげで、その答えはいまや「はい」であり、この記事の残りは、そこに たどり着くのに何が必要だったか、そしてどこにまだ継ぎ目が見えるか、 についてです。

moov アトムを再構築する、ステップごとに

欠けた moov の再構築は看板となるケースなので、順を追って 見ていく価値があります。問題はこうです:あなたの手元にはftyp(「これは MP4 です」という小さなヘッダー)と、巨大なmdat があり、インデックスがない。再構築を可能にする糸口は、mdat がコーデックを理解するパーサーにとっては本当に 不透明なわけではない、という点です。フレームは長さ接頭辞付きの単位として 格納され、各コーデックのビットストリームは認識可能な構造 — フレームの 種類・寸法・境界を示すスタートコードやヘッダー — を持っています。 H.264 や HEVC のアクセスユニットがどのような見た目かを知っている パーサーは、生データをたどり、かつてインデックスが記述していた境界を 復元できます。

これは、オープンソースのツール untrunc が確立した種類の 修復です。私たちは untrunc を厳密にアルゴリズムの参照として のみ扱い — それは GPL ライセンスです — 私たちの動画エンジンは TypeScript でのクリーンルーム再実装です。だからこそ、同一のコードが ブラウザのタブでも Node でも動作します。手順は次のとおりです:

  1. フレームの境界を見つける。 mdat を スキャンし、長さ接頭辞とコーデックのシグネチャを使って、各ビデオ フレームとオーディオフレームがどこで始まりどこで終わるかを特定する。
  2. 分類して測定する。 各フレームをそのストリームに 割り当て、サイズとバイトオフセットを記録し、サンプルサイズ (stsz)テーブルとチャンクオフセット(stco) テーブルの素材を、サンプルごとに再構築していく。
  3. タイミングと設定を復元する。 フレームごとのタイミングと デコーダー設定は、失われたインデックスの中にありました。壊れたファイルが それらを供給できないとき、同じカメラで同じ設定で録画された正常な参照 クリップが、「正常」とはどういうものか — フレームレート、タイムスケール、 コーデックのパラメータセット — をエンジンに教えます。
  4. 新しいインデックスを書き込んでファイナライズする。復元したサンプルテーブルを組み立てて新しい moov にし、 仕様に沿った正しいファイルを書き出す。

その結果は、物理的に生き残ったものすべてを再生し、データが終わるところで 終わります。正直な限界は、この仕組みからそのまま導かれます。最後の数秒 — まだカメラのメモリバッファにあり、ストレージにフラッシュされなかったもの — は本当に失われています。書き込まれなかったフレームを、どんなスキャンも 見つけることはできません。タイミングが極端に不規則な録画は、再構築された クロックが記録されたものではなく推定されたものであるため、わずかな音ズレを 伴って戻ってくることがあります。そして、録画機ではなくストレージが故障した ために mdat 自体がゼロになっている場合、見つけるべきフレームは ありません。MOV は MP4 の元となった QuickTime のフォーマットで、まさに この構造を共有しているため、開かない .mov と開かない.mp4 は、ほとんどの場合、装いを変えただけの同じ問題です。

その他のフォーマット、手短に

破損を「ペイロードは無事、インデックスが壊れている」ととらえ直すと、 同じ形がフォーマットごとに次々と現れます。ほとんどのコンテナ形式や ドキュメント形式は、データを一か所に、目次をどこか別の場所に保持して おり、そして最初に死ぬのはほぼ決まって目次のほうです。以下の各エンジンは、 その目次の異なる種類を再構築します。

各エンジンが何を再構築し、どう実装されているか。
フォーマット壊れているもの再構築の方法
MP4 / MOV欠損または切り詰められた moov インデックス生き残ったフレームからのクリーンルーム TypeScript による再構築
ZIP / OOXML失われたセントラルディレクトリ(アーカイブのインデックス)純粋な TypeScript:ローカルファイルヘッダーをスキャンし、セントラルディレクトリを再構築
PDF壊れたクロスリファレンス(xref)テーブルTypeScript による xref の事前パス、その後 WebAssembly にコンパイルした qpdf
JPEG損傷または欠損したヘッダーテーブルmozjpeg / jSquash によるデコードと、ドナーヘッダーの移植
PNG壊れたチャンク長・CRC・構造チャンクストリームに対する純粋な TypeScript のバイト手術
SQLite損傷した B-tree ページ到達可能なレコードを回収し、データベースを再構築
RAR / 7zアーカイブが展開できないWebAssembly にコンパイルした 7z による展開

いくつかはもう一文添える価値があります。ZIP と OOXML は 同じエンジンです:.docx.xlsx.pptx は ZIP アーカイブであり、ZIP は末尾に、すべての エントリを一覧する「セントラルディレクトリ」を保持しています。それを 失っても、エントリ自体はそれぞれ独自のローカルヘッダーとともにまだ 残っているので、エンジンはファイルをたどってそれらのヘッダーを見つけ、 その周りにディレクトリを再構築します — ライブラリなし、純粋な TypeScript です。PDF は、すべてのオブジェクトをバイト オフセットに対応づけるクロスリファレンステーブルを保持しています。それが 壊れているとき、TypeScript の事前パスがオフセットを導き直してから、 ファイルを qpdf に渡します。qpdf はバイナリを外部呼び出しするのではなく、 WebAssembly にコンパイルしました。JPEG は変わり種です: 圧縮されたスキャンデータはしばしば生き残る一方で、量子化テーブルと ハフマンテーブルを運ぶヘッダーは残らないことが多いので、同じカメラで 撮った正常な写真からドナーヘッダーを移植し、mozjpeg を通してデコード します。テーブルは違えど、全体を貫く原則は同じです:インデックスを 再構築し、データには手を触れない。

アーキテクチャ:WebAssembly、あなたのタブの中で

これらすべてをクライアント側で動かすのは WebAssembly の物語であり、 正直に語るべきいくつかの鋭い角があります。一部のエンジンは純粋な TypeScript で、ただ動きます。ほかは、WASM にコンパイルした既存の ネイティブコードをラップしています — PDF 用の qpdf、アーカイブ用の 7z、 そして動画処理のための libav.js です。libav.js の選択は 意図的で、ライセンスに基づくものです:これは FFmpeg ライブラリの LGPL ビルドで、製品に同梱できます。一方、GPL の ffmpeg ビルドは 同梱できません。ライセンスはここでは後付けではなく、本物の エンジニアリング上の制約です。

より厄介な制約は 2 つ、メモリと分離(アイソレーション)です。

メモリ:バッファせず、ストリームする

WebAssembly は上限のあるアドレス空間で動きます — 実際にはブラウザと ビルドによって 2〜4 GB 程度です。人々が最も修復を必要とするファイルは、 しばしばそれより大きい:ファイナライズされていない録画は何ギガバイトにも なり得ます。そのためエンジンは、ファイル全体をメモリに読み込んで進む、 というわけにはいきません。エンジンはストリーミングします — ファイルを 範囲(レンジ)ごとに読み込んで処理し、あるステップが必要とするものだけを 保持し、出力を逐次書き出します。これによりメモリが一定に保たれ、ファイル 全体を一度に収めることなど到底できないタブの中で、ファイルを修復する ことが可能になります。(性能に関する主張は意図的に定性的にとどめて います:それはファイル、コーデック、マシンに大きく依存するからです。)

分離:意図的に 2 つのオリジン

高速な WASM はスレッドを求め、ブラウザのスレッドはSharedArrayBuffer を必要とします。ブラウザは、クロスオリジン分離(cross-origin isolated)されたページ — 正しい COOP と COEP ヘッダーを送り、それに伴う制約を受け入れるページ — に対してのみ、これを有効にします。それらの制約は、広告のような サードパーティのものを載せる通常のマーケティングページにとっては 扱いにくいものです。一方を他方のために妥協させるのではなく、私たちは 製品を 2 つのオリジンに分けました:広告を載せられる従来型の マーケティングオリジンと、マルチスレッドの WASM エンジンが全速力で 動けるよう COOP/COEP を提供する、独立したクロスオリジン分離済みの アプリオリジンです。単一のサイトより可動部は多いですが、これにより 読み物と修復の機構が、それぞれ必要とする環境を持てるようになります。

戻り値としての正直さ

最も重要な設計上の決定は、どのコーデックの中にもありません。それは、 すべてのエンジンが明示的な結果 — 成功部分的失敗 — を、何が見つかったかを 表す損傷クラスとともに返す、ということです。成功をでっち上げることは 決してありません。

これが重要なのは、ファイル修復が、何があろうとファイルを書き出すツールで あふれた分野だからです。そうしたツールは、何時間も経ってから実は再生 できないと気づかせたり、4 秒だけ再生して固まったりするものを残します。 地図が欠けていてデータが生き残っているなら、私たちはそれを再構築でき、 そうだと言えます。データが物理的に消えているとき — 上書きされた、 故障したディスクでゼロにされた、切り詰められて失われた — 再構築すべき ものは何もなく、正しい答えは、肝心なときに失敗するもっともらしいファイルを 吐き出すことではなく、そのことをはっきり伝えることです。 「部分的」もまた一級の結果です:3 時間の録画のうち最初の 2 時間を回収し、 最後の 1 時間は生き残らなかったと率直に伝えることは、それを完全なものと 偽らない限り、本当に役に立つ結果です。

失敗のケースを、隠すべき恥ではなく、正直で名前の付いた結果として扱う こと — それが、何かの唯一のコピーを託せるツールと、託せないツールとの 違いだと私たちは考えています。

自分で検証する:Network タブを開く

「アップロード0バイト」を鵜呑みにする必要はありません。それは 1 分も かからず確認できる類の主張であり、私たちはむしろ確認してほしいと 思っています:

  1. ブラウザで修復ツールを開く。
  2. DevTools を開き(F12 またはCmd/Ctrl + Shift + I)、Network タブに切り替える。
  3. 壊れたファイルをページにドロップする。
  4. リクエストを見守る。ページ自身のアセットが読み込まれるのは見えますが、 あなたのファイルの内容を運ぶリクエストは一つもありません。バイトは ディスクからタブへ、そしてディスクへと戻ります — 決して回線には 乗りません。

本当に疑り深い人向けの、より強力なテスト:ネットワーク接続を完全に 引き抜き、オフラインで修復を実行してください。それでも動きます。なぜなら、 そもそもループの中にサーバーなど一度も存在しなかったからです。同じ エンジンは、コマンドラインや自動化用途のための Node パッケージにも力を 与えていますが、ウェブサイト上では、修復はあなたのファイルがすでにある 場所で行われます。

よくある質問

IntactFile は私のファイルをサーバーにアップロードしますか?

いいえ。すべてのエンジンは WebAssembly にコンパイルされ、ブラウザのクライアント側で動作します。ファイルは File API を使ってディスクから読み込まれ、タブ内で再構築され、そのままディスクへ保存されます。ファイルのバイトはネットワーク上に1バイトも送信されません。これはご自分で確認できます:DevTools を開き、Network タブを見ながらファイルをドロップすれば、その内容を運ぶリクエストが一つもないことがわかります。同じエンジンはコマンドラインやサーバー用途のために Node でも動作しますが、ウェブサイト上ではあなたのマシンから何も出ていきません。

これは untrunc と同じものですか?

同じ種類の問題を解きます — 生き残ったメディアデータから、失われた MP4/MOV のインデックスを再構築する、という問題です — が、コードは untrunc ではありません。untrunc は GPL ライセンスなので、私たちはそれをアルゴリズムの参照としてのみ用い、TypeScript でクリーンルーム実装を書きました。そのおかげで、まったく同じエンジンがブラウザでも Node でも改変なしに動作します。これはネイティブの C++ ツールでは、別ビルドなしにはできないことです。

なぜ一部の修復では、もう一つの正常なファイルを求められるのですか?

MP4/MOV がインデックスを失うと、そこに収められていたタイミング情報やデコーダーの設定も一緒に消えます。同じカメラで、同じ設定で撮影された正常な参照クリップがあれば、エンジンはあなたの機器にとって「正常な」フレームのタイミングやコーデックの設定がどのようなものかを学び、それを復元したフレームに適用できます。JPEG でも同じ考え方がヘッダーに当てはまります:同じカメラで撮った写真のドナーヘッダーが、デコーダーに必要なテーブルを供給します。壊れたファイルがまだ自身のメタデータを十分に保持していれば、参照は不要です。

どんな壊れたファイルでも直せますか?

いいえ、そしてそう装うこともしません。コンテナとインデックスの修復は、ペイロードが生き残り、それを対応づける地図だけが失われたり損傷したりした場合に機能します — これは現実世界の「破損」の大部分を占めます。実際のデータが物理的に消えているとき(上書きされた、故障したディスクによってゼロで埋められた、切り詰められて失われた)、再構築すべきものは何もなく、正直なツールならそう言うしかありません。各エンジンは、もっともらしく見えるだけで再生できないファイルを吐き出す代わりに、成功・部分的・失敗という明示的な結果を、損傷クラスとともに返します。

非常に大きな動画ファイルはどう扱いますか?

ファイナライズされていない録画はしばしば数ギガバイトに及び、ブラウザの WebAssembly にはアドレス空間の上限があります(ブラウザとビルドにより、おおよそ 2〜4 GB)。そこでエンジンはストリーミングします:ファイル全体を一度にメモリへ読み込むのではなく、範囲(レンジ)ごとに読み込んで処理します。これによりメモリ使用量が一定に保たれ、そのままでは到底収まりきらない数ギガバイトのファイルでも、タブの中で修復できるようになります。

このすべての中心にあるフォーマットの、バイトレベルのツアーをご希望ですか?MP4 の解剖学:moov アトムとは何か、そしてなぜ動画が再生されないのか をお読みください。