Cast to AirPlay and Chromecast
Send playback to AirPlay and Google Cast devices, with availability detection and connection state.
Send playback to an Apple TV over AirPlay or a Chromecast over Google Cast while the browser stays in control.
Recommended approach
Use a GoogleCast component with a CastButton, and add an AirPlayButton for Safari. The prebuilt skins already include both buttons; add GoogleCast next to a streaming media component to enable the configurable Cast route.
import { AirPlayButton, CastButton, Container, createPlayer } from '@videojs/react';
import { GoogleCast } from '@videojs/react/media/google-cast';
import { HlsJsVideo } from '@videojs/react/media/hlsjs-video';
import { videoFeatures } from '@videojs/react/video';
const { Player } = createPlayer({ features: videoFeatures });
export default function BasicUsage() {
return (
<Player>
<Container className="media-container">
<HlsJsVideo src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM.m3u8" autoPlay muted playsInline loop />
<GoogleCast />
<div className="media-remote-buttons">
<AirPlayButton
className="media-airplay-button"
render={(props, state) => (
<button {...props}>{state.state === 'connected' ? 'Stop AirPlay' : 'Start AirPlay'}</button>
)}
/>
<CastButton
className="media-cast-button"
render={(props, state) => (
<button {...props}>{state.connection === 'connected' ? 'Stop casting' : 'Start casting'}</button>
)}
/>
</div>
</Container>
</Player>
);
}
.media-container {
position: relative;
}
.media-container video {
width: 100%;
}
.media-remote-buttons {
position: absolute;
right: 10px;
bottom: 10px;
display: flex;
gap: 8px;
}
.media-airplay-button,
.media-cast-button {
padding-block: 8px;
padding-inline: 20px;
color: black;
cursor: pointer;
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 9999px;
backdrop-filter: blur(10px);
}
.media-airplay-button[data-disabled],
.media-cast-button[data-disabled] {
cursor: not-allowed;
opacity: 0.5;
filter: grayscale(1);
}
<video-player class="video-player">
<media-container>
<hlsjs-video src="https://stream.mux.com/BV3YZtogl89mg9VcNBhhnHm02Y34zI1nlMuMQfAbl3dM.m3u8" autoplay muted playsinline loop></hlsjs-video>
<google-cast></google-cast>
<div class="media-remote-buttons">
<media-airplay-button class="media-airplay-button">
<span class="connected">Stop AirPlay</span>
<span class="not-connected">Start AirPlay</span>
</media-airplay-button>
<media-cast-button class="media-cast-button">
<span class="connected">Stop casting</span>
<span class="disconnected">Start casting</span>
</media-cast-button>
</div>
</media-container>
</video-player>
.video-player {
position: relative;
display: block;
}
.video-player video {
width: 100%;
}
.media-remote-buttons {
position: absolute;
right: 10px;
bottom: 10px;
display: flex;
gap: 8px;
}
.media-airplay-button,
.media-cast-button {
padding-block: 8px;
padding-inline: 20px;
color: black;
cursor: pointer;
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgba(255, 255, 255, 0.3);
border-radius: 9999px;
backdrop-filter: blur(10px);
}
.media-airplay-button .connected,
.media-airplay-button .not-connected,
.media-cast-button .connected,
.media-cast-button .disconnected {
display: none;
}
/* AirPlay: swap label on connection state */
.media-airplay-button[data-airplay-state="connected"] .connected {
display: inline;
}
.media-airplay-button:not([data-airplay-state="connected"]) .not-connected {
display: inline;
}
/* Cast: swap label on connection state */
.media-cast-button[data-cast-state="connected"] .connected {
display: inline;
}
.media-cast-button:not([data-cast-state="connected"]) .disconnected {
display: inline;
}
/* Explicitly disabled while the route is unavailable. */
.media-airplay-button[data-disabled],
.media-cast-button[data-disabled] {
cursor: not-allowed;
opacity: 0.5;
filter: grayscale(1);
}
import '@videojs/html/video/player';
import '@videojs/html/media/google-cast';
import '@videojs/html/media/hlsjs-video';
import '@videojs/html/ui/airplay-button';
import '@videojs/html/ui/cast-button';
The demo pairs the streaming media component with GoogleCast in React or <google-cast> in HTML. Without that component, CastButton drives the browser’s native Remote Playback API instead, which does not provide Video.js receiver or load-request configuration.
How it works
The remote playback feature gives AirPlay and Google Cast one state shape:
remotePlaybackAvailabilityis'available'when a remote device can be reached,'unavailable'when the API exists but no device is reachable, and'unsupported'when the browser has no remote playback route.remotePlaybackStateis'disconnected','connecting', or'connected'.toggleRemotePlayback()opens the browser’s device picker, or disconnects an active session.
AirPlay uses WebKit’s presentation APIs. Google Cast uses a sender and receiver: the browser sends the receiver a media URL and then controls playback. Your UI reads the same Video.js state for either route.
While connected, playback actions — play, pause, seek, volume — proxy to the remote device, and player state keeps reflecting the remote session. Local playback is suspended.
The Google Cast SDK loads only when a GoogleCast component is present in a Chromium browser. Players without that component and browsers that cannot cast do not load it.
Availability and constraints
- Remote playback is asynchronous and can fail mid-session when a device or network disappears. React to
remotePlaybackStaterather than assuming a request succeeds. - Google Cast works only in Chromium-based browsers; elsewhere its route reports
'unsupported'. - AirPlay works only in Safari; availability requires an AirPlay device reachable on the network.
- The remote device fetches the source and tracks itself. Localhost, VPN-only, expired, or cross-origin URLs without the necessary CORS headers will fail on the receiver even if they play in the browser.
- Set
disableRemotePlaybackon the media element to opt out entirely.
Common variations
Custom Cast receiver
By default, Video.js sends the current source to Google’s Default Media Receiver. Set receiver when you have your own receiver application:
<GoogleCast receiver="YOUR_APP_ID" /><google-cast receiver="YOUR_APP_ID"></google-cast>Use src and contentType when the receiver should load a different source than the browser. streamType and customData let you describe that source and attach application-specific data. See the GoogleCast reference for the exact option types and HTML property mapping.
Read connection state
Use remotePlaybackAvailability to decide whether to offer a route and remotePlaybackState to show whether it is disconnected, connecting, or connected. CastButton and AirPlayButton already apply these states to their presentation; read them directly only when another part of your app needs them.
Troubleshooting
The cast button doesn’t render, or renders disabled
The Cast button hides when the route is unsupported. In Chromium with no Cast device reachable, it stays visible but disabled. The AirPlay button hides unless Safari reports an AirPlay device.
Casting starts but the device shows an error
The receiver cannot fetch the media. Confirm the source URL is reachable from the device and served with CORS headers, including caption tracks.
The UI freezes on “connecting”
The user dismissed the picker or the device disconnected. remotePlaybackState returns to 'disconnected'; drive your UI from that state.