Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ implementation("ch.srgssr.pillarbox:pillarbox-cast:<pillarbox_version>")
// Library to handle SRG SSR content through media URNs
implementation("ch.srgssr.pillarbox:pillarbox-core-business:<pillarbox_version>")

// Library to handle SRG SSR Cast receiver with SRG SSR content
implementation("ch.srgssr.pillarbox:pillarbox-core-business-cast:<pillarbox_version>")

// Library to display the video surface
implementation("ch.srgssr.pillarbox:pillarbox-ui:<pillarbox_version>")
```
Expand Down Expand Up @@ -126,6 +129,7 @@ To start using Pillarbox in your project, you can check each module's documentat
- [`pillarbox-analytics`](https://github.com/SRGSSR/pillarbox-android/blob/main/pillarbox-analytics/docs/README.md)
- [`pillarbox-cast`](https://github.com/SRGSSR/pillarbox-android/blob/main/pillarbox-cast/docs/README.md)
- [`pillarbox-core-business`](https://github.com/SRGSSR/pillarbox-android/blob/main/pillarbox-core-business/docs/README.md)
- [`pillarbox-core-business-cast`](https://github.com/SRGSSR/pillarbox-android/blob/main/pillarbox-core-business-cast/docs/README.md)
- [`pillarbox-player`](https://github.com/SRGSSR/pillarbox-android/blob/main/pillarbox-player/docs/README.md)
- [`pillarbox-ui`](https://github.com/SRGSSR/pillarbox-android/blob/main/pillarbox-ui/docs/README.md)

Expand Down
43 changes: 32 additions & 11 deletions pillarbox-cast/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,10 @@ implementations are based on the [PillarboxPlayer][ch.srgssr.pillarbox.player.Pi

## Getting started

### Get the `CastContext` instance

```kotlin
val castContext = context.getCastContext()
```

### Create the player

```kotlin
val castContext = context.getCastContext()
val player = PillarboxCastPlayer(castContext = castContext)
val player = PillarboxCastPlayer(context, Default)
player.setSessionAvailabilityListener(object : SessionAvailabilityListener {
override fun onCastSessionAvailable() {
// The cast session is connected.
Expand All @@ -44,6 +37,31 @@ player.setSessionAvailabilityListener(object : SessionAvailabilityListener {
player.release()
```

The `SessionAvailabilityListener` can also be created this way:

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

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

### Configure [MediaItemConverter][androidx.media3.cast.MediaItemConverter]

```kotlin
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.
Expand All @@ -60,10 +78,10 @@ When switching to remote playback, the local playback has to be stop manually an
remote player.

```kotlin
val localPlayer = PillarboxExoPlayer(context)
val castContext = context.getCastContext()
val remotePlayer = PillarboxCastPlayer(castContext = castContext)
val localPlayer = PillarboxExoPlayer(context, Default)
val remotePlayer = PillarboxCastPlayer(context, Default)
var currentPlayer: PillarboxPlayer = if (remotePlayer.isCastSessionAvailable()) remotePlayer else localPlayer

player.setSessionAvailabilityListener(object : SessionAvailabilityListener {
override fun onCastSessionAvailable() {
setCurrentPlayer(remotePlayer)
Expand All @@ -73,6 +91,8 @@ player.setSessionAvailabilityListener(object : SessionAvailabilityListener {
setCurrentPlayer(localPlayer)
}
})

PlayerView(currentPlayer)
```

## Additional resources
Expand All @@ -83,3 +103,4 @@ player.setSessionAvailabilityListener(object : SessionAvailabilityListener {
[ch.srgssr.pillarbox.player.PillarboxExoPlayer]: https://android.pillarbox.ch/api/pillarbox-player/ch.srgssr.pillarbox.player/-pillarbox-exo-player.html
[ch.srgssr.pillarbox.cast.PillarboxCastPlayer]: https://android.pillarbox.ch/api/ch.srgssr.pillarbox.cast/-pillarbox-cast-player/index.html
[androidx.media3.cast.CastPlayer]: https://developer.android.com/reference/androidx/media3/cast/CastPlayer
[androidx.media3.cast.MediaItemConverter]: https://developer.android.com/reference/androidx/media3/cast/MediaItemConverter
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import androidx.media3.common.Tracks
* Default cast track selector
* Support only [TrackSelectionOverride] from [TrackSelectionParameters.overrides].
*/
class DefaultCastTrackSelector : CastTrackSelector {
object DefaultCastTrackSelector : CastTrackSelector {

override fun getActiveMediaTracks(
parameters: TrackSelectionParameters,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ import android.annotation.SuppressLint
import android.content.Context
import androidx.annotation.IntRange
import androidx.media3.cast.CastPlayer
import androidx.media3.cast.DefaultMediaItemConverter
import androidx.media3.cast.MediaItemConverter
import androidx.media3.cast.SessionAvailabilityListener
import androidx.media3.common.C
Expand All @@ -21,6 +20,7 @@ import androidx.media3.common.TrackSelectionParameters
import androidx.media3.common.Tracks
import androidx.media3.common.util.Clock
import androidx.media3.common.util.ListenerSet
import ch.srgssr.pillarbox.player.PillarboxDsl
import ch.srgssr.pillarbox.player.PillarboxExoPlayer
import ch.srgssr.pillarbox.player.PillarboxPlayer
import com.google.android.gms.cast.MediaQueueItem
Expand All @@ -31,6 +31,34 @@ import com.google.android.gms.cast.framework.CastSession
import com.google.android.gms.cast.framework.SessionManagerListener
import com.google.android.gms.cast.framework.media.RemoteMediaClient

/**
* Create a new instance of [PillarboxCastPlayer].
*
* **Usage**
* ```kotlin
* val player = PillarboxCastPlayer(context, Default) {
* mediaItemConverter(MyMediaItemConverter())
* }
* ```
*
* @param Builder The type of the [PillarboxCastPlayerBuilder].
* @param context The [Context].
* @param type The [CastPlayerConfig].
* @param builder The builder.
*
* @return A new instance of [PillarboxCastPlayer].
*/
@PillarboxDsl
fun <Builder : PillarboxCastPlayerBuilder> PillarboxCastPlayer(
context: Context,
type: CastPlayerConfig<Builder>,
builder: Builder.() -> Unit = {},
): PillarboxCastPlayer {
return type.create()
.apply(builder)
.create(context)
}

/**
* A [PillarboxPlayer] implementation that forwards calls to a [CastPlayer].
*
Expand All @@ -42,16 +70,18 @@ import com.google.android.gms.cast.framework.media.RemoteMediaClient
* @param seekBackIncrementMs The [seekBack] increment, in milliseconds.
* @param seekForwardIncrementMs The [seekForward] increment, in milliseconds.
* @param maxSeekToPreviousPositionMs The maximum position for which [seekToPrevious] seeks to the previous [MediaItem], in milliseconds.
* @param castPlayer The underlying [CastPlayer] instance to which method calls will be forwarded.
* @param trackSelector The [CastTrackSelector] to use when selecting tracks from [TrackSelectionParameters].
* @param castPlayer The underlying [CastPlayer] instance to which method calls will be forwarded.
*/
class PillarboxCastPlayer(
@Suppress("LongParameterList")
class PillarboxCastPlayer internal constructor(
private val castContext: CastContext,
context: Context? = null,
mediaItemConverter: MediaItemConverter = DefaultMediaItemConverter(),
@IntRange(from = 1) seekBackIncrementMs: Long = C.DEFAULT_SEEK_BACK_INCREMENT_MS,
@IntRange(from = 1) seekForwardIncrementMs: Long = C.DEFAULT_SEEK_FORWARD_INCREMENT_MS,
@IntRange(from = 0) maxSeekToPreviousPositionMs: Long = C.DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION_MS,
context: Context?,
mediaItemConverter: MediaItemConverter,
@IntRange(from = 1) seekBackIncrementMs: Long,
@IntRange(from = 1) seekForwardIncrementMs: Long,
@IntRange(from = 0) maxSeekToPreviousPositionMs: Long,
private val trackSelector: CastTrackSelector,
private val castPlayer: CastPlayer = CastPlayer(
context,
castContext,
Expand All @@ -60,7 +90,6 @@ class PillarboxCastPlayer(
seekForwardIncrementMs,
maxSeekToPreviousPositionMs,
),
private val trackSelector: CastTrackSelector = DefaultCastTrackSelector()
) : PillarboxPlayer, Player by castPlayer {
private val listeners = ListenerSet<Player.Listener>(castPlayer.applicationLooper, Clock.DEFAULT) { listener, flags ->
listener.onEvents(this, Player.Events(flags))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
/*
* Copyright (c) SRG SSR. All rights reserved.
* License information is available from the LICENSE file.
*/
package ch.srgssr.pillarbox.cast

import android.content.Context
import androidx.media3.cast.DefaultMediaItemConverter
import androidx.media3.cast.MediaItemConverter
import androidx.media3.cast.SessionAvailabilityListener
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import ch.srgssr.pillarbox.player.PillarboxDsl
import kotlin.time.Duration
import kotlin.time.Duration.Companion.milliseconds

/**
* A builder class for creating instances of [PillarboxCastPlayer].
*
* This builder provides a fluent API for configuring various aspects of the player, such as seek increments, item converters, ...
*/
@PillarboxDsl
abstract class PillarboxCastPlayerBuilder {
private var mediaItemConverter: MediaItemConverter = DefaultMediaItemConverter()
private var seekBackIncrement: Duration = C.DEFAULT_SEEK_BACK_INCREMENT_MS.milliseconds
private var seekForwardIncrement: Duration = C.DEFAULT_SEEK_FORWARD_INCREMENT_MS.milliseconds
private var maxSeekToPreviousPosition: Duration = C.DEFAULT_MAX_SEEK_TO_PREVIOUS_POSITION_MS.milliseconds
private var trackSelector: CastTrackSelector = DefaultCastTrackSelector
private var onCastSessionAvailable: (PillarboxCastPlayer.() -> Unit)? = null
private var onCastSessionUnavailable: (PillarboxCastPlayer.() -> Unit)? = null

/**
* @param seekBackIncrement The [PillarboxCastPlayer.seekBack] increment.
*/
fun seekBackIncrement(seekBackIncrement: Duration) {
check(seekBackIncrement > Duration.ZERO)
this.seekBackIncrement = seekBackIncrement
}

/**
* @param seekForwardIncrement The [PillarboxCastPlayer.seekForward] increment.
*/
fun seekForwardIncrement(seekForwardIncrement: Duration) {
check(seekForwardIncrement > Duration.ZERO)
this.seekForwardIncrement = seekForwardIncrement
}

/**
* @param maxSeekToPreviousPosition The maximum position for which [PillarboxCastPlayer.seekToPrevious] seeks to the previous [MediaItem].
*/
fun maxSeekToPreviousPosition(maxSeekToPreviousPosition: Duration) {
check(maxSeekToPreviousPosition >= Duration.ZERO)
this.maxSeekToPreviousPosition = maxSeekToPreviousPosition
}

/**
* Track selector
*
* @param trackSelector The [CastTrackSelector] to use.
*/
fun trackSelector(trackSelector: CastTrackSelector) {
this.trackSelector = trackSelector
}

/**
* On cast session available
*
* @param onCastSessionAvailable The method to invoke when [SessionAvailabilityListener.onCastSessionAvailable] is called.
*/
fun onCastSessionAvailable(onCastSessionAvailable: PillarboxCastPlayer.() -> Unit) {
this.onCastSessionAvailable = onCastSessionAvailable
}

/**
* On cast session unavailable
*
* @param onCastSessionUnavailable The method to invoke when [SessionAvailabilityListener.onCastSessionUnavailable] is called.
*/
fun onCastSessionUnavailable(onCastSessionUnavailable: PillarboxCastPlayer.() -> Unit) {
this.onCastSessionUnavailable = onCastSessionUnavailable
}

/**
* Media item converter
*
* @param mediaItemConverter The [MediaItemConverter] to use.
*/
fun mediaItemConverter(mediaItemConverter: MediaItemConverter) {
this.mediaItemConverter = mediaItemConverter
}

internal fun create(context: Context): PillarboxCastPlayer {
return PillarboxCastPlayer(
context.getCastContext(),
context,
mediaItemConverter,
seekBackIncrement.inWholeMilliseconds,
seekForwardIncrement.inWholeMilliseconds,
maxSeekToPreviousPosition.inWholeMilliseconds,
trackSelector,
).apply {
if (onCastSessionAvailable == null && onCastSessionUnavailable == null) return@apply
setSessionAvailabilityListener(object : SessionAvailabilityListener {
override fun onCastSessionAvailable() {
onCastSessionAvailable?.invoke(this@apply)
}

override fun onCastSessionUnavailable() {
onCastSessionUnavailable?.invoke(this@apply)
}
})
}
}
}

/**
* Defines a factory for creating instances of [PillarboxCastPlayerBuilder].
*
* @param Builder The type of [PillarboxCastPlayerBuilder] that this factory creates.
*/
interface CastPlayerConfig<Builder : PillarboxCastPlayerBuilder> {
Comment thread
MGaetan89 marked this conversation as resolved.
/**
* Creates a new instance of the [Builder] class.
*
* @return A new instance of the [Builder].
*/
fun create(): Builder
}

/**
* Default configuration for creating a [PillarboxCastPlayer].
*/
object Default : CastPlayerConfig<Default.Builder> {
override fun create(): Builder {
return Builder
}

/**
* A builder class for creating and configuring a [PillarboxCastPlayer].
*/
object Builder : PillarboxCastPlayerBuilder()
}
3 changes: 1 addition & 2 deletions pillarbox-core-business-cast/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,7 @@ The main goal of this module is to be able to connect to a SRG SSR receiver with
## Getting started

```kotlin
val castContext = context.getCastContext()
val player = PillarboxCastPlayer(castContext = castContext, mediaItemConverter = SRGMediaItemConverter())
val player = PillarboxCastPlayer(context)
```

[ch.srgssr.pillarbox.cast.PillarboxCastPlayer]: https://android.pillarbox.ch/api/pillarbox-cast/ch.srgssr.pillarbox.cast/-pillarbox-cast-player/index.html
Expand Down
Loading