pillarbox-cast

Provides a PillarboxPlayer implementation that controls a Cast receiver app.

Integration

To use this module, add the following dependency to your module's build.gradle/build.gradle.kts file:

implementation("ch.srgssr.pillarbox:pillarbox-cast:<pillarbox_version>")

The main goal of this module is to be able to build a user interface that can be used with a PillarboxExoPlayer or PillarboxCastPlayer. Both implementations are based on the PillarboxPlayer interface.

Getting started

Setup CastContext

The framework has a global singleton object, the CastContext, that coordinates all the framework's interactions.

Application must implement the OptionsProvider interface to supply options needed to initialize the CastContext singleton. OptionsProvider provides an instance of CastOptions which contains options that affect the behavior of the framework. The most important of these is the Web Receiver application ID, which is used to filter discovery results and to launch the Web Receiver app when a Cast session is started or the Android TV receiver.

class CastOptionsProvider : OptionsProvider {
override fun getCastOptions(context: Context): CastOptions {
val launchOptions: LaunchOptions = LaunchOptions.Builder()
.setAndroidReceiverCompatible(true) // true if the sender application is compatible with the Android TV receiver.
.build()
return Builder()
.setReceiverApplicationId(RECEIVER_APP_ID)
.setLaunchOptions(launchOptions)
.build()
}

override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
return null
}
}

You must declare the fully qualified name of the implemented OptionsProvider as a metadata field in the AndroidManifest.xml file of the sender app:

<application>
...
<meta-data
android:name=
"com.google.android.gms.cast.framework.OPTIONS_PROVIDER_CLASS_NAME"
android:value="com.foo.CastOptionsProvider" />
</application>

CastContext is lazily initialized when the CastContext.getSharedInstance() is called. Pillarbox provides an extension to easily handle the initialization.

val castContext = context.getCastContext()

Create the player

val player = PillarboxCastPlayer(context, Default)
player.setSessionAvailabilityListener(object : SessionAvailabilityListener {
override fun onCastSessionAvailable() {
// The cast session is connected.
player.setMediaItems(mediaItems)
player.play()
player.prepare()
}

override fun onCastSessionUnavailable() {
// The cast session has been disconnected.
}

})
// When the player is not needed anymore.
player.release()

The SessionAvailabilityListener can also be created this way:

val player = PillarboxCastPlayer(context, Default) {
onCastSessionAvailable {
// The cast session is connected.
setMediaItems(mediaItems)
play()
prepare()
}

onCastSessionUnavailable {
// The cast session has been disconnected.
}
}

Configure MediaItemConverter

val player = PillarboxCastPlayer(context, Default) {
mediaItemConverter(DefaultMediaItemConverter()) // By default
}

Display a Cast button

Somewhere in your application, a Cast button has to be displayed to allow the user to connect to a Cast device.

To do this, you can use either:

Local to remote playback

CastPlayerSynchronizer provide an easy to use local to remote management that synchronized player state when needed.

When using CastPlayerSynchronizer state transition is handled when it is needed. By default the following states are synchronized:

  • MediaItems

  • Playback position

  • Repeat mode

  • PlayWhenReady

  • Track selection

Track selection restoration does best effort, by default it tries to select the first track that matches the language. So if multiple tracks with the same language are present, it may not choose the one that is actually selected.

val localPlayer = PillarboxExoPlayer(context, Default)
val castPlayer = PillarboxCastPlayer(context, Default)

val castSynchronizer = CastPlayerSynchronizer(
castContext = castContext,
coroutineScope = coroutineScope,
castPlayer = castPlayer,
localPlayer = localPlayer,
)
var currentPlayer: StateFlow<PillarboxPlayer> = castSynchronizer.currentPlayer

....

PlayerView(currentPlayer)

The default behavior can be modified by overriding DefaultPlayerSynchronizer or by creating a new PlayerSynchronizer implementation.

class CustomPlayerSynchronizer : CastPlayerSynchronizer.DefaultPlayerSynchronizer() {

override fun onTracksChanged(
newTracks: Tracks,
selectedAudioTrack: AudioTrack?,
selectedTextTrack: TextTrack?
): CastPlayerSynchronizer.Selection {
// An example to disable text track
val selection = super.onTracksChanged(newTracks, selectedAudioTrack, selectedTextTrack)
return CastPlayerSynchronizer.Selection(selection.audioTrack, null)
}

override fun onPlayerChanged(oldPlayer: PillarboxPlayer, newPlayer: PillarboxPlayer) {
super.onPlayerChanged(oldPlayer, newPlayer)
// Update newPlayer with some state and handle the state of the oldPlayer.
newPlayer.prepare()
oldPlayer.stop()
oldPlayer.clearMediaItems()
// Don't call release otherwise the player can't come back after.
}
}

val castSynchronizer = CastPlayerSynchronizer(
castContext = castContext,
coroutineScope = coroutineScope,
castPlayer = castPlayer,
localPlayer = localPlayer,
playerSynchronizer = CustomPlayerSynchronizer()
)

Road map

  • Handle Pillarbox metadata such as chapters, blocked time range and credits.

Additional resources

Packages

Link copied to clipboard