Skip to main content

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') and startTime / endTime (UTC strings) on CastConfig for 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

EventWhen it fires
castSessionStartedSession connected (fresh or restored after kill-and-relaunch)
castSessionStartFailedSession failed to connect
castSessionEndedSession ended cleanly
castSessionEndedErrorSession ended due to an error
castSessionDeviceRegSuccessauthorizeUserData succeeded on the receiver
castSessionDeviceRegErrauthorizeUserData failed
castErrorPlayback-level cast error
castPreloadStatusUpdatedNext queue item metadata available
castCustomMessageReceivedCustom message received from the receiver

API Reference

APIDescription
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