数万人同時視聴イベントの設計パターン
「19時から全員で一斉に視聴しよう!」系のイベント。公式が告知して、数万人がサイトに集まり、同じタイミングで動画を再生する。再生に合わせてWeb上で演出が変わる。
こういう企画を実現するとき、どう設計するか。
Watch Partyとの違い
NetflixのTelepartyやAmazon Watch Partyは「友達同士で動画を同期視聴」するサービス。これらはホストの再生位置に他のメンバーが追従する仕組み。
一方、大規模同時視聴イベントは:
- 数万人規模
- 全員がサーバ時刻という絶対基準に合わせる
- ホストがいない(全員が対等)
- 動画再生に連動したWeb演出がある
設計思想が根本的に違う。
よくある失敗パターン
全部を1台のサーバでやろうとすると死ぬ。
[クライアント] → [本体サーバ]
├── 時刻同期 API
├── 演出データ
├── コメント機能
└── その他全部
数万人が数秒ごとに時刻同期のためにポーリングすると、毎秒数千〜数万リクエスト。普通のサーバは耐えられない。
推奨アーキテクチャ
時刻サーバを分離するのが鍵。
[クライアント] → [時刻サーバ (Cloudflare Workers等)]
↓
[CDN] → 演出タイムシート (JSON)
↓
[本体サーバ] → 動的機能のみ
時刻サーバの役割
超軽量なエンドポイント。やることは2つだけ:
- 現在のサーバ時刻を返す
- イベントの状態(進行中/停止中)を返す
{
"serverTime": 1704067200000,
"status": "running",
"message": null
}
Cloudflare WorkersやVercel Edge Functionsなら、エッジで動くので世界中どこからでも低レイテンシ。しかも安い。
状態管理の相乗り
時刻サーバにstatusを持たせることで、緊急停止にも対応できる。
事故が発生して「いったん止めます」となったとき、status: "paused"を返せば全クライアントに即座に伝わる。復旧したらrunningに戻す。
本体サーバが死んでも、時刻サーバさえ生きていれば一斉通知できる。
クライアント側の時刻同期
初回アクセス時
- 時刻サーバにリクエストを送信
- RTT(往復時間)を計測
- オフセットを計算
const start = Date.now();
const res = await fetch(TIME_SERVER_URL);
const data = await res.json();
const rtt = Date.now() - start;
// サーバ時刻 = 受信時刻 + RTT/2(片道分を補正)
const serverTime = data.serverTime + (rtt / 2);
const offset = serverTime - Date.now();
以後はDate.now() + offsetで「サーバ基準の現在時刻」が取れる。
再同期は控えめに
初回以降はローカルで計算できるので、サーバアクセスは最小限でいい。
- 5分に1回程度で補正
- 時刻サーバにアクセスできなくても進行は止めない
- 状態チェック(緊急停止の検知)を兼ねる
数万人が5分に1回なら、毎秒100〜200リクエスト程度。余裕。
タイムシートの設計
「何秒にどの演出を出すか」を定義したJSON。CDNで事前配布。
[
{ "time": 0, "action": "showOpening" },
{ "time": 560, "action": "startSong1" },
{ "time": 1200, "action": "showParticles" },
{ "time": 6800, "action": "discChange" }
]
クライアント側は補正済み時刻でタイムシートを走査し、該当する演出を発火。完全ローカル処理なのでサーバ負荷ゼロ。
function checkCue(currentTime) {
const elapsed = currentTime - eventStartTime;
for (const cue of timesheet) {
if (cue.time <= elapsed && !cue.fired) {
cue.fired = true;
triggerAnimation(cue.action);
}
}
}
演出設計の現実解
完全同期は無理
どう頑張っても±500ms〜数秒のズレは発生する。原因は:
- ネットワーク遅延のばらつき
- 動画再生開始タイミングの個人差
- デバイス性能の差
ズレに強い演出を選ぶ
| 演出タイプ | 同期しやすさ |
|---|---|
| 背景色がじわっと変わる | ○ |
| パーティクルが降る | ○ |
| テキストがフェードイン | ○ |
| 花火が上がる | × |
| 爆発と同時にフラッシュ | × |
瞬間的な演出は「ズレてる感」が目立つ。「いつ始まってもいい」系の演出を選ぶと破綻しにくい。
スケーリングまとめ
| コンポーネント | 配置 | 理由 |
|---|---|---|
| 時刻サーバ | エッジ (Workers) | 超軽量、高可用性 |
| タイムシート | CDN | 静的ファイル、キャッシュ可 |
| 演出アセット | CDN | 同上 |
| コメント等 | 本体サーバ | 動的機能のみここに集約 |
本体サーバの負荷を極限まで減らすのがポイント。時刻同期とアセット配信をエッジに逃がせば、本体はコメント等のリアルタイム機能だけに集中できる。
まとめ
- 時刻同期を別サーバに分離する
- 初回で時刻取得、以後はローカル計算で進行
- タイムシートはCDNで事前配布
- 演出はズレに強いものを設計
- 状態管理を時刻サーバに相乗りさせて緊急対応も可能に
「全部1台でやる」から「役割ごとに分離する」への発想転換が重要。