From ac30ad95ead1ef210cbc315113e3f88628a59314 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 3 Mar 2026 14:56:21 +0100 Subject: [PATCH 01/29] Keep GitHub Actions up-to-date --- .github/dependabot.yml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .github/dependabot.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..ca79ca5 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,6 @@ +version: 2 +updates: + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly From f701aa16018f08038643b1982bed5baa37a48a94 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 3 Mar 2026 14:59:58 +0100 Subject: [PATCH 02/29] Update GitHub Actions --- .github/workflows/ci.yml | 4 ++-- .github/workflows/create-release.yml | 4 ++-- .github/workflows/docs.yml | 8 ++++---- .github/workflows/publish.yml | 6 +++--- .github/workflows/sync-develop.yml | 4 ++-- 5 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6891662..82fbbc0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,9 +16,9 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: 17 diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index 441594c..be3c02c 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -18,13 +18,13 @@ jobs: # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - name: Create app token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v2 id: app-token with: app-id: ${{ vars.THEOPLAYER_BOT_APP_ID }} private-key: ${{ secrets.THEOPLAYER_BOT_PRIVATE_KEY }} - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: token: ${{ steps.app-token.outputs.token }} - name: Configure Git user diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 777996e..b71f3a4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -25,11 +25,11 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Setup Pages - uses: actions/configure-pages@v4 + uses: actions/configure-pages@v5 - name: Setup Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: 17 @@ -45,7 +45,7 @@ jobs: source: ./site destination: ./_site - name: Upload artifact - uses: actions/upload-pages-artifact@v3 + uses: actions/upload-pages-artifact@v4 # Deployment job deploy: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index fecd023..ea1fac1 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,13 +21,13 @@ jobs: id-token: write steps: - name: Create app token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v2 id: app-token with: app-id: ${{ vars.THEOPLAYER_BOT_APP_ID }} private-key: ${{ secrets.THEOPLAYER_BOT_PRIVATE_KEY }} - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: token: ${{ steps.app-token.outputs.token }} - name: Configure Git user @@ -35,7 +35,7 @@ jobs: git config user.name 'theoplayer-bot[bot]' git config user.email '873105+theoplayer-bot[bot]@users.noreply.github.com' - name: Setup Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: 17 diff --git a/.github/workflows/sync-develop.yml b/.github/workflows/sync-develop.yml index 1c8d03b..a8bd259 100644 --- a/.github/workflows/sync-develop.yml +++ b/.github/workflows/sync-develop.yml @@ -9,13 +9,13 @@ jobs: runs-on: ubuntu-latest steps: - name: Create app token - uses: actions/create-github-app-token@v1 + uses: actions/create-github-app-token@v2 id: app-token with: app-id: ${{ vars.THEOPLAYER_BOT_APP_ID }} private-key: ${{ secrets.THEOPLAYER_BOT_PRIVATE_KEY }} - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 with: token: ${{ steps.app-token.outputs.token }} ref: develop From 53f5e7eb339fc77cdc1d35e25f87ebb5431b95f9 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 3 Mar 2026 15:01:32 +0100 Subject: [PATCH 03/29] Update `setup-gradle` action to version 5 --- .github/workflows/ci.yml | 4 +--- .github/workflows/docs.yml | 4 +--- .github/workflows/publish.yml | 4 +--- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82fbbc0..36d6ccd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,9 +22,7 @@ jobs: with: distribution: temurin java-version: 17 - - name: Validate Gradle wrapper - uses: gradle/actions/wrapper-validation@v3 - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v5 - name: Assemble run: ./gradlew assembleRelease diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b71f3a4..d01e365 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -33,10 +33,8 @@ jobs: with: distribution: temurin java-version: 17 - - name: Validate Gradle wrapper - uses: gradle/actions/wrapper-validation@v3 - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v5 - name: Build API documentation with Dokka run: ./gradlew ui:dokkaGeneratePublicationHtml - name: Build with Jekyll diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index ea1fac1..0e194e3 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -39,10 +39,8 @@ jobs: with: distribution: temurin java-version: 17 - - name: Validate Gradle wrapper - uses: gradle/actions/wrapper-validation@v3 - name: Setup Gradle - uses: gradle/actions/setup-gradle@v3 + uses: gradle/actions/setup-gradle@v5 - name: Publish package run: ./gradlew publish env: From ae364d38c875fd04e7cb1b6be39eaa82c7fd097c Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 3 Mar 2026 15:12:43 +0100 Subject: [PATCH 04/29] Enable Gradle configuration cache https://github.com/gradle/actions/blob/v5.0.0/docs/setup-gradle.md#saving-configuration-cache-data --- .github/workflows/ci.yml | 4 +++- .github/workflows/docs.yml | 4 +++- .github/workflows/publish.yml | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 36d6ccd..e6e4e1c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -24,5 +24,7 @@ jobs: java-version: 17 - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 + with: + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Assemble - run: ./gradlew assembleRelease + run: ./gradlew assembleRelease --configuration-cache diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index d01e365..50db5c1 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -35,8 +35,10 @@ jobs: java-version: 17 - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 + with: + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Build API documentation with Dokka - run: ./gradlew ui:dokkaGeneratePublicationHtml + run: ./gradlew ui:dokkaGeneratePublicationHtml --configuration-cache - name: Build with Jekyll uses: actions/jekyll-build-pages@v1 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0e194e3..aefae44 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -41,8 +41,10 @@ jobs: java-version: 17 - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 + with: + cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Publish package - run: ./gradlew publish + run: ./gradlew publish --configuration-cache env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPOSILITE_USERNAME: ${{ secrets.REPOSILITE_USERNAME }} From b2df3719bbfbd7c51069f53ef5667670f0e3c260 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 3 Mar 2026 15:23:10 +0100 Subject: [PATCH 05/29] Allow writing to cache from `main` and `develop` --- .github/workflows/ci.yml | 1 + .github/workflows/docs.yml | 1 + .github/workflows/publish.yml | 1 + 3 files changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e6e4e1c..17da847 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -25,6 +25,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 with: + cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }} cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Assemble run: ./gradlew assembleRelease --configuration-cache diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 50db5c1..a39ec61 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -36,6 +36,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 with: + cache-read-only: true cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Build API documentation with Dokka run: ./gradlew ui:dokkaGeneratePublicationHtml --configuration-cache diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index aefae44..0c50262 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -42,6 +42,7 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@v5 with: + cache-read-only: true cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Publish package run: ./gradlew publish --configuration-cache From f745452f8d8862f41863c9a4a85ee78d068aabad Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 17 Mar 2026 07:56:57 +0100 Subject: [PATCH 06/29] Bump actions/create-github-app-token from 2 to 3 (#87) Bumps [actions/create-github-app-token](https://github.com/actions/create-github-app-token) from 2 to 3. - [Release notes](https://github.com/actions/create-github-app-token/releases) - [Commits](https://github.com/actions/create-github-app-token/compare/v2...v3) --- updated-dependencies: - dependency-name: actions/create-github-app-token dependency-version: '3' dependency-type: direct:production update-type: version-update:semver-major ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/create-release.yml | 2 +- .github/workflows/publish.yml | 2 +- .github/workflows/sync-develop.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index be3c02c..b38670a 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -18,7 +18,7 @@ jobs: # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ steps: - name: Create app token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 id: app-token with: app-id: ${{ vars.THEOPLAYER_BOT_APP_ID }} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 0c50262..f32c73c 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -21,7 +21,7 @@ jobs: id-token: write steps: - name: Create app token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 id: app-token with: app-id: ${{ vars.THEOPLAYER_BOT_APP_ID }} diff --git a/.github/workflows/sync-develop.yml b/.github/workflows/sync-develop.yml index a8bd259..c0d5ff9 100644 --- a/.github/workflows/sync-develop.yml +++ b/.github/workflows/sync-develop.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Create app token - uses: actions/create-github-app-token@v2 + uses: actions/create-github-app-token@v3 id: app-token with: app-id: ${{ vars.THEOPLAYER_BOT_APP_ID }} From 9f90289c0fb8df85bac61f90b52ade5bba666aa0 Mon Sep 17 00:00:00 2001 From: Alex Date: Tue, 17 Mar 2026 13:00:42 +0000 Subject: [PATCH 07/29] Add unit tests step to the CI workflow (#86) --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 17da847..63f5034 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,3 +29,5 @@ jobs: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Assemble run: ./gradlew assembleRelease --configuration-cache + - name: Run tests + run: ./gradlew test --configuration-cache From 460c031373c877904bd431e15ec8c2797cfd9739 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 16 Mar 2026 17:05:12 +0100 Subject: [PATCH 08/29] Bump minimum THEOplayer version to 7.6.0 --- 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 0b273d0..e1932a3 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -16,7 +16,7 @@ androidx-junit = "1.3.0" androidx-espresso = "3.7.0" androidx-mediarouter = "1.8.1" dokka = "2.0.0" -theoplayer = { prefer="10.11.0", strictly = "[5.0, 11.0)" } +theoplayer = { prefer="10.11.0", strictly = "[7.6.0, 11.0)" } [libraries] androidx-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "ktx" } From 86f3e27c385258ef15b2315e9cc78ebdcddb02c7 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 16 Mar 2026 17:28:20 +0100 Subject: [PATCH 09/29] Keep using `THEOplayerConfig.Builder.pipConfiguration` for now --- .../main/java/com/theoplayer/android/ui/demo/MainActivity.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt b/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt index 14e3ad5..ed29d6b 100644 --- a/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt +++ b/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt @@ -75,7 +75,7 @@ fun MainContent() { val context = LocalContext.current val theoplayerView = remember(context) { val config = THEOplayerConfig.Builder().apply { - pip(PipConfiguration.Builder().build()) + pipConfiguration(PipConfiguration.Builder().build()) }.build() THEOplayerView(context, config).apply { // Add ads integration through Google IMA From 93f0e4c8a7ecf57ccac4b4a551001f10030effc0 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 16 Mar 2026 17:55:06 +0100 Subject: [PATCH 10/29] Add flag to install lowest supported player version --- app/build.gradle.kts | 29 +++++++++++++++++++++++++---- gradle.properties | 2 ++ 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1ecef39..7d6bffe 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -82,7 +82,28 @@ dependencies { releaseImplementation(project(":ui")) "mavenImplementation"("com.theoplayer.android-ui:android-ui:1.+") - implementation(libs.theoplayer) - implementation(libs.theoplayer.ads.ima) - implementation(libs.theoplayer.cast) -} \ No newline at end of file + implementation(libs.theoplayer) { + useLowestSupportedPlayerVersion() + } + implementation(libs.theoplayer.ads.ima) { + useLowestSupportedPlayerVersion() + } + implementation(libs.theoplayer.cast) { + useLowestSupportedPlayerVersion() + } +} + +val installLowestSupportedPlayerVersion: String by project.ext + +fun ExternalDependency.useLowestSupportedPlayerVersion() { + if (installLowestSupportedPlayerVersion.toBooleanStrict()) { + val lowestVersion = versionConstraint.strictVersion + .removePrefix("[") + .split(",", limit = 2) + .first() + version { + prefer(lowestVersion) + strictly(lowestVersion) + } + } +} diff --git a/gradle.properties b/gradle.properties index c396b12..0554f97 100644 --- a/gradle.properties +++ b/gradle.properties @@ -28,3 +28,5 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true # The version of the THEOplayer Open Video UI for Android. version=1.13.2 +# (For testing purposes) Install the lowest supported THEOplayer version. +installLowestSupportedPlayerVersion=false From bc0e8cba9cc17b26e82e5b2cbaf27bd92c1aa094 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 16 Mar 2026 17:55:24 +0100 Subject: [PATCH 11/29] Test lowest supported player version on CI --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 63f5034..2206006 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,5 +29,7 @@ jobs: cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} - name: Assemble run: ./gradlew assembleRelease --configuration-cache + - name: Assemble with lowest supported player version + run: ./gradlew assembleRelease --configuration-cache -PinstallLowestSupportedPlayerVersion=true - name: Run tests run: ./gradlew test --configuration-cache From 3ffcab6b735d21fae0e88037ccc75196e8637191 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 16 Mar 2026 18:06:39 +0100 Subject: [PATCH 12/29] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1a3293..0114e9c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,12 @@ > - 🏠 Internal > - 💅 Polish +## Unreleased + +* 🐛 Changed the minimum supported THEOplayer version to 7.6.0. ([#85](https://github.com/THEOplayer/android-ui/pull/85)) + * This was effectively already the minimum version as of Open Video UI for Android version 1.7.2, but it wasn't noticed until now. + * Future versions will be properly tested with the minimum supported THEOplayer version to avoid similar compatibility issues. + ## v1.13.2 (2026-03-03) * 🐛 Fixed `PictureInPictureButton` to only be shown when the backing `THEOplayerView` has a valid `PiPConfiguration`. ([#81](https://github.com/THEOplayer/android-ui/pull/81)) From f635e6b507aaa724ea5bdf84fdd6219ff740c335 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 17 Mar 2026 14:03:58 +0100 Subject: [PATCH 13/29] Run tests first --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2206006..b0a05ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -27,9 +27,9 @@ jobs: with: cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }} cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + - name: Run tests + run: ./gradlew test --configuration-cache - name: Assemble run: ./gradlew assembleRelease --configuration-cache - name: Assemble with lowest supported player version run: ./gradlew assembleRelease --configuration-cache -PinstallLowestSupportedPlayerVersion=true - - name: Run tests - run: ./gradlew test --configuration-cache From cce9bf896cd65d523c1717e72ed6fce5df48acc1 Mon Sep 17 00:00:00 2001 From: Mattias Buelens <649348+MattiasBuelens@users.noreply.github.com> Date: Wed, 18 Mar 2026 15:14:57 +0100 Subject: [PATCH 14/29] Use product flavor for latest or minimum supported THEOplayer (#88) --- .github/workflows/ci.yml | 2 -- app/build.gradle.kts | 48 +++++++++++++++++++-------------------- gradle/libs.versions.toml | 4 ++++ 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b0a05ca..550d8c4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,5 +31,3 @@ jobs: run: ./gradlew test --configuration-cache - name: Assemble run: ./gradlew assembleRelease --configuration-cache - - name: Assemble with lowest supported player version - run: ./gradlew assembleRelease --configuration-cache -PinstallLowestSupportedPlayerVersion=true diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 7d6bffe..6176a8f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -38,6 +38,19 @@ android { matchingFallbacks += listOf("debug") } } + + flavorDimensions += "player" + productFlavors { + create("latestPlayer") { + // Use the latest supported THEOplayer version + dimension = "player" + } + create("minPlayer") { + // Use the minimum supported THEOplayer version + dimension = "player" + } + } + compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 @@ -59,6 +72,10 @@ android { } dependencies { + val mavenImplementation = configurations.getByName("mavenImplementation") + val latestPlayerImplementation = configurations.getByName("latestPlayerImplementation") + val minPlayerImplementation = configurations.getByName("minPlayerImplementation") + implementation(platform(libs.androidx.compose.bom)) implementation(libs.androidx.ktx) @@ -80,30 +97,13 @@ dependencies { debugImplementation(project(":ui")) releaseImplementation(project(":ui")) - "mavenImplementation"("com.theoplayer.android-ui:android-ui:1.+") + mavenImplementation("com.theoplayer.android-ui:android-ui:1.+") - implementation(libs.theoplayer) { - useLowestSupportedPlayerVersion() - } - implementation(libs.theoplayer.ads.ima) { - useLowestSupportedPlayerVersion() - } - implementation(libs.theoplayer.cast) { - useLowestSupportedPlayerVersion() - } -} + latestPlayerImplementation(libs.theoplayer) + latestPlayerImplementation(libs.theoplayer.ads.ima) + latestPlayerImplementation(libs.theoplayer.cast) -val installLowestSupportedPlayerVersion: String by project.ext - -fun ExternalDependency.useLowestSupportedPlayerVersion() { - if (installLowestSupportedPlayerVersion.toBooleanStrict()) { - val lowestVersion = versionConstraint.strictVersion - .removePrefix("[") - .split(",", limit = 2) - .first() - version { - prefer(lowestVersion) - strictly(lowestVersion) - } - } + minPlayerImplementation(libs.theoplayer.min) + minPlayerImplementation(libs.theoplayer.min.ads.ima) + minPlayerImplementation(libs.theoplayer.min.cast) } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index e1932a3..1869fa5 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,6 +17,7 @@ androidx-espresso = "3.7.0" androidx-mediarouter = "1.8.1" dokka = "2.0.0" theoplayer = { prefer="10.11.0", strictly = "[7.6.0, 11.0)" } +theoplayer-min = { strictly = "7.6.0" } [libraries] androidx-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "ktx" } @@ -44,6 +45,9 @@ junit4 = { group = "junit", name = "junit", version.ref = "junit4" } theoplayer = { group = "com.theoplayer.theoplayer-sdk-android", name = "core", version.ref = "theoplayer" } theoplayer-ads-ima = { group = "com.theoplayer.theoplayer-sdk-android", name = "integration-ads-ima", version.ref = "theoplayer" } theoplayer-cast = { group = "com.theoplayer.theoplayer-sdk-android", name = "integration-cast", version.ref = "theoplayer" } +theoplayer-min = { group = "com.theoplayer.theoplayer-sdk-android", name = "core", version.ref = "theoplayer-min" } +theoplayer-min-ads-ima = { group = "com.theoplayer.theoplayer-sdk-android", name = "integration-ads-ima", version.ref = "theoplayer-min" } +theoplayer-min-cast = { group = "com.theoplayer.theoplayer-sdk-android", name = "integration-cast", version.ref = "theoplayer-min" } [plugins] android-application = { id = "com.android.application", version.ref = "gradle" } From 736997628cf82387df51768fa624f9fe89db7422 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Fri, 20 Mar 2026 10:46:18 +0100 Subject: [PATCH 15/29] Check if Activity is in picture-in-picture mode --- ui/src/main/java/com/theoplayer/android/ui/Player.kt | 5 ++++- ui/src/main/java/com/theoplayer/android/ui/Util.kt | 8 +++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/Player.kt b/ui/src/main/java/com/theoplayer/android/ui/Player.kt index d65f518..0bfb13a 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Player.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Player.kt @@ -530,7 +530,10 @@ internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player } private fun updatePictureInPicture() { - pictureInPicture = theoplayerView?.piPManager?.isInPiP ?: false + val theoplayerView = theoplayerView ?: return + val activity = theoplayerView.context as? Activity + pictureInPicture = theoplayerView.piPManager?.isInPiP == true + || (activity?.isInPictureInPictureModeCompat() ?: false) } val presentationModeChangeListener = diff --git a/ui/src/main/java/com/theoplayer/android/ui/Util.kt b/ui/src/main/java/com/theoplayer/android/ui/Util.kt index 95b8502..eece45d 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Util.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Util.kt @@ -4,7 +4,9 @@ import android.app.Activity import android.content.pm.PackageManager import android.os.Build -// From android.content.pm.ActivityInfo +/** + * From [android.content.pm.ActivityInfo] + */ private const val FLAG_SUPPORTS_PICTURE_IN_PICTURE = 0x400000 /** @@ -15,4 +17,8 @@ internal fun Activity.supportsPictureInPictureMode(): Boolean { if (!packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)) return false val info = packageManager.getActivityInfo(componentName, 0) return (info.flags and FLAG_SUPPORTS_PICTURE_IN_PICTURE) != 0 +} + +internal fun Activity.isInPictureInPictureModeCompat(): Boolean { + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) isInPictureInPictureMode else false } \ No newline at end of file From 2c6a5aca6c5ab6d012d1878421e6b506b61fc339 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Fri, 20 Mar 2026 10:57:51 +0100 Subject: [PATCH 16/29] Move to PictureInPictureUtil --- ui/src/main/java/com/theoplayer/android/ui/Player.kt | 2 ++ .../android/ui/{Util.kt => util/PictureInPictureUtil.kt} | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) rename ui/src/main/java/com/theoplayer/android/ui/{Util.kt => util/PictureInPictureUtil.kt} (92%) diff --git a/ui/src/main/java/com/theoplayer/android/ui/Player.kt b/ui/src/main/java/com/theoplayer/android/ui/Player.kt index 0bfb13a..419614b 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Player.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Player.kt @@ -52,6 +52,8 @@ import com.theoplayer.android.api.player.track.texttrack.TextTrack import com.theoplayer.android.api.player.track.texttrack.TextTrackKind import com.theoplayer.android.api.player.track.texttrack.TextTrackMode import com.theoplayer.android.api.source.SourceDescription +import com.theoplayer.android.ui.util.isInPictureInPictureModeCompat +import com.theoplayer.android.ui.util.supportsPictureInPictureMode import com.theoplayer.android.api.event.track.mediatrack.audio.list.AddTrackEvent as AudioAddTrackEvent import com.theoplayer.android.api.event.track.mediatrack.audio.list.RemoveTrackEvent as AudioRemoveTrackEvent import com.theoplayer.android.api.event.track.mediatrack.audio.list.TrackListChangeEvent as AudioTrackListChangeEvent diff --git a/ui/src/main/java/com/theoplayer/android/ui/Util.kt b/ui/src/main/java/com/theoplayer/android/ui/util/PictureInPictureUtil.kt similarity index 92% rename from ui/src/main/java/com/theoplayer/android/ui/Util.kt rename to ui/src/main/java/com/theoplayer/android/ui/util/PictureInPictureUtil.kt index eece45d..5960705 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Util.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/PictureInPictureUtil.kt @@ -1,4 +1,4 @@ -package com.theoplayer.android.ui +package com.theoplayer.android.ui.util import android.app.Activity import android.content.pm.PackageManager From f2871b8facd58326d99c50c44f3beaadb9e5f743 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Fri, 20 Mar 2026 11:10:01 +0100 Subject: [PATCH 17/29] Listen for PIP mode changes --- .../java/com/theoplayer/android/ui/Player.kt | 11 +++---- .../com/theoplayer/android/ui/UIController.kt | 3 ++ .../android/ui/util/PictureInPictureUtil.kt | 31 +++++++++++++++++-- 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/Player.kt b/ui/src/main/java/com/theoplayer/android/ui/Player.kt index 419614b..c08bebc 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Player.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Player.kt @@ -52,7 +52,6 @@ import com.theoplayer.android.api.player.track.texttrack.TextTrack import com.theoplayer.android.api.player.track.texttrack.TextTrackKind import com.theoplayer.android.api.player.track.texttrack.TextTrackMode import com.theoplayer.android.api.source.SourceDescription -import com.theoplayer.android.ui.util.isInPictureInPictureModeCompat import com.theoplayer.android.ui.util.supportsPictureInPictureMode import com.theoplayer.android.api.event.track.mediatrack.audio.list.AddTrackEvent as AudioAddTrackEvent import com.theoplayer.android.api.event.track.mediatrack.audio.list.RemoveTrackEvent as AudioRemoveTrackEvent @@ -531,11 +530,11 @@ internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player theoplayerView?.piPManager?.exitPiP() } - private fun updatePictureInPicture() { - val theoplayerView = theoplayerView ?: return - val activity = theoplayerView.context as? Activity - pictureInPicture = theoplayerView.piPManager?.isInPiP == true - || (activity?.isInPictureInPictureModeCompat() ?: false) + internal var isActivityInPipMode: Boolean = false + + internal fun updatePictureInPicture() { + pictureInPicture = isActivityInPipMode + || theoplayerView?.piPManager?.isInPiP == true } val presentationModeChangeListener = diff --git a/ui/src/main/java/com/theoplayer/android/ui/UIController.kt b/ui/src/main/java/com/theoplayer/android/ui/UIController.kt index cb1216c..bf21669 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/UIController.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/UIController.kt @@ -50,6 +50,7 @@ import com.theoplayer.android.api.THEOplayerView import com.theoplayer.android.api.cast.chromecast.PlayerCastState import com.theoplayer.android.api.source.SourceDescription import com.theoplayer.android.ui.theme.THEOplayerTheme +import com.theoplayer.android.ui.util.rememberIsInPipMode import kotlinx.coroutines.delay import kotlin.time.Duration.Companion.milliseconds import kotlin.time.Duration.Companion.seconds @@ -500,6 +501,8 @@ internal fun rememberPlayerInternal(theoplayerView: THEOplayerView?): Player { } } + player.isActivityInPipMode = rememberIsInPipMode() + return player } diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/PictureInPictureUtil.kt b/ui/src/main/java/com/theoplayer/android/ui/util/PictureInPictureUtil.kt index 5960705..390a273 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/PictureInPictureUtil.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/PictureInPictureUtil.kt @@ -3,6 +3,16 @@ package com.theoplayer.android.ui.util import android.app.Activity import android.content.pm.PackageManager import android.os.Build +import androidx.activity.compose.LocalActivity +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.core.app.OnPictureInPictureModeChangedProvider +import androidx.core.app.PictureInPictureModeChangedInfo +import androidx.core.util.Consumer /** * From [android.content.pm.ActivityInfo] @@ -19,6 +29,23 @@ internal fun Activity.supportsPictureInPictureMode(): Boolean { return (info.flags and FLAG_SUPPORTS_PICTURE_IN_PICTURE) != 0 } -internal fun Activity.isInPictureInPictureModeCompat(): Boolean { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) isInPictureInPictureMode else false +/** + * Returns whether the activity is in picture-in-picture mode. + */ +@Composable +internal fun rememberIsInPipMode(): Boolean { + // https://developer.android.com/develop/ui/compose/system/picture-in-picture#handle-ui + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return false + val activity = LocalActivity.current + var pipMode by remember { mutableStateOf(activity?.isInPictureInPictureMode ?: false) } + if (activity is OnPictureInPictureModeChangedProvider) { + DisposableEffect(activity) { + val observer = Consumer { info -> + pipMode = info.isInPictureInPictureMode + } + activity.addOnPictureInPictureModeChangedListener(observer) + onDispose { activity.removeOnPictureInPictureModeChangedListener(observer) } + } + } + return pipMode } \ No newline at end of file From f25de993d018180651be3f335a7c7a72a1991a12 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Fri, 20 Mar 2026 14:09:55 +0100 Subject: [PATCH 18/29] Fix `Player.pictureInPicture` not correctly updating --- .../main/java/com/theoplayer/android/ui/Player.kt | 15 ++++++++------- .../com/theoplayer/android/ui/UIController.kt | 2 +- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/Player.kt b/ui/src/main/java/com/theoplayer/android/ui/Player.kt index c08bebc..342eaf7 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Player.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Player.kt @@ -508,8 +508,12 @@ internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player val fullscreenListener = FullscreenHandler.OnFullscreenChangeListener { updateFullscreen() } - override var pictureInPicture: Boolean by mutableStateOf(false) - private set + internal var activityInPipMode: Boolean by mutableStateOf(false) + private var pipManagerInPipMode: Boolean by mutableStateOf(false) + + override val pictureInPicture: Boolean by derivedStateOf { + activityInPipMode || pipManagerInPipMode + } override val pictureInPictureSupported: Boolean by lazy { val theoplayerView = theoplayerView ?: return@lazy false @@ -530,11 +534,8 @@ internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player theoplayerView?.piPManager?.exitPiP() } - internal var isActivityInPipMode: Boolean = false - - internal fun updatePictureInPicture() { - pictureInPicture = isActivityInPipMode - || theoplayerView?.piPManager?.isInPiP == true + private fun updatePictureInPicture() { + pipManagerInPipMode = theoplayerView?.piPManager?.isInPiP == true } val presentationModeChangeListener = diff --git a/ui/src/main/java/com/theoplayer/android/ui/UIController.kt b/ui/src/main/java/com/theoplayer/android/ui/UIController.kt index bf21669..cf94ca4 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/UIController.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/UIController.kt @@ -501,7 +501,7 @@ internal fun rememberPlayerInternal(theoplayerView: THEOplayerView?): Player { } } - player.isActivityInPipMode = rememberIsInPipMode() + player.activityInPipMode = rememberIsInPipMode() return player } From 7b3370f10f8d0955c9518f3b417b61898d3c59a0 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Fri, 20 Mar 2026 14:10:26 +0100 Subject: [PATCH 19/29] Hide all UI controls when in PIP mode --- .../com/theoplayer/android/ui/UIController.kt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/UIController.kt b/ui/src/main/java/com/theoplayer/android/ui/UIController.kt index cf94ca4..219ab76 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/UIController.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/UIController.kt @@ -172,7 +172,9 @@ fun UIController( val uiState by remember { derivedStateOf { val currentMenu = scope.currentMenu - if (player.error != null) { + if (player.pictureInPicture) { + UIState.Hidden + } else if (player.error != null) { UIState.Error } else if (currentMenu != null) { UIState.Menu(currentMenu) @@ -181,10 +183,10 @@ fun UIController( } } } - val backgroundVisible = if (uiState is UIState.Controls) { - controlsVisible.value - } else { - true + val backgroundVisible = when (uiState) { + is UIState.Controls -> controlsVisible.value + is UIState.Hidden -> false + else -> true } val background by animateColorAsState( label = "BackgroundAnimation", @@ -271,6 +273,8 @@ fun UIController( bottomChrome = bottomChrome ) } + + is UIState.Hidden -> {} } } } @@ -312,6 +316,7 @@ private sealed class UIState { object Error : UIState() data class Menu(val menu: MenuContent) : UIState() object Controls : UIState() + object Hidden } @Composable From a8e98794f01644be1644118ce6f56690e6b07115 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Fri, 20 Mar 2026 14:12:21 +0100 Subject: [PATCH 20/29] Extract PlayerContent composable --- .../android/ui/demo/MainActivity.kt | 58 ++++++++++++------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt b/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt index ed29d6b..8424a94 100644 --- a/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt +++ b/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt @@ -124,28 +124,14 @@ fun MainContent() { ) } ) { padding -> - val playerModifier = Modifier - .padding(padding) - .fillMaxSize(1f) - when (theme) { - PlayerTheme.Default -> { - DefaultUI( - modifier = playerModifier, - player = player, - title = stream.title - ) - } - - PlayerTheme.Nitflex -> { - NitflexTheme(useDarkTheme = true) { - NitflexUI( - modifier = playerModifier, - player = player, - title = stream.title - ) - } - } - } + PlayerContent( + modifier = Modifier + .padding(padding) + .fillMaxSize(1f), + player = player, + stream = stream, + theme = theme + ) if (streamMenuOpen) { SelectStreamDialog( @@ -171,6 +157,34 @@ fun MainContent() { } } +@Composable +fun PlayerContent( + modifier: Modifier = Modifier, + player: Player, + stream: Stream, + theme: PlayerTheme +) { + when (theme) { + PlayerTheme.Default -> { + DefaultUI( + modifier = modifier, + player = player, + title = stream.title + ) + } + + PlayerTheme.Nitflex -> { + NitflexTheme(useDarkTheme = true) { + NitflexUI( + modifier = modifier, + player = player, + title = stream.title + ) + } + } + } +} + enum class PlayerTheme(val title: String) { Default(title = "Default theme"), Nitflex(title = "Nitflex theme") From 51b787dd7e17493da8d47175387088795a0b8fed Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Fri, 20 Mar 2026 14:18:24 +0100 Subject: [PATCH 21/29] Add custom PIP button to sample app --- app/build.gradle.kts | 2 + .../android/ui/demo/MainActivity.kt | 82 ++++++++++++++++++- gradle/libs.versions.toml | 6 +- 3 files changed, 86 insertions(+), 4 deletions(-) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6176a8f..7a13303 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -82,6 +82,8 @@ dependencies { implementation(libs.androidx.lifecycle.compose) implementation(libs.androidx.activity.compose) implementation(libs.androidx.appcompat) + implementation(libs.androidx.core) + implementation(libs.androidx.core.pip) implementation(libs.androidx.compose.ui.ui) implementation(libs.androidx.compose.ui.toolingPreview) implementation(libs.androidx.compose.material3) diff --git a/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt b/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt index 8424a94..994405b 100644 --- a/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt +++ b/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt @@ -1,7 +1,11 @@ package com.theoplayer.android.ui.demo +import android.content.res.Configuration +import android.os.Build import android.os.Bundle +import android.util.Rational import androidx.activity.ComponentActivity +import androidx.activity.compose.LocalActivity import androidx.activity.compose.setContent import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Column @@ -14,6 +18,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Brush import androidx.compose.material.icons.rounded.Movie +import androidx.compose.material.icons.rounded.PictureInPicture import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon @@ -26,6 +31,7 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -37,6 +43,10 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog +import androidx.core.app.PictureInPictureParamsCompat +import androidx.core.content.ContextCompat +import androidx.core.pip.PictureInPictureDelegate +import androidx.core.pip.VideoPlaybackPictureInPicture import com.google.android.gms.cast.framework.CastContext import com.theoplayer.android.api.THEOplayerConfig import com.theoplayer.android.api.THEOplayerView @@ -46,29 +56,69 @@ import com.theoplayer.android.api.cast.CastIntegrationFactory import com.theoplayer.android.api.cast.CastStrategy import com.theoplayer.android.api.pip.PipConfiguration import com.theoplayer.android.ui.DefaultUI +import com.theoplayer.android.ui.Player import com.theoplayer.android.ui.demo.nitflex.NitflexUI import com.theoplayer.android.ui.demo.nitflex.theme.NitflexTheme import com.theoplayer.android.ui.rememberPlayer import com.theoplayer.android.ui.theme.THEOplayerTheme -class MainActivity : ComponentActivity() { +class MainActivity : ComponentActivity(), PictureInPictureDelegate.OnPictureInPictureEventListener { + private lateinit var pip: VideoPlaybackPictureInPicture + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Initialize Chromecast immediately, for automatic receiver discovery to work correctly. CastContext.getSharedInstance(this) + initializePictureInPicture() + setContent { THEOplayerTheme(useDarkTheme = true) { - MainContent() + MainContent( + pip = pip, + onEnterPip = ::enterPictureInPicture + ) } } } + + private fun initializePictureInPicture() { + pip = VideoPlaybackPictureInPicture(this) + pip.addOnPictureInPictureEventListener( + ContextCompat.getMainExecutor(this), + this + ) + pip.setAspectRatio(Rational(16, 9)) + pip.setEnabled(true) + } + + private fun enterPictureInPicture() { + val params = PictureInPictureParamsCompat.Builder().build().also { + pip.setPictureInPictureParams(it) + } + enterPictureInPictureMode(params) + } + + override fun onPictureInPictureEvent( + event: PictureInPictureDelegate.Event, + config: Configuration? + ) { + // Do nothing + } + + override fun onDestroy() { + super.onDestroy() + pip.close() + } } @OptIn(ExperimentalMaterial3Api::class) @Composable -fun MainContent() { +fun MainContent( + pip: VideoPlaybackPictureInPicture, + onEnterPip: () -> Unit = {} +) { var stream by rememberSaveable(stateSaver = StreamSaver) { mutableStateOf(streams.first()) } var streamMenuOpen by remember { mutableStateOf(false) } @@ -95,9 +145,27 @@ fun MainContent() { LaunchedEffect(player, stream) { player.source = stream.source } + DisposableEffect(theoplayerView) { + pip.setPlayerView(theoplayerView) + onDispose { pip.setPlayerView(null) } + } var themeMenuOpen by remember { mutableStateOf(false) } var theme by rememberSaveable { mutableStateOf(PlayerTheme.Default) } + val activity = LocalActivity.current as? ComponentActivity + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N && activity?.isInPictureInPictureMode == true) { + // Only show player while in picture-in-picture mode + Surface(modifier = Modifier.fillMaxSize()) { + PlayerContent( + modifier = Modifier.fillMaxSize(), + player = player, + stream = stream, + theme = theme + ) + } + return + } Scaffold( modifier = Modifier.fillMaxSize(), @@ -108,6 +176,14 @@ fun MainContent() { Text(text = "Demo") }, actions = { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + IconButton(onClick = onEnterPip) { + Icon( + Icons.Rounded.PictureInPicture, + contentDescription = "Enter picture-in-picture" + ) + } + } IconButton(onClick = { player.source = stream.source player.play() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1869fa5..945a6a7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -6,7 +6,7 @@ gradle = "8.13.0" kotlin = "2.2.10" ktx = "1.17.0" lifecycle-compose = "2.9.3" -activity-compose = "1.10.1" +activity-compose = "1.13.0" appcompat = "1.7.1" compose-bom = "2025.08.01" junit4 = "4.13.2" @@ -18,6 +18,8 @@ androidx-mediarouter = "1.8.1" dokka = "2.0.0" theoplayer = { prefer="10.11.0", strictly = "[7.6.0, 11.0)" } theoplayer-min = { strictly = "7.6.0" } +core = "1.18.0" +core-pip = "1.0.0-alpha02" [libraries] androidx-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "ktx" } @@ -36,6 +38,8 @@ androidx-compose-ui-toolingPreview = { group = "androidx.compose.ui", name = "ui androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-junit" } androidx-espresso = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso" } androidx-mediarouter = { group = "androidx.mediarouter", name = "mediarouter", version.ref = "androidx-mediarouter" } +androidx-core = { group = "androidx.core", name = "core", version.ref = "core" } +androidx-core-pip = { group = "androidx.core", name = "core-pip", version.ref = "core-pip" } playServices-castFramework = { group = "com.google.android.gms", name = "play-services-cast-framework", version.ref = "playServices-castFramework" } gradle-plugin = { group = "com.android.tools.build", name = "gradle", version.ref = "gradle" } dokka-base = { group = "org.jetbrains.dokka", name = "dokka-base", version.ref = "dokka" } From 73aaa139f9eb13aa7863c76a4ff203379a55d3ee Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Fri, 20 Mar 2026 14:21:18 +0100 Subject: [PATCH 22/29] Create THEOplayerView in onCreate --- .../android/ui/demo/MainActivity.kt | 49 +++++++++---------- 1 file changed, 22 insertions(+), 27 deletions(-) diff --git a/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt b/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt index 994405b..7f90e3a 100644 --- a/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt +++ b/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt @@ -31,7 +31,6 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -39,7 +38,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog @@ -63,6 +61,7 @@ import com.theoplayer.android.ui.rememberPlayer import com.theoplayer.android.ui.theme.THEOplayerTheme class MainActivity : ComponentActivity(), PictureInPictureDelegate.OnPictureInPictureEventListener { + private lateinit var theoplayerView: THEOplayerView private lateinit var pip: VideoPlaybackPictureInPicture override fun onCreate(savedInstanceState: Bundle?) { @@ -71,12 +70,30 @@ class MainActivity : ComponentActivity(), PictureInPictureDelegate.OnPictureInPi // Initialize Chromecast immediately, for automatic receiver discovery to work correctly. CastContext.getSharedInstance(this) + // Initialize THEOplayer + val config = THEOplayerConfig.Builder().apply { + pipConfiguration(PipConfiguration.Builder().build()) + }.build() + theoplayerView = THEOplayerView(this, config).apply { + // Add ads integration through Google IMA + player.addIntegration( + GoogleImaIntegrationFactory.createGoogleImaIntegration(this) + ) + // Add Chromecast integration + val castConfiguration = CastConfiguration.Builder().apply { + castStrategy(CastStrategy.AUTO) + }.build() + player.addIntegration( + CastIntegrationFactory.createCastIntegration(this, castConfiguration) + ) + } + initializePictureInPicture() setContent { THEOplayerTheme(useDarkTheme = true) { MainContent( - pip = pip, + theoplayerView = theoplayerView, onEnterPip = ::enterPictureInPicture ) } @@ -90,6 +107,7 @@ class MainActivity : ComponentActivity(), PictureInPictureDelegate.OnPictureInPi this ) pip.setAspectRatio(Rational(16, 9)) + pip.setPlayerView(theoplayerView) pip.setEnabled(true) } @@ -116,39 +134,16 @@ class MainActivity : ComponentActivity(), PictureInPictureDelegate.OnPictureInPi @OptIn(ExperimentalMaterial3Api::class) @Composable fun MainContent( - pip: VideoPlaybackPictureInPicture, + theoplayerView: THEOplayerView, onEnterPip: () -> Unit = {} ) { var stream by rememberSaveable(stateSaver = StreamSaver) { mutableStateOf(streams.first()) } var streamMenuOpen by remember { mutableStateOf(false) } - val context = LocalContext.current - val theoplayerView = remember(context) { - val config = THEOplayerConfig.Builder().apply { - pipConfiguration(PipConfiguration.Builder().build()) - }.build() - THEOplayerView(context, config).apply { - // Add ads integration through Google IMA - player.addIntegration( - GoogleImaIntegrationFactory.createGoogleImaIntegration(this) - ) - // Add Chromecast integration - val castConfiguration = CastConfiguration.Builder().apply { - castStrategy(CastStrategy.AUTO) - }.build() - player.addIntegration( - CastIntegrationFactory.createCastIntegration(this, castConfiguration) - ) - } - } val player = rememberPlayer(theoplayerView) LaunchedEffect(player, stream) { player.source = stream.source } - DisposableEffect(theoplayerView) { - pip.setPlayerView(theoplayerView) - onDispose { pip.setPlayerView(null) } - } var themeMenuOpen by remember { mutableStateOf(false) } var theme by rememberSaveable { mutableStateOf(PlayerTheme.Default) } From 3d874b02e28120f5d097f13680b229f00b29aeb5 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Fri, 20 Mar 2026 14:29:24 +0100 Subject: [PATCH 23/29] Use `PiPType.CUSTOM` for custom PIP --- .../android/ui/demo/MainActivity.kt | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt b/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt index 7f90e3a..aee9bd1 100644 --- a/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt +++ b/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt @@ -41,7 +41,6 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog -import androidx.core.app.PictureInPictureParamsCompat import androidx.core.content.ContextCompat import androidx.core.pip.PictureInPictureDelegate import androidx.core.pip.VideoPlaybackPictureInPicture @@ -52,6 +51,8 @@ import com.theoplayer.android.api.ads.ima.GoogleImaIntegrationFactory import com.theoplayer.android.api.cast.CastConfiguration import com.theoplayer.android.api.cast.CastIntegrationFactory import com.theoplayer.android.api.cast.CastStrategy +import com.theoplayer.android.api.event.player.PlayerEventTypes +import com.theoplayer.android.api.pip.PiPType import com.theoplayer.android.api.pip.PipConfiguration import com.theoplayer.android.ui.DefaultUI import com.theoplayer.android.ui.Player @@ -109,25 +110,45 @@ class MainActivity : ComponentActivity(), PictureInPictureDelegate.OnPictureInPi pip.setAspectRatio(Rational(16, 9)) pip.setPlayerView(theoplayerView) pip.setEnabled(true) + + theoplayerView.player.addEventListener(PlayerEventTypes.RESIZE) { updatePictureInPictureAspectRatio() } } private fun enterPictureInPicture() { - val params = PictureInPictureParamsCompat.Builder().build().also { - pip.setPictureInPictureParams(it) + theoplayerView.piPManager?.enterPiP(PiPType.CUSTOM) + } + + private fun updatePictureInPictureAspectRatio() { + val player = theoplayerView.player + if (player.videoWidth > 0 && player.videoHeight > 0) { + pip.setAspectRatio(Rational(player.videoWidth, player.videoHeight)) } - enterPictureInPictureMode(params) } override fun onPictureInPictureEvent( event: PictureInPictureDelegate.Event, config: Configuration? ) { - // Do nothing + val pipManager = theoplayerView.piPManager ?: return + when (event) { + PictureInPictureDelegate.Event.ENTERED -> { + if (!pipManager.isInPiP) { + pipManager.enterPiP(PiPType.CUSTOM) + } + } + + PictureInPictureDelegate.Event.EXITED -> { + if (pipManager.isInPiP) { + pipManager.exitPiP() + } + } + } } override fun onDestroy() { super.onDestroy() pip.close() + theoplayerView.onDestroy() } } From 27864655434b5ca97bb41b04ddb127062aa233c3 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Fri, 20 Mar 2026 14:54:57 +0100 Subject: [PATCH 24/29] Rename import --- .../com/theoplayer/android/ui/demo/MainActivity.kt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt b/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt index aee9bd1..b673532 100644 --- a/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt +++ b/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt @@ -21,7 +21,6 @@ import androidx.compose.material.icons.rounded.Movie import androidx.compose.material.icons.rounded.PictureInPicture import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.MaterialTheme @@ -60,6 +59,7 @@ import com.theoplayer.android.ui.demo.nitflex.NitflexUI import com.theoplayer.android.ui.demo.nitflex.theme.NitflexTheme import com.theoplayer.android.ui.rememberPlayer import com.theoplayer.android.ui.theme.THEOplayerTheme +import androidx.compose.material3.Icon as Material3Icon class MainActivity : ComponentActivity(), PictureInPictureDelegate.OnPictureInPictureEventListener { private lateinit var theoplayerView: THEOplayerView @@ -194,7 +194,7 @@ fun MainContent( actions = { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { IconButton(onClick = onEnterPip) { - Icon( + Material3Icon( Icons.Rounded.PictureInPicture, contentDescription = "Enter picture-in-picture" ) @@ -204,13 +204,13 @@ fun MainContent( player.source = stream.source player.play() }) { - Icon(Icons.Rounded.Refresh, contentDescription = "Reload") + Material3Icon(Icons.Rounded.Refresh, contentDescription = "Reload") } IconButton(onClick = { streamMenuOpen = true }) { - Icon(Icons.Rounded.Movie, contentDescription = "Stream") + Material3Icon(Icons.Rounded.Movie, contentDescription = "Stream") } IconButton(onClick = { themeMenuOpen = true }) { - Icon(Icons.Rounded.Brush, contentDescription = "Theme") + Material3Icon(Icons.Rounded.Brush, contentDescription = "Theme") } } ) From 1dc53c06a1ecd7ef78df92a02696f79f7d6f784b Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 23 Mar 2026 17:00:28 +0100 Subject: [PATCH 25/29] Update changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0114e9c..51f2f42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,7 @@ * 🐛 Changed the minimum supported THEOplayer version to 7.6.0. ([#85](https://github.com/THEOplayer/android-ui/pull/85)) * This was effectively already the minimum version as of Open Video UI for Android version 1.7.2, but it wasn't noticed until now. * Future versions will be properly tested with the minimum supported THEOplayer version to avoid similar compatibility issues. +* 🐛 `Player.pictureInPicture` now also checks whether the `Activity` itself is in picture-in-picture mode, in case the activity has custom picture-in-picture logic (that does not use THEOplayer's `PiPManager` API). ([#89](https://github.com/THEOplayer/android-ui/pull/89/)) ## v1.13.2 (2026-03-03) From ca78d3c52ed8c138bb48f8b395fc693d741a9564 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 23 Mar 2026 17:24:27 +0100 Subject: [PATCH 26/29] Check if we're transitioning to PIP mode --- .../android/ui/util/PictureInPictureUtil.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/PictureInPictureUtil.kt b/ui/src/main/java/com/theoplayer/android/ui/util/PictureInPictureUtil.kt index 390a273..c0d673d 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/PictureInPictureUtil.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/PictureInPictureUtil.kt @@ -11,7 +11,9 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.core.app.OnPictureInPictureModeChangedProvider +import androidx.core.app.OnPictureInPictureUiStateChangedProvider import androidx.core.app.PictureInPictureModeChangedInfo +import androidx.core.app.PictureInPictureUiStateCompat import androidx.core.util.Consumer /** @@ -30,7 +32,7 @@ internal fun Activity.supportsPictureInPictureMode(): Boolean { } /** - * Returns whether the activity is in picture-in-picture mode. + * Returns whether the activity is in (or transitioning to) picture-in-picture mode. */ @Composable internal fun rememberIsInPipMode(): Boolean { @@ -47,5 +49,15 @@ internal fun rememberIsInPipMode(): Boolean { onDispose { activity.removeOnPictureInPictureModeChangedListener(observer) } } } - return pipMode + var transitioningToPip by remember { mutableStateOf(false) } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.VANILLA_ICE_CREAM && activity is OnPictureInPictureUiStateChangedProvider) { + DisposableEffect(activity) { + val observer = Consumer { info -> + transitioningToPip = info.isTransitioningToPip + } + activity.addOnPictureInPictureUiStateChangedListener(observer) + onDispose { activity.addOnPictureInPictureUiStateChangedListener(observer) } + } + } + return pipMode || transitioningToPip } \ No newline at end of file From 63ba8aa2f46f6e17ef4cde3c117aae187b3031b1 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 23 Mar 2026 17:25:15 +0100 Subject: [PATCH 27/29] Optimize slightly --- .../com/theoplayer/android/ui/util/PictureInPictureUtil.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/PictureInPictureUtil.kt b/ui/src/main/java/com/theoplayer/android/ui/util/PictureInPictureUtil.kt index c0d673d..96a6da0 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/PictureInPictureUtil.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/PictureInPictureUtil.kt @@ -38,8 +38,8 @@ internal fun Activity.supportsPictureInPictureMode(): Boolean { internal fun rememberIsInPipMode(): Boolean { // https://developer.android.com/develop/ui/compose/system/picture-in-picture#handle-ui if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) return false - val activity = LocalActivity.current - var pipMode by remember { mutableStateOf(activity?.isInPictureInPictureMode ?: false) } + val activity = LocalActivity.current ?: return false + var pipMode by remember { mutableStateOf(activity.isInPictureInPictureMode) } if (activity is OnPictureInPictureModeChangedProvider) { DisposableEffect(activity) { val observer = Consumer { info -> From d1d4235479a2e9c9c9a1ba2ace5eb5f2d2dab87d Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 23 Mar 2026 17:27:19 +0100 Subject: [PATCH 28/29] Simplify --- .../java/com/theoplayer/android/ui/DefaultUI.kt | 2 +- .../android/ui/PictureInPictureButton.kt | 16 +++++++--------- .../com/theoplayer/android/ui/UIController.kt | 13 +++++-------- 3 files changed, 13 insertions(+), 18 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/DefaultUI.kt b/ui/src/main/java/com/theoplayer/android/ui/DefaultUI.kt index 6f83f7d..520b7f3 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/DefaultUI.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/DefaultUI.kt @@ -80,7 +80,7 @@ fun DefaultUI( } }, topChrome = { - if (player.firstPlay && !player.pictureInPicture) { + if (player.firstPlay) { Row(verticalAlignment = Alignment.CenterVertically) { title?.let { Text( diff --git a/ui/src/main/java/com/theoplayer/android/ui/PictureInPictureButton.kt b/ui/src/main/java/com/theoplayer/android/ui/PictureInPictureButton.kt index 808a771..62f48fc 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/PictureInPictureButton.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/PictureInPictureButton.kt @@ -41,21 +41,19 @@ fun PictureInPictureButton( ) } ) { - val player = Player.current - if (player?.pictureInPictureSupported != true) return + val player = Player.current ?: return + if (!player.pictureInPictureSupported) return IconButton( modifier = modifier, contentPadding = contentPadding, onClick = { - player?.let { - if (it.pictureInPicture) { - it.exitPictureInPicture() - } else { - it.enterPictureInPicture(pipType) - } + if (player.pictureInPicture) { + player.exitPictureInPicture() + } else { + player.enterPictureInPicture(pipType) } }) { - if (player?.pictureInPicture == true) { + if (player.pictureInPicture) { exit() } else { enter() diff --git a/ui/src/main/java/com/theoplayer/android/ui/UIController.kt b/ui/src/main/java/com/theoplayer/android/ui/UIController.kt index 219ab76..6686ef8 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/UIController.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/UIController.kt @@ -172,14 +172,11 @@ fun UIController( val uiState by remember { derivedStateOf { val currentMenu = scope.currentMenu - if (player.pictureInPicture) { - UIState.Hidden - } else if (player.error != null) { - UIState.Error - } else if (currentMenu != null) { - UIState.Menu(currentMenu) - } else { - UIState.Controls + when { + player.pictureInPicture -> UIState.Hidden + player.error != null -> UIState.Error + currentMenu != null -> UIState.Menu(currentMenu) + else -> UIState.Controls } } } From d7f9fd87f9f1ed0972534a26d2bbde416cb2e96d Mon Sep 17 00:00:00 2001 From: "theoplayer-bot[bot]" <873105+theoplayer-bot[bot]@users.noreply.github.com> Date: Mon, 23 Mar 2026 16:34:27 +0000 Subject: [PATCH 29/29] 1.13.3 --- CHANGELOG.md | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 51f2f42..ab01b18 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ > - 🏠 Internal > - 💅 Polish -## Unreleased +## v1.13.3 (2026-03-23) * 🐛 Changed the minimum supported THEOplayer version to 7.6.0. ([#85](https://github.com/THEOplayer/android-ui/pull/85)) * This was effectively already the minimum version as of Open Video UI for Android version 1.7.2, but it wasn't noticed until now. diff --git a/gradle.properties b/gradle.properties index 0554f97..19cd3b0 100644 --- a/gradle.properties +++ b/gradle.properties @@ -27,6 +27,6 @@ org.gradle.configuration-cache=true org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true # The version of the THEOplayer Open Video UI for Android. -version=1.13.2 +version=1.13.3 # (For testing purposes) Install the lowest supported THEOplayer version. installLowestSupportedPlayerVersion=false