- 自己紹介
- 1. はじめに
- 2. Motion JPEGの採用とその背景
- 3. HLSの基本構造とキャッシュ戦略
- 4. 最終構成と移行方針
- 5. ブラウザ側の低遅延再生の工夫
- 6. 遅延とLambdaの壁を越える
- 6. 効果検証と現在の配信状況
- 7. 今後の展望
- 8. まとめ
自己紹介
ITインフラ本部 SRE部の小野です。DMMオンクレの映像配信まわりのインフラとアーキテクチャ設計を担当しています。本記事では、動画配信方式をMotion JPEGからHLS(HTTP Live Streaming)へと移行するにあたって、なぜ移行に至ったのか、どのような構成を採用したのか、どんな課題にどう向き合ったのかを整理しています。
1. はじめに
「DMMオンクレ」は、スマートフォンやパソコンを通じて、実際のクレーンゲーム機を遠隔操作できるオンラインクレーンゲームサービスです。24時間いつでもプレイ可能で、約1,500坪の専用スペースに500台以上の筐体を設置し、豊富な景品と多彩な遊び方を提供しています。
オンラインクレーンゲームのプレイや観戦体験を支えるために、AWS環境と自社データセンターを組み合わせたハイブリッドなインフラ構成を採用しています。各オンラインクレーン筐体には、カメラ用のRaspberry Pi(以下、ラズパイ)などの物理デバイスが設置されており、自社データセンター内で稼働しています。
一方、映像配信やリクエストのルーティングには、CloudFront、Lambda、S3、ECS FargateなどのAWSサービスを活用しています。このような構成により、スケーラブルで柔軟なクラウドの特性と、リアルタイム性を担保する物理制御のメリットを両立させています。
本記事ではその中でも、ユーザー体験の中核である"映像配信"に焦点を当てます。なぜなら、操作に対する反応を即座に映像で確認できることが、オンラインクレーンゲームというジャンルにおいては特に重要だからです。遅延の少ない、高品質で安定した映像をリアルタイムに届けることが、ユーザの満足度に直結します。 そうした映像配信のニーズに対し、我々のサービスでは、プレイ中のユーザー向けに届けるライブ映像と、順番を待つユーザーが閲覧する観戦用映像という、2種類の映像配信が同時に行われています。今回紹介するのは、後者(観戦者向け)に対するストリーミング方式をMotion JPEGからHLSへ移行した技術的な取り組みです。
なお補足ですが、プレイヤー向けの映像には少し前からWebRTCの併用も始めています。これにより、より低遅延かつ高画質な体験が可能になりました。今回はHLSへの移行が主題となるためWebRTCには深く触れませんが、"すべてのユーザーによりよい体験を届けたい"というチームの姿勢の一端としてご紹介しておきます。
2. Motion JPEGの採用とその背景
サービス初期に採用していたのが、Motion JPEG(MJPEG)という映像配信方式です。これはJPEG画像を連続的に送信することで、擬似的な動画として再生するシンプルな仕組みです。ブラウザの対応性も高く、導入しやすい利点がありました。 DMMオンクレでは、リアルタイム性を求められる中で、Motion JPEGの低遅延性と手軽さは初期構築において非常に魅力的でした。また、プレイヤーと観戦者で同じ映像を共有できるため、開発・運用の工数を抑えられる点も選定理由のひとつでした。
しかし、サービスの成長に伴ってユーザー数が増加する中、配信スケーラビリティに関する課題も浮き彫りになっていきます。以下の図は、DMMオンクレにおける映像配信部分の従来のシステム構成を示しています。
Motion JPEGは、接続を維持し続ける "keep-alive" 通信方式であり、視聴者の増加に伴ってラズパイへのHTTP接続数も比例して増えます。その結果、CPUやネットワーク帯域に大きな負荷が発生します。
ラズパイは省電力かつ小型という利点を持つ一方で、処理性能には限界があります。多数の接続を同時に処理するのは困難であったため、観戦者のアクセスには事前に接続数の制限を設けることで対応していました。 しかしその結果、視聴を希望するユーザーが配信に接続できないケースが発生し、サービス運用上の深刻な課題となっていました。
キャッシュの導入も検討しましたが、Motion JPEGはHTTPストリーミングとしては特殊な形式です。各フレームが個別のJPEG画像として扱われ、通常の動画のように統一されたヘッダ情報を持ちません。フレームごとにヘッダが付与される構造のため、CDN(CloudFrontなど)ではキャッシュが難しく、リクエストはすべてラズパイに直接届くことになります。
このような制約により、キャッシュや負荷分散といったスケーラビリティ向上策の適用が難しくなっていました。 シンプルで扱いやすかったMotion JPEGも、ユーザー数の増加とともに限界が明確になり、次世代の配信方式としてHLSへの移行が本格的に検討されるようになりました。
3. HLSの基本構造とキャッシュ戦略
HLSは、動画を小さなセグメントファイルに分割し、HTTP経由で順次配信する方式です。具体的には以下の2種類のファイルで構成されます:
- マニフェストファイル(マニフェストファイル/.m3u8 ファイル):
- どのセグメントファイルがどこに存在するか、再生順序、利用可能なビットレートなどの情報が含まれています。
- クライアント(動画プレーヤー)は、まずこのマニフェストファイルをダウンロードして、動画の情報を取得します。
- 複数のビットレートのセグメントファイルの情報が記述されている場合、クライアントはネットワーク環境に応じて最適なビットレートを選択して再生できます。
- ライブ配信においては都度このファイルに最新のセグメントファイルのURLが記載されていますので定期的にアクセスしてその時点の配信コンテンツを特定しています
- セグメントファイル(通常 .ts ファイル):
- 実際の動画や音声データが数秒程度の短い時間に分割されて格納されたファイルです。 マニフェストファイルに記述された順序に従って、クライアントによってダウンロードされ、連続して再生されます。
- セグメントの長さは通常数秒で、例えば 6 秒程度に分割されることが多いです。
キャッシュ戦略
HLS におけるライブ配信でのキャッシュ戦略としては、マニフェストファイル(.m3u8 ファイル)はキャッシュしないように設定することで、常に最新のセグメント情報がプレーヤーに届けられるようにします。一方、セグメントファイル(.ts ファイル)については、CDN を活用して積極的にキャッシュすることで、ラズパイ側の負荷を軽減することが可能になります。
4. 最終構成と移行方針
CDNを活用した移行方針と最終構成
このような制約と要件を踏まえ、観戦者向けの映像配信方式をHLSに移行することを決めました。特に重視したのは、CDNとの親和性とラズパイ負荷の削減です。
理想的には、CloudFrontからオリジンサーバ(ラズパイ)へ直接アクセスする構成が望ましいと考えられます。しかし、DMMオンクレには数百台のラズパイが存在し、それぞれにグローバルIPを割り当てるのは、コスト面・セキュリティ面の両方で現実的ではありません。
ラズパイごとにグローバルIPを持たせる代替として、1つのIPを使ってポートベースで振り分ける(DNAT的なアプローチ)といった手法も技術的には実現可能です。ただし、数百台規模のラズパイをポート単位で管理する方法は、スケーラビリティに乏しく、加えてネットワーク構成の保守やセキュリティ管理が煩雑になるため、今回の構成では採用を見送りました。
この課題に対処するため、Lambda VPCとFunction URLを組み合わせて構成しました。CloudFrontからはそのFunction URLへアクセスしそれを通してオンプレ上のラズパイへリクエストを中継・処理する構成を取りました。 Lambda関数は、そのURLから対象のラズパイを特定しプロキシのように応答します。通信はLambda VPC内で行われるプライベート接続のため、安全かつ柔軟です。 以下はその処理を行っているLambda関数の簡易的なサンプルです(m3u8を取得する例):
exports.handler = async (event) => { const viewerRequest = event.Records[0].cf.request; const cameraIp = extractCameraIp(viewerRequest.uri); const response = await fetch(`http://${cameraIp}/video/front.m3u8`); const body = await response.text(); return { status: '200', statusDescription: 'OK', headers: { 'content-type': [{ key: 'Content-Type', value: 'application/x-mpegURL' }], }, body, }; };
繰り返しになりますがラズパイは、省電力かつ小型という利点を備えている一方で、処理性能には限界があります。このため、多数の接続を同時に処理するのは難しく、観戦者の同時アクセスには事前に接続数を制限することで対応していました。 しかし、その制限により、視聴を希望しても接続できないユーザーが一定数生まれる状況となり、大きな課題の一つでした。
キャッシュの導入も検討しましたが、Motion JPEGはHTTPストリーミングとしては特殊な形式です。各フレームが個別のJPEG画像として送られ、それぞれに個別のヘッダ情報が付与されます。このため、通常の動画のように一貫したヘッダを持たず、CDN(たとえばCloudFront)でのキャッシュが困難です。その結果、すべてのリクエストがラズパイに直接届く構造となっていました。
最終的には以下の構成になりました。
一度はリリースしたS3ベース構成
今回実は一度間違った構成でのリリースを経験しています。
現在のLambdaプロキシ方式に至る前に、HLSファイル(m3u8/ts)をS3にアップロードし、そこから配信する方式を一度実際にリリースしましたが、すぐに取り下げる判断をしました。
この構成では、各ラズパイが数秒おきに .ts ファイルをS3へアップロードし、それをCloudFrontが配信する仕組みとなっています。ラズパイへのリクエストを完全に排除できるため、CDNのみでスケール可能になるという大きなメリットがあり、当初は理想的な配信方式として有望視されていました。
ただし、この構成ではリアルタイムでHLSファイルをS3にアップロードする必要があり、アップロード速度に対する懸念がありました。この課題に対しては、Mountpoint for Amazon S3 を導入することで解決しています。
Mountpoint for Amazon S3は、S3バケットをファイルシステムのようにマウント可能にするツールです。これにより、ラズパイで生成されたHLSファイルをローカルディスクに書き込むことなく、直接S3へ出力できるようになりました。
実際に書き込みテストを行ったところ、2秒間隔で生成されるファイルサイズ35KBの .ts ファイルに対して、99パーセンタイルで0.189ミリ秒という結果が得られました。これは十分に許容可能な書き込み速度と判断しています(S3 スタンダードで計測)。 なお、S3 Express One Zoneでも試行を試みましたが、今回はうまく測定できませんでした。
しかし、S3へのPUTリクエストに課金が発生する仕様を見落としており、今回の構成でリリースした際に想定外の事態が発生しました。 数百台のラズパイから、数秒おきに継続的な書き込みが行われるという、本サービス特有のアクセスパターンによって、短期間で想定を大きく上回るコストが発生してしまったのです。
このため、急遽リリースを取り下げ、いったん従来の構成に戻すという判断を下しました。
その他の構成検討
以下に、検討した他の構成案について具体的に紹介します。
CloudFront VPC オリジン + CloudFront Functions
CloudFrontのオリジンにVPC内リソースを直接接続する構成も検討対象に挙がっていました。これはLambdaや中継サーバを介さず、CloudFrontから直接プライベートネットワーク内のラズパイにリクエストを送る構成です。 しかしこの方式では、CloudFront Functions内でURLから対象ラズパイのIPアドレスを特定する処理が必要になります。 今回はラズパイのアドレスがセキュリティ上の制約のもとで管理されているため、処理が複雑化しました。その結果、CloudFront Functionsの実行時間やメモリ上限といった制約の中では、必要な処理の完了が難しいという結論に至りました。 そこで、安全かつ確実にオリジン振り分けを行えるLambda Function URL構成を採用することにしました。
Lambda + EFS構成
Lambda関数にEFS(Elastic File System)をマウントし、そこにラズパイから映像ファイルを書き込んで配信する構成も検討しました。この方式であれば、ラズパイにリクエストを飛ばさずに済むため、オリジンとなるラズパイの負荷を抑えることができるという点にメリットがありました。 しかし、ラズパイからの書き込みは数秒単位で継続的に発生するため、EFSに対するI/Oリクエストのコストが非常に高額になるという問題に直面し、採用は断念しました。
オンプレミスNAS構成
オンプレミスの自社データセンター内にNASを設置し、ラズパイがそこへ直接書き込む構成も検討しました。この方法であればクラウド経由の遅延や転送コストを避けつつ、安定したファイル共有が可能になるという期待がありました。しかし、機器の調達コストや保守・運用の負担が大きいため、こちらも採用には至りませんでした。
NLB + ECS Fargate + nginx + Lua構成
さらに、NLB+ECS Fargate 上の nginx + Lua スクリプトによる構成も検討しました。nginx に埋め込んだ Lua スクリプトでリクエストを高速に処理・振り分けることで、非常に安定した性能が得られる構成でした。このアプローチは性能面ではもっとも優れており、初期応答やスループットの面でも非常に安定していましたが、コンテナの運用・維持コストやスケール設計の面で Lambda 構成よりも割高になることが試算の中で明らかになり、今回は見送りとなりました。
これらの検討を経て、最終的には現在採用しているLambda Function URL経由での動的プロキシ構成が、コスト・保守性・柔軟性のバランスにおいてもっとも合理的であるという結論に至りました。 このような試行錯誤の末、現在のLambda Function URL経由の軽量・動的ルーティング構成に至っています。
5. ブラウザ側の低遅延再生の工夫
HLS における遅延は、サーバー構成だけでなく、ブラウザ側プレイヤーの設定によっても大きく変動します。 ここでは、クライアントサイドでの再生遅延を最小限に抑えるために実施した工夫を紹介します。
プレイリスト(m3u8)の更新間隔を短く設定
HLSプレイヤーは、プレイリストを定期的に取得し最新のセグメントを確認します。 デフォルトでは数秒間隔での取得が一般的ですが、我々は2秒間隔に設定することでタイムラグを抑制しました。
ただし、間隔を短くしすぎると取得頻度の増加によりHTTPリクエストが増え、ユーザー側だけでなく、ラズパイや中継サーバへの負荷や帯域消費が増大する懸念もあります。 更新頻度と安定性のバランスを考慮し、2秒が最適と判断しています。
セグメントの先読み数を減らす
多くのHLSプレイヤーでは、再生の安定性を重視して3〜4セグメント先読みする設定が標準です。 これを1〜2本に減らすことで、バッファに頼らず映像の即時反映性が向上し、体感遅延を削減できます。
セグメント長の短縮
HLSではセグメントを数秒ごとのファイルとして切り出しますが、6秒や4秒といった長めのセグメントでは、映像の反映にタイムラグが生じやすくなります。 そのため、1〜2秒に区切ることで、反映の速さを優先しました。
プレイヤーライブラリの調整(hls.js)
今回の構成では hls.js を使用しており、再生が最新セグメントに追いついていない場合には、自動的に再生位置をセグメント先頭へ調整する機能を活用しています。 これにより、過度なバッファ遅延や映像ずれの発生を抑制できます。
※一部の実験では、最新セグメントへ強制ジャンプする設定も検証し、有効性を確認しました(ただし実運用は見送り)。
6. 遅延とLambdaの壁を越える
Motion JPEGとの遅延比較
Motion JPEGでは、カメラで取得した画像をそのままストリームで返すシンプルな方式のため、体感的な遅延は約1秒以内と非常に少なく抑えられていました。
一方で、HLSはセグメント化されたファイルを順次保存・配信する構造のため、プレイリストの更新間隔+セグメント長が遅延要因となります。
また、一般的なHLS構成では、バッファの確保が前提となります。しかし、クレーンゲームのように操作と映像が密接に連動する体験では、バッファが大きすぎるとUXを損なう懸念があります。
Lambdaの中継による構成上の遅延
今回の配信構成では、Lambda Function URL を中継として利用しており、CloudFrontからのリクエストをオンプレミスのラズパイへルーティングしています。
ただし、Lambdaにはコールドスタートと呼ばれる初回起動時の遅延があります。 AWSの公式情報によれば、Lambdaの初期起動には100ミリ秒〜数秒の遅延を生じる可能性があり、視聴体験に悪影響を及ぼすケースも確認されています。
サーバー側で実施した最適化
低遅延を実現するため、以下のアプローチでサーバー構成の改善を図りました。
- マニフェスト構成の最適化
- セグメントリストへの反映タイミングや先読み数を調整し、再生開始タイミングを早めました。
- セグメント長の微調整
- 1.5〜2秒程度を基準とし、リクエスト過多による逆効果を調整しつつ応答性を確保しました。
- Lambda処理の軽量化
- リダイレクト・エラーハンドリング含めてロジックを最小限に抑え、平均応答時間を短縮しました。
成果
結果として、Motion JPEG時代と完全に同じ体感レスポンスではないものの、体感2〜3秒程度の遅延に抑えることができました。 操作と映像の同期性に対する体験は十分に許容範囲に収まり、ユーザー満足度の維持に貢献しています。
6. 効果検証と現在の配信状況
今回のHLS構成導入により、以下のメリットがありました。
配信コストの大幅な削減
Motion JPEGは、各フレームを個別にJPEG圧縮する方式のため、HLSと比べるとファイルサイズが大きくなりやすい傾向にあります。 また、今回は中間に存在していたECS Fargateも不要となったことで、全体としてのコストも大幅に削減されました。
スケーラビリティの確保
CloudFrontのキャッシュの利用により大量の視聴者がいても、ラズパイ側の負荷に影響を与えなくなりました。 国内中心ということもあり、ほとんどのアクセスは国内のリージョンキャッシュに乗っていると思われ同一URLに対してのラズパイへの複数回アクセスはほぼない状況となりました。
画質の向上
Motion JPEG時代よりデータ量は下がりつつ滑らかで発色も良好になりました。 具体的な変化は以下の通りです。
唯一のトレードオフは遅延であり、Motion JPEGではゼロに近かった体感が、HLSでは2〜3秒遅れるようになりました。現状は視聴者としてのUXに著しく影響を与えるものではないため問題なしと判断しています。
7. 今後の展望
現在のHLS構成は、安定性を重視したファーストリリースとして実装されていますが、今後は以下の方向性で進化を目指しています:
LL-HLS(Low-Latency HLS)の段階的導入
- セグメント長をさらに短縮(1秒以下)し、より低遅延な配信を実現
- パーシャルセグメントの活用による、セグメント待ち時間の削減
- サーバーサイドの最適化による、エンコード遅延の最小化
画質・配信品質の向上
- より高解像度への対応検討
これらの改善により、Motion JPEG時代の低遅延性を目指しつつ、HLSの持つスケーラビリティと高品質な配信の両立を目指します。
8. まとめ
この移行は、単なる技術選定にとどまらず、スケーラビリティ、遅延、保守性、コストといった複数の観点からシステム全体の構成を再設計するチャレンジでした。 映像配信はUXの根幹を担う重要な要素であり、その仕組みに手を加えることは決して容易な決断ではありませんでした。
振り返れば、S3構成の採用から緊急撤退した判断や、EFSやNASといった構成の試行錯誤、LambdaとFargateを比較してのコスト設計まで、多くの失敗と検証の積み重ねがありました。それでも「ユーザーにとって映像体験をもっと良くしたい」という気持ちが一貫していたからこそ、構成の移行とUX改善が両立できたと感じています。 この記録が、同様にストリーミングの課題や構成見直しに直面している方々のヒントや励みになれば幸いです。
今後もDMMオンクレでは、技術の力でユーザー体験をアップデートし続けていきます。 なお、本稿では私の視点を中心にお話ししてきましたが、実際の移行プロジェクトは私一人の力ではなく、共に動いてくれたチームメンバーの支えがあってこそ実現できたものです。 特に、技術検証から構成調整まで尽力してくれたギョンジュさんの貢献は非常に大きく、この場を借りて感謝を伝えたいと思います。