Cloud Gaming: Streamování s WebRTC a Edge Node
Cloudové hraní slibuje něco zásadně revolučního: hraní AAA her na jakémkoli zařízení – smartphone, Smart TV, Chromebook za 300 USD – bez instalací, bez vyhrazený hardware se stejnou vizuální kvalitou jako GPU za 1 000 USD. Vize je jasná, ale technická implementace je jednou z nejobtížnějších inženýrských výzev v tomto odvětví.
Trh cloudových her dosáhl v roce 2024 15,1 miliardy dolarů a předpokládá se, že dosáhne 52,6 miliardy dolarů do roku 2032 (CAGR 17 %). NVIDIA GeForce NOW, Xbox Cloud Gaming (xCloud), PlayStation Remote Play, Amazon Luna: každý sází na tuto technologii. Ale proč je to tak těžké? proč cloudové hry nejde jen o „streamování videa“: hra musí reagovat na vstupy hráče za méně než 100 ms end-to-end, nebo se zážitek stane nehratelným.
V tomto článku prozkoumáme technickou architekturu cloudových her: od zásobníku WebRTC až po streamování s nízkou latencí, až po okrajové výpočty, aby se vykreslování přiblížilo hráčům Virtualizace GPU pro maximalizaci hustoty serverů až po optimalizační strategie latence je rozdíl mezi 80 ms (přijatelné) a 30 ms (výborné).
Co se naučíte
- protože cloudové hraní se liší od tradičního streamování videa
- Zásobník WebRTC pro streamování her: DTLS, SRTP, ICE, kodek H.264/AV1
- Architektura Edge Computing s MEC (Multi-access Edge Computing)
- Virtualizace GPU: vGPU, GPU passthrough, GPU pooling s Capsule
- Kódovací kanál: NVENC, VAAPI, hardwarová akcelerace
- Rozpočet latence: jak je 100 ms end-to-end rozděleno mezi různé vrstvy
- Adaptivní kvalita: přizpůsobení datové rychlosti v reakci na podmínky sítě
- 5G a MEC: Jak 5G umožňuje mobilní cloudové hraní s nízkou latencí
1. Rozpočet latence: 100 ms End-to-End
Zásadní rozdíl mezi cloudovým hraním a Netflixem a interaktivní smyčka: každá akce hráče musí být zpracována a vizuální výsledek zobrazen před mozkem člověk vnímá zpoždění. U her je tento kritický práh celkem asi 100 ms: víc než tohle a hra se stává "laggy" a frustrující.
Latency Budget: Jak je distribuováno 100 ms
| Vrstvy | Komponent | Cílová latence | Skutečná latence |
|---|---|---|---|
| Vstup | Čtení vstupu zařízení | 2 ms | 1-5 ms |
| Síť nahrávání | Vstupní paket -> server | 10 ms | 5-50 ms |
| Server | Zpracování herní logiky | 5 ms | 3-10 ms |
| Vykreslování | Vykreslování snímků GPU | 16 ms | 8–33 ms (30–120 snímků/s) |
| Kódování | Snímek -> komprimovaný stream | 8 ms | 5-15 ms (NVENC HW) |
| Stáhnout Síť | Video stream -> klient | 10 ms | 5-50 ms |
| Dekódování | Stream -> nezpracované snímky | 5 ms | 3-10 ms (HW dekódování) |
| Zobrazit | Vyrovnávací paměť snímků -> obrazovka | 8 ms | 4-16 ms |
| Celkový | 64 ms | 31-179 ms |
S optimalizovanou okrajovou infrastrukturou (5-10 ms RTT server) lze dosáhnout celkem 50-70 ms. S tradiční infrastrukturou (vzdálené datové centrum, 50 ms RTT) můžete snadno dosáhnout 150 ms+.
2. WebRTC: Protokol pro streamování her
WebRTC byl zrozen pro videohovory mezi prohlížeči, ale díky své architektuře je ideální pro cloudové hraní: latence pod 100 ms, automatické přizpůsobení sítě, podpora NAT traversal a přenos jak videa (herní stream), tak obousměrných dat (vstup hráče).
Implementace cloudových her WebRTC využívá RTCPeerConnection založit komunikační kanál, RTCDataChannel odeslat vstup z klienta na server, e RTCVideoTrack pro příjem video streamu hry.
// 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. Server-Side: Encoding Pipeline a virtualizace GPU
Na straně serveru vyžaduje cloudové hraní vykreslování a kódování v reálném čase: hra běží na vyhrazeném GPU je každý snímek zachycen, komprimován hardwarovým kodérem (NVENC pro NVIDIA, VAAPI pro Intel/AMD) a přenášeny přes WebRTC. Latence kódování je kritická: s NVENC ano dosahují 5-8 ms na snímek, což je při softwarovém kódování nemožné.
// 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. Edge Computing: Přiveďte server blíže k přehrávači
Největší proměnnou v rozpočtu latence je Síťový RTT: rychlost světlo fyzicky omezuje, jak rychle může paket urazit vzdálenost. Z Milána do datové centrum ve Frankfurtu: ~15 ms RTT. Z Milána do datového centra v USA: ~100 ms RTT. The řešení e edge computing: Přibližte herní servery hráčům fyzicky.
// 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. Adaptivní kvalita: Adaptace datového toku v reálném čase
Podmínky sítě se neustále mění: mobilní hráč vstupující do tunelu, síť Přetížená Wi-Fi, změna v pokrytí 5G. Systém se musí přizpůsobit v reálném čase, snížení kvality nebo rozlišení pro udržení přijatelné latence namísto generování vyrovnávací paměti.
// 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. Sdružování GPU a maximalizace hustoty
Hlavní náklady na cloudové hraní jsou GPU: NVIDIA A10G stojí ~ 100 000 $ v hardwaru. Pokud každá relace využívá celý GPU, cena za relaci je nedostupná. Řešením je a sdružování GPU prostřednictvím virtualizace: více relací sdílí stejný GPU.
Technologie sdílení GPU pro cloudové hraní
| Technologie | Relace/GPU | Izolace | Nad hlavou | Případ použití |
|---|---|---|---|---|
| Dedikovaný GPU | 1 | Celkový | 0% | AAA prémiové hry |
| NVIDIA vGPU | 4-16 | Vysoký | 5–15 % | Střední/vysoké hraní |
| MIG (A100) | 7 | Železářské zboží | 2–5 % | Počítač + hry |
| Průchod GPU | 1 (VM) | Celkový | 2–3 % | Windows hry |
| Kapsle (NVIDIA) | 2,25x+ | Střední | 10–15 % | Příležitostné/cloudové hraní |
Optimalizace pro snížení latence
- Nvidia Reflex: Snižuje latenci vykreslování synchronizací CPU a GPU pro eliminovat fronty vykreslování (20 ms až 5 ms v některých scénářích).
- Profil kódování s nízkou latencí: NVENC s přednastaveným "ll" (nízká latence). "hq": mírně nižší kvalita, ale o 30–50 % nižší latence kódování.
- Nulové B-snímky: B-snímky (obousměrné snímky) vyžadují předběžný náhled budoucnost: jejich deaktivací eliminujete 1–2 snímky systematické latence.
- UDP přes TCP: WebRTC používá standardně UDP. Pokud můžete, nepoužívejte TURN TCP vyhnout se tomu: přidává 20–50 ms navíc latenci pro ukládání do vyrovnávací paměti TCP.
- Dedikovaný NIC: Na serverech s více klienty vyhraďte výhradně jednu síťovou kartu hernímu provozu, aby nedošlo k interferenci s jinými pracovními zátěžemi.
Závěry
Cloudové hraní je jednou z nejvíce fascinujících technických výzev v tomto odvětví: vyžaduje optimalizaci na každé úrovni zásobníku, od virtualizace GPU až po edge computing, pomocí protokolu WebRTC na adaptivní bitrate. Trh 15 miliard dolarů v roce 2024 ukazuje, že hráči jsou ochotni zaplatit za tuto vymoženost, ale technická laťka je velmi vysoko: několik desítek milisekund větší latence a rozdíl mezi prodejným produktem a nepoužitelným.
Klíčovým faktorem pro příštích několik let bude 5G s MEC: s více než 2,3 miliardami předplatného 5G na konci roku 2024, mobilní cloudové hraní v mobilních sítích s latencí 10–20 ms se konečně stává realistickým. Okrajové infrastruktury, které stavíme dnes – Kubernetes na uzlech geograficky distribuované, optimalizované WebRTC, hardwarové kódování NVENC - jsou základem, na kterém budou postaveny hry příští dekády.
Další kroky v sérii Game Backend
- Předchozí článek: Game Telemetry Pipeline: Player Analytics ve společnosti Scala
- Další článek: Pozorovatelnost Backend hry: Latence a Tickrate
- Související série: DevOps Frontend – nasazení a infrastruktura







