Cast Integration Guide — Custom Controls
This guide covers how to integrate Google Cast into your React Native application using @quickplay/rn-qp-nxg-player, including session lifecycle, user registration, media playback, and building your own mini and expanded controllers.
Prerequisites
Complete the platform setup in platform-chromecast before proceeding. The castAuthorizer singleton and QpNxgMediaRouteBtnView cast button are imported directly from the SDK:
import { castAuthorizer, QpNxgMediaRouteBtnView } from '@quickplay/rn-qp-nxg-player';
1. Cast Button
Render the native Cast button anywhere in your navigation header or UI. It automatically reflects device availability and connection state:
<QpNxgMediaRouteBtnView tintColour="#FFFFFF" style={{ width: 25, height: 25 }} />
2. Initialization
Register your listener before calling initWithConfig() so no events are missed. The SDK buffers castSessionStarted on iOS if it fires before the JS bridge is ready (kill-and-relaunch restoration), so registering first is the safest pattern.
// Use a stable listener reference (e.g. a ref or module-level object) so
// addListener / removeListener always receive the same object identity.
castAuthorizer.addListener(castListener);
if (await castAuthorizer.isConfigured()) {
// Already initialized (e.g. hot reload) — skip initWithConfig.
} else {
await castAuthorizer.initWithConfig();
}
Important: Call addListener + initWithConfig exactly once per app session (e.g. in a top-level component's useEffect). Registering multiple listeners causes duplicate session events.
3. Session Lifecycle Events
Implement CastEventListener to handle all Cast session transitions:
import { CastEventListener, CastMediaMetadata, CastMessage } from '@quickplay/rn-qp-nxg-player';
const castListener: CastEventListener = {
// ── Session started (fresh connect or kill-and-relaunch restoration) ──────
async castSessionStarted(param) {
const { castDeviceID, playbackState } = param;
// Show your mini controller immediately — param contains the initial
// playbackState so you can render the correct play/pause icon without
// an extra async call.
showMiniController(true);
setPlaybackState(playbackState ?? 'IDLE');
// Attach the player listener to receive ongoing playback events.
try {
const castPlayer = await castAuthorizer.getPlayer();
castPlayer.addListener(playerListener);
} catch (e) {
// Player may not be ready yet on a brand-new connection; the native
// bridge retries internally, but guard here as a safety net.
console.warn('getPlayer failed on session start:', e);
}
// Fetch current item metadata for the controller UI.
const item = await castAuthorizer.getCurrentItem();
setCurrentItemMetadata(item);
},
castSessionStartFailed(param) {
console.warn('Cast session failed to start:', param);
},
// ── Session ended cleanly ─────────────────────────────────────────────────
async castSessionEnded() {
showMiniController(false);
setPlaybackState('IDLE');
setCurrentItemMetadata(null);
const castPlayer = getCastPlayerRef(); // your stored ref
if (castPlayer) {
castPlayer.removeListener(playerListener);
await castPlayer.dispose();
}
await castAuthorizer.dispose();
castAuthorizer.removeListener(castListener);
},
// ── Session ended with error ──────────────────────────────────────────────
async castSessionEndedError(param) {
console.warn('Cast session ended with error:', param);
// Same teardown as castSessionEnded.
const castPlayer = getCastPlayerRef();
if (castPlayer) {
castPlayer.removeListener(playerListener);
await castPlayer.dispose();
}
await castAuthorizer.dispose();
castAuthorizer.removeListener(castListener);
},
// ── Device registration ───────────────────────────────────────────────────
castSessionDeviceRegSuccess() {
// User data was registered on the receiver. Now safe to call authorizeUserData.
setDeviceRegistered(true);
},
castSessionDeviceRegErr(param) {
console.warn('Device registration failed:', param);
},
// ── Cast error (playback-level) ───────────────────────────────────────────
async castError(param) {
console.error('Cast playback error:', param);
showMiniController(false);
// Tear down the player the same way as castSessionEnded.
},
// ── Up-next / queue ───────────────────────────────────────────────────────
castPreloadStatusUpdated(param?: CastMediaMetadata) {
setUpNextMetadata(param ?? null);
},
// ── Custom channel messages from the receiver ─────────────────────────────
castCustomMessageReceived(message: CastMessage) {
console.log('Custom message:', message.name, message.type, message.message);
},
// ── Unused in custom-controls mode (no-ops) ───────────────────────────────
castStateChange() {},
castProgressUpdate() {},
castTrackAvailabilityChanged() {},
castTrackVariantChanged() {},
};
4. User Registration
After castSessionStarted, register the authenticated user with the receiver so it can authorize playback. Trigger this when castState === 'connected' and the device is not yet registered:
const config: CastAuthUserConfig = {
castDeviceID, // received in castSessionStarted param
platformClient: {
id: deviceId, // unique device identifier
type: Platform.OS === 'ios' ? 'iosmobile' : 'androidmobile',
},
};
await castAuthorizer.authorizeUserData(config);
// castSessionDeviceRegSuccess fires on success
5. Loading Media
Build a CastConfig object and call castMedia. The receiver starts playback immediately:
import { CastConfig } from '@quickplay/rn-qp-nxg-player';
const castAsset: CastConfig = {
mediaID: 'content-id',
title: 'My Movie',
imageURI: 'https://example.com/poster.jpg',
consumptionType: 'VOD', // 'VOD' | 'LIVE'
catalogType: 'movie',
mediaType: 'HLS', // 'HLS' | 'DASH'
drmType: 'FAIRPLAY', // 'FAIRPLAY' | 'WIDEVINE'
metadataMediaType: 'MEDIA_TYPE_MOVIE',
initialPlaybackPositionMs: 30000, // optional resume point
};
await castAuthorizer.castMedia(
castAsset,
headers, // optional: { [key: string]: string }
metadata, // optional: ad tag parameters or custom metadata
flAnalyticsData, // optional: Firstlight analytics key-value pairs
videoAnalyticsData // optional: video analytics key-value pairs
);
Live content: set
playbackMode('LIVE'|'RESTART'|'CATCHUP') andstartTime/endTime(UTC strings) onCastConfigfor time-shifted streams.
6. Playback Events
Attach a PlayerListener inside castSessionStarted (see step 3). It provides all real-time playback updates needed to drive both controllers:
import { PlayerListener, PlaybackStateValue } from '@quickplay/rn-qp-nxg-player';
const playerListener: PlayerListener = {
onStateChanged(playbackState, bufferingState, seekingState) {
// Drive the play/pause button and buffering indicator.
setPlaybackState(playbackState); // 'STARTED' | 'PAUSED' | 'IDLE' | ...
},
onProgressUpdate(currentMs, durationMs, playerContext) {
// Drive the seek bar and time labels.
setCurrentPosition(currentMs);
setDuration(durationMs);
},
onError(error) {
console.error('Playback error:', error);
showMiniController(false);
},
// Ad events (if applicable)
onAdBreakStart(adBreakInfo) { setAdActive(true); },
onAdBreakEnd(adBreakInfo) { setAdActive(false); },
onAdProgress(progress) { setAdPosition(progress.progress * 1000); },
// ... other PlayerListener methods as needed
};
7. Mini Controller
The mini controller is a compact persistent bar shown whenever a Cast session is active. Drive it entirely from the state set in castSessionStarted and playerListener:
// Show/hide
{showMiniController && (
<MiniController
title={currentItemMetadata?.title}
imageUri={currentItemMetadata?.images?.[0]}
isPlaying={playbackState === 'STARTED'}
currentPosition={currentPosition}
duration={duration}
onPlayPause={handlePlayPause}
onExpand={handleExpand}
onStop={handleStop}
/>
)}
Play / Pause:
async function handlePlayPause() {
const castPlayer = await castAuthorizer.getPlayer();
if (playbackState === 'STARTED') {
await castPlayer.pause();
} else {
await castPlayer.play();
}
}
Stop / end session:
async function handleStop() {
const castPlayer = await castAuthorizer.getPlayer();
await castPlayer.stop();
// castSessionEnded will fire and tear everything down automatically.
}
8. Expanded Controller
The expanded controller provides full playback controls. Launch it on user tap:
iOS — use the SDK's built-in expanded screen:
// Pass true to hide the stream-position slider for live content.
await castAuthorizer.showExpandedControls(isLive);
Android — launch via the cast native module:
import { NativeModules } from 'react-native';
NativeModules.QpNxgCastModule.showExpandedControls();
Custom expanded UI — if you build your own expanded screen, drive all controls through the Player instance:
const castPlayer = await castAuthorizer.getPlayer();
// Controls
await castPlayer.play();
await castPlayer.pause();
await castPlayer.seek(positionMs); // absolute position in ms
await castPlayer.seek(currentPosition - 30_000); // skip back 30 s
await castPlayer.seek(currentPosition + 30_000); // skip forward 30 s
await castPlayer.muteAudio(true); // mute
await castPlayer.muteAudio(false); // unmute
// Queue
await castPlayer.jumpToNextItem();
// Track selection
await castAuthorizer.setCastPreferredTrackVariant(trackType, trackVariantInfo);
Current cast state:
const state = await castAuthorizer.getCastState();
// 'connected' | 'connecting' | 'notConnected' | 'noDevicesAvailable'
const playbackState = await castAuthorizer.getCastPlaybackState();
// 'STARTED' | 'PAUSED' | 'IDLE' | 'STOPPED' | ...
Current item metadata (for populating the expanded UI):
const item: CastMediaMetadata = await castAuthorizer.getCurrentItem();
// item.title, item.images, item.duration, item.seasonNumber, etc.
9. Custom Channel Messages
Send a custom message to the receiver application at any time after session start:
await castAuthorizer.sendCustomMessage({ type: 'PING', payload: 'hello' });
Incoming messages from the receiver are delivered via castCustomMessageReceived on CastEventListener.
10. Teardown
Always clean up when the session ends (handled automatically in castSessionEnded / castSessionEndedError). If you need to disconnect programmatically:
const castPlayer = await castAuthorizer.getPlayer();
await castPlayer.stop();
// castSessionEnded fires → your listener performs teardown:
// castPlayer.removeListener(playerListener)
// castPlayer.dispose()
// castAuthorizer.dispose()
// castAuthorizer.removeListener(castListener)
Note:
castAuthorizer.dispose()resets the authorizer's auth token and platform context.castAuthorizer.removeListener()deregisters your event listener. Both must be called on session end.
Event Reference
| Event | When it fires |
|---|---|
castSessionStarted | Session connected (fresh or restored after kill-and-relaunch) |
castSessionStartFailed | Session failed to connect |
castSessionEnded | Session ended cleanly |
castSessionEndedError | Session ended due to an error |
castSessionDeviceRegSuccess | authorizeUserData succeeded on the receiver |
castSessionDeviceRegErr | authorizeUserData failed |
castError | Playback-level cast error |
castPreloadStatusUpdated | Next queue item metadata available |
castCustomMessageReceived | Custom message received from the receiver |
API Reference
| API | Description |
|---|---|
castAuthorizer.initWithConfig() | Initialize the Cast manager |
castAuthorizer.addListener(listener) | Register a CastEventListener |
castAuthorizer.removeListener(listener) | Unregister a CastEventListener |
castAuthorizer.authorizeUserData(config) | Register authenticated user on receiver |
castAuthorizer.castMedia(asset, ...) | Load and start media on the receiver |
castAuthorizer.getPlayer() | Get the Player instance for the active Cast session |
castAuthorizer.getCurrentItem() | Get the currently playing item's metadata |
castAuthorizer.getCastState() | Query current connection state |
castAuthorizer.getCastPlaybackState() | Query current playback state |
castAuthorizer.showExpandedControls(disableLiveProgressbar?) | Show expanded controls (iOS) |
castAuthorizer.sendCustomMessage(message) | Send a message to the receiver |
castAuthorizer.jumpToNextItem() | Skip to next item in the queue |
castAuthorizer.isConfigured() | Check if Cast manager is initialized |
castAuthorizer.dispose() | Tear down Cast authorizer state |
castAuthorizer.getDeviceName() | Get the name of the connected Cast device |