スタディサプリ Product Team Blog

株式会社リクルートが開発するスタディサプリのプロダクトチームのブログです

再エンコード無しで、HLS動画への字幕の追加を実現する

こんにちは、動画基盤チームの @kumackey です!

2月17日に、『スタディサプリ高校講座』に生成AIを活用した字幕表示機能が追加されました! まずはブラウザ版・特定講座での提供となりますが、今後提供範囲の拡大も検討していきます。

参考: 『スタディサプリ高校講座』、講義動画に字幕表示 音声文字起こし生成AI活用で、聴覚障がい者も利用しやすく まずは「数学Ⅰ/A」「情報Ⅰ」で

本記事では、字幕をHLSで配信させる方法についてご紹介したいと思います。

字幕の種類

字幕には大きく以下の2種類あります:

  • Open Captions:常に表示される字幕
  • Closed Captions:表示・非表示を切り替えることができる字幕

Open Captionsは動画ファイル本体に字幕を含めさせる(焼き込む)などして実現します。 一方で、Closed Captionsでは字幕ファイルを動画ファイルと分けることができます。 Closed Captionsは、Open Captionsに比べて以下のように多くのメリットがあります:

  • 字幕の修正をしたいときに、動画ファイル本体を再編集する必要がない
  • 字幕を多言語対応させることが可能になる
  • 仕様によって字幕を非表示にさせたいときに、非表示にさせる制御も可能になる

後述のHLSではClosed Captionsとして配信するための仕組みがあります。 HLSの仕様に従ったファイル構成にすることで、HLS準拠のプレイヤーではClosed Captionsを実現できます。 本記事では、Closed Captionsで字幕が表示できるようにすることを前提としています。

エンコードすることの問題

動画のエンコードを行うサービスには、字幕ファイルを含めた動画ファイルを生成するサービスがあります。 例えば『スタディサプリ』でも利用しているAWS Elemental MediaConvertでは、字幕ファイルを入力に取ることができます。 MediaConvertでは字幕ファイルとエンコードされた動画ファイルを同時に生成し、紐づけることが可能です。

字幕を含むエンコード処理

しかし、動画のエンコードはコストの高い処理です。 字幕の追加だけであれば、動画コンテンツ自体は変更する必要がなく、動画のエンコードは大きな無駄となってしまいます。 『スタディサプリ』では4万本以上の動画コンテンツがあります。 字幕追加のためだけに、全ての動画を再エンコードすることはあまり現実的ではありません。

この再エンコードによるコストの問題を解決するため、私たちはHLSの仕様に基づいた独自の字幕追加基盤を実装することを選択しました。

HLSの仕様

HLSにて字幕を配信する方法を説明する前に、先に前提となるHLSの仕様について説明します。 HLS (HTTP Live Streaming)Appleが開発したHTTPベースのストリーミングプロトコルです。 RFC8216にて定義されています。

参考: RFC 8216 - HTTP Live Streaming

この仕組みにより、Web上で動画や音声といったマルチメディアのストリーミング配信を実現できます。

プレイリストの種類

HLSの仕様では、以下の2種類のプレイリスト(拡張子:.m3u8)を使用します:

  • メディアプレイリスト(Media Playlist):メディアセグメントのURLのリストを含むプレイリスト
  • マルチバリアントプレイリスト(Multivariant Playlist):複数の画質の動画などを提供するための、メディアプレイリストの集合となるプレイリスト

メディアプレイリストの中身

メディアプレイリストは以下のような内容となります。 ここではコンテナフォーマットをMPEG2-TSとしており、動画セグメントは拡張子が.tsのファイルとなります。 動画セグメントの中に、実際の動画データと音声データが含まれています。

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA-SEQUENCE:0
#EXT-X-TARGETDURATION:10
#EXTINF:10.0,
720p_segment1.ts  # 最初の10秒の動画セグメント
#EXTINF:10.0,
720p_segment2.ts  # 次の10秒の動画セグメント
#EXTINF:10.0,
720p_segment3.ts  # その次の10秒の動画セグメント
#EXT-X-ENDLIST

マルチバリアントプレイリストの中身

マルチバリアントプレイリストは以下のような内容となります。 これにより、例えば異なる解像度を持つ動画の各ストリームを提供できるようになります。

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-STREAM-INF:BANDWIDTH=831000
720p.m3u8  # 720pのメディアプレイリストへの参照
#EXT-X-STREAM-INF:BANDWIDTH=431000
540p.m3u8  # 540pのメディアプレイリストへの参照
#EXT-X-STREAM-INF:BANDWIDTH=239000
360p.m3u8  # 360pのメディアプレイリストへの参照

HLSにおけるファイル構成

HLSのファイル構成は、以下のような階層的な構造となっています。

HLSのファイル構成

エンドユーザが使うブラウザがファイルを読み込む順番としては、以下のようになります。

エンドユーザがファイルを読み込む順番

HLSの仕様に基づく字幕配信

次にHLSの仕様に基づく字幕配信の方法を説明します。 字幕ファイルはWebVTT(Web Video Text Tracks)フォーマットを使用します。 これはHLSの仕様で字幕を提供する際の標準的なフォーマットの一つです。 WebVTTファイルは拡張子が.vttとなり、以下のような内容となります。

WEBVTT

00:00:06.000 --> 00:00:09.000
こんにちは。くまっきーです。  # 6-9秒の間に表示される字幕

00:00:09.000 --> 00:00:13.000
今日はWebVTTについて解説したいと思います。  # 9-13秒の間に表示される字幕

00:00:13.000 --> 00:00:18.200
それでは、WebVTTのファイルの中身を見ていきましょう。  # 13-18秒の間に表示される字幕

この字幕ファイルをHLSで配信するためには、主に以下2つのファイル変更が必要となります:

  • 字幕用のメディアプレイリストを作成する
  • マルチバリアントプレイリストに字幕用メディアプレイリストへの参照を追加する

字幕用のメディアプレイリストの作成

HLSでは、字幕は独立したレンディション(Rendition)として扱われます。 動画のセグメントと同様に、字幕のセグメントもメディアプレイリストを以下のように作成する必要があります。 字幕は1つのファイルのみを使用するため、メディアプレイリストも単一のファイルへの参照のみとなります。 動画本体の.tsファイルのように、字幕ファイルを複数のセグメントに分けることも可能なようです。 ただし字幕の中身はそこまで大きくないテキストファイルなので、シンプルに1ファイルにしています。

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-TARGETDURATION:19
#EXTINF:19,
subtitles.vtt  # 字幕ファイルへの参照
#EXT-X-ENDLIST

マルチバリアントプレイリストに字幕用メディアプレイリストへの参照の追加

マルチバリアントプレイリストにはその字幕用のメディアプレイリストへの参照を追加します。 マルチバリアントプレイリストの中身は以下のようになります。 EXT-X-MEDIAタグのTYPE=SUBTITLESは字幕のレンディショングループを指定し、GROUP-ID属性はそのグループの識別子となります。 EXT-X-STREAM-INFタグのSUBTITLES属性は、このグループ識別子を参照することで、バリアントストリームと字幕レンディションを関連付けます。 このSUBTITLES属性がないと、 『スタディサプリ』で使用している動画プレイヤーでは字幕が再生されないブラウザもありました。

#EXTM3U
#EXT-X-VERSION:3
#EXT-X-MEDIA:TYPE=SUBTITLES,GROUP-ID="SUB",NAME="日本語",LANGUAGE="ja",URI="subtitles.m3u8"  # 字幕用のメディアプレイリストへの参照
#EXT-X-STREAM-INF:BANDWIDTH=831000,SUBTITLES="SUB"
720p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=431000,SUBTITLES="SUB"
540p.m3u8
#EXT-X-STREAM-INF:BANDWIDTH=239000,SUBTITLES="SUB"
360p.m3u8

ファイル構成

以下のようなファイルの構成となります。

字幕ありのHLSのファイル構成

エンドユーザが使うブラウザがファイルを読み込む順番としても、動画本体と変わりません。

エンドユーザが字幕ファイルを読み込む順番

字幕追加基盤の構成

上記で説明したHLSの仕様に基づく字幕配信を実現するため、以下の処理を基盤として実装する必要があります:

これらの処理を実装するにあたり、スクリプトにはRubyを採用しています。 字幕ファイルやプレイリストの読み込みや更新をするにあたり、以下のようなライブラリが役立ちました:

実装したRubyスクリプトは、『スタディサプリ』で採用しているジョブ基盤である、Argo Workflowsで実行されます。 以下のような構成となります。

字幕追加フロー

CDNキャッシュの制御

CDNによる動画配信

スタディサプリ』では動画ファイルの配信を効率的に行うため、Fastly CDN(Content Delivery Network)を利用しています。 配信の処理順序は以下のようになります:

動画配信フロー

  1. ユーザーがブラウザで動画を再生しようとすると、まずFastly CDNにリクエストが送られます。
  2. リクエストされたファイルがCDNのキャッシュに存在する場合(キャッシュヒット)にはCDNは直接キャッシュからファイルを返します。
  3. リクエストされたファイルがキャッシュに存在しない場合(キャッシュミス)にはCDNAmazon S3の配信用バケットからファイルを取得します。取得したファイルをキャッシュに保存し、以降のリクエストに備えます。

CDNキャッシュのパージ

CDNを利用した動画配信では、CDNキャッシュの扱いに注意が必要です。 Amazon S3配信用バケットの内容を更新しても、CDNキャッシュは残り続けます。 動画ファイルや字幕ファイルがCDNキャッシュに残り続けることで、以下のような問題が発生します:

  • マルチバリアントプレイリストやメディアプレイリストの更新が即座に反映されない
  • 字幕ファイルを修正しても、ユーザーには古い字幕が表示され続ける

このため、配信用バケットの内容を更新した場合には、CDNキャッシュをパージ(削除)する必要があります。 Fastly CDNでは、キャッシュパージのためのAPIが提供されています。

参考: Purging | Fastly Documentation

このAPIを利用して、字幕追加スクリプトによって配信用バケットの内容を更新した際に、合わせてCDNキャッシュをパージするようにします。 以下のような構成となります。

字幕追加フローにおけるキャッシュパージ

CDNキャッシュパージ処理の疎結合

当初は上記のように字幕追加スクリプトにキャッシュパージを実装したのですが、改善点がありそうでした。 例えば、字幕追加スクリプト以外から配信用バケットが更新される場合などには、CDNキャッシュパージが実行されません。 運用を考慮して、スクリプトは字幕の追加のものだけでなく、更新や削除を行えるようなスクリプトも開発されました。 上記のような設計では、それぞれのスクリプトにキャッシュパージを実装する必要が出てきてしまいました。

字幕関連のスクリプトのキャッシュパージの重複

私たちが実現したかったのは、「配信用バケットのオブジェクトが更新されたら、自動的にCDNキャッシュをパージする」という汎用的な操作です。 上記の設計は、この汎用的な操作が字幕関連のスクリプトに依存してしまっていることを意味しています。 このような汎用的な操作は、字幕関連のスクリプトとは分離された疎結合な設計にしておきたいです。

そこで、以下のような構成を採用しました。

  1. S3の配信用バケットでファイルが更新されると、Amazon S3 Event Notificationsがイベントを発行
  2. そのイベントをトリガーとしてAWS Lambda関数が起動
  3. AWS Lambda関数がFastly Cache Purge APIを呼び出し
  4. 対象のキャッシュがパージされる

キャッシュパージの疎結合化

この設計により、字幕関連のスクリプトによるS3の更新と、キャッシュパージの処理とを疎結合にさせることができました。

まとめ

本記事では、HLSの仕様に基づいて、再エンコード無しで字幕を追加する方法についてご紹介しました。 主なポイントは以下のとおりです:

  • HLSでの字幕の配信には、字幕用メディアプレイリストの作成とマルチバリアントプレイリストへの参照追加が必要です。
  • 字幕追加基盤をArgo WorkflowsとRubyで実装しました。
  • CDNのキャッシュパージ処理はAmazon S3 Event NotificationsとAWS Lambda関数を使用して疎結合な設計としました。

この基盤により、動画の再エンコードなしで字幕ファイルの追加・更新を実現できるようになりました。