From 7ede9733247722cdb710eab4f0ad06a69a4d1bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Wed, 20 Nov 2024 11:51:37 +0100 Subject: [PATCH 01/11] Update `pillarbox-player` module documentation (#792) --- ...PillarboxAndroidLibraryPublishingPlugin.kt | 6 +- pillarbox-analytics/docs/README.md | 2 +- pillarbox-cast/docs/README.md | 2 +- pillarbox-core-business/docs/README.md | 2 +- pillarbox-player/Module.md | 10 - pillarbox-player/docs/README.md | 220 ++++++++++-------- pillarbox-ui/docs/README.md | 2 +- 7 files changed, 132 insertions(+), 112 deletions(-) delete mode 100644 pillarbox-player/Module.md diff --git a/build-logic/plugins/src/main/java/ch/srgssr/pillarbox/gradle/PillarboxAndroidLibraryPublishingPlugin.kt b/build-logic/plugins/src/main/java/ch/srgssr/pillarbox/gradle/PillarboxAndroidLibraryPublishingPlugin.kt index 517a81da1..04f40cc8d 100644 --- a/build-logic/plugins/src/main/java/ch/srgssr/pillarbox/gradle/PillarboxAndroidLibraryPublishingPlugin.kt +++ b/build-logic/plugins/src/main/java/ch/srgssr/pillarbox/gradle/PillarboxAndroidLibraryPublishingPlugin.kt @@ -89,11 +89,7 @@ class PillarboxAndroidLibraryPublishingPlugin : Plugin { extensions.configure { dokkaSourceSets.getByName("main") { - if (file("Module.md").exists()) { - includes.from("Module.md") - } else { - includes.from("docs/README.md") - } + includes.from("docs/README.md") externalDocumentationLinks.register("kotlinx.coroutines") { url.set(URI("https://kotlinlang.org/api/kotlinx.coroutines")) diff --git a/pillarbox-analytics/docs/README.md b/pillarbox-analytics/docs/README.md index 8b8fe1dff..e8045e2d4 100644 --- a/pillarbox-analytics/docs/README.md +++ b/pillarbox-analytics/docs/README.md @@ -7,7 +7,7 @@ and custom events. ## Integration -To use this module, add the following dependency to your project's `build.gradle`/`build.gradle.kts` file: +To use this module, add the following dependency to your module's `build.gradle`/`build.gradle.kts` file: ```kotlin implementation("ch.srgssr.pillarbox:pillarbox-analytics:") diff --git a/pillarbox-cast/docs/README.md b/pillarbox-cast/docs/README.md index 4fe5c4818..933ff5b2d 100644 --- a/pillarbox-cast/docs/README.md +++ b/pillarbox-cast/docs/README.md @@ -4,7 +4,7 @@ Provides helpers to integrate Cast with Pillarbox. ## Integration -To use this module, add the following dependency to your project's `build.gradle`/`build.gradle.kts` file: +To use this module, add the following dependency to your module's `build.gradle`/`build.gradle.kts` file: ```kotlin implementation("ch.srgssr.pillarbox:pillarbox-cast:") diff --git a/pillarbox-core-business/docs/README.md b/pillarbox-core-business/docs/README.md index d3a3df78a..4ee240ea1 100644 --- a/pillarbox-core-business/docs/README.md +++ b/pillarbox-core-business/docs/README.md @@ -14,7 +14,7 @@ The supported contents are: ## Integration -To use this module, add the following dependency to your project's `build.gradle`/`build.gradle.kts` file: +To use this module, add the following dependency to your module's `build.gradle`/`build.gradle.kts` file: ```kotlin implementation("ch.srgssr.pillarbox:pillarbox-core-business:") diff --git a/pillarbox-player/Module.md b/pillarbox-player/Module.md deleted file mode 100644 index c3f232650..000000000 --- a/pillarbox-player/Module.md +++ /dev/null @@ -1,10 +0,0 @@ -# Module pillarbox-player - -Provides [`PillarboxPlayer`](ch.srgssr.pillarbox.player.PillarboxPlayer), the -[`Exoplayer`](https://developer.android.com/reference/androidx/media3/exoplayer/ExoPlayer) implementation of media playback on Android. - -To use this module, add the following dependency to your project's `build.gradle`/`build.gradle.kts` file: - -```kotlin -implementation("ch.srgssr.pillarbox:pillarbox-player:") -``` diff --git a/pillarbox-player/docs/README.md b/pillarbox-player/docs/README.md index 295438ad6..6c19fd3c2 100644 --- a/pillarbox-player/docs/README.md +++ b/pillarbox-player/docs/README.md @@ -1,135 +1,127 @@ -[![Pillarbox logo](https://github.com/SRGSSR/pillarbox-apple/blob/main/docs/README-images/logo.jpg)](https://github.com/SRGSSR/pillarbox-android) -[![Last release](https://img.shields.io/github/v/release/SRGSSR/pillarbox-android?label=Release)](https://github.com/SRGSSR/pillarbox-android/releases) -[![Android min SDK](https://img.shields.io/badge/Android-21%2B-34A853)](https://github.com/SRGSSR/pillarbox-android) -[![License](https://img.shields.io/github/license/SRGSSR/pillarbox-android?label=License)](https://github.com/SRGSSR/pillarbox-android/blob/main/LICENSE) +# Module pillarbox-player -# Pillarbox Player module - -Provides [`PillarboxPlayer`][pillarbox-player-source], an AndroidX Media3 [`Player`][player-documentation] implementation for media playback on -Android. +Provides [PillarboxPlayer][ch.srgssr.pillarbox.player.PillarboxPlayer], an AndroidX Media3 +[Player](https://developer.android.com/reference/androidx/media3/common/Player) implementation for media playback on Android. ## Integration -```gradle +To use this module, add the following dependency to your module's `build.gradle`/`build.gradle.kts` file: + +```kotlin implementation("ch.srgssr.pillarbox:pillarbox-player:") ``` -More information can be found in the [top level README](https://github.com/SRGSSR/pillarbox-android#readme). - -## Documentation - -- [Getting started](#getting-started) -- [MediaSession](./MediaSession.md) -- [Tracking](./MediaItemTracking.md) - -## Known issues - -- Playing DRM content on two instances of [`PillarboxPlayer`][pillarbox-player-source] is not supported on all devices. - - Known affected devices: Samsung Galaxy A13. - ## Getting started ### Create the player ```kotlin -val player = PillarboxExoPlayer(context) +val player = PillarboxExoPlayer(context, Default) // Make the player ready to play content player.prepare() // Will start playback when a MediaItem is ready to play player.play() ``` -#### Monitoring playback +#### Playback monitoring -By default, [`PillarboxExoPlayer`][pillarbox-exo-player-source] does not record any monitoring data. You can configure this when creating the player: +By default, [PillarboxExoPlayer][ch.srgssr.pillarbox.player.PillarboxExoPlayer] does not record any monitoring data. You can configure this behaviour +when creating the player: ```kotlin -val player = PillarboxExoPlayer(context) { +val player = PillarboxExoPlayer(context, Default) { + // Disable monitoring recording (default behavior) + disableMonitoring() + + // Output each monitoring event to Logcat monitoring(Logcat) + + // Send each monitoring event to a remote server + monitoring(Remote) { + config(endpointUrl = "https://example.com/monitoring") + } } ``` -Multiple implementations are provided out of the box, but you can also provide your own -[`MonitoringMessageHandler`][monitoring-message-handler-source]: - -- `NoOp()` (default): does nothing. -- `Logcat { config(...) }`: prints each message to Logcat. -- `Remote { config(...) }`: sends each message to a remote server. - ### Create a `MediaItem` -More information about [`MediaItem`][media-item-documentation] creation can be found [here][media-item-creation-documentation]. - ```kotlin -val mediaUri = "https://sample.com/sample.mp4" +val mediaUri = "https://example.com/media.mp4" val mediaItem = MediaItem.fromUri(mediaUri) player.setMediaItem(mediaItem) ``` -### Attaching to UI +More information about [MediaItem][androidx.media3.common.MediaItem] creation can be found in the `MediaItem` +[documentation][media-items-documentation]. -[`PillarboxPlayer`][pillarbox-player-source] can be used with views provided by [Exoplayer][exo-player-documentation] without any modifications. +### Display a `Player` -#### ExoPlayer UI module +[PillarboxPlayer][ch.srgssr.pillarbox.player.PillarboxPlayer] can be used with the [View][android.view.View]s provided by AndroidX Media3 without any +modifications. -Add the following to your module's `build.gradle`/`build.gradle.kts` file: +To quickly get started, add the following to your module's `build.gradle`/`build.gradle.kts` file: -```gradle +```kotlin implementation("androidx.media3:media3-ui:") ``` -#### Set the player to the view - -After adding the [`PlayerView`][player-view-documentation] to your layout, you can then do the following: +Then link your player to a [PlayerView][androidx.media3.ui.PlayerView]: ```kotlin @Override fun onCreate(savedInstanceState: Bundle) { super.onCreate(savedInstanceState) + val player = PillarboxExoPlayer(context, Default) val playerView: PlayerView = findViewById(R.id.player_view) + // A player can only be attached to one View! playerView.player = player } ``` -> [!WARNING] -> A player can be attached to only one [`View`][view-documentation]! +For more detailed information, you can check [AndroidX Media3 UI][media3-ui-documentation]. -### Release the player +**Tip:** for integration with Compose, you can use [pillarbox-ui][pillarbox-ui]. -When you don't need the player anymore, you have to release it. It frees resources used by the player. +### Release a `Player` + +When the player is not needed anymore, you have to release it. This will free resources allocated by the player. ```kotlin player.release() ``` -> [!WARNING] -> The player can't be used anymore after that. +**Warning:** the player can't be used anymore after that. ## Custom `AssetLoader` -`AssetLoader` is used to load content that doesn't directly have a playable URL, for example, a resource id or a URI. -Its responsibility is to provide a `MediaSource` that is playable by the player, [tracking data](./MediaItemTracking.md) and optionally media -metadata. +[AssetLoader][ch.srgssr.pillarbox.player.asset.AssetLoader] is used to load content that doesn't directly have a playable URL, for example, a resource +id or a URI. Its responsibility is to provide a [MediaSource][androidx.media3.exoplayer.source.MediaSource] that: + +- Is playable by the player; +- Contains [tracking data][pillarbox-tracking-data]; +- Provides optional media metadata. ```kotlin -class DemoAssetLoader(context: Context) : AssetLoader(DefaultMediaSourceFactory(context)) { +class CustomAssetLoader(context: Context) : AssetLoader(DefaultMediaSourceFactory(context)) { override fun canLoadAsset(mediaItem: MediaItem): Boolean { - return mediaItem.localConfigruation?.uri.toString().startsWith("demo://") + return mediaItem.localConfigruation?.uri?.scheme == "custom" } override suspend fun loadAsset(mediaItem: MediaItem): Asset { - val data = someService.fetchData(mediaItem.localConfigruation!!.uri) + val data = service.fetchData(mediaItem.localConfigruation!!.uri) val trackerData = MutableMediaItemTrackerData() - trackerData[key] = FactoryData(DemoMediaItemTracker.Factory(), DemoTrackerData("Data1")) + trackerData[KEY] = FactoryData(CustomMediaItemTracker.Factory(), CustomTrackerData("CustomData")) + val mediaMetadata = MediaMetadata.Builder() .setTitle(data.title) .setArtworkUri(data.imageUri) .setChapters(data.chapters) .setCredits(data.credits) .build() - val mediaSource: MediaSource = mediaSourceFactory.createMediaSource(MediaItem.fromUri(data.url)) + val mediaSource = mediaSourceFactory.createMediaSource(MediaItem.fromUri(data.url)) + return Asset( mediaSource = mediaSource, trackersData = trackerData.toMediaItemTrackerData(), @@ -140,14 +132,14 @@ class DemoAssetLoader(context: Context) : AssetLoader(DefaultMediaSourceFactory( } ``` -To play custom content defined above, the custom `AssetLoader` has to be added to [`PillarboxPlayer`][pillarbox-player-source] with the following code: +Now pass your `CustomAssetLoader` to your player, so it can understand and play your custom data: ```kotlin -val player = PillarboxExoPlayer(context) { - +DemoAssetLoader() +val player = PillarboxExoPlayer(context, Default) { + +CustomAssetLoader(context) } player.prepare() -player.setMediaItem(MediaItem.fromUri("demo://video:1234")) +player.setMediaItem(MediaItem.fromUri("custom://video:1234")) player.play() ``` @@ -155,23 +147,37 @@ player.play() Chapters represent the temporal segmentation of the playing media. -A Chapter can be created like that: +A [Chapter][ch.srgssr.pillarbox.player.asset.timeRange.Chapter] can be created like that: ```kotlin -val chapter = Chapter(id = "1", start = 0L, end = 12_000L, mediaMetadata = MediaMetadata.Builder().setTitle("Chapter 1").build()) +val chapter = Chapter( + id = "1", + start = 0L, + end = 12_000L, + mediaMetadata = MediaMetadata.Builder().setTitle("Chapter 1").build(), +) ``` -[`PillarboxPlayer`][pillarbox-player-source] will automatically keep tracks of Chapters change during playback through [`PillarboxPlayer.Listener.onChapterChanged`][pillarbox-player-listener-source]. +[PillarboxPlayer][ch.srgssr.pillarbox.player.PillarboxPlayer] provides methods to observe and access chapters: ```kotlin -val chapterList: List = player.getCurrentChapters() - -val currentChapter: Chapter? = player.getChapterAtPosition() +val player = PillarboxExoPlayer(context, Default) +player.addListener(object : Listener { + override fun onChapterChanged(chapter: Chapter?) { + if (chapter == null) { + // Hide chapter information + } else { + // Display chapter information + } + } +}) -val chapterAt: Chapter? = player.getChapterAtPosition(10_000L) +val chapters = player.getCurrentChapters() +val currentChapter = player.getChapterAtPosition() +val chapterAtPosition = player.getChapterAtPosition(10_000L) ``` -Chapters can be added at anytime to the player inside `MediaItem.mediaMetadata`: +Chapters can be added to a [MediaItem][androidx.media3.common.MediaItem] via its metadata: ```kotlin val mediaMetadata = MediaMetadata.Builder() @@ -184,39 +190,55 @@ val mediaItem = MediaItem.Builder() ### Credits -Credits represent point in the player timeline where opening credits and closing credits should be displayed. -It can be used to display a "skip button" to allow users to not show credits. +Credits represent a point in the player timeline where opening credits or closing credits should be displayed. + +A [Credit][ch.srgssr.pillarbox.player.asset.timeRange.Credit] can be created like that: ```kotlin -val opening: Credit = Credit.Opening(start = 5_000L, end = 10_000L) -val closing: Credit = Credit.Closing(start = 20_000L, end = 30_000L) +val openingCredits = Credit.Opening(start = 5_000L, end = 10_000L) +val closingCredits = Credit.Closing(start = 20_000L, end = 30_000L) ``` -[`PillarboxPlayer`][pillarbox-player-source] will automatically keep tracks of Credits change during playback through [`PillarboxPlayer.Listener.onCreditChanged`][pillarbox-player-listener-source]. +[PillarboxPlayer][ch.srgssr.pillarbox.player.PillarboxPlayer] provides methods to observe and access credits: ```kotlin -val creditList: List = player.getCurrentCredits() - -val currentCredit : Credit? = player.getCreditAtPosition() +val player = PillarboxExoPlayer(context, Default) +player.addListener(object : Listener { + override fun onCreditChanged(credit: Credit?) { + when (credit) { + is Credit.Opening -> Unit // Show "Skip intro" button + is Credit.Closing -> Unit // Show "Skip credits" button + else -> Unot // Hide button + } + } +}) -val creditAt : Credit? = player.getCreditAtPosition(5_000L) +val credits = player.getCurrentCredits() +val currentCredit = player.getCreditAtPosition() +val creditAtPosition = player.getCreditAtPosition(5_000L) ``` -Credits can be added at anytime to the player inside `MediaItem.mediaMetadata`: +Chapters can be added to a [MediaItem][androidx.media3.common.MediaItem] via its metadata: ```kotlin val mediaMetadata = MediaMetadata.Builder() - .setCredits(listOf(opening, closing)) + .setCredits(listOf(openingCredits, closingCredits)) .build() val mediaItem = MediaItem.Builder() .setMediaMetadata(mediaMetadata) .build() ``` -## ExoPlayer +## Known issues -As [`PillarboxExoPlayer`][pillarbox-exo-player-source] extends from [ExoPlayer][exo-player-documentation], all documentation related to ExoPlayer is -also valid for Pillarbox. Here are some useful links to get more information about ExoPlayer: +- Playing DRM content on two instances of [PillarboxPlayer][ch.srgssr.pillarbox.player.PillarboxPlayer] is not supported on all devices. + - Known affected devices: Samsung Galaxy A13, Huawei Nova 5i Pro, Huawei P40 Lite. + - Related issue: [androidx/media#1877](https://github.com/androidx/media/issues/1877). + +## Further reading + +As [PillarboxExoPlayer][ch.srgssr.pillarbox.player.PillarboxExoPlayer] extends from [ExoPlayer][androidx.media3.exoplayer.ExoPlayer], all +documentation related to ExoPlayer is also valid for Pillarbox. Here are some useful links to get more information about ExoPlayer: - [Getting started with ExoPlayer](https://developer.android.com/media/media3/exoplayer/hello-world.html) - [Player events](https://developer.android.com/media/media3/exoplayer/listening-to-player-events) @@ -224,13 +246,25 @@ also valid for Pillarbox. Here are some useful links to get more information abo - [Playlists](https://developer.android.com/media/media3/exoplayer/playlists) - [Track selection](https://developer.android.com/media/media3/exoplayer/track-selection) +You can check the following pages for a deeper understanding of Pillarbox concepts: + +- [Media item tracking][pillarbox-tracking-data] +- [Media session][pillarbox-media-session] + +[android.view.View]: https://developer.android.com/reference/android/view/View +[androidx.media3.common.MediaItem]: https://developer.android.com/reference/androidx/media3/common/MediaItem +[androidx.media3.exoplayer.ExoPlayer]: https://developer.android.com/reference/androidx/media3/exoplayer/ExoPlayer +[androidx.media3.exoplayer.source.MediaSource]: https://developer.android.com/reference/androidx/media3/exoplayer/source/MediaSource +[androidx.media3.ui.PlayerView]: https://developer.android.com/reference/androidx/media3/ui/PlayerView +[ch.srgssr.pillarbox.player.PillarboxExoPlayer]: https://android.pillarbox.ch/api/pillarbox-player/ch.srgssr.pillarbox.player/-pillarbox-exo-player.html +[ch.srgssr.pillarbox.player.PillarboxPlayer]: https://android.pillarbox.ch/api/pillarbox-player/ch.srgssr.pillarbox.player/-pillarbox-player/index.html +[ch.srgssr.pillarbox.player.asset.AssetLoader]: https://android.pillarbox.ch/api/pillarbox-player/ch.srgssr.pillarbox.player.asset/-asset-loader/index.html +[ch.srgssr.pillarbox.player.asset.timeRange.Chapter]: https://android.pillarbox.ch/api/pillarbox-player/ch.srgssr.pillarbox.player.asset.timeRange/-chapter/index.html +[ch.srgssr.pillarbox.player.asset.timeRange.Credit]: https://android.pillarbox.ch/api/pillarbox-player/ch.srgssr.pillarbox.player.asset.timeRange/-credit/index.html +[media-items-documentation]: https://developer.android.com/media/media3/exoplayer/media-items +[media3-ui-documentation]: https://developer.android.com/media/media3/ui/playerview +[pillarbox-media-session]: https://github.com/SRGSSR/pillarbox-android/blob/main/pillarbox-player/docs/MediaSession.md +[pillarbox-ui]: https://android.pillarbox.ch/api/pillarbox-ui/index.html +[pillarbox-tracking-data]: https://github.com/SRGSSR/pillarbox-android/blob/main/pillarbox-player/docs/MediaItemTracking.md + [exo-player-documentation]: https://developer.android.com/media/media3/exoplayer -[media-item-creation-documentation]: https://developer.android.com/media/media3/exoplayer/media-items -[media-item-documentation]: https://developer.android.com/reference/androidx/media3/common/MediaItem -[monitoring-message-handler-source]: https://github.com/SRGSSR/pillarbox-android/blob/main/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/monitoring/MonitoringMessageHandler.kt -[pillarbox-exo-player-source]: https://github.com/SRGSSR/pillarbox-android/blob/main/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxExoPlayer.kt -[pillarbox-player-source]: https://github.com/SRGSSR/pillarbox-android/tree/main/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt -[player-documentation]: https://developer.android.com/reference/androidx/media3/common/Player -[player-view-documentation]: https://developer.android.com/reference/androidx/media3/ui/PlayerView -[view-documentation]: https://developer.android.com/reference/android/view/View.html -[pillarbox-player-listener-source]:https://github.com/SRGSSR/pillarbox-android/blob/571-update-pillarbox-documentation/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPlayer.kt diff --git a/pillarbox-ui/docs/README.md b/pillarbox-ui/docs/README.md index 68117d7e9..a9f7be91a 100644 --- a/pillarbox-ui/docs/README.md +++ b/pillarbox-ui/docs/README.md @@ -10,7 +10,7 @@ This includes: ## Integration -To use this module, add the following dependency to your project's `build.gradle`/`build.gradle.kts` file: +To use this module, add the following dependency to your module's `build.gradle`/`build.gradle.kts` file: ```kotlin implementation("ch.srgssr.pillarbox:pillarbox-ui:") From 125e7430cc0e4f6a5354d52c4b56c72f02669baa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Wed, 20 Nov 2024 11:52:38 +0100 Subject: [PATCH 02/11] Move config files to the `config` folder (#795) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joaquim Stähli --- .github/gradle-ci.properties | 5 ----- .github/workflows/build.yml | 12 ------------ .github/workflows/build_windows.yml | 2 -- .github/workflows/deploy_dokka_documentation.yml | 2 -- .../PillarboxAndroidLibraryPublishingPlugin.kt | 4 ++-- .../pillarbox/gradle/internal/ProjectExtensions.kt | 3 ++- build.gradle.kts | 12 ++++++------ {dokka => config/dokka}/Pillarbox.md | 0 {dokka => config/dokka}/images/logo-icon.svg | 0 {dokka => config/dokka}/styles/pillarbox.css | 0 .../dokka}/templates/includes/page_metadata.ftl | 0 {git_hooks => config/git}/pre-commit | 0 lint.xml => config/lint/lint.xml | 0 13 files changed, 10 insertions(+), 30 deletions(-) delete mode 100644 .github/gradle-ci.properties rename {dokka => config/dokka}/Pillarbox.md (100%) rename {dokka => config/dokka}/images/logo-icon.svg (100%) rename {dokka => config/dokka}/styles/pillarbox.css (100%) rename {dokka => config/dokka}/templates/includes/page_metadata.ftl (100%) rename {git_hooks => config/git}/pre-commit (100%) rename lint.xml => config/lint/lint.xml (100%) diff --git a/.github/gradle-ci.properties b/.github/gradle-ci.properties deleted file mode 100644 index 3d87884da..000000000 --- a/.github/gradle-ci.properties +++ /dev/null @@ -1,5 +0,0 @@ -# -# Copyright (c) SRG SSR. All rights reserved. -# License information is available from the LICENSE file. -# -org.gradle.daemon=false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 0151e17ce..38167b9af 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -29,8 +29,6 @@ jobs: - uses: gradle/actions/setup-gradle@v4 with: cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} - - name: Copy CI gradle.properties - run: mkdir -p ~/.gradle; cp .github/gradle-ci.properties ~/.gradle/gradle.properties - name: Build modules run: ./gradlew :pillarbox-demo:assembleProdDebug :pillarbox-demo-cast:assembleDebug :pillarbox-demo-tv:assembleDebug :pillarbox-player-testutils:assembleDebug @@ -51,8 +49,6 @@ jobs: - uses: gradle/actions/setup-gradle@v4 with: cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} - - name: Copy CI gradle.properties - run: mkdir -p ~/.gradle; cp .github/gradle-ci.properties ~/.gradle/gradle.properties - name: Run Android Lint run: ./gradlew :pillarbox-demo:lintProdDebug :pillarbox-demo-cast:lintDebug :pillarbox-demo-tv:lintDebug :pillarbox-player-testutils:lintDebug - uses: github/codeql-action/upload-sarif@v3 @@ -78,8 +74,6 @@ jobs: - uses: gradle/actions/setup-gradle@v4 with: cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} - - name: Copy CI gradle.properties - run: mkdir -p ~/.gradle; cp .github/gradle-ci.properties ~/.gradle/gradle.properties - name: Run Detekt run: ./gradlew detekt - uses: github/codeql-action/upload-sarif@v3 @@ -105,8 +99,6 @@ jobs: - uses: gradle/actions/setup-gradle@v4 with: cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} - - name: Copy CI gradle.properties - run: mkdir -p ~/.gradle; cp .github/gradle-ci.properties ~/.gradle/gradle.properties - name: Run Dependency Analysis run: ./gradlew buildHealth @@ -129,8 +121,6 @@ jobs: - uses: gradle/actions/setup-gradle@v4 with: cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} - - name: Copy CI gradle.properties - run: mkdir -p ~/.gradle; cp .github/gradle-ci.properties ~/.gradle/gradle.properties - name: Run Unit Tests run: > ./gradlew @@ -179,8 +169,6 @@ jobs: - uses: gradle/actions/setup-gradle@v4 with: cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} - - name: Copy CI gradle.properties - run: mkdir -p ~/.gradle; cp .github/gradle-ci.properties ~/.gradle/gradle.properties - name: Run Android Tests uses: reactivecircus/android-emulator-runner@v2 with: diff --git a/.github/workflows/build_windows.yml b/.github/workflows/build_windows.yml index c0ca27571..a26017251 100644 --- a/.github/workflows/build_windows.yml +++ b/.github/workflows/build_windows.yml @@ -24,7 +24,5 @@ jobs: with: java-version: '17' distribution: 'temurin' - - name: Copy CI gradle.properties - run: mkdir -p ~/.gradle; cp .github/gradle-ci.properties ~/.gradle/gradle.properties - name: Build project run: ./gradlew :pillarbox-demo:assembleProdDebug :pillarbox-demo-tv:assembleDebug diff --git a/.github/workflows/deploy_dokka_documentation.yml b/.github/workflows/deploy_dokka_documentation.yml index b5ba09cc8..042c5085c 100644 --- a/.github/workflows/deploy_dokka_documentation.yml +++ b/.github/workflows/deploy_dokka_documentation.yml @@ -29,8 +29,6 @@ jobs: - uses: gradle/actions/setup-gradle@v4 with: cache-encryption-key: ${{ secrets.GRADLE_CACHE_ENCRYPTION_KEY }} - - name: Copy CI gradle.properties - run: mkdir -p ~/.gradle; cp .github/gradle-ci.properties ~/.gradle/gradle.properties - name: Build Dokka documentation run: ./gradlew :dokkaGenerate - name: Deploy Dokka documentation diff --git a/build-logic/plugins/src/main/java/ch/srgssr/pillarbox/gradle/PillarboxAndroidLibraryPublishingPlugin.kt b/build-logic/plugins/src/main/java/ch/srgssr/pillarbox/gradle/PillarboxAndroidLibraryPublishingPlugin.kt index 04f40cc8d..e2f99e056 100644 --- a/build-logic/plugins/src/main/java/ch/srgssr/pillarbox/gradle/PillarboxAndroidLibraryPublishingPlugin.kt +++ b/build-logic/plugins/src/main/java/ch/srgssr/pillarbox/gradle/PillarboxAndroidLibraryPublishingPlugin.kt @@ -118,11 +118,11 @@ class PillarboxAndroidLibraryPublishingPlugin : Plugin { // Follow https://github.com/Kotlin/dokka/issues/3883 to see if it's necessary to duplicate this config pluginsConfiguration.getByName("html") { - customStyleSheets.from(rootProject.projectDir.resolve("dokka/styles/pillarbox.css")) + customStyleSheets.from(rootProject.projectDir.resolve("config/dokka/styles/pillarbox.css")) footerMessage.set("© SRG SSR") // TODO Enable this once we have some content there // homepageLink.set("https://android.pillarbox.ch/") - templatesDir.set(rootProject.projectDir.resolve("dokka/templates")) + templatesDir.set(rootProject.projectDir.resolve("config/dokka/templates")) } } } 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 ea57ad27d..8d8a32b15 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 @@ -47,8 +47,9 @@ internal fun Project.configureAndroidLintModule(extension: CommonExtension<*, *, abortOnError = true checkAllWarnings = true checkDependencies = true + lintConfig = rootProject.projectDir.resolve("config/lint/lint.xml") sarifReport = true - sarifOutput = file("${rootProject.rootDir}/build/reports/android-lint/$name.sarif") + sarifOutput = rootProject.projectDir.resolve("build/reports/android-lint/$name.sarif") disable.add("LogConditional") } } diff --git a/build.gradle.kts b/build.gradle.kts index a0d95b63f..3755e1653 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -22,18 +22,18 @@ dokka { moduleVersion = providers.environmentVariable("VERSION_NAME").orElse("dev") dokkaPublications.html { - includes.from("dokka/Pillarbox.md") + includes.from("config/dokka/Pillarbox.md") } pluginsConfiguration.html { // See the overridable images here: // https://github.com/Kotlin/dokka/tree/master/dokka-subprojects/plugin-base/src/main/resources/dokka/images - customAssets.from("dokka/images/logo-icon.svg") // TODO Use Pillarbox logo - customStyleSheets.from("dokka/styles/pillarbox.css") + customAssets.from("config/dokka/images/logo-icon.svg") // TODO Use Pillarbox logo + customStyleSheets.from("config/dokka/styles/pillarbox.css") footerMessage.set("© SRG SSR") // TODO Enable this once we have some content there // homepageLink.set("https://android.pillarbox.ch/") - templatesDir.set(file("dokka/templates")) + templatesDir.set(file("config/dokka/templates")) } } @@ -61,8 +61,8 @@ val clean by tasks.getting(Delete::class) { */ val installGitHook by tasks.registering(Copy::class) { description = "Install the Git pre-commit hook locally" - from(file("${rootProject.rootDir}/git_hooks/pre-commit")) - into { file("${rootProject.rootDir}/.git/hooks") } + from(rootProject.projectDir.resolve("config/git/pre-commit")) + into { rootProject.projectDir.resolve(".git/hooks") } filePermissions { unix("rwxr-xr-x") } diff --git a/dokka/Pillarbox.md b/config/dokka/Pillarbox.md similarity index 100% rename from dokka/Pillarbox.md rename to config/dokka/Pillarbox.md diff --git a/dokka/images/logo-icon.svg b/config/dokka/images/logo-icon.svg similarity index 100% rename from dokka/images/logo-icon.svg rename to config/dokka/images/logo-icon.svg diff --git a/dokka/styles/pillarbox.css b/config/dokka/styles/pillarbox.css similarity index 100% rename from dokka/styles/pillarbox.css rename to config/dokka/styles/pillarbox.css diff --git a/dokka/templates/includes/page_metadata.ftl b/config/dokka/templates/includes/page_metadata.ftl similarity index 100% rename from dokka/templates/includes/page_metadata.ftl rename to config/dokka/templates/includes/page_metadata.ftl diff --git a/git_hooks/pre-commit b/config/git/pre-commit similarity index 100% rename from git_hooks/pre-commit rename to config/git/pre-commit diff --git a/lint.xml b/config/lint/lint.xml similarity index 100% rename from lint.xml rename to config/lint/lint.xml From ce72a7ace1a7c24a1fca2e3ba478fad7ce83579e Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 20 Nov 2024 13:37:42 +0100 Subject: [PATCH 03/11] Bump the androidx group with 6 updates (#796) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: Joaquim Stähli Co-authored-by: Gaëtan Muller --- gradle/libs.versions.toml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 64058be6b..7e93b0ac9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -2,15 +2,15 @@ android-gradle-plugin = "8.7.2" androidx-activity = "1.9.3" androidx-annotation = "1.9.1" -androidx-compose = "2024.10.01" +androidx-compose = "2024.11.00" androidx-compose-material-navigation = "1.7.0-beta01" # TODO Remove this once https://issuetracker.google.com/issues/347719428 is resolved androidx-core = "1.15.0" androidx-datastore = "1.1.1" androidx-fragment = "1.8.5" androidx-lifecycle = "2.8.7" androidx-media3 = "1.4.1" -androidx-navigation = "2.8.3" -androidx-paging = "3.3.2" +androidx-navigation = "2.8.4" +androidx-paging = "3.3.4" androidx-test-core = "1.6.1" androidx-test-ext-junit = "1.2.1" androidx-test-monitor = "1.7.2" From 000986de77815de401a1b1dc06651c4e4f0938c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Wed, 20 Nov 2024 13:51:55 +0100 Subject: [PATCH 04/11] Update to Gradle 8.11 (#788) --- build-logic/plugins/build.gradle.kts | 5 +++-- gradle.properties | 17 +++++++++++++---- gradle/wrapper/gradle-wrapper.properties | 2 +- 3 files changed, 17 insertions(+), 7 deletions(-) diff --git a/build-logic/plugins/build.gradle.kts b/build-logic/plugins/build.gradle.kts index e1e711f56..dcfbc3eec 100644 --- a/build-logic/plugins/build.gradle.kts +++ b/build-logic/plugins/build.gradle.kts @@ -3,6 +3,7 @@ * License information is available from the LICENSE file. */ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget import org.jetbrains.kotlin.gradle.tasks.KotlinCompile plugins { @@ -18,8 +19,8 @@ java { } tasks.withType().configureEach { - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.majorVersion + compilerOptions { + jvmTarget = JvmTarget.JVM_17 } } diff --git a/gradle.properties b/gradle.properties index 2574f1adb..f1d917fe5 100644 --- a/gradle.properties +++ b/gradle.properties @@ -12,31 +12,40 @@ # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. +# https://docs.gradle.org/current/userguide/performance.html#increase_the_heap_size org.gradle.jvmargs=-Xmx4g -Dfile.encoding=UTF-8 # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit -# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# https://docs.gradle.org/current/userguide/performance.html#parallel_execution org.gradle.parallel=true # AndroidX package structure to make it clearer which packages are bundled with the # Android operating system, and which are packaged with your app"s APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn +# https://developer.android.com/jetpack/androidx#using_androidx_libraries_in_your_project android.useAndroidX=true # Kotlin code style for this project: "official" or "obsolete": kotlin.code.style=official +# Gradle's Build Cache +# https://docs.gradle.org/current/userguide/performance.html#enable_the_build_cache org.gradle.caching=true + +# Gradle's Configuration Cache +# https://docs.gradle.org/current/userguide/performance.html#enable_configuration_cache # Disable configuration cache until Dokka supports it: https://github.com/Kotlin/dokka/issues/1217 org.gradle.configuration-cache=false +org.gradle.configuration-cache.parallel=true # Print dependency analysis report to the console dependency.analysis.print.build.health=true -# Let Detekt use Gradle's Worker API (https://detekt.dev/docs/gettingstarted/gradle/#options-for-detekt-gradle-properties) +# Let Detekt use Gradle's Worker API +# https://detekt.dev/docs/gettingstarted/gradle/#options-for-detekt-gradle-properties detekt.use.worker.api=true -# Use Dokka 2 Gradle Plugin: https://kotlinlang.org/docs/dokka-migration.html#migrate-your-project +# Use Dokka 2 Gradle Plugin +# https://kotlinlang.org/docs/dokka-migration.html#migrate-your-project org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 79eb9d003..7cf748e74 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME From 786c5462a35f702c9aa20f1224453214d9d5b2ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Wed, 20 Nov 2024 16:52:39 +0100 Subject: [PATCH 05/11] Make some symbols `internal`/`private` (#800) --- .../pillarbox/player/PillarboxBuilder.kt | 2 +- .../analytics/metrics/MetricsCollector.kt | 255 +++++++++--------- .../monitoring/MonitoringMessageHandler.kt | 2 +- 3 files changed, 133 insertions(+), 126 deletions(-) diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxBuilder.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxBuilder.kt index 3dd0eeec7..13bfd5af2 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxBuilder.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxBuilder.kt @@ -199,7 +199,7 @@ abstract class PillarboxBuilder { * * @return A new instance of [PillarboxExoPlayer]. */ - fun create(context: Context): PillarboxExoPlayer { + internal fun create(context: Context): PillarboxExoPlayer { return PillarboxExoPlayer( context = context, coroutineContext = coroutineContext, diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/analytics/metrics/MetricsCollector.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/analytics/metrics/MetricsCollector.kt index ec75be0ae..a413cb17a 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/analytics/metrics/MetricsCollector.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/analytics/metrics/MetricsCollector.kt @@ -27,7 +27,7 @@ import java.io.IOException */ class MetricsCollector private constructor( private val timeProvider: () -> Long, -) : PillarboxAnalyticsListener, PlaybackSessionManager.Listener { +) { /** * Listener */ @@ -40,7 +40,8 @@ class MetricsCollector private constructor( fun onMetricSessionReady(metrics: PlaybackMetrics) = Unit } - private val window = Window() + private val metricsAnalyticsListeners = MetricsAnalyticsListener() + private val metricsSessionManagerListener = MetricsSessionManagerListener() private var currentSession: PlaybackSessionManager.Session? = null private val listeners = mutableSetOf() private lateinit var player: PillarboxExoPlayer @@ -53,8 +54,8 @@ class MetricsCollector private constructor( * Set player at [PillarboxExoPlayer] creation. */ fun setPlayer(player: PillarboxExoPlayer) { - player.sessionManager.addListener(this) - player.addAnalyticsListener(this) + player.sessionManager.addListener(metricsSessionManagerListener) + player.addAnalyticsListener(metricsAnalyticsListeners) this.player = player } @@ -76,160 +77,166 @@ class MetricsCollector private constructor( listeners.remove(listener) } - private fun notifyMetricsReady(playbackMetrics: PlaybackMetrics) { - if (currentSession?.sessionId != playbackMetrics.sessionId) return - DebugLogger.debug(TAG, "notifyMetricsReady $playbackMetrics") - listeners.toList().forEach { - it.onMetricSessionReady(metrics = playbackMetrics) + private inner class MetricsSessionManagerListener : PlaybackSessionManager.Listener { + private fun notifyMetricsReady(playbackMetrics: PlaybackMetrics) { + if (currentSession?.sessionId != playbackMetrics.sessionId) return + DebugLogger.debug(TAG, "notifyMetricsReady $playbackMetrics") + listeners.toList().forEach { + it.onMetricSessionReady(metrics = playbackMetrics) + } } - } - override fun onSessionCreated(session: PlaybackSessionManager.Session) { - DebugLogger.debug(TAG, "onSessionCreated ${session.sessionId}") - getOrCreateSessionMetrics(session.periodUid) - } + override fun onSessionCreated(session: PlaybackSessionManager.Session) { + DebugLogger.debug(TAG, "onSessionCreated ${session.sessionId}") + getOrCreateSessionMetrics(session.periodUid) + } - override fun onCurrentSessionChanged(oldSession: PlaybackSessionManager.SessionInfo?, newSession: PlaybackSessionManager.SessionInfo?) { - DebugLogger.debug(TAG, "onCurrentSession ${oldSession?.session?.sessionId} -> ${newSession?.session?.sessionId}") - currentSession = newSession?.session - currentSession?.let { session -> - getOrCreateSessionMetrics(session.periodUid).apply { - setIsPlaying(player.isPlaying) - setPlaybackState(player.playbackState) + override fun onCurrentSessionChanged(oldSession: PlaybackSessionManager.SessionInfo?, newSession: PlaybackSessionManager.SessionInfo?) { + DebugLogger.debug(TAG, "onCurrentSession ${oldSession?.session?.sessionId} -> ${newSession?.session?.sessionId}") + currentSession = newSession?.session + currentSession?.let { session -> + getOrCreateSessionMetrics(session.periodUid).apply { + setIsPlaying(player.isPlaying) + setPlaybackState(player.playbackState) + } } } - } - - override fun onSessionDestroyed(session: PlaybackSessionManager.Session) { - DebugLogger.debug(TAG, "onSessionDestroyed ${session.sessionId}") - metricsSessions.remove(session.periodUid) - } - /** - * Get session metrics - * - * @param eventTime - * @return `null` if there is no item in the timeline or session already finished. - */ - private fun getSessionMetrics(eventTime: EventTime): SessionMetrics? { - if (eventTime.timeline.isEmpty) return null - return metricsSessions[(eventTime.getUidOfPeriod(window))] - } + override fun onSessionDestroyed(session: PlaybackSessionManager.Session) { + DebugLogger.debug(TAG, "onSessionDestroyed ${session.sessionId}") + metricsSessions.remove(session.periodUid) + } - private fun getOrCreateSessionMetrics(periodUid: Any): SessionMetrics { - return metricsSessions.getOrPut(periodUid) { - SessionMetrics(timeProvider) { sessionMetrics -> - player.sessionManager.getSessionFromPeriodUid(periodUid)?.let { - notifyMetricsReady(createPlaybackMetrics(session = it, metrics = sessionMetrics)) + private fun getOrCreateSessionMetrics(periodUid: Any): SessionMetrics { + return metricsSessions.getOrPut(periodUid) { + SessionMetrics(timeProvider) { sessionMetrics -> + player.sessionManager.getSessionFromPeriodUid(periodUid)?.let { + notifyMetricsReady(createPlaybackMetrics(session = it, metrics = sessionMetrics)) + } } } } } - override fun onStallChanged(eventTime: EventTime, isStall: Boolean) { - getSessionMetrics(eventTime)?.setIsStall(isStall) - } + private inner class MetricsAnalyticsListener : PillarboxAnalyticsListener { + private val window = Window() - override fun onIsPlayingChanged(eventTime: EventTime, isPlaying: Boolean) { - getSessionMetrics(eventTime)?.setIsPlaying(isPlaying) - } + override fun onStallChanged(eventTime: EventTime, isStall: Boolean) { + getSessionMetrics(eventTime)?.setIsStall(isStall) + } - override fun onBandwidthEstimate(eventTime: EventTime, totalLoadTimeMs: Int, totalBytesLoaded: Long, bitrateEstimate: Long) { - getSessionMetrics(eventTime)?.setBandwidthEstimate(totalLoadTimeMs, totalBytesLoaded, bitrateEstimate) - } + override fun onIsPlayingChanged(eventTime: EventTime, isPlaying: Boolean) { + getSessionMetrics(eventTime)?.setIsPlaying(isPlaying) + } - override fun onVideoInputFormatChanged(eventTime: EventTime, format: Format, decoderReuseEvaluation: DecoderReuseEvaluation?) { - getSessionMetrics(eventTime)?.videoFormat = format - } + override fun onBandwidthEstimate(eventTime: EventTime, totalLoadTimeMs: Int, totalBytesLoaded: Long, bitrateEstimate: Long) { + getSessionMetrics(eventTime)?.setBandwidthEstimate(totalLoadTimeMs, totalBytesLoaded, bitrateEstimate) + } - /** - * On video disabled is called when releasing the player - * - * @param eventTime - * @param decoderCounters - */ - override fun onVideoDisabled(eventTime: EventTime, decoderCounters: DecoderCounters) { - if (player.playbackState == Player.STATE_IDLE || eventTime.timeline.isEmpty) return - getSessionMetrics(eventTime)?.videoFormat = null - } + override fun onVideoInputFormatChanged(eventTime: EventTime, format: Format, decoderReuseEvaluation: DecoderReuseEvaluation?) { + getSessionMetrics(eventTime)?.videoFormat = format + } - override fun onAudioInputFormatChanged(eventTime: EventTime, format: Format, decoderReuseEvaluation: DecoderReuseEvaluation?) { - getSessionMetrics(eventTime)?.audioFormat = format - } + /** + * On video disabled is called when releasing the player + * + * @param eventTime + * @param decoderCounters + */ + override fun onVideoDisabled(eventTime: EventTime, decoderCounters: DecoderCounters) { + if (player.playbackState == Player.STATE_IDLE || eventTime.timeline.isEmpty) return + getSessionMetrics(eventTime)?.videoFormat = null + } - override fun onAudioDisabled(eventTime: EventTime, decoderCounters: DecoderCounters) { - if (player.playbackState == Player.STATE_IDLE || eventTime.timeline.isEmpty) return - getSessionMetrics(eventTime)?.audioFormat = null - } + override fun onAudioInputFormatChanged(eventTime: EventTime, format: Format, decoderReuseEvaluation: DecoderReuseEvaluation?) { + getSessionMetrics(eventTime)?.audioFormat = format + } - override fun onPlaybackStateChanged(eventTime: EventTime, state: Int) { - getSessionMetrics(eventTime)?.setPlaybackState(state) - } + override fun onAudioDisabled(eventTime: EventTime, decoderCounters: DecoderCounters) { + if (player.playbackState == Player.STATE_IDLE || eventTime.timeline.isEmpty) return + getSessionMetrics(eventTime)?.audioFormat = null + } - override fun onRenderedFirstFrame(eventTime: EventTime, output: Any, renderTimeMs: Long) { - getSessionMetrics(eventTime)?.setRenderFirstFrameOrAudioPositionAdvancing() - } + override fun onPlaybackStateChanged(eventTime: EventTime, state: Int) { + getSessionMetrics(eventTime)?.setPlaybackState(state) + } - override fun onAudioPositionAdvancing(eventTime: EventTime, playoutStartSystemTimeMs: Long) { - getSessionMetrics(eventTime)?.setRenderFirstFrameOrAudioPositionAdvancing() - } + override fun onRenderedFirstFrame(eventTime: EventTime, output: Any, renderTimeMs: Long) { + getSessionMetrics(eventTime)?.setRenderFirstFrameOrAudioPositionAdvancing() + } - override fun onLoadCompleted(eventTime: EventTime, loadEventInfo: LoadEventInfo, mediaLoadData: MediaLoadData) { - getSessionMetrics(eventTime)?.setLoadCompleted(loadEventInfo, mediaLoadData) - } + override fun onAudioPositionAdvancing(eventTime: EventTime, playoutStartSystemTimeMs: Long) { + getSessionMetrics(eventTime)?.setRenderFirstFrameOrAudioPositionAdvancing() + } - override fun onLoadStarted(eventTime: EventTime, loadEventInfo: LoadEventInfo, mediaLoadData: MediaLoadData) { - getSessionMetrics(eventTime)?.setLoadStarted(loadEventInfo) - } + override fun onLoadCompleted(eventTime: EventTime, loadEventInfo: LoadEventInfo, mediaLoadData: MediaLoadData) { + getSessionMetrics(eventTime)?.setLoadCompleted(loadEventInfo, mediaLoadData) + } - override fun onLoadError( - eventTime: EventTime, - loadEventInfo: LoadEventInfo, - mediaLoadData: MediaLoadData, - error: IOException, - wasCanceled: Boolean - ) { - getSessionMetrics(eventTime)?.setLoadCompleted(loadEventInfo, mediaLoadData) - } + override fun onLoadStarted(eventTime: EventTime, loadEventInfo: LoadEventInfo, mediaLoadData: MediaLoadData) { + getSessionMetrics(eventTime)?.setLoadStarted(loadEventInfo) + } - override fun onDrmSessionAcquired(eventTime: EventTime, state: Int) { - DebugLogger.debug(TAG, "onDrmSessionAcquired $state") - if (state == DrmSession.STATE_OPENED) { - getSessionMetrics(eventTime)?.setDrmSessionAcquired() + override fun onLoadError( + eventTime: EventTime, + loadEventInfo: LoadEventInfo, + mediaLoadData: MediaLoadData, + error: IOException, + wasCanceled: Boolean + ) { + getSessionMetrics(eventTime)?.setLoadCompleted(loadEventInfo, mediaLoadData) } - } - override fun onDrmSessionReleased(eventTime: EventTime) { - DebugLogger.debug(TAG, "onDrmSessionReleased") - } + override fun onDrmSessionAcquired(eventTime: EventTime, state: Int) { + DebugLogger.debug(TAG, "onDrmSessionAcquired $state") + if (state == DrmSession.STATE_OPENED) { + getSessionMetrics(eventTime)?.setDrmSessionAcquired() + } + } - override fun onDrmKeysLoaded(eventTime: EventTime) { - DebugLogger.debug(TAG, "onDrmKeysLoaded") - getSessionMetrics(eventTime)?.setDrmKeyLoaded() - } + override fun onDrmSessionReleased(eventTime: EventTime) { + DebugLogger.debug(TAG, "onDrmSessionReleased") + } - override fun onDrmKeysRestored(eventTime: EventTime) { - DebugLogger.debug(TAG, "onDrmKeysRestored") - getSessionMetrics(eventTime)?.setDrmKeyLoaded() - } + override fun onDrmKeysLoaded(eventTime: EventTime) { + DebugLogger.debug(TAG, "onDrmKeysLoaded") + getSessionMetrics(eventTime)?.setDrmKeyLoaded() + } - override fun onDrmKeysRemoved(eventTime: EventTime) { - DebugLogger.debug(TAG, "onDrmKeysRemoved") - getSessionMetrics(eventTime)?.setDrmKeyLoaded() - } + override fun onDrmKeysRestored(eventTime: EventTime) { + DebugLogger.debug(TAG, "onDrmKeysRestored") + getSessionMetrics(eventTime)?.setDrmKeyLoaded() + } - override fun onPlayerReleased(eventTime: EventTime) { - listeners.clear() - } + override fun onDrmKeysRemoved(eventTime: EventTime) { + DebugLogger.debug(TAG, "onDrmKeysRemoved") + getSessionMetrics(eventTime)?.setDrmKeyLoaded() + } - override fun onDroppedVideoFrames(eventTime: EventTime, droppedFrames: Int, elapsedMs: Long) { - getSessionMetrics(eventTime)?.let { - it.totalDroppedFrames += droppedFrames + override fun onPlayerReleased(eventTime: EventTime) { + listeners.clear() + } + + override fun onDroppedVideoFrames(eventTime: EventTime, droppedFrames: Int, elapsedMs: Long) { + getSessionMetrics(eventTime)?.let { + it.totalDroppedFrames += droppedFrames + } + } + + override fun onSurfaceSizeChanged(eventTime: EventTime, width: Int, height: Int) { + surfaceSize = Size(width, height) } - } - override fun onSurfaceSizeChanged(eventTime: EventTime, width: Int, height: Int) { - surfaceSize = Size(width, height) + /** + * Get session metrics + * + * @param eventTime + * @return `null` if there is no item in the timeline or session already finished. + */ + private fun getSessionMetrics(eventTime: EventTime): SessionMetrics? { + if (eventTime.timeline.isEmpty) return null + return metricsSessions[(eventTime.getUidOfPeriod(window))] + } } /** diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/monitoring/MonitoringMessageHandler.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/monitoring/MonitoringMessageHandler.kt index 9da48e250..55d1a7884 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/monitoring/MonitoringMessageHandler.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/monitoring/MonitoringMessageHandler.kt @@ -50,7 +50,7 @@ interface MonitoringMessageHandlerFactory { * @param Config The config used to create a new [MonitoringMessageHandler]. */ @PillarboxDsl -class MonitoringConfigFactory +class MonitoringConfigFactory internal constructor() /** * Represents a specific type of [MonitoringMessageHandler]. From 19f9f03f94920fd19ef9ba03bda7c9473f2c57a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Fri, 22 Nov 2024 17:16:56 +0100 Subject: [PATCH 06/11] Sprite sheet (#777) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Gaëtan Muller Co-authored-by: Gaëtan Muller --- gradle/libs.versions.toml | 1 + pillarbox-core-business/build.gradle.kts | 1 + .../business/integrationlayer/data/Chapter.kt | 2 + .../integrationlayer/data/SpriteSheet.kt | 29 +++ .../core/business/source/SRGAssetLoader.kt | 8 +- .../business/source/SRGAssetLoaderConfig.kt | 24 +++ .../core/business/source/SpriteSheetLoader.kt | 45 +++++ .../business/source/SpriteSheetMediaPeriod.kt | 186 ++++++++++++++++++ .../business/source/SpriteSheetMediaSource.kt | 52 +++++ .../CommandersActTrackerIntegrationTest.kt | 6 + .../ComScoreTrackerIntegrationTest.kt | 6 + .../demo/shared/ui/NavigationRoutes.kt | 3 + .../demo/ui/showcases/ShowcasesHome.kt | 8 + .../demo/ui/showcases/ShowcasesNavigation.kt | 5 +- .../layouts/thumbnail/ThumbnailView.kt | 50 +++++ .../layouts/thumbnail/ThumbnailViewModel.kt | 87 ++++++++ .../src/main/res/values/strings.xml | 1 + .../pillarbox/player/extension/Tracks.kt | 10 + pillarbox-ui/build.gradle.kts | 2 +- .../ui/SmoothProgressTrackerState.kt | 26 ++- 20 files changed, 541 insertions(+), 11 deletions(-) create mode 100644 pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/data/SpriteSheet.kt create mode 100644 pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SpriteSheetLoader.kt create mode 100644 pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SpriteSheetMediaPeriod.kt create mode 100644 pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SpriteSheetMediaSource.kt create mode 100644 pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/thumbnail/ThumbnailView.kt create mode 100644 pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/thumbnail/ThumbnailViewModel.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7e93b0ac9..f94e1e707 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -102,6 +102,7 @@ androidx-media3-cast = { group = "androidx.media3", name = "media3-cast", versio androidx-media3-common = { group = "androidx.media3", name = "media3-common", version.ref = "androidx-media3" } androidx-media3-datasource = { group = "androidx.media3", name = "media3-datasource", version.ref = "androidx-media3" } androidx-media3-datasource-okhttp = { group = "androidx.media3", name = "media3-datasource-okhttp", version.ref = "androidx-media3" } +androidx-media3-decoder = { group = "androidx.media3", name = "media3-decoder", version.ref = "androidx-media3" } androidx-media3-exoplayer = { group = "androidx.media3", name = "media3-exoplayer", version.ref = "androidx-media3" } androidx-media3-ui = { group = "androidx.media3", name = "media3-ui", version.ref = "androidx-media3" } androidx-media3-dash = { group = "androidx.media3", name = "media3-exoplayer-dash", version.ref = "androidx-media3" } diff --git a/pillarbox-core-business/build.gradle.kts b/pillarbox-core-business/build.gradle.kts index 9355ed6b0..149643242 100644 --- a/pillarbox-core-business/build.gradle.kts +++ b/pillarbox-core-business/build.gradle.kts @@ -26,6 +26,7 @@ dependencies { api(libs.androidx.media3.common) api(libs.androidx.media3.datasource) implementation(libs.androidx.media3.datasource.okhttp) + implementation(libs.androidx.media3.decoder) api(libs.androidx.media3.exoplayer) implementation(libs.guava) runtimeOnly(libs.kotlinx.coroutines.android) diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/data/Chapter.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/data/Chapter.kt index 703c5740e..4b7b4dddf 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/data/Chapter.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/data/Chapter.kt @@ -28,6 +28,7 @@ import kotlinx.serialization.Serializable * @property timeIntervalList List of time intervals relevant to the chapter. * @property validFrom The [Instant] when the [Chapter] becomes valid. * @property validTo The [Instant] until when the [Chapter] is valid. + * @property spriteSheet The [SpriteSheet] information if available. */ @Serializable data class Chapter( @@ -51,6 +52,7 @@ data class Chapter( val timeIntervalList: List? = null, val validFrom: Instant? = null, val validTo: Instant? = null, + val spriteSheet: SpriteSheet? = null, ) : DataWithAnalytics { /** * Indicates whether this represents a full-length chapter. diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/data/SpriteSheet.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/data/SpriteSheet.kt new file mode 100644 index 000000000..9927319dc --- /dev/null +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/integrationlayer/data/SpriteSheet.kt @@ -0,0 +1,29 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.core.business.integrationlayer.data + +import kotlinx.serialization.Serializable + +/** + * Represents a sprite sheet containing multiple thumbnail images arranged in a grid. + * + * @property urn The URN of the media. + * @property rows The number of rows in the sprite sheet. + * @property columns The number of columns in the sprite sheet. + * @property thumbnailHeight The height of each thumbnail image, in pixels. + * @property thumbnailWidth The width of each thumbnail image, in pixels. + * @property interval The interval between two thumbnail images, in milliseconds. + * @property url The URL of the sprite sheet image. + */ +@Serializable +data class SpriteSheet( + val urn: String, + val rows: Int, + val columns: Int, + val thumbnailHeight: Int, + val thumbnailWidth: Int, + val interval: Long, + val url: String +) diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SRGAssetLoader.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SRGAssetLoader.kt index a8f0b3684..0078b0234 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SRGAssetLoader.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SRGAssetLoader.kt @@ -12,6 +12,7 @@ import androidx.media3.common.MediaMetadata import androidx.media3.common.MimeTypes import androidx.media3.datasource.DataSource.Factory import androidx.media3.exoplayer.source.DefaultMediaSourceFactory +import androidx.media3.exoplayer.source.MergingMediaSource import ch.srgssr.pillarbox.core.business.HttpResultException import ch.srgssr.pillarbox.core.business.akamai.AkamaiTokenDataSource import ch.srgssr.pillarbox.core.business.akamai.AkamaiTokenProvider @@ -99,6 +100,7 @@ class SRGAssetLoader internal constructor( private val customTrackerData: (MutableMediaItemTrackerData.(Resource, Chapter, MediaComposition) -> Unit)?, private val customMediaMetadata: (suspend MediaMetadata.Builder.(MediaMetadata, Chapter, MediaComposition) -> Unit)?, private val resourceSelector: ResourceSelector, + private val spriteSheetLoader: SpriteSheetLoader, ) : AssetLoader( mediaSourceFactory = DefaultMediaSourceFactory(AkamaiTokenDataSource.Factory(akamaiTokenProvider, dataSourceFactory)) ) { @@ -157,8 +159,12 @@ class SRGAssetLoader internal constructor( .setDrmConfiguration(fillDrmConfiguration(resource)) .setUri(uri) .build() + val contentMediaSource = mediaSourceFactory.createMediaSource(loadingMediaItem) + val mediaSource = chapter.spriteSheet?.let { + MergingMediaSource(contentMediaSource, SpriteSheetMediaSource(it, loadingMediaItem, spriteSheetLoader)) + } ?: contentMediaSource return Asset( - mediaSource = mediaSourceFactory.createMediaSource(loadingMediaItem), + mediaSource = mediaSource, trackersData = trackerData.toMediaItemTrackerData(), mediaMetadata = mediaItem.mediaMetadata.buildUpon().apply { defaultMediaMetadata.invoke(this, mediaItem.mediaMetadata, chapter, result) diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SRGAssetLoaderConfig.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SRGAssetLoaderConfig.kt index 6482f4377..5da310ef0 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SRGAssetLoaderConfig.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SRGAssetLoaderConfig.kt @@ -5,6 +5,7 @@ package ch.srgssr.pillarbox.core.business.source import android.content.Context +import android.graphics.Bitmap import androidx.annotation.VisibleForTesting import androidx.media3.common.MediaMetadata import androidx.media3.datasource.DataSource.Factory @@ -16,6 +17,7 @@ import ch.srgssr.pillarbox.core.business.integrationlayer.ResourceSelector import ch.srgssr.pillarbox.core.business.integrationlayer.data.Chapter import ch.srgssr.pillarbox.core.business.integrationlayer.data.MediaComposition import ch.srgssr.pillarbox.core.business.integrationlayer.data.Resource +import ch.srgssr.pillarbox.core.business.integrationlayer.data.SpriteSheet import ch.srgssr.pillarbox.core.business.integrationlayer.service.HttpMediaCompositionService import ch.srgssr.pillarbox.core.business.integrationlayer.service.MediaCompositionService import ch.srgssr.pillarbox.core.business.tracker.commandersact.CommandersActTracker @@ -37,6 +39,7 @@ import kotlinx.coroutines.Dispatchers * - Setting an HTTP client for network requests. * - Injecting custom data into media item tracker data. * - Overriding the default media metadata. + * - Providing a custom [Bitmap] loader for sprite sheet. * * @param context The Android [Context]. */ @@ -50,6 +53,7 @@ class SRGAssetLoaderConfig internal constructor(context: Context) { private var commanderActTrackerFactory: MediaItemTracker.Factory = CommandersActTracker.Factory(SRGAnalytics.commandersAct, Dispatchers.Default) private var comscoreTrackerFactory: MediaItemTracker.Factory = ComScoreTracker.Factory() + private var spriteSheetLoader: SpriteSheetLoader = SpriteSheetLoader.Default() @VisibleForTesting internal fun commanderActTrackerFactory(commanderActTrackerFactory: MediaItemTracker.Factory) { @@ -142,6 +146,25 @@ class SRGAssetLoaderConfig internal constructor(context: Context) { mediaMetadataOverride = block } + /** + * Sets the [SpriteSheetLoader] to be used to load a [Bitmap] from a [SpriteSheet]. + * + * **Example** + * + * ```kotlin + * val srgAssetLoader = SRGAssetLoader(context) { + * spriteSheetLoader { spriteSheet, onComplete -> + * onComplete(loadBitmap(spriteSheet.url)) + * } + * } + * ``` + * + * @param spriteSheetLoader The [SpriteSheetLoader] instance to use. + */ + fun spriteSheetLoader(spriteSheetLoader: SpriteSheetLoader) { + this.spriteSheetLoader = spriteSheetLoader + } + internal fun create(): SRGAssetLoader { return SRGAssetLoader( akamaiTokenProvider = akamaiTokenProvider, @@ -152,6 +175,7 @@ class SRGAssetLoaderConfig internal constructor(context: Context) { comscoreTrackerFactory = comscoreTrackerFactory, mediaCompositionService = mediaCompositionService, resourceSelector = ResourceSelector(), + spriteSheetLoader = spriteSheetLoader ) } } diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SpriteSheetLoader.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SpriteSheetLoader.kt new file mode 100644 index 000000000..b70673d92 --- /dev/null +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SpriteSheetLoader.kt @@ -0,0 +1,45 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.core.business.source + +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import ch.srgssr.pillarbox.core.business.integrationlayer.data.SpriteSheet +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.launch +import java.net.URL + +/** + * Load a [Bitmap] from a [SpriteSheet]. + * + * This interface allows integrators to use their own implementation to load [Bitmap]s using an external library like + * [Glide](https://bumptech.github.io/glide/), [Coil](https://coil-kt.github.io/coil/), ... + */ +fun interface SpriteSheetLoader { + /** + * Load sprite sheet + * + * @param spriteSheet The [SpriteSheet] to load the [Bitmap] from. + * @param onComplete The callback to call when the [Bitmap] has been loaded. Passing `null` means that the [Bitmap] could not be loaded. + */ + fun loadSpriteSheet(spriteSheet: SpriteSheet, onComplete: (Bitmap?) -> Unit) + + /** + * Default + * + * @param dispatcher The [CoroutineDispatcher] to use for loading the sprite sheet. Should not be on the main thread. + */ + class Default(private val dispatcher: CoroutineDispatcher = Dispatchers.IO) : SpriteSheetLoader { + override fun loadSpriteSheet(spriteSheet: SpriteSheet, onComplete: (Bitmap?) -> Unit) { + MainScope().launch(dispatcher) { + URL(spriteSheet.url).openStream().use { + onComplete(BitmapFactory.decodeStream(it)) + } + } + } + } +} diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SpriteSheetMediaPeriod.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SpriteSheetMediaPeriod.kt new file mode 100644 index 000000000..a3b877da8 --- /dev/null +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SpriteSheetMediaPeriod.kt @@ -0,0 +1,186 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.core.business.source + +import android.graphics.Bitmap +import androidx.media3.common.C +import androidx.media3.common.Format +import androidx.media3.common.MimeTypes +import androidx.media3.common.TrackGroup +import androidx.media3.decoder.DecoderInputBuffer +import androidx.media3.exoplayer.FormatHolder +import androidx.media3.exoplayer.LoadingInfo +import androidx.media3.exoplayer.SeekParameters +import androidx.media3.exoplayer.source.MediaPeriod +import androidx.media3.exoplayer.source.SampleStream +import androidx.media3.exoplayer.source.TrackGroupArray +import androidx.media3.exoplayer.trackselection.ExoTrackSelection +import ch.srgssr.pillarbox.core.business.integrationlayer.data.SpriteSheet +import java.io.ByteArrayOutputStream +import java.io.IOException +import java.util.concurrent.atomic.AtomicBoolean +import kotlin.math.max +import kotlin.time.Duration.Companion.milliseconds + +/** + * A [MediaPeriod] that loads a [Bitmap] and pass it to a [SampleStream]. + */ +internal class SpriteSheetMediaPeriod( + private val spriteSheet: SpriteSheet, + private val spriteSheetLoader: SpriteSheetLoader, +) : MediaPeriod { + private var bitmap: Bitmap? = null + private val isLoading = AtomicBoolean(true) + private val format = Format.Builder() + .setId("SpriteSheet") + .setFrameRate(1f / spriteSheet.interval.milliseconds.inWholeSeconds) + .setCustomData(spriteSheet) + .setRoleFlags(C.ROLE_FLAG_MAIN) + .setContainerMimeType(MimeTypes.IMAGE_JPEG) + .setSampleMimeType(MimeTypes.IMAGE_JPEG) + .build() + private val tracks = TrackGroupArray(TrackGroup("sprite-sheet-srg", format)) + private var positionUs = 0L + + override fun prepare(callback: MediaPeriod.Callback, positionUs: Long) { + callback.onPrepared(this) + this.positionUs = positionUs + isLoading.set(true) + spriteSheetLoader.loadSpriteSheet(spriteSheet) { bitmap -> + this.bitmap = bitmap + isLoading.set(false) + } + } + + fun releasePeriod() { + bitmap?.recycle() + bitmap = null + } + + override fun selectTracks( + selections: Array, + mayRetainStreamFlags: BooleanArray, + streams: Array, + streamResetFlags: BooleanArray, + positionUs: Long + ): Long { + this.positionUs = getAdjustedSeekPositionUs(positionUs, SeekParameters.DEFAULT) + for (i in selections.indices) { + if (streams[i] != null && (selections[i] == null || !mayRetainStreamFlags[i])) { + streams[i] = null + } + if (streams[i] == null && selections[i] != null) { + val stream = SpriteSheetSampleStream() + streams[i] = stream + streamResetFlags[i] = true + } + } + return positionUs + } + + override fun getTrackGroups(): TrackGroupArray { + return tracks + } + + override fun getBufferedPositionUs(): Long { + return C.TIME_UNSET + } + + override fun getNextLoadPositionUs(): Long { + return C.TIME_UNSET + } + + override fun continueLoading(loadingInfo: LoadingInfo): Boolean { + return isLoading() + } + + override fun isLoading(): Boolean { + return isLoading.get() + } + + override fun reevaluateBuffer(positionUs: Long) = Unit + + override fun maybeThrowPrepareError() = Unit + + override fun discardBuffer(positionUs: Long, toKeyframe: Boolean) = Unit + + override fun readDiscontinuity(): Long { + return C.TIME_UNSET + } + + override fun seekToUs(positionUs: Long): Long { + this.positionUs = positionUs + return positionUs + } + + override fun getAdjustedSeekPositionUs(positionUs: Long, seekParameters: SeekParameters): Long { + val intervalUs = spriteSheet.interval.milliseconds.inWholeMicroseconds + return (positionUs / intervalUs) * intervalUs + } + + internal inner class SpriteSheetSampleStream : SampleStream { + + override fun isReady(): Boolean { + return !isLoading() + } + + override fun maybeThrowError() { + if (bitmap == null && !isLoading.get()) { + throw IOException("Can't decode ${spriteSheet.url}") + } + } + + @Suppress("ReturnCount") + override fun readData(formatHolder: FormatHolder, buffer: DecoderInputBuffer, readFlags: Int): Int { + if ((readFlags and SampleStream.FLAG_REQUIRE_FORMAT) != 0) { + formatHolder.format = tracks[0].getFormat(0) + return C.RESULT_FORMAT_READ + } + + if (isLoading.get()) { + return C.RESULT_NOTHING_READ + } + + val intervalUs = spriteSheet.interval.milliseconds.inWholeMicroseconds + val tileIndex = positionUs / intervalUs + buffer.addFlag(C.BUFFER_FLAG_KEY_FRAME) + buffer.timeUs = positionUs + bitmap?.let { bitmap -> + val data = cropTileFromImageGrid(bitmap, max((tileIndex.toInt() - 1), 0)) + buffer.ensureSpaceForWrite(data.size) + buffer.data?.put(data, /* offset= */0, data.size) + } + return C.RESULT_BUFFER_READ + } + + override fun skipData(positionUs: Long): Int { + return 0 + } + + private fun cropTileFromImageGrid(bitmap: Bitmap, tileIndex: Int): ByteArray { + val tileWidth: Int = spriteSheet.thumbnailWidth + val tileHeight: Int = spriteSheet.thumbnailHeight + val tileStartXCoordinate: Int = tileWidth * (tileIndex % spriteSheet.columns) + val tileStartYCoordinate: Int = tileHeight * (tileIndex / spriteSheet.columns) + val tile = Bitmap.createBitmap(bitmap, tileStartXCoordinate, tileStartYCoordinate, tileWidth, tileHeight) + return bitmapToByteArray(tile) + } + } + + private companion object { + private const val MAX_QUALITY = 100 + + private fun bitmapToByteArray( + bitmap: Bitmap, + format: Bitmap.CompressFormat = Bitmap.CompressFormat.JPEG, + quality: Int = MAX_QUALITY + ): ByteArray { + return ByteArrayOutputStream().use { + bitmap.compress(format, quality, it) + it.toByteArray() + } + } + } +} diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SpriteSheetMediaSource.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SpriteSheetMediaSource.kt new file mode 100644 index 000000000..7ad26d24b --- /dev/null +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/source/SpriteSheetMediaSource.kt @@ -0,0 +1,52 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.core.business.source + +import android.graphics.Bitmap +import androidx.media3.common.MediaItem +import androidx.media3.datasource.TransferListener +import androidx.media3.exoplayer.source.BaseMediaSource +import androidx.media3.exoplayer.source.MediaPeriod +import androidx.media3.exoplayer.source.MediaSource +import androidx.media3.exoplayer.source.SinglePeriodTimeline +import androidx.media3.exoplayer.upstream.Allocator +import ch.srgssr.pillarbox.core.business.integrationlayer.data.SpriteSheet +import kotlin.time.Duration.Companion.milliseconds + +/** + * An implementation of a [BaseMediaSource] that loads a [SpriteSheet]. + * + * @param spriteSheet The [SpriteSheet] to build thumbnails. + * @param mediaItem The [MediaItem]. + * @param spriteSheetLoader The [SpriteSheetLoader] to use to load a [Bitmap] from a [SpriteSheet]. + */ +internal class SpriteSheetMediaSource( + private val spriteSheet: SpriteSheet, + private val mediaItem: MediaItem, + private val spriteSheetLoader: SpriteSheetLoader, +) : BaseMediaSource() { + + override fun getMediaItem(): MediaItem { + return mediaItem + } + + override fun maybeThrowSourceInfoRefreshError() = Unit + + override fun createPeriod(id: MediaSource.MediaPeriodId, allocator: Allocator, startPositionUs: Long): MediaPeriod { + return SpriteSheetMediaPeriod(spriteSheet, spriteSheetLoader) + } + + override fun releasePeriod(mediaPeriod: MediaPeriod) { + (mediaPeriod as SpriteSheetMediaPeriod).releasePeriod() + } + + override fun prepareSourceInternal(mediaTransferListener: TransferListener?) { + val duration = (spriteSheet.rows * spriteSheet.columns * spriteSheet.interval).milliseconds + val timeline = SinglePeriodTimeline(duration.inWholeMicroseconds, true, false, false, null, mediaItem) + refreshSourceInfo(timeline) + } + + override fun releaseSourceInternal() = Unit +} diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerIntegrationTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerIntegrationTest.kt index 210e2ff4c..4944b9696 100644 --- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerIntegrationTest.kt +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActTrackerIntegrationTest.kt @@ -6,6 +6,7 @@ package ch.srgssr.pillarbox.core.business.tracker.commandersact import android.content.Context import android.os.Looper +import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.exoplayer.ExoPlayer @@ -79,6 +80,11 @@ class CommandersActTrackerIntegrationTest { clock(clock) // Use other CoroutineContext to avoid infinite loop because Heartbeat is also running in Pillarbox. coroutineContext(EmptyCoroutineContext) + }.apply { + // FIXME Investigate why we need to disable the image track in tests + trackSelectionParameters = trackSelectionParameters.buildUpon() + .setTrackTypeDisabled(C.TRACK_TYPE_IMAGE, true) + .build() } } diff --git a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/comscore/ComScoreTrackerIntegrationTest.kt b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/comscore/ComScoreTrackerIntegrationTest.kt index 726e38986..8709c23fd 100644 --- a/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/comscore/ComScoreTrackerIntegrationTest.kt +++ b/pillarbox-core-business/src/test/java/ch/srgssr/pillarbox/core/business/tracker/comscore/ComScoreTrackerIntegrationTest.kt @@ -9,6 +9,7 @@ import android.os.Looper import android.view.SurfaceView import android.view.ViewGroup import androidx.core.view.updateLayoutParams +import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.test.utils.FakeClock @@ -65,6 +66,11 @@ class ComScoreTrackerIntegrationTest { comscoreTrackerFactory(comScoreFactory) commanderActTrackerFactory(mockk(relaxed = true)) } + }.apply { + // FIXME Investigate why we need to disable the image track in tests + trackSelectionParameters = trackSelectionParameters.buildUpon() + .setTrackTypeDisabled(C.TRACK_TYPE_IMAGE, true) + .build() } } diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/NavigationRoutes.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/NavigationRoutes.kt index 1d70f3727..87870f358 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/NavigationRoutes.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/ui/NavigationRoutes.kt @@ -77,4 +77,7 @@ sealed interface NavigationRoutes { @Serializable data object CountdownShowcase : NavigationRoutes + + @Serializable + data object ThumbnailShowcase : NavigationRoutes } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesHome.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesHome.kt index 9a9557870..4a6684066 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesHome.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesHome.kt @@ -85,6 +85,14 @@ fun ShowcasesHome(navController: NavController) { modifier = itemModifier, onClick = { navController.navigate(NavigationRoutes.Chapters) } ) + + HorizontalDivider() + + DemoListItemView( + title = stringResource(R.string.thumbnail), + modifier = itemModifier, + onClick = { navController.navigate(NavigationRoutes.ThumbnailShowcase) } + ) } DemoListHeaderView( diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesNavigation.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesNavigation.kt index ba67df6ea..f0af5928d 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesNavigation.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/ShowcasesNavigation.kt @@ -14,6 +14,7 @@ import ch.srgssr.pillarbox.demo.ui.showcases.integrations.ExoPlayerShowcase import ch.srgssr.pillarbox.demo.ui.showcases.layouts.ChapterShowcase import ch.srgssr.pillarbox.demo.ui.showcases.layouts.SimpleLayoutShowcase import ch.srgssr.pillarbox.demo.ui.showcases.layouts.StoryLayoutShowcase +import ch.srgssr.pillarbox.demo.ui.showcases.layouts.thumbnail.ThumbnailView import ch.srgssr.pillarbox.demo.ui.showcases.misc.ContentNotYetAvailable import ch.srgssr.pillarbox.demo.ui.showcases.misc.MultiPlayerShowcase import ch.srgssr.pillarbox.demo.ui.showcases.misc.ResizablePlayerShowcase @@ -67,10 +68,12 @@ fun NavGraphBuilder.showcasesNavGraph(navController: NavController) { composable(DemoPageView("Chapters", Levels)) { ChapterShowcase() } - composable(DemoPageView("CountdownShowcase", Levels)) { ContentNotYetAvailable() } + composable(DemoPageView("ThumbnailShowcase", Levels)) { + ThumbnailView() + } } private val Levels = listOf("app", "pillarbox", "showcase") diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/thumbnail/ThumbnailView.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/thumbnail/ThumbnailView.kt new file mode 100644 index 000000000..2dbe39baa --- /dev/null +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/thumbnail/ThumbnailView.kt @@ -0,0 +1,50 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.demo.ui.showcases.layouts.thumbnail + +import android.graphics.Bitmap +import androidx.compose.foundation.Image +import androidx.compose.foundation.interaction.MutableInteractionSource +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.lifecycle.compose.LifecycleResumeEffect +import androidx.lifecycle.viewmodel.compose.viewModel +import ch.srgssr.pillarbox.demo.ui.player.controls.PlayerControls +import ch.srgssr.pillarbox.ui.widget.player.PlayerSurface + +/** + * Thumbnail view + */ +@Composable +fun ThumbnailView() { + val thumbnailViewModel = viewModel() + val player = thumbnailViewModel.player + LifecycleResumeEffect(player) { + player.play() + onPauseOrDispose { + player.pause() + } + } + + Box { + PlayerSurface(player) { + val thumbnail: Bitmap? = thumbnailViewModel.thumbnail + thumbnail?.let { + Image(bitmap = it.asImageBitmap(), contentDescription = null, modifier = Modifier.fillMaxSize()) + } + } + val interactionSource = remember { MutableInteractionSource() } + PlayerControls( + modifier = Modifier.matchParentSize(), + player = player, + progressTracker = thumbnailViewModel.progressTrackerState, + interactionSource = interactionSource + ) {} + } +} diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/thumbnail/ThumbnailViewModel.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/thumbnail/ThumbnailViewModel.kt new file mode 100644 index 000000000..d775f3118 --- /dev/null +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/thumbnail/ThumbnailViewModel.kt @@ -0,0 +1,87 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.demo.ui.showcases.layouts.thumbnail + +import android.app.Application +import android.graphics.Bitmap +import android.graphics.drawable.BitmapDrawable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.viewModelScope +import androidx.media3.exoplayer.image.ImageOutput +import ch.srgssr.pillarbox.core.business.PillarboxExoPlayer +import ch.srgssr.pillarbox.core.business.SRGMediaItem +import ch.srgssr.pillarbox.demo.shared.data.DemoItem +import ch.srgssr.pillarbox.player.network.PillarboxOkHttp +import ch.srgssr.pillarbox.ui.ProgressTrackerState +import ch.srgssr.pillarbox.ui.SmoothProgressTrackerState +import coil.imageLoader +import coil.request.ImageRequest +import coil.size.Scale + +/** + * A ViewModel to demonstrate how to work with Image track. + * + * @param application The [Application]. + */ +class ThumbnailViewModel(application: Application) : AndroidViewModel(application), ImageOutput { + private val imageLoader = application.imageLoader.newBuilder() + .okHttpClient(PillarboxOkHttp()) + .build() + + /** + * Player + */ + val player = PillarboxExoPlayer(application) { + srgAssetLoader(application) { + spriteSheetLoader { spriteSheet, onComplete -> + val request = ImageRequest.Builder(application) + .data(spriteSheet.url) + .scale(Scale.FILL) // FILL to have the source image size! + .allowConversionToBitmap(enable = true) + .target { result -> + val bitmap = (result as BitmapDrawable).bitmap + onComplete(bitmap) + } + .build() + imageLoader.enqueue(request) + } + } + } + + /** + * Thumbnail + */ + var thumbnail by mutableStateOf(null) + private set + + /** + * Progress tracker state + */ + val progressTrackerState: ProgressTrackerState = SmoothProgressTrackerState(player, viewModelScope, this) + + init { + player.prepare() + player.addMediaItem(SRGMediaItem("urn:srf:video:881be9c2-65ec-4fa9-ba4a-926d15d046ef")) + player.addMediaItem(DemoItem.OnDemandHorizontalVideo.toMediaItem()) + player.addMediaItem(SRGMediaItem("urn:rsi:video:2366175")) + player.addMediaItem(DemoItem.UnifiedStreamingOnDemand_Dash_TiledThumbnails.toMediaItem()) + player.addMediaItem(DemoItem.UnifiedStreamingOnDemand_Dash_TrickPlay.toMediaItem()) + } + + override fun onCleared() { + player.release() + } + + override fun onImageAvailable(presentationTimeUs: Long, bitmap: Bitmap) { + thumbnail = bitmap + } + + override fun onDisabled() { + thumbnail = null + } +} diff --git a/pillarbox-demo/src/main/res/values/strings.xml b/pillarbox-demo/src/main/res/values/strings.xml index 529112c83..d7afe8e43 100644 --- a/pillarbox-demo/src/main/res/values/strings.xml +++ b/pillarbox-demo/src/main/res/values/strings.xml @@ -35,6 +35,7 @@ all Pause at end of media items Chapters + Thumbnail Library version Choose text color Choose text size diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Tracks.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Tracks.kt index 6d1daad38..8b3f93777 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Tracks.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Tracks.kt @@ -4,6 +4,7 @@ */ package ch.srgssr.pillarbox.player.extension +import androidx.media3.common.C import androidx.media3.common.Tracks import ch.srgssr.pillarbox.player.asset.timeRange.BlockedTimeRange import ch.srgssr.pillarbox.player.source.PillarboxMediaSource @@ -27,3 +28,12 @@ fun Tracks.getBlockedTimeRangeOrNull(): List? { it.type == PillarboxMediaSource.TRACK_TYPE_PILLARBOX_BLOCKED }?.getTrackFormat(0)?.customData as? List } + +/** + * Contains image track + * + * @return `true` if there is a track of type [C.TRACK_TYPE_IMAGE], `false` otherwise + */ +fun Tracks.containsImageTrack(): Boolean { + return containsType(C.TRACK_TYPE_IMAGE) +} diff --git a/pillarbox-ui/build.gradle.kts b/pillarbox-ui/build.gradle.kts index c294110e0..d6de4fffe 100644 --- a/pillarbox-ui/build.gradle.kts +++ b/pillarbox-ui/build.gradle.kts @@ -30,7 +30,7 @@ dependencies { implementation(libs.androidx.lifecycle.runtime.compose) implementation(libs.guava) api(libs.androidx.media3.common) - implementation(libs.androidx.media3.exoplayer) + api(libs.androidx.media3.exoplayer) api(libs.androidx.media3.ui) implementation(libs.kotlinx.coroutines.core) diff --git a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/SmoothProgressTrackerState.kt b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/SmoothProgressTrackerState.kt index c30c031cb..f4aeb0831 100644 --- a/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/SmoothProgressTrackerState.kt +++ b/pillarbox-ui/src/main/java/ch/srgssr/pillarbox/ui/SmoothProgressTrackerState.kt @@ -7,7 +7,9 @@ package ch.srgssr.pillarbox.ui import androidx.media3.common.C import androidx.media3.common.Player import androidx.media3.exoplayer.SeekParameters +import androidx.media3.exoplayer.image.ImageOutput import ch.srgssr.pillarbox.player.PillarboxExoPlayer +import ch.srgssr.pillarbox.player.extension.containsImageTrack import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.StateFlow import kotlin.time.Duration @@ -17,10 +19,12 @@ import kotlin.time.Duration * * @param player The [Player] whose progress needs to be tracked. * @param coroutineScope The [CoroutineScope] used for managing [StateFlow]s. + * @param imageOutput The [ImageOutput] to render the image track. */ class SmoothProgressTrackerState( private val player: PillarboxExoPlayer, - coroutineScope: CoroutineScope + coroutineScope: CoroutineScope, + private val imageOutput: ImageOutput = ImageOutput.NO_OP, ) : ProgressTrackerState { private var storedSeekParameters = player.seekParameters private var storedPlayWhenReady = player.playWhenReady @@ -41,13 +45,18 @@ class SmoothProgressTrackerState( player.setSeekParameters(SeekParameters.CLOSEST_SYNC) player.smoothSeekingEnabled = true player.playWhenReady = false - player.trackSelectionParameters = player.trackSelectionParameters.buildUpon() - .setPreferredVideoRoleFlags(C.ROLE_FLAG_TRICK_PLAY) - .setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true) - .setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, true) - .setTrackTypeDisabled(C.TRACK_TYPE_METADATA, true) - .setTrackTypeDisabled(C.TRACK_TYPE_IMAGE, true) - .build() + player.trackSelectionParameters = player.trackSelectionParameters.buildUpon().apply { + setPreferredVideoRoleFlags(C.ROLE_FLAG_TRICK_PLAY) + setTrackTypeDisabled(C.TRACK_TYPE_TEXT, true) + setTrackTypeDisabled(C.TRACK_TYPE_AUDIO, true) + setTrackTypeDisabled(C.TRACK_TYPE_METADATA, true) + if (player.currentTracks.containsImageTrack() && imageOutput != ImageOutput.NO_OP) { + setPrioritizeImageOverVideoEnabled(true) + } else { + setTrackTypeDisabled(C.TRACK_TYPE_IMAGE, true) + } + }.build() + player.setImageOutput(imageOutput) } player.seekTo(progress.inWholeMilliseconds) } @@ -59,5 +68,6 @@ class SmoothProgressTrackerState( player.smoothSeekingEnabled = storedSmoothSeeking player.setSeekParameters(storedSeekParameters) player.playWhenReady = storedPlayWhenReady + player.setImageOutput(null) } } From 57bf81232b81abae8c1de5fd59fb3fc00f885246 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Mon, 25 Nov 2024 11:39:25 +0100 Subject: [PATCH 07/11] Upgrade to Coil 3.0.3 (#801) --- gradle/libs.versions.toml | 10 ++++-- gradle/wrapper/gradle-wrapper.properties | 2 +- pillarbox-demo-tv/build.gradle.kts | 7 +++- .../src/main/AndroidManifest.xml | 1 + .../pillarbox/demo/tv/DemoApplication.kt | 33 +++++++++++++++++++ .../demo/tv/ui/examples/ExamplesHome.kt | 2 +- .../pillarbox/demo/tv/ui/lists/ListsHome.kt | 2 +- .../tv/ui/player/compose/MediaMetadataView.kt | 2 +- pillarbox-demo/build.gradle.kts | 6 +++- .../srgssr/pillarbox/demo/DemoApplication.kt | 26 +++++++++++++-- .../ui/player/controls/MediaMetadataView.kt | 2 +- .../ui/showcases/layouts/ChapterShowcase.kt | 2 +- .../layouts/thumbnail/ThumbnailViewModel.kt | 16 ++++----- 13 files changed, 88 insertions(+), 23 deletions(-) create mode 100644 pillarbox-demo-tv/src/main/java/ch/srgssr/pillarbox/demo/tv/DemoApplication.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f94e1e707..4d170abca 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ androidx-test-ext-junit = "1.2.1" androidx-test-monitor = "1.7.2" androidx-test-runner = "1.6.2" androidx-tv-material = "1.0.0" -coil = "2.7.0" +coil = "3.0.3" comscore = "6.11.1" dependency-analysis-gradle-plugin = "2.4.2" detekt = "1.23.7" @@ -63,8 +63,12 @@ androidx-test-core = { module = "androidx.test:core", version.ref = "androidx-te androidx-test-monitor = { module = "androidx.test:monitor", version.ref = "androidx-test-monitor" } androidx-test-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } androidx-tv-material = { module = "androidx.tv:tv-material", version.ref = "androidx-tv-material" } -coil = { group = "io.coil-kt", name = "coil-compose", version.ref = "coil" } -coil-base = { module = "io.coil-kt:coil-compose-base", version.ref = "coil" } +coil = { group = "io.coil-kt.coil3", name = "coil", version.ref = "coil" } +coil-compose = { group = "io.coil-kt.coil3", name = "coil-compose", version.ref = "coil" } +coil-core = { group = "io.coil-kt.coil3", name = "coil-core", version.ref = "coil" } +coil-network-cache-control = { group = "io.coil-kt.coil3", name = "coil-network-cache-control", version.ref = "coil" } +coil-network-core = { group = "io.coil-kt.coil3", name = "coil-network-core", version.ref = "coil" } +coil-network-okhttp = { group = "io.coil-kt.coil3", name = "coil-network-okhttp", version.ref = "coil" } dokka-gradle-plugin = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "dokka" } json = { module = "org.json:json", version.ref = "json" } kotlin-gradle-plugin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7cf748e74..c1d5e0185 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/pillarbox-demo-tv/build.gradle.kts b/pillarbox-demo-tv/build.gradle.kts index c9a27220e..892f6dba2 100644 --- a/pillarbox-demo-tv/build.gradle.kts +++ b/pillarbox-demo-tv/build.gradle.kts @@ -41,10 +41,15 @@ dependencies { implementation(libs.androidx.paging.compose) implementation(libs.androidx.tv.material) implementation(libs.coil) - implementation(libs.coil.base) + implementation(libs.coil.compose) + implementation(libs.coil.core) + implementation(libs.coil.network.cache.control) + implementation(libs.coil.network.core) + implementation(libs.coil.network.okhttp) implementation(libs.kotlin.stdlib) implementation(libs.kotlinx.coroutines.core) implementation(libs.kotlinx.serialization.core) + implementation(libs.okhttp) implementation(libs.srg.data) implementation(libs.srg.dataprovider.retrofit) diff --git a/pillarbox-demo-tv/src/main/AndroidManifest.xml b/pillarbox-demo-tv/src/main/AndroidManifest.xml index 40291e0f5..ac6094667 100644 --- a/pillarbox-demo-tv/src/main/AndroidManifest.xml +++ b/pillarbox-demo-tv/src/main/AndroidManifest.xml @@ -12,6 +12,7 @@ android:required="false" /> - val bitmap = (result as BitmapDrawable).bitmap + val bitmap = (result as BitmapImage).bitmap onComplete(bitmap) } .build() From 036051f92a4ec90d96c7be7c51b909ccdd6e55c5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 25 Nov 2024 21:15:15 +0100 Subject: [PATCH 08/11] Bump robolectric from 4.14 to 4.14.1 (#806) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 4d170abca..69933bedf 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -32,7 +32,7 @@ kotlinx-serialization = "1.7.3" ktor = "3.0.1" mockk = "1.13.13" okhttp = "4.12.0" -robolectric = "4.14" +robolectric = "4.14.1" srg-data-provider = "0.10.1" tag-commander-core = "5.4.3" tag-commander-server-side = "5.5.2" From 527a9d1eadd17476cb0892624ccbc6d44380cfd8 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 26 Nov 2024 07:26:21 +0100 Subject: [PATCH 09/11] Bump com.autonomousapps.dependency-analysis from 2.4.2 to 2.5.0 (#807) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- gradle/libs.versions.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 69933bedf..d31cee48b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,7 +18,7 @@ androidx-test-runner = "1.6.2" androidx-tv-material = "1.0.0" coil = "3.0.3" comscore = "6.11.1" -dependency-analysis-gradle-plugin = "2.4.2" +dependency-analysis-gradle-plugin = "2.5.0" detekt = "1.23.7" dokka = "2.0.0-Beta" guava = "33.0.0-android" From fae1570c4baa5fe79f348eac4aa74a76789a4200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Joaquim=20St=C3=A4hli?= Date: Tue, 26 Nov 2024 11:46:56 +0100 Subject: [PATCH 10/11] Move lint.xml to the root of the project (#809) --- .../ch/srgssr/pillarbox/gradle/internal/ProjectExtensions.kt | 1 - config/lint/lint.xml => lint.xml | 0 2 files changed, 1 deletion(-) rename config/lint/lint.xml => lint.xml (100%) 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 8d8a32b15..0d40743bf 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 @@ -47,7 +47,6 @@ internal fun Project.configureAndroidLintModule(extension: CommonExtension<*, *, abortOnError = true checkAllWarnings = true checkDependencies = true - lintConfig = rootProject.projectDir.resolve("config/lint/lint.xml") sarifReport = true sarifOutput = rootProject.projectDir.resolve("build/reports/android-lint/$name.sarif") disable.add("LogConditional") diff --git a/config/lint/lint.xml b/lint.xml similarity index 100% rename from config/lint/lint.xml rename to lint.xml From 0a658dcf6e21ce2c15e159020fcccf5fe24d8cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABtan=20Muller?= Date: Wed, 27 Nov 2024 08:10:46 +0100 Subject: [PATCH 11/11] Update to AndroidX Media3 1.5.0 (#794) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Joaquim Stähli --- .github/dependabot.yml | 1 + gradle/libs.versions.toml | 4 +- .../commandersact/CommandersActStreaming.kt | 3 +- .../pillarbox/demo/shared/data/Playlist.kt | 4 +- .../pillarbox/demo/shared/di/PlayerModule.kt | 3 ++ .../ui/showcases/layouts/StoryViewModel.kt | 27 +++++++------ .../pillarbox/player/PillarboxBuilder.kt | 14 ++++++- .../pillarbox/player/PillarboxLoadControl.kt | 21 ++++++---- .../player/PillarboxPreloadManager.kt | 39 ++++++++----------- .../pillarbox/player/PreloadConfiguration.kt | 17 ++++++++ .../pillarbox/player/extension/Format.kt | 3 ++ .../session/PillarboxMediaController.kt | 23 +---------- .../pillarbox/player/extension/FormatTest.kt | 5 ++- 13 files changed, 89 insertions(+), 75 deletions(-) create mode 100644 pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PreloadConfiguration.kt diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 8dce1c64c..125249b62 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -42,3 +42,4 @@ updates: ignore: - dependency-name: "com.comscore:*" - dependency-name: "com.tagcommander.lib:*" + - dependency-name: "com.google.guava:*" # Guava is updated together with AndroidX Media3 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d31cee48b..8dc69f15d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -8,7 +8,7 @@ androidx-core = "1.15.0" androidx-datastore = "1.1.1" androidx-fragment = "1.8.5" androidx-lifecycle = "2.8.7" -androidx-media3 = "1.4.1" +androidx-media3 = "1.5.0" androidx-navigation = "2.8.4" androidx-paging = "3.3.4" androidx-test-core = "1.6.1" @@ -21,7 +21,7 @@ comscore = "6.11.1" dependency-analysis-gradle-plugin = "2.5.0" detekt = "1.23.7" dokka = "2.0.0-Beta" -guava = "33.0.0-android" +guava = "33.3.1-android" json = "20240303" junit = "4.13.2" kotlin = "2.0.21" diff --git a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreaming.kt b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreaming.kt index 19f6bbbd0..1543a9ff6 100644 --- a/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreaming.kt +++ b/pillarbox-core-business/src/main/java/ch/srgssr/pillarbox/core/business/tracker/commandersact/CommandersActStreaming.kt @@ -202,8 +202,7 @@ internal class CommandersActStreaming( ?: C.LANGUAGE_UNDETERMINED event.audioTrackLanguage = audioTrackLanguage - - event.audioTrackHasAudioDescription = currentAudioTrack?.format?.hasAccessibilityRoles() ?: false + event.audioTrackHasAudioDescription = currentAudioTrack?.format?.hasAccessibilityRoles() == true } override fun onIsPlayingChanged(eventTime: EventTime, isPlaying: Boolean) { diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/Playlist.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/Playlist.kt index a48d6d844..1f33d0ce0 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/Playlist.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/data/Playlist.kt @@ -498,9 +498,9 @@ data class Playlist(val title: String, val items: List, val descriptio urn = "urn:srf:video:unknown", description = "Content that does not exist" ), - DemoItem.URN( + DemoItem.URL( title = "Custom MediaSource", - urn = "https://custom-media.ch/fondue", + uri = "https://custom-media.ch/fondue", description = "Using a custom CustomMediaSource" ), BlockedTimeRangeAssetLoader.DemoItemBlockedTimeRangeAtStartAndEnd, diff --git a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt index cf0db2a8e..18bd2e10c 100644 --- a/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt +++ b/pillarbox-demo-shared/src/main/java/ch/srgssr/pillarbox/demo/shared/di/PlayerModule.kt @@ -15,9 +15,11 @@ import ch.srgssr.pillarbox.demo.shared.source.BlockedTimeRangeAssetLoader import ch.srgssr.pillarbox.demo.shared.source.CustomAssetLoader import ch.srgssr.pillarbox.demo.shared.ui.integrationLayer.data.ILRepository import ch.srgssr.pillarbox.player.PillarboxExoPlayer +import ch.srgssr.pillarbox.player.PreloadConfiguration import okhttp3.Interceptor import okhttp3.Response import java.net.URL +import kotlin.time.Duration.Companion.seconds import ch.srg.dataProvider.integrationlayer.request.IlHost as DataProviderIlHost /** @@ -31,6 +33,7 @@ object PlayerModule { return PillarboxExoPlayer(context = context) { +CustomAssetLoader(context) +BlockedTimeRangeAssetLoader(context) + preloadConfiguration(PreloadConfiguration(10.seconds)) } } diff --git a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/StoryViewModel.kt b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/StoryViewModel.kt index fcf224389..6d846f994 100644 --- a/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/StoryViewModel.kt +++ b/pillarbox-demo/src/main/java/ch/srgssr/pillarbox/demo/ui/showcases/layouts/StoryViewModel.kt @@ -14,7 +14,7 @@ import androidx.media3.common.MediaItem import androidx.media3.common.Player import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.preload.DefaultPreloadManager.Status -import androidx.media3.exoplayer.source.preload.DefaultPreloadManager.Status.STAGE_LOADED_TO_POSITION_MS +import androidx.media3.exoplayer.source.preload.DefaultPreloadManager.Status.STAGE_LOADED_FOR_DURATION_MS import androidx.media3.exoplayer.source.preload.TargetPreloadStatusControl import ch.srgssr.pillarbox.core.business.PillarboxExoPlayer import ch.srgssr.pillarbox.core.business.source.SRGAssetLoader @@ -36,6 +36,14 @@ class StoryViewModel(application: Application) : AndroidViewModel(application) { private val mediaSourceFactory = PillarboxMediaSourceFactory(application).apply { addAssetLoader(SRGAssetLoader(application)) } + private val loadControl = PillarboxLoadControl( + bufferDurations = PillarboxLoadControl.BufferDurations( + minBufferDuration = 5.seconds, + maxBufferDuration = 20.seconds, + bufferForPlayback = 500.milliseconds, + bufferForPlaybackAfterRebuffer = 1.seconds, + ), + ) private val preloadManager = PillarboxPreloadManager( context = application, targetPreloadStatusControl = StoryPreloadStatusControl(), @@ -44,17 +52,8 @@ class StoryViewModel(application: Application) : AndroidViewModel(application) { parameters = parameters.buildUpon() .setForceLowestBitrate(true) .build() - } - ) - - private val loadControl = PillarboxLoadControl( - bufferDurations = PillarboxLoadControl.BufferDurations( - minBufferDuration = 5.seconds, - maxBufferDuration = 20.seconds, - bufferForPlayback = 500.milliseconds, - bufferForPlaybackAfterRebuffer = 1_000.milliseconds, - ), - allocator = preloadManager.allocator, + }, + loadControl = loadControl, ) private var currentPage = C.INDEX_UNSET @@ -160,8 +159,8 @@ class StoryViewModel(application: Application) : AndroidViewModel(application) { val offset = abs(rankingData - currentPage) return when (offset) { - 1 -> Status(STAGE_LOADED_TO_POSITION_MS, 1.seconds.inWholeMicroseconds) - 2, 3, 4 -> Status(STAGE_LOADED_TO_POSITION_MS, 1.milliseconds.inWholeMicroseconds) + 1 -> Status(STAGE_LOADED_FOR_DURATION_MS, 1.seconds.inWholeMilliseconds) + 2, 3, 4 -> Status(STAGE_LOADED_FOR_DURATION_MS, 1.milliseconds.inWholeMilliseconds) else -> null } } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxBuilder.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxBuilder.kt index 13bfd5af2..a8cdeb0b3 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxBuilder.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxBuilder.kt @@ -58,6 +58,7 @@ abstract class PillarboxBuilder { private var playbackLooper: Looper? = null private var seekBackwardIncrement: Duration = C.DEFAULT_SEEK_BACK_INCREMENT_MS.milliseconds private var seekForwardIncrement: Duration = C.DEFAULT_SEEK_FORWARD_INCREMENT_MS.milliseconds + private var preloadConfiguration = ExoPlayer.PreloadConfiguration.DEFAULT /** * Add an [AssetLoader] to the [PillarboxExoPlayer]. @@ -192,6 +193,15 @@ abstract class PillarboxBuilder { this.seekForwardIncrement = seekForwardIncrement } + /** + * Set the [ExoPlayer.PreloadConfiguration] used by the player. + * + * @param preloadConfiguration The [ExoPlayer.PreloadConfiguration]. + */ + fun preloadConfiguration(preloadConfiguration: ExoPlayer.PreloadConfiguration) { + this.preloadConfiguration = preloadConfiguration + } + /** * Create a new instance of [PillarboxExoPlayer]. * @@ -205,7 +215,9 @@ abstract class PillarboxBuilder { coroutineContext = coroutineContext, exoPlayer = createExoPlayerBuilder(context).build(), monitoringMessageHandler = monitoring, - ) + ).apply { + preloadConfiguration = this@PillarboxBuilder.preloadConfiguration + } } /** 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 d74d1c4ee..e58cfec1b 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 @@ -6,9 +6,9 @@ package ch.srgssr.pillarbox.player import androidx.media3.common.C import androidx.media3.common.Timeline +import androidx.media3.common.util.NullableType 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 @@ -46,6 +46,14 @@ class PillarboxLoadControl( defaultLoadControl.onPrepared(playerId) } + override fun onTracksSelected( + parameters: LoadControl.Parameters, + trackGroups: TrackGroupArray, + trackSelections: Array, + ) { + defaultLoadControl.onTracksSelected(parameters, trackGroups, trackSelections) + } + override fun onStopped(playerId: PlayerId) { defaultLoadControl.onStopped(playerId) } @@ -70,15 +78,12 @@ class PillarboxLoadControl( return defaultLoadControl.shouldContinueLoading(parameters) } - override fun onTracksSelected( - playerId: PlayerId, + override fun shouldContinuePreloading( timeline: Timeline, mediaPeriodId: MediaSource.MediaPeriodId, - renderers: Array, - trackGroups: TrackGroupArray, - trackSelections: Array - ) { - defaultLoadControl.onTracksSelected(playerId, timeline, mediaPeriodId, renderers, trackGroups, trackSelections) + bufferedDurationUs: Long, + ): Boolean { + return defaultLoadControl.shouldContinuePreloading(timeline, mediaPeriodId, bufferedDurationUs) } override fun shouldStartPlayback(parameters: LoadControl.Parameters): Boolean { diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPreloadManager.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPreloadManager.kt index 3a9ebca57..fae7a0b9f 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPreloadManager.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PillarboxPreloadManager.kt @@ -10,17 +10,15 @@ import android.os.Looper import android.os.Process import androidx.media3.common.C import androidx.media3.common.MediaItem -import androidx.media3.exoplayer.DefaultRendererCapabilitiesList -import androidx.media3.exoplayer.RendererCapabilitiesList +import androidx.media3.exoplayer.LoadControl +import androidx.media3.exoplayer.RenderersFactory import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.preload.DefaultPreloadManager import androidx.media3.exoplayer.source.preload.DefaultPreloadManager.Status -import androidx.media3.exoplayer.source.preload.DefaultPreloadManager.Status.STAGE_LOADED_TO_POSITION_MS +import androidx.media3.exoplayer.source.preload.DefaultPreloadManager.Status.STAGE_LOADED_FOR_DURATION_MS import androidx.media3.exoplayer.source.preload.TargetPreloadStatusControl import androidx.media3.exoplayer.trackselection.TrackSelector -import androidx.media3.exoplayer.upstream.Allocator import androidx.media3.exoplayer.upstream.BandwidthMeter -import androidx.media3.exoplayer.upstream.DefaultAllocator import ch.srgssr.pillarbox.player.source.PillarboxMediaSourceFactory import kotlin.math.abs import kotlin.time.Duration.Companion.milliseconds @@ -34,8 +32,8 @@ import kotlin.time.Duration.Companion.seconds * @param mediaSourceFactory The [PillarboxMediaSourceFactory] to create each [MediaSource]. * @param trackSelector The [TrackSelector] for this preload manager. * @param bandwidthMeter The [BandwidthMeter] for this preload manager. - * @param rendererCapabilitiesListFactory The [RendererCapabilitiesList.Factory] for this preload manager. - * @property allocator The [Allocator] for this preload manager. Have to be the same as the one used by the Player. + * @param renderersFactory The [RenderersFactory] for this preload manager. + * @param loadControl The [LoadControl] for this preload manager. * @param playbackThread The [Thread] on which the players run. Its lifecycle is handled internally by [PillarboxPreloadManager]. * * @see DefaultPreloadManager @@ -46,10 +44,8 @@ class PillarboxPreloadManager( mediaSourceFactory: PillarboxMediaSourceFactory = PillarboxMediaSourceFactory(context), trackSelector: TrackSelector = PillarboxTrackSelector(context), bandwidthMeter: BandwidthMeter = PillarboxBandwidthMeter(context), - rendererCapabilitiesListFactory: RendererCapabilitiesList.Factory = DefaultRendererCapabilitiesList.Factory( - PillarboxRenderersFactory(context) - ), - val allocator: DefaultAllocator = DefaultAllocator(false, C.DEFAULT_BUFFER_SEGMENT_SIZE), + renderersFactory: RenderersFactory = PillarboxRenderersFactory(context), + loadControl: LoadControl = PillarboxLoadControl(), private val playbackThread: HandlerThread = HandlerThread("PillarboxPreloadManager:Playback", Process.THREAD_PRIORITY_AUDIO), ) { private val preloadManager: DefaultPreloadManager @@ -82,15 +78,14 @@ class PillarboxPreloadManager( playbackThread.start() playbackLooper = playbackThread.looper trackSelector.init({}, bandwidthMeter) - preloadManager = DefaultPreloadManager( - targetPreloadStatusControl ?: DefaultTargetPreloadStatusControl(), - mediaSourceFactory, - trackSelector, - bandwidthMeter, - rendererCapabilitiesListFactory, - allocator, - playbackLooper, - ) + preloadManager = DefaultPreloadManager.Builder(context, targetPreloadStatusControl ?: DefaultTargetPreloadStatusControl()) + .setMediaSourceFactory(mediaSourceFactory) + .setTrackSelectorFactory { trackSelector } + .setBandwidthMeter(bandwidthMeter) + .setRenderersFactory(renderersFactory) + .setLoadControl(loadControl) + .setPreloadLooper(playbackLooper) + .build() } /** @@ -187,8 +182,8 @@ class PillarboxPreloadManager( val offset = abs(rankingData - currentPlayingIndex) return when (offset) { - 1 -> Status(STAGE_LOADED_TO_POSITION_MS, 1.seconds.inWholeMicroseconds) - 2, 3 -> Status(STAGE_LOADED_TO_POSITION_MS, 500.milliseconds.inWholeMicroseconds) + 1 -> Status(STAGE_LOADED_FOR_DURATION_MS, 1.seconds.inWholeMilliseconds) + 2, 3 -> Status(STAGE_LOADED_FOR_DURATION_MS, 500.milliseconds.inWholeMilliseconds) else -> null } } diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PreloadConfiguration.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PreloadConfiguration.kt new file mode 100644 index 000000000..85a45ec53 --- /dev/null +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/PreloadConfiguration.kt @@ -0,0 +1,17 @@ +/* + * Copyright (c) SRG SSR. All rights reserved. + * License information is available from the LICENSE file. + */ +package ch.srgssr.pillarbox.player + +import androidx.media3.common.C +import androidx.media3.exoplayer.ExoPlayer.PreloadConfiguration +import kotlin.time.Duration + +/** + * @param targetPreloadDuration The target duration to preload or `null` to disable preloading. + * @return [PreloadConfiguration] + */ +fun PreloadConfiguration(targetPreloadDuration: Duration?): PreloadConfiguration { + return PreloadConfiguration(targetPreloadDuration?.inWholeMicroseconds ?: C.TIME_UNSET) +} diff --git a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Format.kt b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Format.kt index b6ca4ece2..240a6b7d3 100644 --- a/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Format.kt +++ b/pillarbox-player/src/main/java/ch/srgssr/pillarbox/player/extension/Format.kt @@ -115,6 +115,9 @@ fun Format.roleString(): String { if (hasRole(C.ROLE_FLAG_TRICK_PLAY)) { roleFlags.add("trick-play") } + if (hasRole(C.ROLE_FLAG_AUXILIARY)) { + roleFlags.add("auxiliary") + } return roleFlags.joinToString(",") } 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 7f939ba2f..de411e0e3 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 @@ -201,7 +201,7 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { */ @get:UnstableApi val customLayout: ImmutableList - get() = mediaController.getCustomLayout() + get() = mediaController.customLayout /** * Session extras @@ -519,31 +519,10 @@ open class PillarboxMediaController internal constructor() : PillarboxPlayer { mediaController.seekForward() } - @UnstableApi - @Deprecated("Use #hasPreviousMediaItem() instead.", ReplaceWith("hasPreviousMediaItem()")) - override fun hasPrevious(): Boolean { - @Suppress("DEPRECATION") - return mediaController.hasPrevious() - } - - @UnstableApi - @Deprecated("Use #hasPreviousMediaItem() instead.", ReplaceWith("hasPreviousMediaItem()")) - override fun hasPreviousWindow(): Boolean { - @Suppress("DEPRECATION") - return mediaController.hasPreviousWindow() - } - override fun hasPreviousMediaItem(): Boolean { return mediaController.hasPreviousMediaItem() } - @UnstableApi - @Deprecated("Use #seekToPreviousMediaItem() instead.", ReplaceWith("seekToPreviousMediaItem()")) - override fun previous() { - @Suppress("DEPRECATION") - mediaController.previous() - } - @UnstableApi @Deprecated("Use #seekToPreviousMediaItem() instead.", ReplaceWith("seekToPreviousMediaItem()")) override fun seekToPreviousWindow() { diff --git a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/extension/FormatTest.kt b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/extension/FormatTest.kt index f077b2d2a..36391788f 100644 --- a/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/extension/FormatTest.kt +++ b/pillarbox-player/src/test/java/ch/srgssr/pillarbox/player/extension/FormatTest.kt @@ -164,13 +164,14 @@ class FormatTest { C.ROLE_FLAG_ENHANCED_DIALOG_INTELLIGIBILITY or C.ROLE_FLAG_TRANSCRIBES_DIALOG or C.ROLE_FLAG_EASY_TO_READ or - C.ROLE_FLAG_TRICK_PLAY + C.ROLE_FLAG_TRICK_PLAY or + C.ROLE_FLAG_AUXILIARY ) .build() assertEquals( "main,alt,supplementary,commentary,dub,emergency,caption,subtitle,sign,describes-video,describes-music," + - "enhanced-intelligibility,transcribes-dialog,easy-read,trick-play", + "enhanced-intelligibility,transcribes-dialog,easy-read,trick-play,auxiliary", format.roleString() ) }