Hands-On Lab: Building a Simple Second-Screen Remote Control Using Web APIs
Hands-on lab to build a web second-screen controller (play/pause, seek) with Chromecast, Remote Playback API, and fallbacks — step-by-step in 2026.
Hook: Stop guessing how to control strangers’ TVs — build a reliable second-screen controller
Searching for a clear, practical way to build a web-based second-screen controller? You’re juggling confusing docs, broken examples, and device incompatibilities — and you don’t have time to learn every vendor SDK. This hands-on lab walks you through a compact, practical build: a web app that can act as a second-screen controller (play/pause, seek) using public Web APIs with progressive enhancement for Chromecast and Nest devices. By the end you’ll have working JavaScript, a simple Chromecast receiver option, and robust fallbacks for devices that block casting.
Why this matters in 2026
In early 2026 the streaming landscape changed: some services tightened casting support, and device vendors continued evolving their web SDKs and platform controls. That makes native app-based casting less predictable — but second-screen playback control (remote play/pause/seek) remains valuable for educational demos, group viewing, and accessibility features.
"Last month, Netflix removed the ability to cast videos from its mobile apps to many smart TVs and streaming devices." — reporting, Jan 2026
That headline reminds us: you can’t rely on one vendor. The robust approach is progressive enhancement: try standardized Web APIs first, then add vendor SDK integrations (Google Cast) and fallbacks (WebSocket, WebRTC, cloud sync). This lab teaches that practical stack.
What you’ll learn (quick list)
- How to build a minimal controller UI (play/pause, seek).
- How to use the Chromecast Web Sender SDK to control playback remotely.
- How to fallback to the Remote Playback API or a simple WebSocket-based receiver when casting is restricted.
- Testing tips for Nest Hub and common TVs, and how to handle services that block casting.
Prerequisites
- Basic knowledge of HTML, CSS, and JavaScript.
- Latest Chrome desktop (for Cast sender) and a Chromecast or Nest Hub on the same Wi‑Fi network for testing.
- HTTPS hosting or localhost with a secure context (required by many Web APIs and Cast).
- Optional: Node.js for a simple WebSocket receiver or local file server.
Architecture overview
We’ll build a single-page controller that uses progressive enhancement:
- Attempt a Chromecast session via the Google Cast Web Sender SDK.
- If unavailable, try the Remote Playback API (supported on some browsers/devices).
- If both fail, fallback to a WebSocket or Firebase-based relay to a simple receiver app running on a smart display or TV web engine.
Step 0 — Project scaffold
Create a folder with these files:
- index.html — controller UI
- controller.js — JavaScript logic
- styles.css — optional styling
- receiver/ — optional Chromecast Receiver app or a simple web receiver (for advanced lab)
index.html (UI)
Minimal controller UI: play/pause, 0–100 seek slider, and a connect button.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Second-Screen Controller Lab</title>
<link rel="stylesheet" href="styles.css">
</head>
<body>
<main>
<h2>Second-Screen Controller</h2>
<div id="status">Not connected</div>
<button id="connectBtn">Connect</button>
<div id="controls" aria-hidden="true">
<button id="playPause">Play</button>
<input id="seek" type="range" min="0" max="100" value="0">
<span id="time">0:00</span>
</div>
</main>
<script src="controller.js" type="module"></script>
</body>
</html>
controller.js — progressive control logic
The file that wires UI to three control paths: Cast SDK, Remote Playback API, WebSocket fallback. This example focuses on the key flows; adapt as needed.
/* controller.js (ES module) */
const connectBtn = document.getElementById('connectBtn');
const playPauseBtn = document.getElementById('playPause');
const seekInput = document.getElementById('seek');
const statusEl = document.getElementById('status');
const controls = document.getElementById('controls');
let transport = null; // {type: 'cast'|'remote'|'ws', send: fn, onState: fn}
connectBtn.addEventListener('click', async () => {
statusEl.textContent = 'Trying Chromecast...';
// 1) Try Cast
if (await tryCast()) return enableControls();
statusEl.textContent = 'Trying Remote Playback API...';
if (await tryRemotePlayback()) return enableControls();
statusEl.textContent = 'Falling back to WebSocket...';
if (await tryWebSocket()) return enableControls();
statusEl.textContent = 'No compatible remote found.';
});
function enableControls() {
controls.setAttribute('aria-hidden', 'false');
statusEl.textContent = `Connected (${transport.type})`;
}
playPauseBtn.addEventListener('click', () => {
if (!transport) return;
transport.send({cmd: 'togglePlay'});
});
seekInput.addEventListener('input', () => {
if (!transport) return;
const pct = Number(seekInput.value);
transport.send({cmd: 'seekPct', pct});
});
// ---------- Cast integration (sender) ----------
async function tryCast() {
if (!window.chrome || !window.cast || !window.cast.framework) return false;
try {
const context = cast.framework.CastContext.getInstance();
context.setOptions({receiverApplicationId: chrome.cast.media.DEFAULT_MEDIA_RECEIVER_APP_ID});
// request session UI
await context.requestSession();
const session = context.getCurrentSession();
// Use Media Control API or send messages to custom receiver
transport = {
type: 'cast',
send: (msg) => session.sendMessage('urn:x-cast:com.example.controls', msg),
onState: (fn) => {
// optional: listen for receiver messages
session.addMessageListener('urn:x-cast:com.example.controls', (ns, data) => fn(data));
}
};
// initialize state from receiver if available
return true;
} catch (e) {
console.warn('Cast failed', e);
return false;
}
}
// ---------- Remote Playback API ----------
async function tryRemotePlayback() {
// This API requires an HTMLMediaElement that can trigger remote playback
const video = document.createElement('video');
if (!video.remote) return false; // remote playback not supported
try {
// Attempt to start a remote playback prompt
await video.remote.prompt();
transport = {
type: 'remote',
send: async (msg) => {
// Use play/pause/seek methods
if (msg.cmd === 'togglePlay') {
if (video.paused) await video.play(); else video.pause();
} else if (msg.cmd === 'seekPct') {
const target = (msg.pct / 100) * video.duration;
video.currentTime = target;
}
},
onState: (fn) => {
video.addEventListener('timeupdate', () => fn({time: video.currentTime, duration: video.duration}));
}
};
return true;
} catch (e) {
console.warn('Remote Playback failed', e);
return false;
}
}
// ---------- WebSocket fallback (simple relay) ----------
async function tryWebSocket() {
// In this lab we assume a receiver host is running ws://:3000
// In production, implement discovery or cloud signaling (Firebase, SSE, WebRTC)
try {
const ws = new WebSocket('wss://example-relay.example.com'); // replace with your relay
await new Promise((resolve, reject) => { ws.onopen = resolve; ws.onerror = reject; });
transport = {
type: 'ws',
send: (msg) => ws.send(JSON.stringify(msg)),
onState: (fn) => { ws.onmessage = (ev) => fn(JSON.parse(ev.data)); }
};
return true;
} catch (e) {
console.warn('WebSocket fallback failed', e);
return false;
}
}
// Optional: Listen for state updates and update UI
function attachStateHandlers() {
if (!transport) return;
transport.onState((state) => {
if (state.duration) {
const pct = Math.round((state.time / state.duration) * 100);
seekInput.value = pct;
const mins = Math.floor(state.time / 60);
const secs = Math.floor(state.time % 60).toString().padStart(2, '0');
document.getElementById('time').textContent = `${mins}:${secs}`;
}
});
}
Step 4 — Chromecast receiver (optional)
The easiest way to control playback on Chromecast is to use an existing media receiver. For custom behavior (like reading messages from our web controller), host a simple custom receiver app and listen on a namespace for messages:
// receiver/index.html
<script src="https://www.gstatic.com/cast/sdk/libs/caf_receiver/v3/cast_receiver_framework.js"></script>
<script>
const context = cast.framework.CastReceiverContext.getInstance();
const options = new cast.framework.CastReceiverOptions();
options.disableIdleTimeout = true;
context.start(options);
// Listen for custom control messages
const messageBus = context.getCastMessageBus('urn:x-cast:com.example.controls');
messageBus.onMessage = (event) => {
const msg = event.data;
// Implement play/pause/seek by hooking into the player manager
const playerManager = context.getPlayerManager();
if (msg.cmd === 'togglePlay') playerManager.togglePause();
if (msg.cmd === 'seekPct') {
const dur = playerManager.getMediaInformation().contentDuration;
playerManager.seekTo(msg.pct / 100 * dur);
}
};
</script>
Register the receiver app in the Google Cast Developer Console during testing; you can also use the Default Media Receiver if you only need standard media playback.
Step 5 — Testing on Nest Hub and other devices
- Make sure the controller and the target device are on the same local network.
- Use Chrome for the Cast sender. For Nest Hub, the device presents as a Cast target and supports both media playback and custom receiver messaging.
- If a streaming service has blocked casting (e.g., recent changes from some providers), the remote device won’t accept direct streams for DRM-protected content. Use the controller only for controlling your own hosted media or for custom receiver content.
Troubleshooting and debugging tips
- Open chrome://cast-internals/ to see Cast session logs and diagnose session failures.
- Chromecast and Nest devices require HTTPS origins for production sender apps. Use localhost for development or a valid TLS host.
- Check browser console for CORS and mixed-content errors — they are the most common blockers.
- For WebSocket fallback, ensure your relay server handles authentication and discovery; use one relay per session to avoid cross-user interference.
Advanced strategies (2026 considerations)
Recent platform moves in late 2025/early 2026 have made progressive enhancement even more important. Here are advanced ideas to increase reliability and UX:
- WebRTC data channels for low-latency control: Peer-to-peer connections provide faster control than cloud relays and can carry both signaling and state sync.
- Cloud state sync using Firebase or Supabase Realtime: Keep controller and receiver in sync across networks and handle multi-controller scenarios.
- Graceful degradation: If a streaming service blocks casting, let your controller control only transport commands and metadata (no stream handoff).
- Instrumentation: Log control commands and round-trip time to understand latency and user experience on different devices.
Security & privacy
When designing second-screen control, consider:
- Authentication and authorization: Don’t let any device join a session without user consent. Use OAuth tokens or short-lived session codes.
- Encrypt signaling: Use WSS/WSS and TLS for all communication.
- Limit control scope: Avoid sending sensitive tokens or DRM-protected URLs over untrusted channels.
- Respect platform policies: Some services forbid third-party proxies that circumvent DRM or content rules.
Practical exercises
- Implement the full Cast sender and test with a Chromecast on your network. Use the Default Media Receiver to load a public MP4 and control it.
- Build a WebSocket receiver using Node + ws that runs on a Raspberry Pi attached to a TV. Accept messages and control a simple HTML5 player on the Pi.
- Swap the WebSocket relay for a WebRTC data channel so your controller connects directly to the receiver. Measure latency differences.
Common pitfalls and how to avoid them
- Assuming every TV supports the Presentation API — many do not. Use it as a progressive enhancement only.
- Ignoring service policies — attempt to control only your own content or content you have permission to manage.
- Skipping HTTPS — many APIs and Cast sessions fail silently without secure contexts.
Key takeaways
- Progressive enhancement is essential: prefer standardized Web APIs, add vendor SDKs (Cast) for wider device support, and provide networked fallbacks like WebSocket or WebRTC.
- Chromecast + Nest support: Still the best path for media devices, but platform changes in 2025–2026 (service-level restrictions) mean you must handle blocked streams gracefully.
- Testing & security: test on real devices, use HTTPS, and secure your signaling channels. For debugging and observability, integrate monitoring and logs.
Further reading and references (2024–2026 trend notes)
- Google Cast Web Sender & Receiver documentation — for updated SDK options and receiver app registration.
- Remote Playback API status — check browser compatibility; adoption improved through 2025 but remains partial.
- Recent reporting (Jan 2026) on casting policy changes — useful for understanding vendor/service limitations.
Next steps: extend this lab
- Add authentication and session codes so controllers require permission to connect.
- Implement multi-controller arbitration (who gets control when two devices connect?).
- Integrate analytics to measure command latency and success rates across device types (tie this back to instrumentation).
- Create an accessibility-first controller UI (large targets, voice commands, haptics).
Call to action
Ready to build your second-screen controller? Fork this lab, test it with a Chromecast or Nest Hub, and share your receiver code with your classmates. If you want a trimmed starter repo or a Node-based receiver example, request it below — I’ll curate a ready-to-run kit with deployment scripts and a WebRTC signaling server so you can run a local lab in 15 minutes.
Related Reading
- Serverless Edge for Tiny Multiplayer: Compliance, Latency, and Developer Tooling in 2026
- News & Analysis: Low-Latency Tooling for Live Problem-Solving Sessions — What Organizers Must Know in 2026
- Field Review: Portable Edge Kits and Mobile Creator Gear for Micro‑Events (2026)
- Monitoring and Observability for Caches: Tools, Metrics, and Alerts
- Firmware Patch Checklist for Smart Cameras: What Every Installer Should Do
- Small-Batch Beverage Brands: How Liber & Co. Scaled Up — Lessons Small Businesses Can Use
- How Real Estate Leadership Changes Affect Salon Leases and Local Business Climate
- Music Rights Deals to Watch: What Cutting Edge’s Catalog Acquisition Signals for Composers
- Prefab Cabins and Tiny Houses for Rent: The New Wave of Eco-Friendly Glamping
Related Topics
knowable
Contributor
Senior editor and content strategist. Writing about technology, design, and the future of digital media. Follow along for deep dives into the industry's moving parts.
Up Next
More stories handpicked for you