Cloud gaming
Last updated Sep 22, 2026 Run Freedoom in a Cloudflare Container, stream its audio and video through Realtime SFU, and send browser input back over DataChannels. Multiple viewers can watch while one browser tab controls the game.
This experimental example demonstrates a native publisher and browser receivers. The same media-and-control topology can inform remote application and device interfaces.
How it works
The Container captures H.264 video and Opus audio. Its native publisher sends both tracks through a WebRTC connection to Realtime SFU. Each browser uses one SFU session and PeerConnection for received media and input DataChannels.
Application boundary
Browser tab
Worker
Game state
Realtime SFU
Container publisher
Access identity + per-tab capability · One SFU session and PeerConnection per browser tab
App request
Typed RPC
Start or stop
Register publisher
Create SFU resources
Publish H.264 + Opus
Publish two input channels
Subscribe to media + channels
Enable controller replies
Reliable keyboard + buttons
Replaceable pointer movement
Forward controller input
A Worker authenticates browser requests and holds the SFU credentials. A Durable Object owns the game run, viewer membership, controller selection, and resource cleanup. The Container's publisher and the browser exchange media and input through the SFU.
Requirements
- A Cloudflare account with Workers Paid and Containers.
- Node.js 22.12 or later and Docker or a compatible container engine.
- An SFU App ID and App Secret.
- A desktop browser with keyboard and pointer-lock support.
- Cloudflare Access for a deployed application.
Run and verify
- Prepare the example. Run:
sh
git clone https://github.com/cloudflare/realtime-examples.git
cd realtime-examples/cloud-gaming
npmyarnpnpmbun
npm install
yarn install
pnpm install
bun install
Create the ignored configuration file:
sh
cp .dev.vars.example .dev.vars
chmod 600 .dev.vars
Set REALTIME_SFU_APP_ID and REALTIME_SFU_BEARER_TOKEN to your SFU app values.
2. Start the application. With your container engine running, start development:npmyarnpnpm
npm run dev
yarn run dev
pnpm run dev
Open http://localhost:8787/. The local identity path works on loopback hosts only.
3. Start the game. Select Start and wait for Running. Open a second browser tab and confirm both tabs receive video and audio.
4. Take control. In one tab, select Take control. Once control is ready, select the game to capture the pointer, then use the keyboard and mouse. The other tab should remain view-only.
Use Send Esc for the game menu. The browser's Escape action releases pointer lock. Select Release control to allow another tab to take control.
For deployment, follow the example's deployment instructions ↗︎. They install the SFU values as Worker secrets and configure the Access team domain and audience. Protect the hostname with the matching Access policy before inviting viewers.
Choose delivery settings for input
The example uses separate named channels for discrete actions and replaceable input:
| Input | Delivery | Reason |
|---|---|---|
| Keyboard, buttons, wheel, and reset | Reliable and ordered | Preserve discrete actions and their order |
| Pointer movement | Unordered, maxRetransmits: 0 |
New movement replaces older movement |
The publisher chooses each channel's policy, and every subscriber mirrors it. Each browser uses the matching delivery settings with its own allocated channel ID. Refer to DataChannel delivery settings.
Control ownership
Viewers initially subscribe with canReply: false and acknowledge channel readiness with waitForAck. The backend grants reply access by updating the selected viewer's existing subscriptions.
The application identifies each controller selection with a generation. The browser sends input only after the publisher confirms its viewer and generation. The application releases held input when control changes, pointer lock is lost, or the run ends.
canReply restricts the SFU return path. The backend still authenticates and authorizes the operator. Refer to reply access and the example's architecture ↗︎.
Stop and clean up
Select Stop game and wait for Offline before deleting a deployment. The application stops the Container and closes known SFU tracks and DataChannels. Follow the cleanup instructions ↗︎ to remove the Worker and Access application.
A failed media pipeline ends the run; transparent publisher restart is not implemented.
Adapt the example
The example implements one fixed game slot and one controller. Admission limits, rate limiting, and Access policy provisioning are deployment responsibilities.
For a shared CAD viewer or simulation, replace Freedoom and adapt application startup, capture, and input handling. Preserve viewer authorization, controller ownership, and cleanup. Add the file ownership and save/export behavior your application needs. Refer to production integration ↗︎ for the application policies and controls to add.
Inspect the implementation
Inspect the example's input channel definitions ↗︎ to compare reliable controls with transient pointer movement.
Troubleshooting
Use the troubleshooting guide ↗︎ to diagnose Container startup, media, and control failures.
Try another example
Pocket Radio applies a related observer/controller pattern on embedded hardware.
Was this helpful?
YesNo
On this page
{"@context":"https://schema.org","@type":"TechArticle","@id":"https://developers.cloudflare.com/realtime/sfu/examples/cloud-gaming/#page","headline":"Cloud gaming","description":"Stream a Container's audio and video to browsers and return authorized keyboard and pointer input through Realtime SFU.","url":"https://developers.cloudflare.com/realtime/sfu/examples/cloud-gaming/","inLanguage":"en","image":"https://developers.cloudflare.com/realtime/sfu/examples/cloud-gaming/og.png?v=469bafc757092260","dateModified":"2026-09-22","publisher":{"@type":"Organization","name":"Cloudflare","description":"One platform for your apps, agents, and workforce. Build, secure, and scale without managing infrastructure","url":"https://www.cloudflare.com/","sameAs":["https://github.com/cloudflare","https://www.linkedin.com/company/cloudflare","https://x.com/cloudflare"],"logo":{"@type":"ImageObject","url":"https://developers.cloudflare.com/logo.svg"},"address":{"@type":"PostalAddress","streetAddress":"101 Townsend St","addressLocality":"San Francisco","addressRegion":"CA","postalCode":"94107","addressCountry":"US"},"contactPoint":[{"@type":"ContactPoint","contactType":"Customer Support","url":"https://support.cloudflare.com/","availableLanguage":["English"]},{"@type":"ContactPoint","contactType":"Sales","url":"https://www.cloudflare.com/contact/","availableLanguage":["English"]}]},"isPartOf":{"@type":"WebSite","@id":"https://developers.cloudflare.com/#website","name":"Cloudflare Docs","url":"https://developers.cloudflare.com/"}}
description: Connect embedded firmware to Realtime SFU for audio, telemetry, and browser control using the ESP32 Pocket Radio example. title: Embedded devices and remote control image: https://developers.cloudflare.com/realtime/sfu/examples/embedded-devices/og.png?v=39e1a846a14cc8cf
Documentation Index
Fetch the complete documentation index at: https://developers.cloudflare.com/realtime/llms.txt
Use this file to discover all available pages before exploring further.
Docs