S3上のMP4ファイルを切り抜いて高速にレスポンスするWeb APIを作る

Advent Calendar 2022

この記事は最終更新日から1年以上が経過しています。

この記事はレコチョク Advent Calendar 2022の13日目の記事となります。

前段

本記事ではストレージサービス上のメディアファイルをオンデマンドで加工する方法に焦点を当てます。
例えば、既存の動画から一部だけを切り抜いた所謂”切り抜き動画”をユーザーの入力に応じて生成する、といった機能の実装に役立ちます。

その中でもAmazon S3上のMP4ファイルの冒頭n秒を切り抜き、それを高速に返す具体的な方法を紹介します。
※ MP4ファイル(動画ファイル)とM4Aファイル(音楽ファイル)は内部構造が共通しているため、M4Aファイルにも全く同じ方法を適用することができます。

本記事の最後には、切り抜いたMP4ファイルを返すWeb APIを実装する方法を紹介します。

また、MP4ファイルの加工には、広く使われているFFmpegを使用することとします。

課題

FFmpegを用いて「S3上のMP4ファイルの冒頭n秒を切り抜き、それをレスポンスとして返す」処理を逐次的に行うと、レスポンス速度に問題が生じます

具体的には、下記のような方法となります。

  1. S3から加工元のMP4ファイルをダウンロードする
  2. ダウンロード完了まで待機
  3. ダウンロードしたファイルをFFmpegによって加工する
  4. 加工が終了するまで待機
  5. 加工したファイルをレスポンスへ流す

FFmpegは通常、加工元のファイルが存在している状態でなければ、処理を始めることはできません。
また、通常、加工が終了し、ファイルが生成されるまで別のプロセスが加工後のファイルに関与することができません(すなわち同期的に処理が行われる)。
このため、上記手順の2.と4.のように待機する時間が必要となります。

この方法をUnixコマンドを使って簡単に実践してみて、時間を測ってみます。
約868MBのMP4ファイルをダウンロードし、FFmpegで冒頭30秒を切り出すとします。
これを実現するには下記ようになり、このケースでは55秒くらいの時間がかかってしまいました。
冒頭30秒分のデータが欲しいだけなのに、全体のデータをダウンロードするまで待たなければならないためです。

したがって、このFFmpegによるボトルネックを解消する必要があります。

解決法(概略)

FFmpegの pipeオプションを用いて、パイプを使って加工元ファイルをFFmpegへ流し、逐一加工を行なってもらいます。
Unixコマンドで行なうと以下の通りとなります。

※ ただし、加工対象のファイルのmoov Boxの方がmdat Boxより先に配置されている必要があるという条件付きです(詳細は後述)。

解説

概要

ダウンロードされたデータを逐一標準出力に書き出します( curl --output -)。
FFmpeg側では標準入力から加工対象のデータを受け取るようにします( ffmpeg -i pipe:0)。
これにより、ダウンロードしながら加工を進行させることができます。
加工が終わった部分だけ逐一吐き出すように出力先を標準出力にします( pipe:1)。
これにより、加工が進行中の際に標準出力を読み取ることで、加工とレスポンスの同時進行が可能です。

以上のことから、データのダウンロード、FFmpegによる加工、レスポンスを全て同時に進行することが可能です。

コマンドの細かいオプションについては本質とはあまり関係ないので説明しません(調べれば出てくる内容かと思います)。

効果

実際にどのくらい時間が短縮されるか見てみます。
先ほどと同じく、868MBのMP4ファイルをダウンロードし、FFmpegで冒頭30秒を切り出してみます。
そうすると3秒で処理が完了します。

条件

この方法を適用するためには条件が存在します。対象のMP4ファイル内部の構造を見た時にmoov Boxの方がmdat Boxよりも先に配置されていることです。

Boxについて

概要

MP4におけるBoxというのは、MP4ファイルを構成する要素のことを指します。
Boxには色々と種類がありますが、本記事において重要なのは、実際の音声・動画データを含むmdat Box、音声・動画データのメタデータを含むmoov Boxです。

Boxは置換可能です。したがってmoov Box → mdat Boxの順に並んでいることもあればmdat Box → moov Boxの順に並んでいることもあります。

moov Boxが先に来ていなければ本記事の方法を使うことができないのは、そうでなければ途中までダウンロードしたデータにメタデータが含まれておらず、mdat Boxのデータを読み取ることができないためと思われます。

Boxの並び順の調べ方

moov Boxが先に来ているかどうか、といった並び順は、バイナリエディタで対象のファイルを開くと分かります。
(例: $ cat {対象のファイル} | xxd | less)

基本的にBOXは、
[BOXの大きさ(4バイト)] → [BOXの名前(4バイト)] → [データ] もしくは、
[BOXの大きさ(4バイト)] → [BOXの名前(4バイト)] → [他のBOX]の構造を持ちます。
(BOXは入れ子構造にすることが出来るため、BOXの中にBOXを配置することができます)

image-20221206092720250.png

したがって、BOXの名前である”moov”(0x6d6f6f76)と”mdat”(0x6d646174)の位置が知ることができれば、
どちらのBOXが先に来ているか調べることが可能です。

moovが先に来ている場合

mdatが先に来ている場合

moov Boxを先に持ってくる方法

FFmpegで実現できます。 faststartオプションを付加します。

こうすればmdat Boxが前にに来ているMP4ファイルにも本記事の方法を適用することができますが…
そもそもmoov Boxを前に持ってくるための本コマンドを実行するには、事前にデータ全体を取得していなければならないという事実には注意です。
したがって、対象のファイル全てのmoov Boxを前に持ってくる処理を事前にやっておく、といった処置が必要となります。

実際にS3にあるMP4ファイルを加工して返すWeb APIを実装する

上記のFFmpegのテクニックを用いて、S3に置いているファイルの冒頭30秒を切り抜き、それをレスポンスとして高速に返すWeb APIを実装します。

方法

概要

下記4つの処理を並列(非同期)に行います。

  • S3から加工前のデータをダウンロード
  • 加工前のデータをFFmpegへ流し込む
  • 加工後のデータをFFmpegから出力する
  • 出力された加工後のデータをレスポンスへ流す

このようにすることで、ダウンロードや加工を待たずにレスポンスを開始することが可能になります。
加えて、全てデータを Streamで扱うため、メモリを圧迫することもありません。

実装

下記のように実装します。
言語はJavaで、フレームワークはSpring Bootを使用します。
本筋とは関係がないため、例外処理は適当に実装しています。

curl --output data.m4a -X GET "http://xxx/data/edit?key_name=123455678.m4a"といったリクエストでデータを取得することができます。

参考

MP4のファイル構造を解説
第26回 携帯ゲーム機PSPの動画ファイル「MP4」とは何か
QuickTime File Format Specification
ffmpeg Documentation


明日のレコチョク Advent Calendarは14日目 【Android】Canvas APIの使い方 です。お楽しみに!