Skip to content

Commit

Permalink
708 revisit timerange trackers (#713)
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 Sep 18, 2024
1 parent 78986fa commit 5234a20
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 247 deletions.
1 change: 0 additions & 1 deletion .idea/detekt.xml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import ch.srgssr.pillarbox.player.analytics.metrics.PlaybackMetrics
import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange
import ch.srgssr.pillarbox.player.asset.timeRange.Chapter
import ch.srgssr.pillarbox.player.asset.timeRange.Credit
import ch.srgssr.pillarbox.player.asset.timeRange.TimeRange
import ch.srgssr.pillarbox.player.extension.getPlaybackSpeed
import ch.srgssr.pillarbox.player.extension.setPreferredAudioRoleFlagsToAccessibilityManagerSettings
import ch.srgssr.pillarbox.player.extension.setSeekIncrements
Expand All @@ -39,10 +40,11 @@ import ch.srgssr.pillarbox.player.monitoring.RemoteMonitoringMessageHandler
import ch.srgssr.pillarbox.player.network.PillarboxHttpClient
import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory
import ch.srgssr.pillarbox.player.tracker.AnalyticsMediaItemTracker
import ch.srgssr.pillarbox.player.tracker.BlockedTimeRangeTracker
import ch.srgssr.pillarbox.player.tracker.CurrentMediaItemPillarboxDataTracker
import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerProvider
import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerRepository
import ch.srgssr.pillarbox.player.tracker.TimeRangeTracker
import ch.srgssr.pillarbox.player.tracker.PillarboxMediaMetaDataTracker
import ch.srgssr.pillarbox.player.utils.PillarboxEventLogger
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
Expand Down Expand Up @@ -121,36 +123,17 @@ class PillarboxExoPlayer internal constructor(
}
get() = analyticsTracker.enabled

private val timeRangeTracker = TimeRangeTracker(
this,
object : TimeRangeTracker.Callback {
override fun onBlockedTimeRange(blockedTimeRange: BlockedTimeRange) {
listeners.sendEvent(PillarboxPlayer.EVENT_BLOCKED_TIME_RANGE_REACHED) { listener ->
listener.onBlockedTimeRangeReached(blockedTimeRange)
}
handleBlockedTimeRange(blockedTimeRange)
}

override fun onChapterChanged(chapter: Chapter?) {
listeners.sendEvent(PillarboxPlayer.EVENT_CHAPTER_CHANGED) { listener ->
listener.onChapterChanged(chapter)
}
}

override fun onCreditChanged(credit: Credit?) {
listeners.sendEvent(PillarboxPlayer.EVENT_CREDIT_CHANGED) { listener ->
listener.onCreditChanged(credit)
}
}
}
)
private val blockedTimeRangeTracker = BlockedTimeRangeTracker(this::notifyTimeRangeChanged)
private val mediaMetadataTracker = PillarboxMediaMetaDataTracker(this::notifyTimeRangeChanged)

init {
sessionManager.setPlayer(this)
metricsCollector.setPlayer(this)
mediaMetadataTracker.setPlayer(this)
blockedTimeRangeTracker.setPlayer(this)
addListener(analyticsCollector)
exoPlayer.addListener(ComponentListener())
itemPillarboxDataTracker.addCallback(timeRangeTracker)
itemPillarboxDataTracker.addCallback(blockedTimeRangeTracker)
itemPillarboxDataTracker.addCallback(analyticsTracker)
if (BuildConfig.DEBUG) {
addAnalyticsListener(PillarboxEventLogger())
Expand Down Expand Up @@ -358,11 +341,34 @@ class PillarboxExoPlayer internal constructor(
*/
override fun release() {
clearSeeking()
mediaMetadataTracker.release()
blockedTimeRangeTracker.release()
exoPlayer.release()
listeners.release()
itemPillarboxDataTracker.release()
}

private fun notifyTimeRangeChanged(timeRange: TimeRange?) {
when (timeRange) {
is Chapter? -> listeners.sendEvent(PillarboxPlayer.EVENT_CHAPTER_CHANGED) { listener ->
listener.onChapterChanged(timeRange)
}

is Credit? -> listeners.sendEvent(PillarboxPlayer.EVENT_CREDIT_CHANGED) { listener ->
listener.onCreditChanged(timeRange)
}

is BlockedTimeRange -> {
listeners.sendEvent(PillarboxPlayer.EVENT_BLOCKED_TIME_RANGE_REACHED) { listener ->
listener.onBlockedTimeRangeReached(timeRange)
}
handleBlockedTimeRange(timeRange)
}

else -> Unit
}
}

override fun setPlaybackParameters(playbackParameters: PlaybackParameters) {
if (isPlaybackSpeedPossibleAtPosition(currentPosition, playbackParameters.speed, window)) {
exoPlayer.playbackParameters = playbackParameters
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
* Copyright (c) SRG SSR. All rights reserved.
* License information is available from the LICENSE file.
*/
package ch.srgssr.pillarbox.player.tracker

import androidx.media3.common.Player
import androidx.media3.exoplayer.PlayerMessage
import ch.srgssr.pillarbox.player.PillarboxExoPlayer
import ch.srgssr.pillarbox.player.asset.PillarboxData
import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange
import ch.srgssr.pillarbox.player.asset.timeRange.TimeRange
import ch.srgssr.pillarbox.player.asset.timeRange.firstOrNullAtPosition

internal class BlockedTimeRangeTracker(
private val callback: (TimeRange?) -> Unit
) : CurrentMediaItemPillarboxDataTracker.Callback, Player.Listener {
private val playerMessages = mutableListOf<PlayerMessage>()
private var timeRanges: List<BlockedTimeRange>? = null
private lateinit var player: PillarboxExoPlayer

fun setPlayer(player: PillarboxExoPlayer) {
this.player = player
player.addListener(this)
}

/*
* Called when the callback is added, and we already have a [PillarboxData].
*/
override fun onPillarboxDataChanged(data: PillarboxData?) {
clear()
data?.let {
timeRanges = it.blockedTimeRanges
it.blockedTimeRanges.firstOrNullAtPosition(player.currentPosition)?.let { timeRange ->
callback(timeRange)
}
createMessages(it.blockedTimeRanges)
}
}

override fun onEvents(player: Player, events: Player.Events) {
val blockedInterval = timeRanges?.firstOrNullAtPosition(player.currentPosition)
blockedInterval?.let {
// Ignore blocked time ranges that end at the same time as the media. Otherwise, infinite seeks operations.
if (player.currentPosition >= player.duration) return@let
callback(it)
}
}

private fun createMessages(timeRanges: List<BlockedTimeRange>) {
val target = PlayerMessage.Target { _, message ->
callback(message as BlockedTimeRange)
}
playerMessages.addAll(
timeRanges.map {
player.createMessage(target).apply {
deleteAfterDelivery = false
looper = player.applicationLooper
payload = it
setPosition(it.start)
send()
}
}
)
}

private fun clear() {
playerMessages.forEach { playerMessage ->
playerMessage.cancel()
}
playerMessages.clear()
timeRanges = null
}

fun release() {
clear()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
/*
* Copyright (c) SRG SSR. All rights reserved.
* License information is available from the LICENSE file.
*/
package ch.srgssr.pillarbox.player.tracker

import androidx.media3.common.MediaMetadata
import androidx.media3.common.Player
import androidx.media3.exoplayer.PlayerMessage
import ch.srgssr.pillarbox.player.PillarboxExoPlayer
import ch.srgssr.pillarbox.player.asset.timeRange.Chapter
import ch.srgssr.pillarbox.player.asset.timeRange.Credit
import ch.srgssr.pillarbox.player.asset.timeRange.TimeRange
import ch.srgssr.pillarbox.player.asset.timeRange.firstOrNullAtPosition
import ch.srgssr.pillarbox.player.extension.chapters
import ch.srgssr.pillarbox.player.extension.credits

internal class PillarboxMediaMetaDataTracker(private val callback: (TimeRange?) -> Unit) : Player.Listener {
private var currentChapterTracker: Tracker<Chapter>? = null
private var currentCreditTracker: Tracker<Credit>? = null
private lateinit var player: PillarboxExoPlayer

private fun clear() {
currentCreditTracker?.clear()
currentChapterTracker?.clear()
currentChapterTracker = null
currentCreditTracker = null
}

override fun onPositionDiscontinuity(oldPosition: Player.PositionInfo, newPosition: Player.PositionInfo, reason: Int) {
when (reason) {
Player.DISCONTINUITY_REASON_SEEK, Player.DISCONTINUITY_REASON_SEEK_ADJUSTMENT -> {
if (oldPosition.mediaItemIndex == newPosition.mediaItemIndex) {
val position = newPosition.positionMs
currentCreditTracker?.setCurrentPosition(position)
currentChapterTracker?.setCurrentPosition(position)
} else {
clear()
}
}

else -> {
clear()
}
}
}

fun setPlayer(player: PillarboxExoPlayer) {
this.player = player
this.player.addListener(this)
}

fun release() {
clear()
}

override fun onMediaMetadataChanged(mediaMetadata: MediaMetadata) {
mediaMetadata.chapters?.let {
if (currentChapterTracker?.timeRanges != it) {
currentChapterTracker?.clear()
currentChapterTracker = Tracker(player = player, timeRanges = it, callback = callback)
}
}

mediaMetadata.credits?.let {
if (currentCreditTracker?.timeRanges != it) {
currentCreditTracker?.clear()
currentCreditTracker = Tracker(player = player, timeRanges = it, callback = callback)
}
}
}

private class Tracker<T : TimeRange>(
private val player: PillarboxExoPlayer,
val timeRanges: List<T>,
private val callback: (T?) -> Unit,
) {
private val messages: List<PlayerMessage> = createMessages()

private var currentTimeRange: T? = null
set(value) {
if (field != value) {
callback(value)
field = value
}
}

init {
currentTimeRange = timeRanges.firstOrNullAtPosition(player.currentPosition)
}

fun setCurrentPosition(currentPosition: Long) {
val currentTimeRange = currentTimeRange
?.takeIf { timeRange -> currentPosition in timeRange }
?: timeRanges.firstOrNullAtPosition(currentPosition)
this.currentTimeRange = currentTimeRange
}

private fun createMessages(): List<PlayerMessage> {
val messageHandler = PlayerMessage.Target { messageType, message ->
@Suppress("UNCHECKED_CAST")
val timeRange = message as? T ?: return@Target
when (messageType) {
TYPE_ENTER -> currentTimeRange = timeRange
TYPE_EXIT -> {
val nextTimeRange = timeRanges.firstOrNullAtPosition(player.currentPosition)
if (nextTimeRange == null) currentTimeRange = null
}
}
}
val playerMessages = mutableListOf<PlayerMessage>()
timeRanges.forEach { timeRange ->
val messageEnter = player.createMessage(messageHandler).apply {
deleteAfterDelivery = false
looper = player.applicationLooper
payload = timeRange
setPosition(timeRange.start)
type = TYPE_ENTER
send()
}
val messageExit = player.createMessage(messageHandler).apply {
deleteAfterDelivery = false
looper = player.applicationLooper
payload = timeRange
setPosition(timeRange.end)
type = TYPE_EXIT
send()
}
playerMessages.add(messageEnter)
playerMessages.add(messageExit)
}
return playerMessages
}

fun clear() {
messages.forEach { it.cancel() }
currentTimeRange = null
}

private companion object {
private const val TYPE_ENTER = 1
private const val TYPE_EXIT = 2
}
}
}
Loading

0 comments on commit 5234a20

Please sign in to comment.