
QuickTime でトリミングした MOV を FFmpeg で GIF に変換すると冒頭が切れる問題を解明したら意外な仕様が判明した
まずは忙しい方向けに解決方法をまとめました。なんのこっちゃ、という方は経緯セクションまで飛ばしてください!
困っている方向けの解決方法まとめ
入力オプションに -ignore_editlist 1 を付ければ直ります。必要に応じて-ss/-toでトリミングを調整してください。
なお、-ignore_editlist 1 は -i より前(入力側)に、-ss/-to は -i より後(出力側)に書かないと意味がないのでご注意ください。
ffmpeg -ignore_editlist 1 -i input.mov -vf "fps=<フレームレート>" -ss <開始位置の秒数> -to <終了位置の秒数> output.gif
-ss/-to に指定する秒数
ここには、QuickTime Player 上で見える時刻にオフセットを足した値を指定する必要があります。
オフセットは「エディットリストの time ÷ タイムスケール」で求められます。それぞれ以下のコマンドで確認できます。
# エディットリストの time を確認
ffprobe -v trace input.mov 2>&1 | grep -A 3 elst
# タイムスケールを確認
ffprobe -v trace input.mov 2>&1 | grep time_scale
トリミング前のファイルが残っている場合、あるいは撮り直しができる場合は、無編集の MOV ファイルを使えば以下のコマンドで OK です。秒数も QuickTime Player 上で見える時刻で指定できます。
ffmpeg -i input.mov -vf "fps=<フレームレート>" -ss <開始位置の秒数> -to <終了位置の秒数> output.gif
なお、本記事では QuickTime Player 10.5、FFmpeg 8.1.2 を使用しています。
経緯
DevelopersIO では、技術ブログという性質上、デモ等で何かと動画を埋め込みたい場面があります。しかし 2026 年 7 月 24 日現在、Zenn 記法では動画ファイルの埋め込みはサポートされていません。YouTube の埋め込みは可能ですが、再生に手間がかかるので私はあまり好みではなく、もっぱら GIF に変換して埋め込んでいます。
画面収録には QuickTime Player を使っているため、ファイル形式は MOV になります。MOV を GIF に変換するには、お手軽なコマンドラインツールの FFmpeg を使用しています。
さて、先日こちらのブログ記事を執筆していた時のことです。
自作アプリ紹介記事なので、冒頭にデモ動画を載せる必要がありました。早速 QuickTime Player で画面収録します。
GIF は勝手にループするという性質上、冒頭と末尾に静止している画を数秒設けないと急に画が飛んで見づらいという特徴があります。今回は AI チャットのストリーミングを末尾まで見せると長くなりすぎてしまうので、冒頭にのみ長めに静止部分を設けるようにして収録しました。その後、最適な長さに冒頭と末尾をトリミングして保存します。冒頭は 1 秒ほど、末尾は 3 秒ほど切りました。
その MOV を、FFmpeg を使って以下のコマンドで GIF に変換しました。
ffmpeg -i input.mov -vf "fps=10" output.gif
すると、なぜか出力された GIF の冒頭の静止部分が丸々削れていました。一方、末尾は問題ありませんでした。

実際の GIF の比較はこちら

冒頭から一部が消えています。
こんなことは初めてです。普段と違う操作をしなかったかしばらく考えてみたところ、そういえば今回は前後のトリミングを普段より多めにしていたような……?
試しに一切トリミングしていない MOV を同じコマンドで変換してみたところ、問題は発生しませんでした。つまり原因はトリミングです。
エディットリストを調査する
QuickTime のトリミングは、エディットリスト(elst) という仕組みを利用しています。エディットリストとは、MOV/MP4 のコンテナに存在するメタデータです。これは動画データ本体には手を付けず、再生範囲だけを変えるためのものです。
エディットリストは ffprobe の trace ログで確認できます。
ffprobe -v trace input.mov 2>&1 | grep -A 3 elst
トリミング後の MOV ファイルではこうなりました。
[mov,mp4,m4a,3gp,3g2,mj2 @ 0xae0c0c000] track[0].edit_count = 1
[mov,mp4,m4a,3gp,3g2,mj2 @ 0xae0c0c000] duration=8709 time=648 rate=1.000000
time=648 は「メディアデータの先頭から 648 単位進んだ位置から再生を始める」という意味で、duration=8709 は「8709 単位進んだ位置まで再生する」という意味です。

この単位はトラックのタイムスケールです。QuickTime のタイムスケールの初期設定は 600 で、今回のファイルもそうでした。つまり time=648 は 648 ÷ 600 = 1.08 秒を冒頭から飛ばすという意味、duration=8709 は 8709 ÷ 600 = 約 14.5 秒再生したら打ち切るという意味です。これは私が MOV ファイルに施したトリミング処理と一致します。
つまり、ファイル自体は既にトリミング済みな一方で、エディットリストにもトリミング用の指示が入ったままの状態でした。FFmpeg はデフォルトでエディットリストに従うので、このスキップが既存のトリミングに重ねて適用され、冒頭のトリミングが二重にかかった状態で GIF が生成されていたのでは? というのが最初の推理です。それだと末尾のトリミングは説明できませんが……。

ひとまず、次は以下のコマンドを使って GIF に変換してみました。 -ignore_editlist 1 は、エディットリストの指示を無視し、存在する全メディアデータを先頭からデコードするオプションです[1]。
ffmpeg -ignore_editlist 1 -i input.mov -vf "fps=10" output.gif
GIF を確認すると、冒頭の静止部分もきちんと残っていました。やった!しかし何かがおかしいような……?
よく確認すると、MOV でトリミングしたはずの冒頭部分まで完全に復活してしまっています。末尾の方は完全には戻っていませんが、部分的に復活しています。

奇妙な挙動ですね。この挙動は先ほどの二重トリミング説だけでは説明がつきません。
GIF 変換自体はここから余分な部分を消せばいいだけなので、冒頭で紹介した通り -ignore_editlist 1 のほかに -ss/-to(入出力をカットするオプション)を併用することでうまくいきましたが、ここまで来たらこの謎の挙動を解き明かしたい気持ちが湧いてきました!
手始めに、QuickTime と FFmpeg の仕様について更なる考察が必要そうです。
QuickTime におけるトリミングの仕様
そもそも、トリミング後のファイルにエディットリストを残す意味とは何なのでしょうか。QuickTime Player ではトリミング済みの動画を一度開き直すと元に戻せないですし、トリミングの履歴をわざわざ残しておく意味はないように思えます。
この点について詳しく調べてみたところ、どうやらこれは QuickTime におけるトリミングの特殊な仕様に関係するようです。以下で詳しく説明します。
前提の用語解説
QuickTime の画面収録では、H.264 という動画圧縮コーデックが使われています。動画は毎秒数十枚のフレームの連続ですが、全フレームを画像として保存するとサイズが膨大になるので、H.264 は大半のフレームを「他のフレームとの差分」として記録し、一定間隔で完全な画像情報を持つ キーフレーム を挟みます。
あるキーフレームから次のキーフレームの手前までのひとまとまりは GOP (Group of Pictures) と呼ばれます。キーフレームはデータが大きいので、変化の少ない映像ではエンコーダーは間隔をどんどん広げます。そのため、動きが少ない画面収録だと GOP が数秒に及ぶこともあります。

詳しい解説を読みたい方は下記 AWS ブログがわかりやすいです。
エディットリストとプリロールの扱い
差分フレームは参照先のフレームがないとデコードできません。一方で QuickTime のトリミングは、キーフレームの位置を気にせず好きなフレームで切れて、しかも再エンコードしない無劣化トリミングです[2]。
この二つを両立させるには、少なくともトリミング点から直前のキーフレームまでのデータ(プリロール)はファイルに残す必要があります。プリロールがないとトリミング点直後のフレームがデコードできないからです。その上で、エディットリストで「再生はトリミング点から」と指示して余計な映像を隠しているわけです。
要は、エディットリスト(とプリロール)は無劣化トリミングを実現するために必ず残しておかねばならない情報であるということです。
QuickTime におけるトリミングの細かい仕様については、トリミング位置を変えたファイルを何本か作って検証しました。検証の詳細は割愛しますが、結果は次のとおりでした。
- 冒頭側: トリミング点の直前のキーフレームより前のデータを削除する。残るプリロールはデコードに必要な最小限で、エディットリストの
timeはトリミング点から直前キーフレームまでの距離になる - 末尾側: トリミング点を含むフレーム + さらに 2 フレームまで残し、その先のデータを削除する。H.264 では未来のフレームも参照する都合上、格納順と表示順が数フレームずれることがあるため、おそらくその並べ替え用の余裕?

ところで、QuickTime の画面収録は基本的に 可変フレームレート(VFR) です。変化があったときだけフレームを記録するので、今回の冒頭のような静止区間では同じフレームが何秒も表示され続けます。
これを踏まえて今回のファイルのキーフレーム一覧を確認すると、1.08 秒のトリミング点は最初のキーフレームと 2 番目のキーフレームの間にありました。

実際にキーフレームを確認した流れ
キーフレーム位置は次のコマンドで確認できます。
ffprobe -v error -skip_frame nokey -select_streams v \
-show_frames -show_entries frame=pts_time -of csv=p=0 input.mov
トリミング済みの収録ファイルで実行した結果がこちらです。
0.773333
1.760000
2.746667
4.908333
6.841667
9.015000
最初のキーフレームが 0.773 秒の位置にあり、時刻 0 にはキーフレームがありません。ffprobe も基本的にエディットリストを反映した時刻を返すため、time の指示に従って冒頭のキーフレームを隠します。そのため先頭にキーフレームがないように見えているのでしょう。
ということで、次に -ignore_editlist 1 を付けて同じ一覧を出してみます。
0.000000
3.135000
4.121667
5.108333
7.270000
9.203333
11.376667
先ほどの一覧にはなかったキーフレームが増え、0 秒時点に現れました。こちらが本当の先頭キーフレームであり、元動画の起点です。
これで先ほどの模式図が正しかったことが確認でき、QuickTime が行った処理について概ね理解できました。
つまり、トリミングしたはずの冒頭 1.08 秒はデータ的には削除されておらず、丸ごとプリロールとして残っていたということになります。また、末尾についても 2 フレームと少し余分に残っているはずです。
FFmpeg と QuickTime の仕様の違い
さて、それでは本題です。なぜ FFmpeg は冒頭の静止部分を丸ごと消してしまうのでしょうか。
キーフレーム一覧の時刻を突き合わせると、エディットリストを適用した場合としなかった場合で全行が約 2.36 秒ずれていました。これが FFmpeg が MOV ファイルに対して適用したスキップ量です。
一方でエディットリストの指示は time=648(1.08 秒)で、指示より約 1.28 秒余計に削っていることになります。
この原因は FFmpeg と QuickTime のトリミングにまつわる仕様の違いなのではないか、と推測しました。FFmpeg のソースコード等を調査した結果、以下のような仕様が判明しました。
- QuickTime Player: トリミング点が跨いでいるフレーム(キーフレームではなくフレーム)をトリミング点から残り時間ぶん表示するので、静止した冒頭が見える。末尾も同様に跨いでいるフレームをトリミング点で打ち切って表示するため、意図した位置で終わる。
- FFmpeg: 開始時刻がトリミング点より前のフレームを捨ててしまい、トリミング点以降に始まる最初のフレームから再生するので、静止した冒頭が消える。末尾は逆に、終了点を跨ぐフレームも捨てずに含めるため、静止した末尾は消えず、むしろ意図より長くなりうる。[3]
さらに、実際にキーフレーム以外も含んだ全フレームの時刻一覧を確認した結果、0.53 秒から 2.36 秒の 1.8 秒間も続く長いフレームが存在しました。

つまり今回の事象の原因は、トリミング点を含む長いフレームを FFmpeg が丸ごとスキップしてしまったことでした。
ちなみに、末尾側についても実ファイルで確認してみたところ、前述の削除規則の通りトリミング点を跨ぐフレーム + 2 フレームがエディットリストで隠されて残っていました。-ignore_editlist 1 で末尾のトリミング分の一部まで復活するのはこのためです。
実際の検証過程
キーフレーム以外も含んだ全フレームの時刻一覧を確認します。仮説が正しければ、1.08 秒より前に始まるフレームの次がいきなり 2.361667 秒に飛んでいるはずです。
ffprobe -v error -ignore_editlist 1 -select_streams v \
-show_frames -show_entries frame=pts_time -of csv=p=0 input.mov | head -10
0.000000
0.026667
0.066667
0.093333
0.466667
0.506667
0.533333
0.560000 ← 1.08 秒の直前のフレーム
2.361667
2.388333
思った通り、0.560000 の次が 2.361667 になっています。0.56 秒に始まるフレームが約 1.8 秒間表示され続けており、トリミング点の 1.08 秒を跨いでいます。超過スキップの実測 2.361667 − 指示 1.08 = 1.281667 秒で、キーフレーム一覧から測った超過量とほぼ一致しました!
冒頭の静止部分が丸ごと消えた理由がこれでようやく明らかになりました。
ちなみに、末尾側についても実ファイルで確認してみました。
12.525000
12.631667
13.045000
13.538333
14.098333
14.538333
15.031667 ← トリミング点が跨ぐフレーム開始
15.525000 ← 終了
16.060000
17.166667
前述の削除規則のとおり、トリミング点を跨ぐフレーム + 2 フレームがエディットリストで隠されて残っています。
なぜ仕様が違うの?
FFmpeg がフレームを捨ててしまう理由については明確なソースが見つけられなかったので、ここで考察してみようと思います。
QuickTime Player は動画を再生するためのアプリケーションであり、その時点で表示すべき画像さえ描画できれば、その時刻がフレームの表示期間の途中であっても問題ありません。
一方、FFmpeg はファイルを読んでフレームやパケット単位のデータに切り出す仕組みで、フレームを 1 枚ずつ次の処理に渡します。実装上、「このフレームは含めるが表示は途中から始める」という指定を伝える手段は用意されていません[3:1]。
そのため、FFmpeg では VFR で 1 フレームが極度に長いケースを想定していないのではないか、というのが現段階での推測になります。もし本当の理由を知っている方がいたら教えてください!
まとめ
今回判明した仕様を整理すると以下の通りです。
QuickTime Player のトリミング
- エディットリストを利用した無劣化トリミングであり、映像データ自体は完全には削除されない
- 冒頭側はトリミング点の直前のキーフレームより前のみを削除し、その間のデータはプリロールとして残る。末尾側はトリミング点を跨ぐフレーム + 2 フレームまでを残す
FFmpeg によるエディットリストの解釈
- デフォルトでエディットリストに従うが、冒頭側は
timeより前に開始するフレームを丸ごと破棄する。末尾側は逆に、終了点を跨ぐフレームも破棄せず含める - 結果として、冒頭は指示より余分に削られ、末尾は意図より長く残ることがある
画面収録の仕様
- QuickTime の画面収録は VFR のため、静止区間では 1 フレームの表示時間が数秒に及ぶことがある
- その長いフレームがトリミング点を跨いでいると、FFmpeg 側では冒頭の静止部分が丸ごと消える
余談ですが、今回判明した仕様は、裏を返すと QuickTime のトリミングで落とした映像は一部消えずに残りうるということになります。この記事でやったように -ignore_editlist を付ければ復元できてしまうので、機密情報などの映り込みをトリミングして対処するのは危険かもしれませんね。
同じ症状で困っている人のお役に立てれば幸いです!







