If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}. + * + * @param player The [Player]. + * @param expectedEvent The expected [Player.Event] if null wait until first [Player.Listener.onEvents]. + * @throws TimeoutException If the [RobolectricUtil.DEFAULT_TIMEOUT_MS] is exceeded. + */ + @Throws(TimeoutException::class) + fun runUntilEvent(player: Player, expectedEvent: @Player.Event Int? = null) { + verifyMainTestThread(player) + if (player is ExoPlayer) { + verifyPlaybackThreadIsAlive(player) + } + val receivedCallback = AtomicBoolean(false) + val listener: Player.Listener = object : Player.Listener { + override fun onEvents(player: Player, events: Player.Events) { + if (expectedEvent?.let { events.contains(it) } != false) { + receivedCallback.set(true) + } + } + } + player.addListener(listener) + RobolectricUtil.runMainLooperUntil({ receivedCallback.get() || player.playerError != null }, 20_000, Clock.DEFAULT) + player.removeListener(listener) + if (player.playerError != null) { + throw IllegalStateException(player.playerError) + } + } + + /** + * Runs tasks of the main Looper until [Player.Listener.onPlaybackParametersChanged] is called or a playback error occurs. + * + *
If a playback error occurs it will be thrown wrapped in an {@link IllegalStateException}.
+ *
+ * @param player The [Player].
+ * @throws TimeoutException If the [RobolectricUtil.DEFAULT_TIMEOUT_MS] is exceeded.
+ */
+ @Throws(TimeoutException::class)
+ fun runUntilPlaybackParametersChanged(player: Player) {
+ verifyMainTestThread(player)
+ if (player is ExoPlayer) {
+ verifyPlaybackThreadIsAlive(player)
+ }
+ val receivedCallback = AtomicBoolean(false)
+ val listener: Player.Listener = object : Player.Listener {
+ override fun onPlaybackParametersChanged(playbackParameters: PlaybackParameters) {
+ receivedCallback.set(true)
+ }
+ }
+ player.addListener(listener)
+ RobolectricUtil.runMainLooperUntil { receivedCallback.get() || player.playerError != null }
+ player.removeListener(listener)
+ if (player.playerError != null) {
+ throw IllegalStateException(player.playerError)
+ }
+ }
+
+ /**
+ * Same as [TestPlayerRunHelper.playUntilStartOfMediaItem], but doesn't pause the player afterwards.
+ *
+ * @param player The [Player].
+ * @param mediaItemIndex The index of the media item.
+ *
+ * @throws TimeoutException If the [default timeout][RobolectricUtil.DEFAULT_TIMEOUT_MS] is exceeded.
+ *
+ * @see TestPlayerRunHelper.playUntilStartOfMediaItem
+ */
+ @Throws(TimeoutException::class)
+ fun runUntilStartOfMediaItem(player: ExoPlayer, mediaItemIndex: Int) {
+ verifyMainTestThread(player)
+ verifyPlaybackThreadIsAlive(player)
+
+ val applicationLooper = Util.getCurrentOrMainLooper()
+ val messageHandled = AtomicBoolean(false)
+
+ player
+ .createMessage { _, _ ->
+ // Block playback thread until pause command has been sent from test thread.
+ val blockPlaybackThreadCondition = ConditionVariable()
+
+ player.clock
+ .createHandler(applicationLooper, null)
+ .post {
+ messageHandled.set(true)
+ blockPlaybackThreadCondition.open()
+ }
+
+ try {
+ player.clock.onThreadBlocked()
+ blockPlaybackThreadCondition.block()
+ } catch (e: InterruptedException) {
+ // Ignore.
+ }
+ }
+ .setPosition(mediaItemIndex, 0L)
+ .send()
+ player.play()
+ RobolectricUtil.runMainLooperUntil { messageHandled.get() || player.playerError != null }
+ if (player.playerError != null) {
+ throw IllegalStateException(player.playerError)
+ }
+ }
+}
diff --git a/pillarbox-player/build.gradle.kts b/pillarbox-player/build.gradle.kts
index 9c9b7ca26..539edc6da 100644
--- a/pillarbox-player/build.gradle.kts
+++ b/pillarbox-player/build.gradle.kts
@@ -90,6 +90,8 @@ dependencies {
testImplementation(libs.androidx.media3.test.utils)
testImplementation(libs.androidx.media3.test.utils.robolectric)
+ testImplementation(libs.androidx.test.core)
+ testImplementation(libs.androidx.test.ext.junit)
testImplementation(libs.junit)
testImplementation(libs.kotlin.test)
testImplementation(libs.kotlinx.coroutines.test)
diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt
index dc919c9ce..07cfcceff 100644
--- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt
+++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt
@@ -102,10 +102,10 @@ class PillarboxPlayer internal constructor(
constructor(
context: Context,
mediaItemSource: MediaItemSource,
- dataSourceFactory: DataSource.Factory,
- loadControl: LoadControl,
- mediaItemTrackerProvider: MediaItemTrackerProvider,
- seekIncrement: SeekIncrement,
+ dataSourceFactory: DataSource.Factory = DefaultHttpDataSource.Factory(),
+ loadControl: LoadControl = PillarboxLoadControl(),
+ mediaItemTrackerProvider: MediaItemTrackerProvider = MediaItemTrackerRepository(),
+ seekIncrement: SeekIncrement = SeekIncrement(),
clock: Clock,
) : this(
ExoPlayer.Builder(context)
diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/MediaItem.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/MediaItem.kt
index 50ffbd09c..6728d0247 100644
--- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/MediaItem.kt
+++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/MediaItem.kt
@@ -12,13 +12,8 @@ import ch.srgssr.pillarbox.player.tracker.MediaItemTrackerData
*
* @return null if localConfiguration.tag is null or tag is not a [MediaItemTrackerData].
*/
-@Suppress("SwallowedException")
fun MediaItem.getMediaItemTrackerDataOrNull(): MediaItemTrackerData? {
- return try {
- return localConfiguration?.tag as MediaItemTrackerData?
- } catch (e: ClassCastException) {
- null
- }
+ return localConfiguration?.tag as? MediaItemTrackerData
}
/**
diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTracker.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTracker.kt
index 838cf6364..bb8843b93 100644
--- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTracker.kt
+++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/tracker/MediaItemTracker.kt
@@ -47,7 +47,7 @@ interface MediaItemTracker {
/**
* Factory
*/
- interface Factory {
+ fun interface Factory {
/**
* Create a new instance of a [MediaItemTracker]
*
diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestPillarboxPlayerPlaybackSpeed.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestPillarboxPlayerPlaybackSpeed.kt
new file mode 100644
index 000000000..9015ca60f
--- /dev/null
+++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/TestPillarboxPlayerPlaybackSpeed.kt
@@ -0,0 +1,129 @@
+/*
+ * Copyright (c) SRG SSR. All rights reserved.
+ * License information is available from the LICENSE file.
+ */
+package ch.srgssr.pillarbox.player
+
+import android.content.Context
+import androidx.media3.common.C
+import androidx.media3.common.MediaItem
+import androidx.media3.common.Player
+import androidx.media3.common.Timeline.Window
+import androidx.media3.test.utils.FakeClock
+import androidx.media3.test.utils.robolectric.TestPlayerRunHelper
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import ch.srgssr.pillarbox.player.data.MediaItemSource
+import ch.srgssr.pillarbox.player.extension.getPlaybackSpeed
+import ch.srgssr.pillarbox.player.test.utils.TestPillarboxRunHelper
+import org.junit.After
+import org.junit.Assert
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class TestPillarboxPlayerPlaybackSpeed {
+ private lateinit var player: PillarboxPlayer
+
+ @Before
+ fun createPlayer() {
+ val context = ApplicationProvider.getApplicationContext