2026-08-21 公開 / Webツール
同じ仕事に2つの経路を用意したら、片方だけ壊れて気づかなかった
長い動画を一定時間ごとに切り分けるツールを作りました。手元のMP4では問題なく動いていたのに、WebMとMKVを入れると失敗すると分かりました。原因はファイル形式の違いそのものではなく、「切る位置の調べ方」を形式によって2通りに分けていたことでした。
作ったもの: 指定時間を超えないように切る
「2分ごとに分割」のような指定で動画を切り分けるツールです。再エンコードはしません。元のデータをそのまま詰め替えるだけなので速く、画質も落ちません。
ただし、この方式には制約があります。動画は好きな位置で切れるわけではなく、キーフレーム(それ単体で絵を復元できるコマ)の位置でしか切れません。だから「2分ごと」と言われたら、2分を超えない範囲でいちばん後ろにあるキーフレームを探して、そこで切ることになります。
つまりこのツールの中心的な仕事は「キーフレームがどこにあるかを調べること」です。そして、ここを形式によって2通りに分けたのが間違いの始まりでした。
2つの経路を用意していた
- MP4 / MOV: ファイルの中の索引(キーフレーム一覧が入っている箇所)を自分のコードで直接読む
- WebM / MKV: 索引の読み方を実装するのが面倒だったので、動画変換エンジンに問い合わせて教えてもらう
後者はいわば手抜きです。ただ、そのエンジンは分割そのものにも使うので、すでに読み込んである。「ついでに聞けばいい」と考えました。実装は数行で済みます。
この判断自体は、そのときの状況では悪くなかったと思います。問題は、この2つがまったく別の仕組みで動いているのに、手元で試したのがMP4ばかりだったことです。
症状: エンジンが「正常終了」して、以降が全部動かない
実際のブラウザで確認したところ、WebMを入れると処理が止まりました。出ていたのは異常終了ではなく「終了コード0で終了しました」というメッセージです。
0は普通、成功を意味します。ところが実際には何も出力されていません。
調べると、キーフレーム位置を問い合わせるためのコマンドを実行した時点で、エンジンが自分ごと終了していました。ブラウザ内で動く変換エンジンは、その場に読み込まれた1つのインスタンスとして生きています。その1回の実行で終了してしまうと、後続の分割処理には実行する相手がもういません。
ここが厄介だった点です。失敗したのは「問い合わせ」なのに、死んだのは「エンジン全体」でした。問い合わせが失敗しただけなら、諦めて別の方法に切り替えることもできます。しかしエンジンごと落ちるので、その後に何をしようとしても動きません。エラーの見た目は「終了コード0」なので、成功したようにしか見えません。
そしてMP4が無事だったのは、MP4だけがこの問い合わせを通らなかったからです。自前で索引を読んでいたので、エンジンに何も聞いていません。壊れていたのは最初から片方の経路だけでした。
直し方: 聞くのをやめて、両方とも自前で読む
エンジンの終了を回避する方法を探すこともできましたが、そちらは選びませんでした。問い合わせという行為そのものをやめるほうが確実だと考えたからです。
WebM / MKV の中身は、入れ子の箱が並んだ構造をしています。その中にキーフレームの索引が入った箱があり、そこを読めばエンジンに聞かなくても位置が分かります。
// 索引の箱を見つけたら、そこだけ読む。
// 中身が詰まっている箱は、サイズ分をまとめて読み飛ばす
if (id === CUES_ID) {
readCuePoints(view, pos, size); // キーフレーム時刻を取り出す
} else {
pos += size; // 中身は見ない
}
実装してみると、思っていたより短く済みました。大きな箱(実際の映像データ)は中身を見ずにサイズ分だけ飛ばすので、長時間の動画でも一瞬で終わります。時間の刻み方や全体の長さも同じ場所から取れました。
結果として、エンジンを呼ぶのは実際に分割する1回だけになりました。調べる工程と実行する工程が、はっきり分かれた形です。
そもそも二本立てにしたのが問題だった
直したあとで振り返ると、不具合の直接の原因はエンジンの挙動ですが、それを見つけられなかった原因は設計のほうでした。
- 同じ「キーフレームを調べる」という仕事に、まったく別の実装が2つあった
- 手元の確認は片方の経路にしか通っていなかった
- それでもツール全体としては動いて見えた
もし最初から片方に揃えていれば、壊れたときは全形式が壊れるので、すぐ気づきます。二本立てにすると、片方だけが静かに壊れて、動いている側が「動いている」という誤った安心を供給し続けます。
「面倒な形式だけ外部に任せる」という判断は、実装量としては正しく見えます。ただ実装量が減った分だけ、確認の対象は増えていました。そこを勘定に入れていませんでした。
その後: エンジンごと置き換えた
翌日、分割そのものに使うエンジンも別のものへ置き換えました。両方の索引を自前で読む形になった時点で、重量級の変換エンジンを使い続ける理由が薄くなっていたからです。
- 読み込む量が 23.3MB から 647KB に減った(ツールに同梱できる大きさになった)
- 400秒の動画の分割が 4秒から2秒 に
- 入力を必要な範囲だけ読むので、長い動画でもメモリ使用量が頭打ちになる
この置き換えができたのは、先に「調べる」と「実行する」を分けていたからです。二本立てを1つに畳んだ時点で、実行側だけを差し替えられる形になっていました。直したことが、次の改善の下ごしらえになっていたという順序でした。
持ち帰れること
- 同じ仕事に2つの実装を持たない。持つなら、両方に等しく確認を通す前提で見積もる。実装量が減った分、確認の対象は増えています
- 「面倒な部分だけ外部に任せる」は、そこだけ性質の違う経路を作る。楽をした場所が、いちばん確認から漏れます
- 終了コード0を成功と信じない。出力が空なら、成功していません
- 失敗の巻き添え範囲を確かめる。「問い合わせが失敗した」のか「相手ごと落ちた」のかで、打てる手がまったく違います
- 調べる工程と実行する工程を分ける。分かれていれば、あとから実行側だけ差し替えられます
ちなみに、この不具合は自分では見つけられませんでした。手元の動画がたまたま全部MP4だったからです。自分の持っているファイルは、自分の使い方に偏っているという当たり前のことを、あらためて確認した一件でした。