クラウド ゲーム: WebRTC とエッジ ノードを使用したストリーミング
クラウド ゲームは根本的に革新的なことを約束します。それは、AAA ゲームをどのデバイスでもプレイできることです。 デバイス - スマートフォン、スマート TV、300 ドルの Chromebook - インストールなし、インストールなし 専用ハードウェアで、1,000 ドルの GPU と同じビジュアル品質を備えています。ビジョンは明確ですが、 技術的な実装は、業界で最も困難なエンジニアリング課題の 1 つです。
クラウド ゲーム市場は 2024 年に 151 億ドルに達し、526 億ドルに達すると予測されています 2032 年までに (CAGR 17%)。 NVIDIA GeForce NOW、Xbox クラウド ゲーミング (xCloud)、PlayStation リモート プレイ、 Amazon Luna: 誰もがこのテクノロジーに賭けています。しかし、なぜそんなに難しいのでしょうか?なぜクラウドゲーミングなのか それは単なる「ビデオ ストリーミング」ではありません。ゲームはプレーヤーの入力に 1 秒以内に応答する必要があります。 エンドツーエンドで 100 ミリ秒かかると、エクスペリエンスが再生できなくなります。
この記事では、WebRTC スタックから 低遅延ストリーミング、エッジ コンピューティングによりレンダリングをゲーマーに近づけ、 サーバー密度を最大化するための GPU 仮想化、最適化戦略まで 遅延の 80 ミリ秒 (許容範囲) と 30 ミリ秒 (優れた) の差。
何を学ぶか
- クラウドゲームは従来のビデオストリーミングとは異なるため
- ゲームストリーミング用のWebRTCスタック: DTLS、SRTP、ICE、H.264/AV1コーデック
- MEC (マルチアクセス エッジ コンピューティング) を使用したエッジ コンピューティング アーキテクチャ
- GPU 仮想化: vGPU、GPU パススルー、Capsule による GPU プーリング
- エンコーディングパイプライン: NVENC、VAAPI、ハードウェアアクセラレーション
- レイテンシ バジェット: 100 ミリ秒がエンドツーエンドでさまざまなレイヤーにどのように分散されるか
- 適応型品質: ネットワーク状況に応じたビットレート適応
- 5G と MEC: 5G が低遅延モバイル クラウド ゲームを実現する仕組み
1. レイテンシ バジェット: エンドツーエンドで 100 ミリ秒
クラウド ゲームと Netflix の根本的な違い インタラクティブループ: プレイヤーのあらゆるアクションが処理され、視覚的な結果が脳の前に表示される必要があります。 人間は遅延を認識します。ゲームの場合、この重要なしきい値は合計約 100 ミリ秒です。 これを超えると、ゲームプレイが「遅れ」てイライラするものになります。
レイテンシ バジェット: 100 ミリ秒の配分方法
| レイヤー | 成分 | ターゲットレイテンシ | 実際のレイテンシー |
|---|---|---|---|
| 入力 | デバイス入力の読み取り | 2ミリ秒 | 1~5ms |
| アップロードネットワーク | 入力パケット -> サーバー | 10ミリ秒 | 5~50ミリ秒 |
| サーバ | ゲームロジック処理 | 5ミリ秒 | 3~10ミリ秒 |
| レンダリング | GPUフレームレンダリング | 16ミリ秒 | 8-33ms (30-120fps) |
| エンコーディング | フレーム -> 圧縮ストリーム | 8ミリ秒 | 5~15ms (NVENC HW) |
| ダウンロードネットワーク | ビデオストリーム -> クライアント | 10ミリ秒 | 5~50ミリ秒 |
| デコード | ストリーム -> 生のフレーム | 5ミリ秒 | 3~10ms(HWデコード) |
| 画面 | フレームバッファ -> スクリーン | 8ミリ秒 | 4~16ミリ秒 |
| 合計 | 64ミリ秒 | 31~179ミリ秒 |
最適化されたエッジ インフラストラクチャ (5 ~ 10 ミリ秒の RTT サーバー) を使用すると、合計 50 ~ 70 ミリ秒を達成できます。 従来のインフラストラクチャ (リモート データセンター、50 ミリ秒 RTT) では、150 ミリ秒以上に簡単に到達できます。
2. WebRTC: ゲームストリーミング用のプロトコル
WebRTC はブラウザ間のビデオ通話のために生まれましたが、そのアーキテクチャはそれを理想的なものにします クラウド ゲーム向け: 100 ミリ秒未満の遅延、自動ネットワーク適応、NAT トラバーサル サポート ビデオ (ゲーム ストリーム) と双方向データ (プレイヤー入力) の両方の送信。
WebRTC クラウド ゲームの実装では、 RTCPeerConnection 確立する コミュニケーションチャネル、 RTCデータチャネル 入力をクライアントからサーバーに送信するには、 e RTCビデオトラック ゲームのビデオストリームを受信します。
// Cloud Gaming Client - JavaScript/TypeScript
class CloudGameClient {
private peerConnection: RTCPeerConnection;
private inputChannel: RTCDataChannel;
private videoElement: HTMLVideoElement;
private statsInterval: ReturnType<typeof setInterval>;
constructor(videoEl: HTMLVideoElement) {
this.videoElement = videoEl;
// Configurazione ICE server (STUN/TURN per NAT traversal)
this.peerConnection = new RTCPeerConnection({
iceServers: [
{ urls: 'stun:stun.l.google.com:19302' },
{
urls: 'turn:turn.mygame.com:3478',
username: 'cloudgaming',
credential: 'secret'
}
],
iceTransportPolicy: 'all',
bundlePolicy: 'max-bundle',
// Preferisci UDP per latenza minima
rtcpMuxPolicy: 'require'
});
// Data channel per input player (unreliable per massima velocità)
this.inputChannel = this.peerConnection.createDataChannel('input', {
ordered: false, // Non garantire ordine (input recenti sovrascrivono)
maxRetransmits: 0 // Nessun retransmit (meglio perdere un frame di input
// che riceverlo in ritardo)
});
this.setupVideoReceiver();
this.setupConnectionHandlers();
this.startStatsCollection();
}
private setupVideoReceiver(): void {
this.peerConnection.ontrack = (event) => {
if (event.track.kind === 'video') {
const stream = new MediaStream([event.track]);
this.videoElement.srcObject = stream;
this.videoElement.play().catch(console.error);
}
};
}
// Invia input al server via DataChannel (target: < 1ms overhead)
sendInput(input: GameInput): void {
if (this.inputChannel.readyState !== 'open') return;
// Serializzazione compatta: TypedArray invece di JSON
const buffer = new ArrayBuffer(16);
const view = new DataView(buffer);
view.setFloat32(0, input.dx); // 4 bytes: movimento X
view.setFloat32(4, input.dy); // 4 bytes: movimento Y
view.setUint8(8, input.buttons); // 1 byte: bitmask pulsanti
view.setUint32(12, Date.now() & 0xFFFFFFFF); // 4 bytes: timestamp client
this.inputChannel.send(buffer);
}
// Colleziona statistiche WebRTC per monitoring
private startStatsCollection(): void {
this.statsInterval = setInterval(async () => {
const stats = await this.peerConnection.getStats();
stats.forEach(stat => {
if (stat.type === 'inbound-rtp' && stat.kind === 'video') {
console.debug('Video stats:', {
packetsLost: stat.packetsLost,
framesDecoded: stat.framesDecoded,
framesDropped: stat.framesDropped,
decoderImplementation: stat.decoderImplementation,
frameWidth: stat.frameWidth,
frameHeight: stat.frameHeight,
framesPerSecond: stat.framesPerSecond,
jitterBufferDelay: stat.jitterBufferDelay * 1000
});
}
if (stat.type === 'candidate-pair' && stat.state === 'succeeded') {
console.debug('Network stats:', {
currentRoundTripTime: stat.currentRoundTripTime * 1000,
availableOutgoingBitrate: stat.availableOutgoingBitrate,
bytesSent: stat.bytesSent
});
}
});
}, 1000);
}
// Signaling: negozia SDP con il server di gioco
async connect(serverEndpoint: string): Promise<void> {
// Crea offer SDP
const offer = await this.peerConnection.createOffer({
offerToReceiveVideo: true,
offerToReceiveAudio: true
});
await this.peerConnection.setLocalDescription(offer);
// Invia offer al server di gioco via HTTP
const response = await fetch(serverEndpoint + '/webrtc/offer', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
sdp: offer.sdp,
player_token: this.getPlayerToken()
})
});
const { sdp: answerSdp } = await response.json();
await this.peerConnection.setRemoteDescription(
new RTCSessionDescription({ type: 'answer', sdp: answerSdp })
);
}
}
3. サーバー側: エンコーディング パイプラインと GPU 仮想化
サーバー側では、クラウド ゲームにはリアルタイムのレンダリングとエンコードのパイプラインが必要です。ゲームは実行されます。 専用 GPU 上で各フレームがキャプチャされ、ハードウェア エンコーダ (NVIDIA の場合は NVENC) で圧縮されます。 VAAPI for Intel/AMD) を使用し、WebRTC 経由で送信されます。エンコーディングの遅延は重要です: NVENC を使用すると、はい フレームあたり 5 ~ 8 ミリ秒に達しますが、これはソフトウェア エンコーディングでは不可能な目標です。
// Cloud Gaming Server - Golang con GStreamer/WebRTC
// Gestisce la sessione di gioco per un singolo player
package cloudgaming
import (
"context"
"fmt"
webrtc "github.com/pion/webrtc/v4"
"github.com/pion/rtp"
)
type GameSession struct {
playerID string
peerConnection *webrtc.PeerConnection
videoTrack *webrtc.TrackLocalStaticRTP
inputChannel *webrtc.DataChannel
gameProcess *GameProcess // Processo del gioco isolato
encoder *NVENCEncoder // Hardware encoder
display *VirtualDisplay // X virtual framebuffer
}
func NewGameSession(playerID string) (*GameSession, error) {
// Configurazione WebRTC con codec preferiti per cloud gaming
m := &webrtc.MediaEngine{}
m.RegisterCodec(webrtc.RTPCodecParameters{
RTPCodecCapability: webrtc.RTPCodecCapability{
MimeType: webrtc.MimeTypeH264,
ClockRate: 90000,
// Profilo H.264: High 4.1 per alta qualità a basso bitrate
SDPFmtpLine: "level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=640028",
},
PayloadType: 102,
}, webrtc.RTPCodecTypeVideo)
api := webrtc.NewAPI(webrtc.WithMediaEngine(m))
pc, err := api.NewPeerConnection(webrtc.Configuration{
ICEServers: []webrtc.ICEServer{
{URLs: []string{"stun:stun.l.google.com:19302"}},
},
})
if err != nil {
return nil, fmt.Errorf("failed to create peer connection: %w", err)
}
videoTrack, _ := webrtc.NewTrackLocalStaticRTP(
webrtc.RTPCodecCapability{MimeType: webrtc.MimeTypeH264},
"video", "game-stream",
)
pc.AddTrack(videoTrack)
// Avvia display virtuale e processo di gioco
display := NewVirtualDisplay(1920, 1080, 60) // 1080p@60fps
gameProcess := NewGameProcess(display)
// Avvia NVENC encoder collegato al display virtuale
encoder := NewNVENCEncoder(NVENCConfig{
Width: 1920,
Height: 1080,
Framerate: 60,
Bitrate: 8_000_000, // 8 Mbps per 1080p60
Profile: "high",
Preset: "llhq", // Low-latency high quality
RateControl: "cbr", // Constant bitrate per streaming
LookaheadDepth: 0, // Disabilita lookahead per latenza minima
BFrames: 0, // Nessun B-frame: aumenta latenza
})
return &GameSession{
playerID: playerID,
peerConnection: pc,
videoTrack: videoTrack,
gameProcess: gameProcess,
encoder: encoder,
display: display,
}, nil
}
// Capture e transmission loop: cattura frames e li trasmette via WebRTC
func (s *GameSession) StartCaptureLoop(ctx context.Context) {
frameBuffer := s.display.GetFrameBuffer()
rtpPacker := rtp.NewPacketizer(1200, 102, 0, &H264Payloader{}, &rtp.RandomSequencer{}, 90000)
for {
select {
case <-ctx.Done():
return
case frame := <-frameBuffer:
// 1. Comprimi il frame con NVENC (5-8ms)
encodedData, pts, err := s.encoder.EncodeFrame(frame)
if err != nil {
continue
}
// 2. Pacchettizza in RTP (< 1ms)
packets := rtpPacker.Packetize(encodedData, uint32(pts))
// 3. Invia via WebRTC (contribuisce alla latenza di rete)
for _, packet := range packets {
s.videoTrack.WriteRTP(packet)
}
}
}
}
4. エッジ コンピューティング: サーバーをプレーヤーに近づける
レイテンシ バジェットの最大の変数は、 ネットワークRTT: の速度 光は、パケットが長距離を移動できる速度を物理的に制限します。ミラノから フランクフルトのデータセンター: ~15ms RTT。ミラノから米国のデータセンターまで: ~100ms RTT。の 溶液e エッジコンピューティング: ゲームサーバーを物理的にプレイヤーに近づけます。
// Edge deployment orchestration - Go
// Gestisce il deployment dei game server sugli edge node più vicini ai player
type EdgeOrchestrator struct {
edgeNodes []*EdgeNode // Lista di edge location disponibili
geoResolver *GeoIPResolver // Risolve IP -> coordinate geografiche
kubernetes *k8s.Client // Per deploy su edge Kubernetes cluster
}
type EdgeNode struct {
ID string
Region string // "eu-west-milan", "eu-central-frankfurt"
Latitude float64
Longitude float64
Capacity int // GPU slots disponibili
Used int
RTT map[string]float64 // RTT verso le principali citta
}
// FindOptimalEdge: trova il nodo edge ottimale per un player
func (o *EdgeOrchestrator) FindOptimalEdge(
playerIP string, gameMode string) (*EdgeNode, error) {
// Risolvi posizione geografica del player
playerLoc, err := o.geoResolver.Resolve(playerIP)
if err != nil {
return nil, fmt.Errorf("geo resolution failed: %w", err)
}
var bestNode *EdgeNode
var bestScore float64 = -1
for _, node := range o.edgeNodes {
// Skip se il nodo e saturo
if float64(node.Used) / float64(node.Capacity) > 0.90 {
continue
}
// Calcola distanza geografica (proxy per latenza)
dist := haversineKm(playerLoc.Lat, playerLoc.Lon, node.Latitude, node.Longitude)
// Score: inverso della distanza, penalizzato per carico
loadFactor := 1.0 - float64(node.Used)/float64(node.Capacity)
score := (1.0 / (dist + 1.0)) * loadFactor
if score > bestScore {
bestScore = score
bestNode = node
}
}
if bestNode == nil {
return nil, fmt.Errorf("no available edge nodes")
}
return bestNode, nil
}
// DeployGameSession: avvia una sessione di gioco sull'edge node scelto
func (o *EdgeOrchestrator) DeployGameSession(
ctx context.Context, node *EdgeNode, sessionConfig SessionConfig) (*GameEndpoint, error) {
// Crea pod Kubernetes sull'edge cluster del nodo
pod := &k8sPod{
Name: fmt.Sprintf("game-%s", sessionConfig.SessionID),
Namespace: "cloud-gaming",
Spec: k8sPodSpec{
Containers: []k8sContainer{{
Name: "game-session",
Image: "mygame/cloud-session:latest",
Resources: k8sResources{
Limits: k8sResourceList{
"nvidia.com/gpu": "1", // 1 GPU dedicata per sessione
"memory": "8Gi",
"cpu": "4",
},
},
Env: []k8sEnvVar{
{Name: "SESSION_ID", Value: sessionConfig.SessionID},
{Name: "PLAYER_ID", Value: sessionConfig.PlayerID},
{Name: "GAME_MODE", Value: sessionConfig.GameMode},
{Name: "REGION", Value: node.Region},
},
}},
NodeSelector: map[string]string{
"edge-node": node.ID, // Forza scheduling sul nodo specifico
},
},
}
return o.kubernetes.CreatePod(ctx, pod)
}
5. アダプティブ品質: リアルタイムでのビットレート適応
ネットワークの状態は常に変化しています。モバイル プレーヤーがトンネルに入ると、ネットワークが変化します。 Wi-Fi の混雑、5G カバレッジの変化。システムはリアルタイムで適応する必要があります。 バッファリングを生成する代わりに、品質または解像度を下げてレイテンシを許容範囲内に保ちます。
// Adaptive bitrate controller per cloud gaming (TypeScript)
class AdaptiveBitrateController {
private readonly RTT_HISTORY_SIZE = 10;
private rttHistory: number[] = [];
private currentBitrate: number;
private currentResolution: Resolution;
private readonly QUALITY_LEVELS: QualityLevel[] = [
{ name: 'ultra', width: 1920, height: 1080, bitrate: 12_000_000, minRTT: 0, maxRTT: 40 },
{ name: 'high', width: 1920, height: 1080, bitrate: 8_000_000, minRTT: 40, maxRTT: 60 },
{ name: 'medium', width: 1280, height: 720, bitrate: 4_000_000, minRTT: 60, maxRTT: 80 },
{ name: 'low', width: 960, height: 540, bitrate: 2_000_000, minRTT: 80, maxRTT: 120 },
{ name: 'mobile', width: 640, height: 360, bitrate: 800_000, minRTT: 120, maxRTT: 200 },
];
constructor() {
this.currentBitrate = 8_000_000;
this.currentResolution = { width: 1920, height: 1080 };
}
// Aggiorna con le ultime statistiche WebRTC
update(stats: RTCStats): QualityChange | null {
const rtt = stats.currentRoundTripTime * 1000; // in ms
this.rttHistory.push(rtt);
if (this.rttHistory.length > this.RTT_HISTORY_SIZE) {
this.rttHistory.shift();
}
// Usa RTT medio per evitare oscillazioni su spike temporanei
const avgRTT = this.rttHistory.reduce((a, b) => a + b, 0) / this.rttHistory.length;
const packetLoss = stats.packetsLost / stats.packetsReceived;
// Trova il livello di qualità appropriato per l'RTT corrente
const targetLevel = this.QUALITY_LEVELS.find(
level => avgRTT >= level.minRTT && avgRTT < level.maxRTT
) ?? this.QUALITY_LEVELS[this.QUALITY_LEVELS.length - 1];
// Se la qualità non e cambiata, non fare nulla
if (targetLevel.bitrate === this.currentBitrate) return null;
const change: QualityChange = {
previousBitrate: this.currentBitrate,
newBitrate: targetLevel.bitrate,
newResolution: { width: targetLevel.width, height: targetLevel.height },
reason: `RTT avg=${avgRTT.toFixed(0)}ms, loss=${(packetLoss*100).toFixed(2)}%`,
qualityName: targetLevel.name
};
this.currentBitrate = targetLevel.bitrate;
this.currentResolution = change.newResolution;
return change;
}
}
6. GPU プーリングと密度の最大化
クラウド ゲームの主なコストは GPU です。NVIDIA A10G のハードウェア費用は約 10 万ドルです。 各セッションで GPU 全体を使用する場合、セッションあたりのコストは手頃ではありません。解決策は の GPUプーリング 仮想化経由: 複数のセッションが同じ GPU を共有します。
クラウド ゲーム向けの GPU 共有テクノロジー
| テクノロジー | セッション/GPU | 絶縁 | オーバーヘッド | 使用事例 |
|---|---|---|---|---|
| 専用GPU | 1 | 合計 | 0% | AAA プレミアム ゲーム |
| NVIDIA vGPU | 4-16 | 高い | 5~15% | 中級/高レベルのゲーム |
| ミグ(A100) | 7 | ハードウェア | 2~5% | コンピューティング + ゲーム |
| GPUパススルー | 1 (VM) | 合計 | 2~3% | Windows ゲーム |
| カプセル (NVIDIA) | 2.25倍以上 | 中くらい | 10~15% | カジュアル/クラウド ゲーム |
レイテンシーを削減するための最適化
- Nvidia リフレックス: CPU と GPU を同期することでレンダリングのレイテンシーを短縮します。 レンダリング キューを排除します (シナリオによっては 20 ミリ秒から 5 ミリ秒)。
- 低遅延エンコードプロファイル: 代わりにプリセット「ll」(低遅延) を使用した NVENC 「hq」の場合: 品質はわずかに低くなりますが、エンコードの遅延は 30 ~ 50% 減少します。
- ゼロ B フレーム: B フレーム (双方向フレーム) には先読みが必要です 将来: それらを無効にすると、1 ~ 2 フレームの体系的な遅延が解消されます。
- TCP上のUDP: WebRTC はデフォルトで UDP を使用します。可能であれば TURN TCP を使用しないでください これは避けてください。TCP バッファリングに 20 ~ 50 ミリ秒の余分な遅延が追加されます。
- 専用NIC: マルチテナントサーバーでは、1 つの NIC を排他的に専用にします。 他のワークロードとの干渉を避けるために、ゲーム トラフィックに影響を与えません。
結論
クラウド ゲームは、業界で最も魅力的なエンジニアリング課題の 1 つであり、最適化が必要です。 GPU 仮想化からエッジ コンピューティングまで、スタックのあらゆるレベルで WebRTC プロトコルによって実現 アダプティブビットレートに。 2024 年の 150 億ドル市場は、プレーヤーの意欲を示しています この便利さのためにお金を払う必要がありますが、技術的なハードルは非常に高く、数十ミリ秒です。 待ち時間が長くなり、売れる製品と使えない製品の差が大きくなります。
今後数年間の鍵となるのは、 MEC を使用した 5G: 23 億以上 2024 年末の 5G 契約の割合、遅延 10 ~ 20 ミリ秒のセルラー ネットワーク上のモバイル クラウド ゲーム ようやく現実的になってきました。現在構築しているエッジ インフラストラクチャ - ノード上の Kubernetes 地理的に分散され、最適化された WebRTC、NVENC ハードウェア エンコーディングが基盤となります。 次の10年のゲームが構築されるでしょう。
ゲーム バックエンド シリーズの次のステップ
- 前の記事: ゲーム テレメトリ パイプライン: Scala でのプレーヤー分析
- 次の記事: 可観測性ゲーム バックエンド: レイテンシーとティックレート
- 関連シリーズ: DevOps フロントエンド - デプロイとインフラストラクチャ







