Skip to content

Commit

Permalink
580 implements custom listeners like media3 (#589)
Browse files Browse the repository at this point in the history
Co-authored-by: Gaëtan Muller <[email protected]>
Co-authored-by: Gaëtan Muller <[email protected]>
  • Loading branch information
3 people authored Jun 13, 2024
1 parent 794bb54 commit be67017
Show file tree
Hide file tree
Showing 8 changed files with 311 additions and 48 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@ package ch.srgssr.pillarbox.core.business.tracker

import android.util.Log
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.util.EventLogger
import ch.srgssr.pillarbox.player.tracker.MediaItemTracker
import ch.srgssr.pillarbox.player.utils.PillarboxEventLogger
import kotlin.time.Duration.Companion.milliseconds

/**
* Enable/Disable EventLogger when item is currently active.
*/
class SRGEventLoggerTracker : MediaItemTracker {
private val eventLogger = EventLogger(TAG)
private val eventLogger = PillarboxEventLogger(TAG)

override fun start(player: ExoPlayer, initialData: Any?) {
Log.w(TAG, "---- Start")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,13 @@ import androidx.media3.common.Player
import androidx.media3.common.Timeline.Window
import androidx.media3.common.TrackSelectionParameters
import androidx.media3.common.util.Clock
import androidx.media3.common.util.ListenerSet
import androidx.media3.exoplayer.DefaultRenderersFactory
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.LoadControl
import androidx.media3.exoplayer.trackselection.DefaultTrackSelector
import androidx.media3.exoplayer.upstream.DefaultBandwidthMeter
import androidx.media3.exoplayer.util.EventLogger
import ch.srgssr.pillarbox.player.analytics.PillarboxAnalyticsCollector
import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange
import ch.srgssr.pillarbox.player.asset.timeRange.Chapter
import ch.srgssr.pillarbox.player.asset.timeRange.Credit
Expand All @@ -31,20 +32,25 @@ 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.utils.PillarboxEventLogger

/**
* Pillarbox player
*
* @param exoPlayer
* @param mediaItemTrackerProvider
* @param analyticsCollector
*
* @constructor
*/
class PillarboxExoPlayer internal constructor(
private val exoPlayer: ExoPlayer,
mediaItemTrackerProvider: MediaItemTrackerProvider
mediaItemTrackerProvider: MediaItemTrackerProvider,
analyticsCollector: PillarboxAnalyticsCollector,
) : PillarboxPlayer, ExoPlayer by exoPlayer {
private val listeners = HashSet<PillarboxPlayer.Listener>()
private val listeners = ListenerSet<PillarboxPlayer.Listener>(applicationLooper, clock) { listener, flags ->
listener.onEvents(this, Player.Events(flags))
}
private val itemPillarboxDataTracker = CurrentMediaItemPillarboxDataTracker(this)
private val analyticsTracker = AnalyticsMediaItemTracker(this, mediaItemTrackerProvider)
private val window = Window()
Expand All @@ -56,9 +62,8 @@ class PillarboxExoPlayer internal constructor(
seekEnd()
}
clearSeeking()
val listeners = HashSet(listeners)
listeners.forEach {
it.onSmoothSeekingEnabledChanged(value)
listeners.sendEvent(PillarboxPlayer.EVENT_SMOOTH_SEEKING_ENABLED_CHANGED) { listener ->
listener.onSmoothSeekingEnabledChanged(value)
}
}
}
Expand All @@ -72,9 +77,8 @@ class PillarboxExoPlayer internal constructor(
set(value) {
if (analyticsTracker.enabled != value) {
analyticsTracker.enabled = value
val listeners = HashSet(listeners)
listeners.forEach {
it.onTrackingEnabledChanged(value)
listeners.sendEvent(PillarboxPlayer.EVENT_TRACKING_ENABLED_CHANGED) { listener ->
listener.onTrackingEnabledChanged(value)
}
}
}
Expand All @@ -84,26 +88,33 @@ class PillarboxExoPlayer internal constructor(
this,
object : TimeRangeTracker.Callback {
override fun onBlockedTimeRange(blockedTimeRange: BlockedTimeRange) {
notifyBlockedTimeRangeReached(blockedTimeRange)
listeners.sendEvent(PillarboxPlayer.EVENT_BLOCKED_TIME_RANGE_REACHED) { listener ->
listener.onBlockedTimeRangeReached(blockedTimeRange)
}
handleBlockedTimeRange(blockedTimeRange)
}

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

override fun onCreditChanged(credit: Credit?) {
notifyCreditChanged(credit)
listeners.sendEvent(PillarboxPlayer.EVENT_CREDIT_CHANGED) { listener ->
listener.onCreditChanged(credit)
}
}
}
)

init {
addListener(analyticsCollector)
exoPlayer.addListener(ComponentListener())
itemPillarboxDataTracker.addCallback(timeRangeTracker)
itemPillarboxDataTracker.addCallback(analyticsTracker)
if (BuildConfig.DEBUG) {
addAnalyticsListener(EventLogger())
addAnalyticsListener(PillarboxEventLogger())
}
}

Expand All @@ -130,6 +141,7 @@ class PillarboxExoPlayer internal constructor(
mediaItemTrackerProvider: MediaItemTrackerProvider = MediaItemTrackerRepository(),
seekIncrement: SeekIncrement = SeekIncrement(),
clock: Clock,
analyticsCollector: PillarboxAnalyticsCollector = PillarboxAnalyticsCollector(clock),
) : this(
ExoPlayer.Builder(context)
.setClock(clock)
Expand All @@ -151,9 +163,11 @@ class PillarboxExoPlayer internal constructor(
.build()
)
)
.setAnalyticsCollector(analyticsCollector)
.setDeviceVolumeControlEnabled(true) // allow player to control device volume
.build(),
mediaItemTrackerProvider = mediaItemTrackerProvider
mediaItemTrackerProvider = mediaItemTrackerProvider,
analyticsCollector = analyticsCollector
)

override fun addListener(listener: Player.Listener) {
Expand All @@ -170,24 +184,6 @@ class PillarboxExoPlayer internal constructor(
}
}

private fun notifyChapterChanged(chapter: Chapter?) {
HashSet(listeners).forEach {
it.onChapterChanged(chapter)
}
}

private fun notifyBlockedTimeRangeReached(blockedTimeRange: BlockedTimeRange) {
HashSet(listeners).forEach {
it.onBlockedTimeRangeReached(blockedTimeRange)
}
}

private fun notifyCreditChanged(timeRange: Credit?) {
HashSet(listeners).forEach {
it.onCreditChanged(timeRange)
}
}

override fun setMediaItem(mediaItem: MediaItem) {
exoPlayer.setMediaItem(mediaItem.clearTag())
}
Expand Down Expand Up @@ -330,6 +326,7 @@ class PillarboxExoPlayer internal constructor(
if (playbackState != Player.STATE_IDLE) {
stop()
}
listeners.release()
exoPlayer.release()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ interface PillarboxPlayer : Player {
* Listener
*/
interface Listener : Player.Listener {

/**
* On smooth seeking enabled changed
*
Expand Down Expand Up @@ -76,4 +77,32 @@ interface PillarboxPlayer : Player {
* Enable or disable MediaItem tracking
*/
var trackingEnabled: Boolean

companion object {

/**
* Event Blocked Time Range Reached.
*/
const val EVENT_BLOCKED_TIME_RANGE_REACHED = 100

/**
* The current [Chapter] has changed.
*/
const val EVENT_CHAPTER_CHANGED = 101

/**
* The current [Credit] Changed.
*/
const val EVENT_CREDIT_CHANGED = 102

/**
* [trackingEnabled] has changed.
*/
const val EVENT_TRACKING_ENABLED_CHANGED = 103

/**
* [smoothSeekingEnabled] has changed.
*/
const val EVENT_SMOOTH_SEEKING_ENABLED_CHANGED = 104
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* Copyright (c) SRG SSR. All rights reserved.
* License information is available from the LICENSE file.
*/
package ch.srgssr.pillarbox.player.analytics

import androidx.media3.common.util.Clock
import androidx.media3.common.util.ListenerSet.Event
import androidx.media3.exoplayer.analytics.AnalyticsListener.EventTime
import androidx.media3.exoplayer.analytics.DefaultAnalyticsCollector
import ch.srgssr.pillarbox.player.PillarboxPlayer
import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange
import ch.srgssr.pillarbox.player.asset.timeRange.Chapter
import ch.srgssr.pillarbox.player.asset.timeRange.Credit

/**
* Pillarbox analytics collector
*
* @constructor
*
* @param clock The [Clock] used to generate timestamps.
*/
class PillarboxAnalyticsCollector(
clock: Clock = Clock.DEFAULT
) : DefaultAnalyticsCollector(clock), PillarboxPlayer.Listener {

override fun onSmoothSeekingEnabledChanged(smoothSeekingEnabled: Boolean) {
val eventTime = generateCurrentPlayerMediaPeriodEventTime()

sendEventPillarbox(
eventTime, PillarboxAnalyticsListener.EVENT_SMOOTH_SEEKING_ENABLED_CHANGED
) { listener -> listener.onSmoothSeekingEnabledChanged(eventTime, smoothSeekingEnabled) }
}

override fun onTrackingEnabledChanged(trackingEnabled: Boolean) {
val eventTime = generateCurrentPlayerMediaPeriodEventTime()

sendEventPillarbox(
eventTime, PillarboxAnalyticsListener.EVENT_TRACKING_ENABLED_CHANGED
) { listener -> listener.onTrackingEnabledChanged(eventTime, trackingEnabled) }
}

override fun onChapterChanged(chapter: Chapter?) {
val eventTime = generateCurrentPlayerMediaPeriodEventTime()

sendEventPillarbox(
eventTime, PillarboxAnalyticsListener.EVENT_CHAPTER_CHANGED
) { listener -> listener.onChapterChanged(eventTime, chapter) }
}

override fun onCreditChanged(credit: Credit?) {
val eventTime = generateCurrentPlayerMediaPeriodEventTime()

sendEventPillarbox(
eventTime, PillarboxAnalyticsListener.EVENT_CREDIT_CHANGED
) { listener -> listener.onCreditChanged(eventTime, credit) }
}

override fun onBlockedTimeRangeReached(blockedTimeRange: BlockedTimeRange) {
val eventTime = generateCurrentPlayerMediaPeriodEventTime()

sendEventPillarbox(
eventTime, PillarboxAnalyticsListener.EVENT_BLOCKED_TIME_RANGE_REACHED
) { listener -> listener.onBlockedTimeRangeReached(eventTime, blockedTimeRange) }
}

private fun sendEventPillarbox(eventTime: EventTime, eventFlag: Int, event: Event<PillarboxAnalyticsListener>) {
sendEvent(
eventTime, eventFlag
) { listener -> if (listener is PillarboxAnalyticsListener) event.invoke(listener) }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Copyright (c) SRG SSR. All rights reserved.
* License information is available from the LICENSE file.
*/
package ch.srgssr.pillarbox.player.analytics

import androidx.media3.exoplayer.analytics.AnalyticsListener
import androidx.media3.exoplayer.analytics.AnalyticsListener.EventTime
import ch.srgssr.pillarbox.player.PillarboxPlayer
import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange
import ch.srgssr.pillarbox.player.asset.timeRange.Chapter
import ch.srgssr.pillarbox.player.asset.timeRange.Credit

/**
* Pillarbox analytics listener
*
* @see [AnalyticsListener]
*/
interface PillarboxAnalyticsListener : AnalyticsListener {
/**
* On smooth seeking enabled changed
*
* @param eventTime The [EventTime].
* @param smoothSeekingEnabled The new value of [PillarboxPlayer.smoothSeekingEnabled]
*/
fun onSmoothSeekingEnabledChanged(eventTime: EventTime, smoothSeekingEnabled: Boolean) {}

/**
* On tracking enabled changed
*
* @param eventTime The [EventTime].
* @param trackingEnabled The new value of [PillarboxPlayer.trackingEnabled]
*/
fun onTrackingEnabledChanged(eventTime: EventTime, trackingEnabled: Boolean) {}

/**
* `onChapterChanged` is called when either:
* - The player position changes while playing automatically.
* - The use seeks to a new position.
* - The playlist changes.
*
* @param eventTime The [EventTime].
* @param chapter `null` when the current position is not in a chapter.
*/
fun onChapterChanged(eventTime: EventTime, chapter: Chapter?) {}

/**
* On blocked time range reached
*
* @param eventTime The [EventTime].
* @param blockedTimeRange The [BlockedTimeRange] reached by the player.
*/
fun onBlockedTimeRangeReached(eventTime: EventTime, blockedTimeRange: BlockedTimeRange) {}

/**
* `onCreditChanged` is called when either:
* - The player position changes while playing automatically.
* - The use seeks to a new position.
* - The playlist changes.
*
* @param eventTime The [EventTime]
* @param credit `null` when the current position is not in a Credit.
*/
fun onCreditChanged(eventTime: EventTime, credit: Credit?) {}

companion object {
/**
* @see [PillarboxPlayer.EVENT_BLOCKED_TIME_RANGE_REACHED]
*/
const val EVENT_BLOCKED_TIME_RANGE_REACHED = PillarboxPlayer.EVENT_BLOCKED_TIME_RANGE_REACHED

/**
* @see [PillarboxPlayer.EVENT_CREDIT_CHANGED]
*/
const val EVENT_CREDIT_CHANGED = PillarboxPlayer.EVENT_CREDIT_CHANGED

/**
* @see [PillarboxPlayer.EVENT_CHAPTER_CHANGED]
*/
const val EVENT_CHAPTER_CHANGED = PillarboxPlayer.EVENT_CHAPTER_CHANGED

/**
* @see [PillarboxPlayer.EVENT_TRACKING_ENABLED_CHANGED]
*/
const val EVENT_TRACKING_ENABLED_CHANGED = PillarboxPlayer.EVENT_TRACKING_ENABLED_CHANGED

/**
* @see [PillarboxPlayer.EVENT_SMOOTH_SEEKING_ENABLED_CHANGED]
*/
const val EVENT_SMOOTH_SEEKING_ENABLED_CHANGED = PillarboxPlayer.EVENT_SMOOTH_SEEKING_ENABLED_CHANGED
}
}
Loading

0 comments on commit be67017

Please sign in to comment.