プレイリストは読み込んだのに、画面が真っ暗
M3U8 URL をプレーヤーに貼り付けると、プレイリストは正しく解析され、画質レベルも表示されます。そして——何も起きません。映像エリアは真っ暗です。プレイリストの構文にも URL にも明らかなミスはありません。ストリームが再生されないだけです。
十中八九、これは CORS の問題です。
CORS は Cross-Origin Resource Sharing の略で、ブラウザのセキュリティメカニズムです。あるドメイン上のウェブページが別のドメインからリソースを取得できるかどうかを制御します。cdn.example.com からプレイリストを読み込み、セグメントファイルが media.cdn.example.com にある場合、ブラウザはそれらのサーバーがあなたのページからのアクセスを明示的に許可しているかを確認します。許可されていない場合、ブラウザは JavaScript レベルでリクエストをサイレントにブロックし、プレーヤーにはネットワークエラーとして表示されます。
イライラするのは、プレイリスト自体は問題なく読み込まれるのに、.ts や .m4s の各セグメントが全部失敗することです。これはプレイリストエンドポイントとメディアエンドポイントで CORS ヘッダーが異なる可能性があるためです。サーバーが m3u8 ファイルには Access-Control-Allow-Origin: * を返していても、セグメントファイルには付け忘れていることがあります。
CORS ヘッダーの実際の中身
ブラウザがクロスオリジンリクエストを送るとき、サーバーは正しいヘッダーを返す必要があります。最も一般的なのは:
Access-Control-Allow-Origin: *
特定のオリジンを指定する場合:
Access-Control-Allow-Origin: https://tools.anoiona.net
リクエストによっては、ブラウザが最初にプリフライトの OPTIONS リクエストを送ります。サーバーはこのプリフライトにも正しい CORS ヘッダーを返す必要があります。プリフライトが失敗すると、実際のリクエストは送信されません。
DevTools で確認できます。Network タブを開き、セグメントリクエストをフィルタしてクリックします。Response Headers セクションを見てください。Access-Control-Allow-Origin がなければ、そのセグメントはプレーヤーでは失敗します。ブラウザのアドレスバーで同じ URL を直接開けるのに、です。
「アドレスバーで開ける」と「プレーヤーで再生できる」は別物
ここで引っかかる人が多いです。M3U8 URL をブラウザのアドレスバーで開くと読み込まれます。セグメント URL をクリックしてファイルをダウンロードすることもできます。なのにプレーヤーで再生できないのはなぜでしょう?
ブラウザのアドレスバーで URL を直接開いた場合、クロスオリジンのコンテキストはありません。ブラウザはファイルをトップレベルナビゲーションとして取得します。CORS は適用されません。
ウェブページ内の JavaScript が fetch() や XMLHttpRequest で同じ URL をリクエストする場合、ページとリソースが異なるドメインであれば、ブラウザはクロスオリジンリクエストとして扱います。このとき CORS ヘッダーが重要になります。
HLS.js は JavaScript でプレイリストとセグメントを取得します。一部ブラウザのネイティブ HLS も Media Source Extensions を通じて同じ方法を使います。つまり、すべてのセグメントリクエストが CORS チェックを通過する必要があります。Safari のアドレスバーで正常に再生できるストリームでも、プレーヤーページでは CORS ヘッダーが間違っていれば失敗します。
実践的なデバッグ手順
Network タブから始めましょう。m3u8、ts、m4s でフィルタします。ページをリロードして再生をトリガーします。赤いエントリやステータスコードが 200 以外のエントリを探します。
失敗したセグメントリクエストをクリックします。ステータスコードから多くのことがわかります:
- 403 Forbidden:サーバーはリクエストを認識したがアクセスを拒否しました。トークンベースの認証、リンクの期限切れ、リファラー制限などでよく起きます。
- 404 Not Found:セグメント URL が間違っているか、ファイルが削除されています。プレイリストの相対パスが正しく解決されているか確認してください。
- 200 だが CORS ヘッダーなし:ファイルは読み込まれますが、JavaScript はレスポンスを読み取れません。プレーヤーには空のレスポンスまたは失敗として表示されます。
200 で CORS ヘッダーがないケースでは、レスポンスヘッダーの Access-Control-Allow-Origin を確認します。なければサーバー側で設定する必要があります。クライアント側で修正する方法はありません。
サーバーによっては Access-Control-Allow-Methods や Access-Control-Allow-Headers で許可するリクエストタイプを制限していることもあります。HLS 再生では通常 GET が必要で、場合によっては HEAD も必要です。
リファラーとオリジンヘッダーでも再生が止まる
一部の CDN ストリーミングサーバーは、受信リクエストの Referer や Origin ヘッダーをチェックします。リクエストがホワイトリストにないドメインから来た場合、403 を返したりリダイレクトしたりします。
DevTools でも確認できます。セグメントリクエストをクリックして Request Headers セクションを見ます。Referer ヘッダーにはリクエストをトリガーしたページの URL が表示されます。サーバーが tools.anoiona.net からのリクエストを拒否するが example.com からのリクエストは許可する場合、それで失敗が説明できます。
厄介なのは Referer ポリシーがさまざまであることです。ページが Referrer-Policy: no-referrer を設定して URL の漏洩を防いでいる場合もあります。サーバーにリファラーが必要な場合もあります。この 2 つの要件は相反し、結果はどちら側がポリシーを制御するかに依存します。
私たちの M3U8 プレーヤーの CORS エラー対応
私たちのプレーヤーは CORS をバイパスしません。それはセキュリティ違反であり、意図的に運用していないサーバーサイドプロキシが必要になるためです。
代わりに、診断パネルで CORS エラーを表示します。セグメントリクエストがネットワークエラーで失敗した場合、どのリクエストが失敗したか、なぜ失敗したかを記録します。プレーヤーはメインのステータスエリアにも失敗状態を表示するので、診断情報を掘り下げなくても何が起きたか理解できます。
プレイリストは読み込まれたがセグメントが失敗した場合、プレーヤーはストリームを再生できなかったと報告します。静かに無限リトライしたり、読み込み中を装ったりはしまわせ。この正直さは意図的です。CORS の問題はサーバーの設定問題であり、隠すとデバッグがさらに難しくなるだけです。
ストリーム運用者向け CORS チェックリスト
HLS ストリームを配信していて、ブラウザベースのプレーヤーで正常に動作させたい場合:
- プレイリストとセグメントの両方のレスポンスに
Access-Control-Allow-Origin: *を追加する。 - トークンベースの URL を使用している場合、トークンがプレイリストの終了前に期限切れにならないようにする。
- ストリームをプレーヤーページでテストする。アドレスバーだけでなく。CORS の挙動は異なります。
- CDN が CORS ヘッダーを通過させるか確認する。一部の CDN 設定ではヘッダーが除去されます。
Referer制限を使用している場合、プレーヤーのドメインを許可するか、別の認証方法を使う。
CORS はプレーヤーのバグではありません。悪意あるクロスオリジンリクエストからユーザーを保護するセキュリティ機能です。修正は常にサーバー側で行います。オリジンがリソースへのアクセスを許可されているかをサーバーに伝えるのです。