diff --git a/build-logic/plugins/src/main/java/ch/srgssr/pillarbox/gradle/internal/ProjectExtensions.kt b/build-logic/plugins/src/main/java/ch/srgssr/pillarbox/gradle/internal/ProjectExtensions.kt index 12aed5955..4008e942a 100644 --- a/build-logic/plugins/src/main/java/ch/srgssr/pillarbox/gradle/internal/ProjectExtensions.kt +++ b/build-logic/plugins/src/main/java/ch/srgssr/pillarbox/gradle/internal/ProjectExtensions.kt @@ -6,16 +6,9 @@ package ch.srgssr.pillarbox.gradle.internal import com.android.build.api.dsl.CommonExtension import org.gradle.api.Project -import org.gradle.api.artifacts.VersionCatalog -import org.gradle.api.artifacts.VersionCatalogsExtension -import org.gradle.kotlin.dsl.dependencies -import org.gradle.kotlin.dsl.getByType import org.gradle.kotlin.dsl.withType import org.jetbrains.kotlin.gradle.tasks.KotlinCompile -internal val Project.libs: VersionCatalog - get() = extensions.getByType().named("libs") - internal fun Project.configureAndroidModule(extension: CommonExtension<*, *, *, *, *, *>) = with(extension) { namespace = "ch.srgssr.pillarbox." + name.removePrefix("pillarbox-").replace('-', '.') compileSdk = AppConfig.compileSdk @@ -27,18 +20,12 @@ internal fun Project.configureAndroidModule(extension: CommonExtension<*, *, *, compileOptions { sourceCompatibility = AppConfig.javaVersion targetCompatibility = AppConfig.javaVersion - isCoreLibraryDesugaringEnabled = true } buildFeatures { resValues = false shaders = false } - - dependencies { - // coreLibraryDesugaring(libs.findLibrary("android-desugar-jdk-libs").get()) - add("coreLibraryDesugaring", libs.findLibrary("android-desugar-jdk-libs").get()) - } } internal fun Project.configureKotlinModule() { diff --git a/docs/README.md b/docs/README.md index 1c144630d..7f2fea425 100644 --- a/docs/README.md +++ b/docs/README.md @@ -120,25 +120,6 @@ kotlinOptions { } ``` -### Support Android API < 24 - -A change in AndroidX Media3 1.3.0 requires applications to use library desugaring, as described in the corresponding [Android documentation](https://developer.android.com/studio/write/java8-support#library-desugaring): - -```kotlin -compileOptions { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - isCoreLibraryDesugaringEnabled = true -} - -dependencies { - coreLibraryDesugaring("com.android.tools:desugar_jdk_libs:2.0.4") -} -``` - -> [!IMPORTANT] -> This should be done even if your min SDK version is 24+. - ### Integrate Pillarbox To start using Pillarbox in your project, you can check each module's documentation: diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 3a2b71783..8f16c47c1 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,6 +1,5 @@ [versions] accompanist = "0.34.0" -android-desugar-jdk-libs = "2.0.4" android-gradle-plugin = "8.5.1" androidx-activity = "1.9.0" androidx-annotation = "1.8.0" @@ -9,8 +8,7 @@ androidx-core = "1.13.1" androidx-fragment = "1.8.1" androidx-leanback = "1.0.0" androidx-lifecycle = "2.8.3" -androidx-media = "1.7.0" -androidx-media3 = "1.3.1" +androidx-media3 = "1.4.0" androidx-navigation = "2.7.7" androidx-paging = "3.3.0" androidx-test-core = "1.6.1" @@ -24,7 +22,7 @@ comscore = "6.11.1" dependency-analysis-gradle-plugin = "1.32.0" detekt = "1.23.6" dokka = "1.9.20" -guava = "32.1.3-android" +guava = "33.0.0-android" json = "20240303" junit = "4.13.2" kotlin = "2.0.0" @@ -42,7 +40,6 @@ turbine = "1.1.0" [libraries] accompanist-navigation-material = { module = "com.google.accompanist:accompanist-navigation-material", version.ref = "accompanist" } -android-desugar-jdk-libs = { module = "com.android.tools:desugar_jdk_libs", version.ref = "android-desugar-jdk-libs" } android-gradle-api = { module = "com.android.tools.build:gradle-api", version.ref = "android-gradle-plugin" } androidx-activity = { module = "androidx.activity:activity", version.ref = "androidx-activity" } androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } @@ -108,7 +105,6 @@ androidx-media3-hls = { group = "androidx.media3", name = "media3-exoplayer-hls" androidx-media3-session = { group = "androidx.media3", name = "media3-session", version.ref = "androidx-media3" } androidx-media3-test-utils = { module = "androidx.media3:media3-test-utils", version.ref = "androidx-media3" } androidx-media3-test-utils-robolectric = { module = "androidx.media3:media3-test-utils-robolectric", version.ref = "androidx-media3" } -androidx-media = { group = "androidx.media", name = "media", version.ref = "androidx-media" } okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } okhttp-logging-interceptor = { group = "com.squareup.okhttp3", name = "logging-interceptor", version.ref = "okhttp" } tagcommander-core = { group = "com.tagcommander.lib", name = "core", version.ref = "tag-commander-core" } diff --git a/pillarbox-demo/build.gradle.kts b/pillarbox-demo/build.gradle.kts index fcb95f587..81c20798e 100644 --- a/pillarbox-demo/build.gradle.kts +++ b/pillarbox-demo/build.gradle.kts @@ -73,7 +73,6 @@ dependencies { implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.androidx.lifecycle.viewmodel) implementation(libs.androidx.lifecycle.viewmodel.compose) - implementation(libs.androidx.media) implementation(libs.androidx.media3.common) implementation(libs.androidx.media3.exoplayer) implementation(libs.androidx.media3.session) diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/UpdatableMediaItemViewModel.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/UpdatableMediaItemViewModel.kt index 8fd4fbf3a..1ed9f1a23 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/UpdatableMediaItemViewModel.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/misc/UpdatableMediaItemViewModel.kt @@ -50,7 +50,7 @@ class UpdatableMediaItemViewModel(application: Application) : AndroidViewModel(a .setMediaDescriptionAdapter(PillarboxMediaDescriptionAdapter(context = application, pendingIntent = null)) .build() notificationManager.setPlayer(player) - notificationManager.setMediaSessionToken(mediaSession.sessionCompatToken) + notificationManager.setMediaSessionToken(mediaSession.platformToken) timer = timer(name = "update-item", period = 3.seconds.inWholeMilliseconds) { viewModelScope.launch(Dispatchers.Main) { diff --git a/pillarbox-player/build.gradle.kts b/pillarbox-player/build.gradle.kts index fb81daf13..60f552010 100644 --- a/pillarbox-player/build.gradle.kts +++ b/pillarbox-player/build.gradle.kts @@ -29,7 +29,6 @@ android { dependencies { implementation(libs.androidx.annotation) implementation(libs.androidx.core) - api(libs.androidx.media) api(libs.androidx.media3.common) implementation(libs.androidx.media3.dash) implementation(libs.androidx.media3.datasource) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxLoadControl.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxLoadControl.kt index c229c58a6..830f90cdc 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxLoadControl.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxLoadControl.kt @@ -9,6 +9,7 @@ import androidx.media3.common.Timeline import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.LoadControl import androidx.media3.exoplayer.Renderer +import androidx.media3.exoplayer.analytics.PlayerId import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.TrackGroupArray import androidx.media3.exoplayer.trackselection.ExoTrackSelection @@ -41,57 +42,47 @@ class PillarboxLoadControl( .setBackBuffer(BACK_BUFFER_DURATION_MS, true) .build() - override fun onPrepared() { - defaultLoadControl.onPrepared() + override fun onPrepared(playerId: PlayerId) { + defaultLoadControl.onPrepared(playerId) } - override fun onStopped() { - defaultLoadControl.onStopped() + override fun onStopped(playerId: PlayerId) { + defaultLoadControl.onStopped(playerId) } - override fun onReleased() { - defaultLoadControl.onReleased() + override fun onReleased(playerId: PlayerId) { + defaultLoadControl.onReleased(playerId) } override fun getAllocator(): Allocator { return allocator } - override fun getBackBufferDurationUs(): Long { - return defaultLoadControl.backBufferDurationUs + override fun getBackBufferDurationUs(playerId: PlayerId): Long { + return defaultLoadControl.getBackBufferDurationUs(playerId) } - override fun retainBackBufferFromKeyframe(): Boolean { - return defaultLoadControl.retainBackBufferFromKeyframe() + override fun retainBackBufferFromKeyframe(playerId: PlayerId): Boolean { + return defaultLoadControl.retainBackBufferFromKeyframe(playerId) } - override fun shouldContinueLoading( - playbackPositionUs: Long, - bufferedDurationUs: Long, - playbackSpeed: Float - ): Boolean { - return defaultLoadControl.shouldContinueLoading(playbackPositionUs, bufferedDurationUs, playbackSpeed) + override fun shouldContinueLoading(parameters: LoadControl.Parameters): Boolean { + return defaultLoadControl.shouldContinueLoading(parameters) } override fun onTracksSelected( + playerId: PlayerId, timeline: Timeline, mediaPeriodId: MediaSource.MediaPeriodId, renderers: Array, trackGroups: TrackGroupArray, trackSelections: Array ) { - defaultLoadControl.onTracksSelected(timeline, mediaPeriodId, renderers, trackGroups, trackSelections) + defaultLoadControl.onTracksSelected(playerId, timeline, mediaPeriodId, renderers, trackGroups, trackSelections) } - override fun shouldStartPlayback( - timeline: Timeline, - mediaPeriodId: MediaSource.MediaPeriodId, - bufferedDurationUs: Long, - playbackSpeed: Float, - rebuffering: Boolean, - targetLiveOffsetUs: Long - ): Boolean { - return defaultLoadControl.shouldStartPlayback(timeline, mediaPeriodId, bufferedDurationUs, playbackSpeed, rebuffering, targetLiveOffsetUs) + override fun shouldStartPlayback(parameters: LoadControl.Parameters): Boolean { + return defaultLoadControl.shouldStartPlayback(parameters) } /** diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/notification/PillarboxNotificationManager.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/notification/PillarboxNotificationManager.kt index 6e29a33ba..62b83f510 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/notification/PillarboxNotificationManager.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/notification/PillarboxNotificationManager.kt @@ -44,7 +44,7 @@ object PillarboxNotificationManager { override fun build(): PlayerNotificationManager { val notificationManager = super.build() mediaSession?.let { - notificationManager.setMediaSessionToken(it.sessionCompatToken) + notificationManager.setMediaSessionToken(it.platformToken) } return notificationManager } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PlaybackService.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PlaybackService.kt index 1dd11c9b1..cf08a9a17 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PlaybackService.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/service/PlaybackService.kt @@ -100,7 +100,7 @@ abstract class PlaybackService : Service() { } mediaSession?.let { it.player = player - notificationManager.setMediaSessionToken(it.sessionCompatToken) + notificationManager.setMediaSessionToken(it.platformToken) } } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt index 8d68dcdf7..480ebe6d5 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaController.kt @@ -38,6 +38,7 @@ import androidx.media3.session.MediaController import androidx.media3.session.MediaSessionService import androidx.media3.session.SessionCommand import androidx.media3.session.SessionCommands +import androidx.media3.session.SessionError import androidx.media3.session.SessionResult import androidx.media3.session.SessionToken import ch.srgssr.pillarbox.player.PillarboxPlayer @@ -113,7 +114,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { command: SessionCommand, args: Bundle ): ListenableFuture { - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED)) + return Futures.immediateFuture(SessionResult(SessionError.ERROR_NOT_SUPPORTED)) } /** diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibrarySession.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibrarySession.kt index b0fe4a1d8..2081f1fe9 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibrarySession.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaLibrarySession.kt @@ -7,11 +7,11 @@ package ch.srgssr.pillarbox.player.session import android.app.PendingIntent import androidx.annotation.IntRange import androidx.media3.common.MediaItem -import androidx.media3.common.MediaMetadata import androidx.media3.session.LibraryResult import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaLibraryService.MediaLibrarySession import androidx.media3.session.MediaSession +import androidx.media3.session.SessionError import ch.srgssr.pillarbox.player.PillarboxPlayer import ch.srgssr.pillarbox.player.session.PillarboxMediaLibrarySession.Builder import ch.srgssr.pillarbox.player.utils.PendingIntentUtils @@ -48,7 +48,7 @@ open class PillarboxMediaLibrarySession internal constructor() : PillarboxMediaS browser: MediaSession.ControllerInfo, params: MediaLibraryService.LibraryParams?, ): ListenableFuture> { - return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED)) + return Futures.immediateFuture(LibraryResult.ofError(SessionError.ERROR_NOT_SUPPORTED)) } /** @@ -63,7 +63,7 @@ open class PillarboxMediaLibrarySession internal constructor() : PillarboxMediaS @IntRange(from = 1) pageSize: Int, params: MediaLibraryService.LibraryParams?, ): ListenableFuture>> { - return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED)) + return Futures.immediateFuture(LibraryResult.ofError(SessionError.ERROR_NOT_SUPPORTED)) } /** @@ -75,7 +75,7 @@ open class PillarboxMediaLibrarySession internal constructor() : PillarboxMediaS browser: MediaSession.ControllerInfo, mediaId: String ): ListenableFuture> { - return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED)) + return Futures.immediateFuture(LibraryResult.ofError(SessionError.ERROR_NOT_SUPPORTED)) } /** @@ -88,7 +88,7 @@ open class PillarboxMediaLibrarySession internal constructor() : PillarboxMediaS query: String, params: MediaLibraryService.LibraryParams? ): ListenableFuture> { - return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED)) + return Futures.immediateFuture(LibraryResult.ofError(SessionError.ERROR_NOT_SUPPORTED)) } /** @@ -103,7 +103,7 @@ open class PillarboxMediaLibrarySession internal constructor() : PillarboxMediaS pageSize: Int, params: MediaLibraryService.LibraryParams? ): ListenableFuture>> { - return Futures.immediateFuture(LibraryResult.ofError(LibraryResult.RESULT_ERROR_NOT_SUPPORTED)) + return Futures.immediateFuture(LibraryResult.ofError(SessionError.ERROR_NOT_SUPPORTED)) } } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt index 4c60273a4..4bbd9e29e 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/session/PillarboxMediaSession.kt @@ -7,7 +7,6 @@ package ch.srgssr.pillarbox.player.session import android.app.PendingIntent import android.content.Context import android.os.Bundle -import android.support.v4.media.session.MediaSessionCompat import androidx.media3.common.MediaItem import androidx.media3.common.util.Util import androidx.media3.session.MediaLibraryService @@ -15,6 +14,7 @@ import androidx.media3.session.MediaSession import androidx.media3.session.MediaSession.MediaItemsWithStartPosition import androidx.media3.session.SessionCommand import androidx.media3.session.SessionCommands +import androidx.media3.session.SessionError import androidx.media3.session.SessionResult import ch.srgssr.pillarbox.player.PillarboxPlayer import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange @@ -145,11 +145,11 @@ open class PillarboxMediaSession internal constructor() { } /** - * @see MediaSession.getSessionCompatToken + * @see MediaSession.getPlatformToken */ - val token: MediaSessionCompat.Token + val token: android.media.session.MediaSession.Token get() { - return _mediaSession.sessionCompatToken + return _mediaSession.platformToken } /** @@ -288,7 +288,7 @@ open class PillarboxMediaSession internal constructor() { } } DebugLogger.warning(TAG, "Unsupported session command ${customCommand.customAction}") - return Futures.immediateFuture(SessionResult(SessionResult.RESULT_ERROR_NOT_SUPPORTED)) + return Futures.immediateFuture(SessionResult(SessionError.ERROR_NOT_SUPPORTED)) } override fun onSetMediaItems( diff --git a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/player/PlayerSurface.kt b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/player/PlayerSurface.kt index b3a658ab1..a9426f3e1 100644 --- a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/player/PlayerSurface.kt +++ b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/widget/player/PlayerSurface.kt @@ -5,7 +5,14 @@ package ch.srgssr.pillarbox.ui.widget.player import android.content.Context +import android.graphics.Canvas +import android.os.Build +import android.os.Handler +import android.os.Looper +import android.view.SurfaceControl import android.view.SurfaceView +import android.window.SurfaceSyncGroup +import androidx.annotation.RequiresApi import androidx.compose.foundation.Canvas import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.BoxScope @@ -179,6 +186,12 @@ private fun AndroidPlayerSurfaceView(player: Player, modifier: Modifier = Modifi * Player surface view */ private class PlayerSurfaceView(context: Context) : SurfaceView(context) { + private val playerListener = PlayerListener() + private val surfaceSyncGroupV34 = when { + isInEditMode -> null + needSurfaceSyncWorkaround() -> SurfaceSyncGroupCompatV34() + else -> null + } /** * Player if null is passed just clear surface @@ -187,8 +200,59 @@ private class PlayerSurfaceView(context: Context) : SurfaceView(context) { set(value) { if (field != value) { field?.clearVideoSurfaceView(this) + field?.removeListener(playerListener) value?.setVideoSurfaceView(this) + value?.addListener(playerListener) } field = value } + + override fun dispatchDraw(canvas: Canvas) { + super.dispatchDraw(canvas) + + if (needSurfaceSyncWorkaround()) { + surfaceSyncGroupV34?.maybeMarkSyncReadyAndClear() + } + } + + // Workaround for a surface sync issue on API 34: https://github.com/androidx/media/issues/1237 + // Imported from https://github.com/androidx/media/commit/30cb76269a67e09f6e1662ea9ead6aac70667028 + private fun needSurfaceSyncWorkaround(): Boolean { + return Build.VERSION.SDK_INT == Build.VERSION_CODES.UPSIDE_DOWN_CAKE + } + + private inner class PlayerListener : Player.Listener { + private val mainLooperHandler = Handler(Looper.getMainLooper()) + + override fun onSurfaceSizeChanged(width: Int, height: Int) { + if (needSurfaceSyncWorkaround()) { + surfaceSyncGroupV34?.postRegister(mainLooperHandler, this@PlayerSurfaceView) + } + } + } + + @RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + private class SurfaceSyncGroupCompatV34 { + private var surfaceSyncGroup: SurfaceSyncGroup? = null + + fun postRegister( + mainLooperHandler: Handler, + surfaceView: SurfaceView, + ) { + mainLooperHandler.post { + // The SurfaceView isn't attached to a window, so don't apply the workaround. + val rootSurfaceControl = surfaceView.getRootSurfaceControl() ?: return@post + + surfaceSyncGroup = SurfaceSyncGroup("exo-sync-b-334901521") + surfaceSyncGroup?.add(rootSurfaceControl) {} + surfaceView.invalidate() + rootSurfaceControl.applyTransactionOnDraw(SurfaceControl.Transaction()) + } + } + + fun maybeMarkSyncReadyAndClear() { + surfaceSyncGroup?.markSyncReady() + surfaceSyncGroup = null + } + } }