JavaScript Presentation API Table
| Piece | What it does | Field note |
|---|---|---|
new PresentationRequest(urls) | The session handle | receiver page URLs the big screen will load; one request drives start, reconnect and availability for the whole lifecycle |
request.start() | The cast trigger | must run from a user gesture; opens the browser screen picker and resolves a PresentationConnection to the chosen screen |
PresentationConnection | The session object | an opaque id, a state machine (connecting, connected, closed, terminated) and a two-way message channel in one object |
conn.send() / onmessage | The control channel | string, Blob or typed payloads both directions - slides advance, pointers move and state syncs across the cast |
request.reconnect(id) | The session resume | keep the connection id in localStorage and reattach after a crash or tomorrow - presentations outlive the controller tab |
request.getAvailability() | The screen radar | promise of a live available flag: render the cast button only while a target exists, hide it when none does |
navigator.presentation.receiver | The receiver side | the projected page gets its connections here - a separate browser context running its own copy of your code |
close() vs terminate() | The exit grammar | close() ends one controller politely while the TV keeps running; terminate() shuts the presentation for every controller |
The Presentation API is the web's native cast: it sends one of your pages to a second screen - a Chromecast, a smart TV, a plugged-in monitor - while the original tab keeps running as the remote control. The architecture has two named halves: your page is the controller, and the page you cast is the receiver, a completely separate browser context running its own copy of your code on the big screen. new PresentationRequest(['receiver.html']) names the receiver URL, request.start() opens the browser's screen picker and establishes a PresentationConnection, and from then on the two pages talk through connection.send() and onmessage like a tiny messaging system with a television on one end.
Bottom line: the API is a session protocol, not a display mirror. start() must run from a user gesture and resolves a connection carrying an id, a state machine (connecting, connected, closed, terminated) and a two-way message channel - string, Blob or typed-array payloads both directions, which is how slides advance, pointers move and state syncs. The id is the underrated feature: save it (localStorage) and request.reconnect(id) reattaches to a running presentation after a tab crash, a network blip or tomorrow morning - presentations are designed to outlive the controller tab that started them. On the discovery side, request.getAvailability() gives you a live available flag, so the cast button only appears when there is actually a screen to cast to.
The integration reality: Chromium territory (Chrome, Edge, and cast targets that speak the protocol - Chromecast devices, some TVs and monitors), with the receiver side having its own API surface: the projected page finds its connections through navigator.presentation.receiver and typically runs a slimmer, big-screen variant of your UI - touch it like a separate app, not a media query. The classic mistakes are all architectural: treating the receiver as a mirror (it is not - it renders fresh), forgetting the receiver needs its own close/terminate handling, and skipping reconnect until the first user reports a dropped session. Get the id-persistence and the message protocol right and the API delivers something native apps pay for: a resumable, two-way, multi-hop session with a television.
How to use
- Declare the request once and reuse it: const request = new PresentationRequest(['receiver.html']) - the URL is what the big screen loads; one request object drives start, reconnect and availability for the whole session lifecycle.
- Cast from a real click and hold the connection: castBtn.onclick = async () => { const conn = await request.start(); saveId(conn.id); wire(conn); } - start() opens the browser screen picker, and conn.id is your resume token.
- Build the message protocol small: conn.send(JSON.stringify({type: 'slide', n: 3})) on the controller; receiver side conn.onmessage = e => handle(JSON.parse(e.data)) - one type field up front saves a month of ambiguity later.
- Reconnect on load instead of restarting: const saved = localStorage.getItem('presId'); if (saved) { const conn = await request.reconnect(saved).catch(() => null); if (conn) wire(conn); } - sessions survive tab crashes and phone reboots by design.
- Handle the receiver side explicitly: in receiver.html, navigator.presentation.receiver.getConnections then conn.onmessage for commands - and decide your close policy: conn.close() ends one controller politely, while terminate() shuts the presentation for everyone watching.
Frequently asked questions
What is the difference between the controller and the receiver - and why does the receiver run its own copy?
Because the API casts URLs, not pixels. request.start() does not mirror your tab; it tells a second browser context (on the TV, the Chromecast, the monitor) to load the receiver URL you declared in new PresentationRequest(['receiver.html']) - a fresh page load with its own JavaScript, its own storage origin, its own lifecycle. This is why the API scales to real casting hardware: a Chromecast cannot run your React app's dev server state, but it can absolutely load a URL and run vanilla web code on its own. The consequence is a protocol mindset: your controller and your receiver are two cooperating endpoints that happen to share a codebase, and every interaction crosses the PresentationConnection as a message - conn.send() from the controller, onmessage on the receiver, and the reverse for state reports (slide changed, video paused, timer ticked). The receiver-side entry point is navigator.presentation.receiver: it hands the projected page its incoming connections. Practically: keep receiver.html thin (render + obey commands), do not assume shared variables or shared localStorage behavior between the two contexts, and never let the two sides drift out of protocol - one message-type enum shared by both files is the cheapest insurance. The mirror alternative exists in the platform (screen capture, tab sharing) but it is a different tool with different costs: no resumable session, no two-way channel, and the content dies with the tab.
How does reconnect() actually work, and what belongs in a robust session-persistence story?
Every PresentationConnection carries an id - an opaque string that identifies the running presentation on the target device. Save it the moment start() resolves (localStorage.setItem('presId', conn.id)), and reconnect(savedId) reattaches a new controller to the same running presentation, provided it is still alive on the device. This is the API's answer to the fact that controller tabs die constantly: phone locked, tab crashed, user walked to another room and back. The robust story has four parts. Save immediately, not on unload - unload handlers are where ids go missing. Reconnect on page load before showing the cast button: try reconnect(saved) and wire the returned connection; catch or null-check the rejection (the presentation ended while you were gone) and fall through to a normal start() flow. Listen to the state machine: conn.onstatechange tells you connecting, connected, closed (your connection ended - maybe another controller terminated, maybe the receiver closed) versus terminated (the whole presentation is gone) - update UI from states, do not assume. And version your protocol: a receiver page that was cast yesterday may outlive your deploy - both sides should tolerate unknown message types silently rather than throw, so an old session survives a frontend update. The one thing reconnect does not do is wake a dead presentation: if the device was rebooted, the id is a tombstone - your catch branch is the feature.
What does getAvailability() buy me, and how should the cast button behave?
It is the difference between a cast button that works and one that lies: request.getAvailability() resolves a PresentationAvailability object whose live available property tells you whether any cast target is reachable - a Chromecast on the network, a monitor plugged in. Watch it (availability.onchange) and render the cast button only while available is true; hide it or swap to explanatory text when there is nothing to cast to. Three behaviors worth engineering. First, availability is dynamic: a TV wakes up, the user unplugs the monitor - the object updates, and your UI should follow without a reload. Second, availability is privacy-shaped: browsers may return a vague or optimistic value to avoid fingerprinting the network environment, so treat it as a hint for UI, not a guarantee - start() remains the moment of truth, and the screen picker is where the user actually chooses and authorizes. Third, there is a default-request shortcut: assigning navigator.presentation.defaultRequest = request lets the browser surface its own cast affordances in some integration modes, but the explicit availability-plus-button flow is the one you control end to end. The failure mode to avoid is the always-on button that opens a picker showing zero devices - one session of that and users conclude casting is broken, even when the API did exactly what the network allowed.
What are the exit semantics - close versus terminate, and who ends what?
Two distinct shutdown verbs on PresentationConnection, and mixing them up produces the classic stuck-on-the-TV bug. conn.close() is polite and local: it ends this controller's connection while the presentation itself keeps running on the big screen - other controllers (another tab, a reconnected one) stay attached, and the receiver page keeps rendering. Use close() when the user dismisses your control tab but the TV should keep showing the content (a lobby display, a running timer). conn.terminate() is the kill switch, controller-side: it shuts down the entire presentation - the receiver page unloads, every controller's connection ends, the screen goes back to idle. Use terminate() for a real end-present action. The receiver has a say too: it can call close() on its own connection or navigate itself away, which the controller observes through onclose/onterminate events - handle both, because from the controller's view the session can end without your code initiating it. Two hygiene rules close the story: always clear the saved connection id when you observe onterminate (it is unrecoverable at that point - reconnecting to a terminated presentation just rejects), and give the receiver page a graceful onmessage command like {type: 'bye'} mapped to its own cleanup, so scripted ends (user closes your app) leave the TV in a chosen state rather than a frozen last frame.