Skip to content

Commit

Permalink
868 handle text and audio tracks with castplayer (#879)
Browse files Browse the repository at this point in the history
Co-authored-by: Gaëtan Muller <[email protected]>
  • Loading branch information
StaehliJ and MGaetan89 authored Feb 4, 2025
1 parent abc9def commit 5be871f
Show file tree
Hide file tree
Showing 5 changed files with 243 additions and 40 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
* Copyright (c) SRG SSR. All rights reserved.
* License information is available from the LICENSE file.
*/
package ch.srgssr.pillarbox.cast

import androidx.media3.common.TrackSelectionParameters
import androidx.media3.common.Tracks

/**
* Cast track selector
*/
interface CastTrackSelector {

/**
* Returns the indices of the currently selected media tracks.
*
* This function determines the active tracks based on the provided [parameters]
* and the available [tracks]. It returns an array containing the indices of these tracks.
*
* @param parameters The track selection preferences.
* @param tracks The available media tracks.
* @return An array of track indices for the selected tracks. Returns an empty array if no tracks are selected.
* @see TrackSelectionParameters
* @see Tracks
*/
fun getActiveMediaTracks(parameters: TrackSelectionParameters, tracks: Tracks): LongArray
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* Copyright (c) SRG SSR. All rights reserved.
* License information is available from the LICENSE file.
*/
package ch.srgssr.pillarbox.cast

import androidx.media3.common.TrackSelectionOverride
import androidx.media3.common.TrackSelectionParameters
import androidx.media3.common.Tracks

/**
* Default cast track selector
* Support only [TrackSelectionOverride] from [TrackSelectionParameters.overrides].
*/
class DefaultCastTrackSelector : CastTrackSelector {

override fun getActiveMediaTracks(
parameters: TrackSelectionParameters,
tracks: Tracks
): LongArray {
return parameters.overrides.keys
.mapNotNull { it.id.toLongOrNull() }
.toLongArray()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,29 @@
*/
package ch.srgssr.pillarbox.cast

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
import androidx.media3.common.FlagSet
import androidx.media3.common.Format
import androidx.media3.common.MediaItem
import androidx.media3.common.MimeTypes
import androidx.media3.common.Player
import androidx.media3.common.TrackGroup
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.PillarboxExoPlayer
import ch.srgssr.pillarbox.player.PillarboxPlayer
import com.google.android.gms.cast.MediaQueueItem
import com.google.android.gms.cast.MediaStatus
import com.google.android.gms.cast.MediaTrack
import com.google.android.gms.cast.framework.CastContext
import com.google.android.gms.cast.framework.CastSession
import com.google.android.gms.cast.framework.SessionManagerListener
Expand All @@ -36,9 +44,10 @@ import com.google.android.gms.cast.framework.media.RemoteMediaClient
* @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].
*/
class PillarboxCastPlayer(
castContext: CastContext,
private val castContext: CastContext,
context: Context? = null,
mediaItemConverter: MediaItemConverter = DefaultMediaItemConverter(),
@IntRange(from = 1) seekBackIncrementMs: Long = C.DEFAULT_SEEK_BACK_INCREMENT_MS,
Expand All @@ -52,21 +61,15 @@ 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))
}
private val remoteClientCallback = RemoteClientCallback()
private val sessionManagerListener = SessionListener()

private var trackSelectionParameters: TrackSelectionParameters = TrackSelectionParameters.DEFAULT_WITHOUT_CONTEXT
private var remoteMediaClient: RemoteMediaClient? = null
set(value) {
if (field != value) {
field?.unregisterCallback(remoteClientCallback)
value?.registerCallback(remoteClientCallback)
field = value
}
}
private var tracks: Tracks = Tracks.EMPTY

/**
* Smooth seeking is not supported on [CastPlayer]. By its very nature (ie. being remote), seeking **smoothly** is impossible to achieve.
Expand All @@ -79,17 +82,20 @@ class PillarboxCastPlayer(
*/
override var trackingEnabled: Boolean = false
set(value) {}
private val castPlayerListener = InternalCastPlayerListener()

init {
remoteMediaClient = castContext.sessionManager.currentCastSession?.remoteMediaClient

castContext.sessionManager.addSessionManagerListener(sessionManagerListener, CastSession::class.java)
castPlayer.addListener(castPlayerListener)
updateCurrentTracksAndNotify()
}

castPlayer.addListener(object : Player.Listener {
override fun onAvailableCommandsChanged(availableCommands: Player.Commands) {
notifyOnAvailableCommandsChange()
}
})
override fun release() {
castContext.sessionManager.removeSessionManagerListener(sessionManagerListener, CastSession::class.java)
listeners.release()
castPlayer.removeListener(castPlayerListener) // CastPlayer doesn't remove listeners.
castPlayer.release()
}

/**
Expand All @@ -109,6 +115,51 @@ class PillarboxCastPlayer(
return castPlayer.isCastSessionAvailable
}

override fun getTrackSelectionParameters(): TrackSelectionParameters {
return trackSelectionParameters
}

override fun getCurrentTracks(): Tracks {
return tracks
}

private fun getMediaStatus(): MediaStatus? {
return remoteMediaClient?.mediaStatus
}

override fun setTrackSelectionParameters(parameters: TrackSelectionParameters) {
if (remoteMediaClient == null || parameters == trackSelectionParameters) return
val oldParameters = this.trackSelectionParameters
this.trackSelectionParameters = parameters
notifyTrackSelectionParametersChanged()
val selectedTrackIds = trackSelector.getActiveMediaTracks(trackSelectionParameters, tracks = currentTracks)
remoteMediaClient?.setActiveMediaTracks(selectedTrackIds)?.setResultCallback {
if (!it.status.isSuccess) {
this.trackSelectionParameters = oldParameters
notifyTrackSelectionParametersChanged()
}
}
}

private fun updateCurrentTracksAndNotify() {
if (remoteMediaClient == null) return
val mediaTracks = getMediaStatus()?.mediaInfo?.mediaTracks ?: emptyList<MediaTrack>()
val tracks = if (mediaTracks.isEmpty()) {
Tracks.EMPTY
} else {
val selectedTrackIds: LongArray = getMediaStatus()?.activeTrackIds ?: longArrayOf()
val tabTrackGroup = mediaTracks.map { mediaTrack ->
val trackGroup = TrackGroup(mediaTrack.id.toString(), mediaTrack.toFormat())
Tracks.Group(trackGroup, false, intArrayOf(C.FORMAT_HANDLED), booleanArrayOf(selectedTrackIds.contains(mediaTrack.id)))
}
Tracks(tabTrackGroup)
}
if (tracks != this.tracks) {
this.tracks = tracks
notifyTracksChanged(this.tracks)
}
}

/**
* Sets a listener for updates on the cast session availability.
*
Expand All @@ -119,20 +170,22 @@ class PillarboxCastPlayer(
}

override fun addListener(listener: Player.Listener) {
castPlayer.addListener(listener)
castPlayer.addListener(ForwardingListener(this, listener))
listeners.add(listener)
}

@SuppressLint("ImplicitSamInstance")
override fun removeListener(listener: Player.Listener) {
castPlayer.removeListener(listener)
castPlayer.removeListener(ForwardingListener(this, listener))
listeners.remove(listener)
}

override fun getAvailableCommands(): Player.Commands {
val isShuffleAvailable = remoteMediaClient?.mediaStatus?.isMediaCommandSupported(MediaStatus.COMMAND_QUEUE_SHUFFLE) == true

val isShuffleAvailable = getMediaStatus()?.isMediaCommandSupported(MediaStatus.COMMAND_QUEUE_SHUFFLE) == true
val isEditTracksAvailable = getMediaStatus()?.isMediaCommandSupported(MediaStatus.COMMAND_EDIT_TRACKS) == true
return castPlayer.availableCommands
.buildUpon()
.addIf(Player.COMMAND_SET_TRACK_SELECTION_PARAMETERS, isEditTracksAvailable)
.addIf(Player.COMMAND_SET_SHUFFLE_MODE, isShuffleAvailable)
.build()
}
Expand Down Expand Up @@ -167,14 +220,16 @@ class PillarboxCastPlayer(
listeners.flushEvents()
}

private inner class RemoteClientCallback : RemoteMediaClient.Callback() {
override fun onStatusUpdated() {
notifyOnAvailableCommandsChange()
private fun notifyTrackSelectionParametersChanged() {
listeners.queueEvent(Player.EVENT_TRACK_SELECTION_PARAMETERS_CHANGED) {
it.onTrackSelectionParametersChanged(trackSelectionParameters)
}
listeners.flushEvents()
}

override fun onQueueStatusUpdated() {
notifyOnAvailableCommandsChange()
}
private fun notifyTracksChanged(tracks: Tracks) {
listeners.queueEvent(Player.EVENT_TRACKS_CHANGED) { listener -> listener.onTracksChanged(tracks) }
listeners.flushEvents()
}

private inner class SessionListener : SessionManagerListener<CastSession> {
Expand Down Expand Up @@ -209,4 +264,78 @@ class PillarboxCastPlayer(
remoteMediaClient = null
}
}

private class ForwardingListener(
private val player: Player,
private val listener: Player.Listener
) : Player.Listener by listener {

override fun onTracksChanged(tracks: Tracks) {
// Do not forward this event.
}

override fun onAvailableCommandsChanged(availableCommands: Player.Commands) {
// Do not forward this event.
}

override fun onEvents(player: Player, events: Player.Events) {
// Filter Events triggered by CastPlayer
if (events.containsAny(Player.EVENT_AVAILABLE_COMMANDS_CHANGED, Player.EVENT_TRACKS_CHANGED)) {
return
}
val flagSet = FlagSet.Builder()
.apply {
for (index in 0 until events.size()) {
val event = events.get(index)
addIf(event, event != Player.EVENT_TRACKS_CHANGED && event != Player.EVENT_AVAILABLE_COMMANDS_CHANGED)
}
}
.build()
listener.onEvents(this.player, Player.Events(flagSet))
}

override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false

other as ForwardingListener

if (player != other.player) return false
if (listener != other.listener) return false

return true
}

override fun hashCode(): Int {
var result = player.hashCode()
result = 31 * result + listener.hashCode()
return result
}
}

private inner class InternalCastPlayerListener : Player.Listener {
override fun onAvailableCommandsChanged(availableCommands: Player.Commands) {
notifyOnAvailableCommandsChange()
}

override fun onTracksChanged(tracks: Tracks) {
updateCurrentTracksAndNotify()
}
}

private companion object {
private const val CAST_TEXT_TRACK = MimeTypes.BASE_TYPE_TEXT + "/cast"

private fun MediaTrack.toFormat(): Format {
val builder = Format.Builder()
if (type == MediaTrack.TYPE_TEXT && MimeTypes.getTrackType(contentType) == C.TRACK_TYPE_UNKNOWN) {
builder.setSampleMimeType(CAST_TEXT_TRACK)
}
return builder
.setId(contentId)
.setContainerMimeType(contentType)
.setLanguage(language)
.build()
}
}
}
Loading

0 comments on commit 5be871f

Please sign in to comment.