DMM.comの、一番深くておもしろいトコロ。

エンコーダーを支えるffmpeg活用

エンコーダーを支えるffmpeg活用

  • このエントリーをはてなブックマークに追加

はじめに

こんにちは。 動画配信事業部・配信基盤チームの八田です。
今回お届けするのは、「進化する動画配信基盤」についての連載第8回目の記事です。

目次記事はこちらです。

inside.dmm.com

第6・7回の記事でお伝えしたとおり、JIROで高速かつ高画質にエンコードができるようになりました。このJIROを支えているのは皆さんご存知のffmpegです。今回の記事では、JIROでどのようなことが行われているのかについて、もう少し詳細にffmpegのエンコードコマンドを添えてご紹介致します。

inside.dmm.com

inside.dmm.com

分散エンコード

JIROの重要な機能の一つが分散エンコードによる高速化です。1本の動画ファイルを1台のエンコーダーでエンコードするのではなく、複数のファイルに分割し、複数のエンコーダーでエンコードを行っています。

f:id:yanoshi:20200318011801p:plain
分散エンコード

分散エンコードの処理を順を追ってご紹介致します。

前処理 (プリエンコード~ファイル分割)

(1)プリエンコード

ここでは、JIROで扱いやすい形、かつバックアップに最適なファイルを生成するために一度エンコードを行います。配信用のファイルではないので、ビットレートの設定はせずに品質指定でエンコードしています。プリエンコードで重要なのはKeyframeの挿入位置です。後に行うファイル分割の位置がKeyframeの挿入位置で決まってしまうからです。ここでは60フレーム毎にKeyframeを挿入し、シーンチェンジのタイミングではKeyframeでなくI-frameを挿入しています。このあたりは皆さんこだわりがあるところかと思いますが、どの様に設定されていますか? というのも実は、JIROでも試行錯誤を繰り返していまして...。
当初は下記の設定でエンコードを行っていました。

  • 60秒毎にKeyframeを挿入
  • シーンチェンジのタイミングでもKeyframeを挿入
ffmpeg -y -i input.avi \
    -x264-params keyint=60:min-keyint=1:scenecut=20 \
    out.mp4

ですが、意図する箇所でファイル分割されない問題が発生しました。例えば、10秒ごとにファイルを分割したく設定するのですが、9.9秒あたりにシーンチェンジで検出したKeyframeが入っていると、9.9秒でファイルが分割されてしまいました。

これを避けるために、
GOPの+-200msecでシーンチェンジを検出しても、Keyframeを挿入しないようにプログラムで検出し、下記のようにKeyframeの挿入タイミングの調整を行っていました。 例えば、2秒ごとにKeyframeを挿入。5.5秒と9.9秒でシーンチェンジ検出した場合は下記になります。

ffmpeg -i input.mp4 \
    -sc_threshold 0 \
    -force_key_frames 2,4,5.5,6,8,10,12 \
    out.mp4

追記: -sc_threshold 0 がなかったので、上記コマンドに追加しました。

といったように、9.9秒のシーンチェンジ検出はKeyframeを入れない処理にしました。
ですが、これを実際に2時間の動画で検証すると、ffmpegのコマンドがものすごく長くなってしまいました。Linux, macOS環境では問題ないのですが、Windows環境で実行するとエラーになってしまいました。
JIROは .NET Coreで開発しているのですが、Argumentsプロパティは32699文字未満にする必要があるようです。

docs.microsoft.com

こちらは実際にはWindowsの制約のようで、Linuxはディストリビューションによって異なるようですが、64KB、128KBまで可能なようです。 プログラムでエンコードコマンドを生成していたためコマンドの長さは意識していなかったのですが、まさかそこまで長くなっているとは思っていませんでした。皆さん、気を付けましょう。
結局、シーンチェンジのタイミングではKeyframeを挿入する必要はなく、I-frameで十分画質が向上したので、上記の案は採用しませんでした。

それでは、実際に使用しているKeyframeとI-frameの挿入を調整するエンコードコマンドをご紹介致します。 x264では、min-keyint値以下でシーンチェンジを検出すると、KeyframeではなくI-frameを挿入してくれます。

ffmpeg -y -i input.avi \
    -x264-params keyint=infinite:min-keyint=60:scenecut=20 \
    -force_key_frames "expr:gte(n,n_forced*60)" \
    out.mp4

下記のコマンドでも実現可能に思えますが、実際にはシーンチェンジ検出時でもkeyframeが入ったため使用できませんでした。

ffmpeg -y -i input.avi \
    -x264-params keyint=60:min-keyint=60:scenecut=20 \
    out.mp4

ちなみに、シーンチェンジ時にKeyframeやI-frameを入れて効果があるのは、ビットレートを指定しないエンコードの場合です。ビットレートを指定したエンコードでシーンチェンジ検出を有効にするとKeyframeやI-frameに多くのビットを費やされるので、画質は悪くなることがあります。

この連載の第9回でテーマになるアーカイブ配信では、このプリエンコードは行っていません。理由は、納品の時点でプリエンコードと同等のファイル形式になっていて、キーフレームも同等の位置で挿入されているからです。この処理を省略することで当日24時公開が可能になりました。

(2)ファイルのバックアップ

プリエンコードしたファイルに音量調整したものをAWS S3にバックアップしています。連載の第10回で、バックアップ後のファイルの活用について紹介するのでぜひ御覧いただければと思います。

(3)ファイルを分割

ここで分散エンコードのキーになるファイル分割を行っています。分割もffmpegの機能を使っています。特別なことはしていないので、早速コマンドをご紹介します。

ffmpeg -i input.mp4 \
    -an -vcodec copy \
    -f segment \
    -segment_time 60 \
    -flags +global_header \
    -segment_format_options movflags=+faststart \
    -reset_timestamps 1 \
    video%04d.mp4

1本の動画から映像を抽出し、segmentを使って複数の映像ファイルに分割しています。音声は分割せず、1本の音声として処理します。

エンコード

大量のエンコーダーを使用し、分割したファイルをそれぞれエンコードします。 2Dで最大9種類。VRでは最大18種類のビットレート・画質違いのファイルを生成しています。
VRで16種類ものファイルを生成している理由は、2018年に大きく分けて2つの画質(Androidデバイスに最適化した画質HQ高画質)を追加したためです。特にHQ高画質はデバイスごとに最適なパラメータでエンコードしているので、多くのファイルを生成する必要があります。 1回のエンコードで複数のファイルをエンコードするために下記のコマンドを使用しています。

ffmpeg -i input.mp4 \
    -filter_complex split=3[out1][out2][out3] \
    -map [out1] -s 1920x1080 out1.mp4 \
    -map [out2] -s 1280x720 out2.mp4 \
    -map [out3] -s 768x432 out3.mp4 

追記: 複数の出力ファイルを生成する場合、filter_complexの処理を統一するようにしています。出力ファイル毎に異なる設定のフィルターを使用すると、フィルターによってはエンコード負荷が高くなる為です。

上記では3種類のファイルを生成していますが、エンコーダーのスペックに合わせて一度に作成するファイル数を変更しています。 エンコードのパラメータの詳細については、Androidデバイスに最適化した画質HQ高画質で行った工夫を次章でご紹介致します。

後処理 (映像と音声ファイルの結合)

最後に、分割されている映像を繋ぎ合わせ、音声と結合して1つのファイルを生成します。

ffmpeg -f concat \
    -safe 0 \
    -i VideoList \
    -i audio.mp4 \
    -map 0:v \
    -map 1:a \
    -codec copy \
    output.mp4

ここで、問題なく映像と音声が再生できれば完成なのですがJIROでは音ずれが発生してしまいました。原因は、エンコード前後でトータルのフレーム数が異なっていたことでした。1分割ファイルあたり1フレーム差があると、1時間で最大2秒のずれ。2時間で最大4秒ずれてしまいます。 エンコードの前後でフレーム数が変わらないように、下記をエンコードコマンドに追加することで音ズレが解消されました。

ffmpeg -i input.mp4 \
    -filter_complex fps=fps=30000/1001,split=3[out1][out2][out3] \
    -map [out1] -s 1920x1080 out1.mp4 \
    -map [out2] -s 1280x720 out2.mp4 \
    -map [out3] -s 768x432 out3.mp4 

これで、無事JIROの重要機能である分散エンコードが完成しました!

高品質なエンコード

ここでは、Androidデバイスに最適化した画質HQ高画質のエンコード例を2つご紹介致します。

Androidデバイスに最適化した画質

皆さんが使用されているスマートフォンですが、iPhoneと違いAndroidは端末の種類が多くスペックも様々です。廉価なAndroid端末の多くはFullHD以上の動画が再生できません。そこで、もちろん画質は下がるのですが、まずVRがどのようなものかを体験していただくためにFullHD相当の画質を準備致しました。

と、そのエンコードの例を説明する前に、皆さんはVRのフォーマットをご存知ですか? 弊社では以下のフォーマットを使用しています。

  • モノラル
    1枚の映像を両眼で視聴するタイプのVRです
  • ステレオ3D
    左目用、右目用の映像をそれぞれの目で視聴するVRです。立体的に見えるのが特徴です。1つの動画で左右両目用の動画を再生する必要があるのですが、そのフォーマットとしてサイドバイサイドトップボトムがあります。

f:id:yanoshi:20200318012055p:plain

(c) copyright 2008, Blender Foundation / www.bigbuckbunny.org

そしてこちらが、通常画質とAndroidデバイス用に新設・最適化した画質です。

サイドバイサイド
(アスペクト比2:1)
トップボトム
(アスペクト比1:1)
サイドバイサイド
(アスペクト比4:1)
通常解像度 3840×1920 1920×1920 3840×960
Android専用解像度 1920×960 1400×1400 1400×1400

サイドバイサイド(アスペクト比2:1)は単純に1/2して、FullHDと同等の解像度に設定しています。
次にトップボトム(アスペクト比1:1)です。 FullHDの解像度は、1920×1080なので、トータルのピクセル数と縦・横のサイズを範囲内の1080×1080として検証を進めていました。ですが、それではFullHDと比べても画質が落ちたので、トータルのピクセル数のみをFullHDと同等となるように1400×1400とすることで、再生も可能で画質もFullHDと同等になりました。
続いてサイドバイサイド(アスペクト比4:1)ですが、こちらは少し複雑です。
3840×960からどうして1400×1400になるの? と思われている方もいるかと思います。当初は1920×480での提供を考えていたのですが、これではFullHD解像度の約半分程度のピクセル数しかありません。 そこで、3840×960のサイドバイサイドを一度1920×1920のトップボトムに変換することにしました。 すると、トップボトム(アスペクト比1:1) 1920×1920と同じファイルに変換ができて、1400×1400の解像度とすることが可能になります。 1920×480 サイドバイサイドとするのではなく、1400×1400 トップボトムとすることで、前者の2倍以上の解像度で再生することが可能になるというわけです。

下記のコマンドで、サイドバイサイドからトップボトムへの変換と解像度を1400×1400に設定しています。

ffmpeg -i input.mp4 \
    -s 1400x1400 \
    -filter_complex stereo3d=sbsl:abl \
    output.mp4

ちなみにAndroid端末では再生可能な解像度のチェックをアプリ内で行っています。そのため、通常解像度で再生できない端末では自動的にAndroidデバイスに最適化した画質で再生されます。

VRHQ高画質

もう1つがこのVRHQ高画質対応です。とても苦労したVRHQの高画質対応ですが、なかでも一番大変だった某VR機器への対応についてご紹介致します。某ゲーム機にてご視聴いただいている方はご存知かもしれませんが、HQ高画質リリース時にはこちらは対応デバイスに入っていませんでした。理由は、定めたHQ高画質にて配信できる見込みがなかったためです。当時、多数のユーザーの皆様から「HQ高画質に対応してほしい」とお問い合わせを頂いていたのですが、全く解決策がありませんでした。最悪「60fpsで配信するために解像度を下げる」ことまで考え、かなり追い込まれていたこのHQ高画質対応ですが、ここからどのようにリリースまでこぎ着けたかをご紹介致します。

※コーデックはH.264を使用しています。

プレイヤーの最適化

まずはプレイヤーの最適化です。検討を始めてからかなり早い段階で4K 60fpsが再生可能になりました。ですが、baselineプロファイルを使用することがその条件でした。理由は「mainhighプロファイルはデコード負荷が高く、安定して60fps再生できない」から。ビットレートの制限をなくしてbaselineでエンコードすればUHQ高画質版として提供可能な画質にはなるのですが、こちらのVR端末はストリーミング配信で行っているので、ビットレートの設定は非常に重要です。そこで、高画質かつデコードに負荷がかからないエンコードパラメータの調整を始めました。

エンコードパラメータの調整

highプロファイルにすることでビットレートの削減ができ、期待する画質になることは確認できていたので、ここからデコード負荷を下げて60fpsを滑らかに再生できるように調整していきます。
まず、効果がありそうな下記2点を試しました。

  • 符号化方式をCAVLCに
  • B-frameは使用しない

baselineと同じ符号化方式にすることと、B-frameを使用しないように変更をしました。その結果、画質は問題なかったのですが、まだまだ再生負荷が高く、60fpsを滑らかに再生はできていませんでした。

その後、ここからなかなか進展を得られなかったのですが、どのような設定にするとデコーダーの負荷が下がるかを継続して考え、見つかった設定があります。

  • マルチスライス
    この設定がHQ高画質対応の決定打になるのですが、少しマルチスライスについて説明したいと思います。H.264は、正確にはフレームという単位ではなくスライスという単位でエンコード/デコードを行います。弊社で配信している動画もほとんど、1枚のピクチャ = 1スライスでエンコードした動画です。ですが1枚のピクチャを複数のスライスで生成ができます。

下記は、マルチスライスを使用してエンコードしたものです。4スライス設定でエンコードしたものなので、4つに分割されています。

f:id:yanoshi:20200318012138p:plain

(c) copyright 2008, Blender Foundation / www.bigbuckbunny.org

こちらが一部抜粋したエンコードパラメータです。

ffmpeg -i input.mp4 \
    -vcodec libx264 \
    -coder 0 \
    -profile:v high \
    -bf 0 \
    -slices 4 \
    output.mp4

このようにすることでスライス単位での処理が可能になり、並列処理にはかなり効果がありそうです。

f:id:yanoshi:20200318012113p:plain

(c) copyright 2008, Blender Foundation / www.bigbuckbunny.org

※マルチスライスにすることで圧縮率が少し落ちるので注意が必要です。
結果、見事にデコード負荷が軽減されデコード済のフレームも十分にバッファがある状態で、カクツクことなく再生できるようになりました。HQ高画質の初期リリースから遅れましたが、無事リリースができました。 この設定で高画質化が期待できるかもしれないので、60fpsで再生させるために解像度を落とされている方はぜひ試してみてください。

おしまい

弊社では、Windows VRではVP9を使用していますが、ほとんどのエンコードをH.264で行っております。今後、さらなる高解像度化を進めていくにあたり、AV1など新たなコーデックの採用も検討しながら、より一層高画質安定した配信を目指しますので、引き続きよろしくお願い致します。