From 7d381de74a76823685282af9313ad8e7deb783f4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 24 Mar 2026 11:27:11 +0100 Subject: [PATCH 01/46] Merge pull request #92 from THEOplayer/dependabot/github_actions/gradle/actions-6 Bump gradle/actions from 5 to 6 --- .github/workflows/ci.yml | 10 +++++----- .github/workflows/docs.yml | 8 ++++---- .github/workflows/publish.yml | 8 ++++---- 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 550d8c4..93919fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,11 +23,11 @@ jobs: distribution: temurin java-version: 17 - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@v6 with: - cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }} - cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + # https://github.com/gradle/actions/blob/v6.0.0/README.md#licensing-notice + cache-disabled: true - name: Run tests - run: ./gradlew test --configuration-cache + run: ./gradlew test - name: Assemble - run: ./gradlew assembleRelease --configuration-cache + run: ./gradlew assembleRelease diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index a39ec61..59f8ee5 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -34,12 +34,12 @@ jobs: distribution: temurin java-version: 17 - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@v6 with: - cache-read-only: true - cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + # https://github.com/gradle/actions/blob/v6.0.0/README.md#licensing-notice + cache-disabled: true - name: Build API documentation with Dokka - run: ./gradlew ui:dokkaGeneratePublicationHtml --configuration-cache + run: ./gradlew ui:dokkaGeneratePublicationHtml - name: Build with Jekyll uses: actions/jekyll-build-pages@v1 with: diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index f32c73c..ed1fb0d 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -40,12 +40,12 @@ jobs: distribution: temurin java-version: 17 - name: Setup Gradle - uses: gradle/actions/setup-gradle@v5 + uses: gradle/actions/setup-gradle@v6 with: - cache-read-only: true - cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }} + # https://github.com/gradle/actions/blob/v6.0.0/README.md#licensing-notice + cache-disabled: true - name: Publish package - run: ./gradlew publish --configuration-cache + run: ./gradlew publish env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} REPOSILITE_USERNAME: ${{ secrets.REPOSILITE_USERNAME }} From 91d3c1855128f2ac0c52e203d9a1e34359f93620 Mon Sep 17 00:00:00 2001 From: Alex Dadukin Date: Mon, 16 Mar 2026 11:27:17 +0000 Subject: [PATCH 02/46] OPTI-1528: extract language localisation logic into a separate file, add unit tests --- gradle/libs.versions.toml | 2 + ui/build.gradle.kts | 2 + .../java/com/theoplayer/android/ui/Helper.kt | 11 +-- .../com/theoplayer/android/ui/TrackExts.kt | 32 ++++++++ .../theoplayer/android/ui/ExampleUnitTest.kt | 17 ---- .../theoplayer/android/ui/TrackExtsTest.kt | 82 +++++++++++++++++++ 6 files changed, 121 insertions(+), 25 deletions(-) create mode 100644 ui/src/main/java/com/theoplayer/android/ui/TrackExts.kt delete mode 100644 ui/src/test/java/com/theoplayer/android/ui/ExampleUnitTest.kt create mode 100644 ui/src/test/java/com/theoplayer/android/ui/TrackExtsTest.kt diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 945a6a7..22062b9 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,7 @@ activity-compose = "1.13.0" appcompat = "1.7.1" compose-bom = "2025.08.01" junit4 = "4.13.2" +mockk = "1.14.9" playServices-castFramework = "21.5.0" ui-test-junit4 = "1.9.0" # ...not in BOM for some reason? androidx-junit = "1.3.0" @@ -46,6 +47,7 @@ dokka-base = { group = "org.jetbrains.dokka", name = "dokka-base", version.ref = dokka-plugin = { group = "org.jetbrains.dokka", name = "android-documentation-plugin", version.ref = "dokka" } kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } junit4 = { group = "junit", name = "junit", version.ref = "junit4" } +mockk = { module = "io.mockk:mockk", version.ref = "mockk" } 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" } diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts index af840ff..918d0db 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -78,7 +78,9 @@ dependencies { implementation(libs.androidx.compose.ui.toolingPreview) implementation(libs.androidx.compose.material3) implementation(libs.androidx.compose.material.iconsExtended) + testImplementation(libs.junit4) + testImplementation(libs.mockk) androidTestImplementation(libs.androidx.junit) androidTestImplementation(libs.androidx.espresso) androidTestImplementation(libs.androidx.compose.ui.testJunit4) diff --git a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt index cef88a0..32e14aa 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt @@ -68,14 +68,9 @@ fun formatTrackLabel(track: Track): String { if (!label.isNullOrEmpty()) { return label } - val languageCode = track.language - if (!languageCode.isNullOrEmpty()) { - val locale = Locale.forLanguageTag(languageCode) - val languageName = locale.getDisplayName(locale) - if (languageName.isNotEmpty()) { - return languageName - } - return languageCode + val localisedLanguage = track.localisedLanguage + if (localisedLanguage != null) { + return localisedLanguage } return stringResource(R.string.theoplayer_ui_track_unknown) } \ No newline at end of file diff --git a/ui/src/main/java/com/theoplayer/android/ui/TrackExts.kt b/ui/src/main/java/com/theoplayer/android/ui/TrackExts.kt new file mode 100644 index 0000000..4056070 --- /dev/null +++ b/ui/src/main/java/com/theoplayer/android/ui/TrackExts.kt @@ -0,0 +1,32 @@ +package com.theoplayer.android.ui + +import androidx.annotation.CheckResult +import com.theoplayer.android.api.player.track.Track +import java.util.Locale + +private const val LANGUAGE_UNDEFINED = "und" + +/** + * Returns a name for the [Track.language] in the + * [Locale.Category.DISPLAY] locale that is appropriate + * for display to the user. + * If such conversion is not possible, for instance + * when [Track.language] is `null`, blank, or `"und"`, + * returns `null`. + */ +@get:CheckResult +internal val Track.localisedLanguage: String? + get() { + val languageCode = this.language + if (languageCode.isNullOrBlank() || languageCode == LANGUAGE_UNDEFINED) { + return null + } + + val localisedLanguage = + Locale.forLanguageTag(languageCode).displayLanguage + if (localisedLanguage.isNullOrBlank()) { + return null + } + + return localisedLanguage + } diff --git a/ui/src/test/java/com/theoplayer/android/ui/ExampleUnitTest.kt b/ui/src/test/java/com/theoplayer/android/ui/ExampleUnitTest.kt deleted file mode 100644 index b281c16..0000000 --- a/ui/src/test/java/com/theoplayer/android/ui/ExampleUnitTest.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.theoplayer.android.ui - -import org.junit.Test - -import org.junit.Assert.* - -/** - * Example local unit test, which will execute on the development machine (host). - * - * See [testing documentation](http://d.android.com/tools/testing). - */ -class ExampleUnitTest { - @Test - fun addition_isCorrect() { - assertEquals(4, 2 + 2) - } -} \ No newline at end of file diff --git a/ui/src/test/java/com/theoplayer/android/ui/TrackExtsTest.kt b/ui/src/test/java/com/theoplayer/android/ui/TrackExtsTest.kt new file mode 100644 index 0000000..62d1b96 --- /dev/null +++ b/ui/src/test/java/com/theoplayer/android/ui/TrackExtsTest.kt @@ -0,0 +1,82 @@ +package com.theoplayer.android.ui + +import com.theoplayer.android.api.player.track.Track +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Before +import org.junit.Test +import org.junit.experimental.runners.Enclosed +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 +import java.util.Locale + +@RunWith(Enclosed::class) +class TrackExtsTest { + + @RunWith(JUnit4::class) + class LocalisedLanguageTest { + + private val track = mockk() + private val locale = mockk() + + @Before + fun setUp() { + mockkStatic(Locale::class) + } + + @Test + fun `GIVEN language is null THEN localised language is also null`() { + every { track.language } returns null + assertNull(track.localisedLanguage) + } + + @Test + fun `GIVEN language is und THEN localised language is null`() { + every { track.language } returns LANGUAGE_CODE_UNDEFINED + assertNull(track.localisedLanguage) + } + + @Test + fun `GIVEN language is blank THEN localised language is null`() { + every { track.language } returns TEST_BLANK_STRING + assertNull(track.localisedLanguage) + } + + @Test + fun `GIVEN locale returns null as displayLanguage THEN localised language is null`() { + every { track.language } returns LANGUAGE_CODE_ENGLISH + every { Locale.forLanguageTag(eq(LANGUAGE_CODE_ENGLISH)) } returns locale + every { locale.displayLanguage } returns null + + assertNull(track.localisedLanguage) + } + + @Test + fun `GIVEN locale returns a blank string as displayLanguage THEN localised language is null`() { + every { track.language } returns LANGUAGE_CODE_ENGLISH + every { Locale.forLanguageTag(eq(LANGUAGE_CODE_ENGLISH)) } returns locale + every { locale.displayLanguage } returns TEST_BLANK_STRING + + assertNull(track.localisedLanguage) + } + + @Test + fun `GIVEN locale returns a valid display name THEN returns localised name`() { + every { track.language } returns LANGUAGE_CODE_ENGLISH + every { Locale.forLanguageTag(eq(LANGUAGE_CODE_ENGLISH)) } returns locale + every { locale.displayLanguage } returns LOCALISED_ENGLISH_CODE_NAME + + assertEquals(LOCALISED_ENGLISH_CODE_NAME, track.localisedLanguage) + } + + private companion object { + const val LANGUAGE_CODE_UNDEFINED = "und" + const val LANGUAGE_CODE_ENGLISH = "en" + const val LOCALISED_ENGLISH_CODE_NAME = "English" + const val TEST_BLANK_STRING = " " + } + } +} From 39120bbc2667ecb3b23085dc74a19ac1463108da Mon Sep 17 00:00:00 2001 From: Alex Dadukin Date: Mon, 16 Mar 2026 12:29:27 +0000 Subject: [PATCH 03/46] OPTI-1528: add util to conditionally check player version --- .../java/com/theoplayer/android/ui/Helper.kt | 2 +- .../android/ui/{ => util}/TrackExts.kt | 2 +- .../theoplayer/android/ui/util/VersionUtil.kt | 26 ++++++ .../android/ui/{ => util}/TrackExtsTest.kt | 19 ++-- .../android/ui/util/VersionUtilTest.kt | 90 +++++++++++++++++++ 5 files changed, 127 insertions(+), 12 deletions(-) rename ui/src/main/java/com/theoplayer/android/ui/{ => util}/TrackExts.kt (95%) create mode 100644 ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt rename ui/src/test/java/com/theoplayer/android/ui/{ => util}/TrackExtsTest.kt (84%) create mode 100644 ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt diff --git a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt index 32e14aa..d85aefe 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt @@ -3,7 +3,7 @@ package com.theoplayer.android.ui import androidx.compose.runtime.Composable import androidx.compose.ui.res.stringResource import com.theoplayer.android.api.player.track.Track -import java.util.Locale +import com.theoplayer.android.ui.util.localisedLanguage import kotlin.math.absoluteValue /** diff --git a/ui/src/main/java/com/theoplayer/android/ui/TrackExts.kt b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt similarity index 95% rename from ui/src/main/java/com/theoplayer/android/ui/TrackExts.kt rename to ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt index 4056070..b4d6254 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/TrackExts.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt @@ -1,4 +1,4 @@ -package com.theoplayer.android.ui +package com.theoplayer.android.ui.util import androidx.annotation.CheckResult import com.theoplayer.android.api.player.track.Track diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt b/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt new file mode 100644 index 0000000..97eb1b5 --- /dev/null +++ b/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt @@ -0,0 +1,26 @@ +package com.theoplayer.android.ui.util + +import com.theoplayer.android.api.THEOplayerGlobal + +private const val VERSION_DELIMITER = "." + +/** + * Performs a player version check and executes an appropriate action: + * if the major version is equal or above to the [desiredMajorVersion] + * then [actionIfEqualOrAbove] is triggered, otherwise [actionIfBelow]. + */ +internal inline fun runForPlayerWith( + desiredMajorVersion: Int, + actionIfEqualOrAbove: () -> T, + actionIfBelow: () -> T, +): T { + val version: String? = THEOplayerGlobal.getVersion() + val versionSplits = version?.split(VERSION_DELIMITER) + val majorVersionNumber = versionSplits?.getOrNull(0)?.toIntOrNull() + + return if (majorVersionNumber == null || majorVersionNumber < desiredMajorVersion) { + actionIfBelow() + } else { + actionIfEqualOrAbove() + } +} diff --git a/ui/src/test/java/com/theoplayer/android/ui/TrackExtsTest.kt b/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt similarity index 84% rename from ui/src/test/java/com/theoplayer/android/ui/TrackExtsTest.kt rename to ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt index 62d1b96..280d81f 100644 --- a/ui/src/test/java/com/theoplayer/android/ui/TrackExtsTest.kt +++ b/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt @@ -1,11 +1,10 @@ -package com.theoplayer.android.ui +package com.theoplayer.android.ui.util import com.theoplayer.android.api.player.track.Track import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic -import org.junit.Assert.assertEquals -import org.junit.Assert.assertNull +import org.junit.Assert import org.junit.Before import org.junit.Test import org.junit.experimental.runners.Enclosed @@ -30,19 +29,19 @@ class TrackExtsTest { @Test fun `GIVEN language is null THEN localised language is also null`() { every { track.language } returns null - assertNull(track.localisedLanguage) + Assert.assertNull(track.localisedLanguage) } @Test fun `GIVEN language is und THEN localised language is null`() { every { track.language } returns LANGUAGE_CODE_UNDEFINED - assertNull(track.localisedLanguage) + Assert.assertNull(track.localisedLanguage) } @Test fun `GIVEN language is blank THEN localised language is null`() { every { track.language } returns TEST_BLANK_STRING - assertNull(track.localisedLanguage) + Assert.assertNull(track.localisedLanguage) } @Test @@ -51,7 +50,7 @@ class TrackExtsTest { every { Locale.forLanguageTag(eq(LANGUAGE_CODE_ENGLISH)) } returns locale every { locale.displayLanguage } returns null - assertNull(track.localisedLanguage) + Assert.assertNull(track.localisedLanguage) } @Test @@ -60,7 +59,7 @@ class TrackExtsTest { every { Locale.forLanguageTag(eq(LANGUAGE_CODE_ENGLISH)) } returns locale every { locale.displayLanguage } returns TEST_BLANK_STRING - assertNull(track.localisedLanguage) + Assert.assertNull(track.localisedLanguage) } @Test @@ -69,7 +68,7 @@ class TrackExtsTest { every { Locale.forLanguageTag(eq(LANGUAGE_CODE_ENGLISH)) } returns locale every { locale.displayLanguage } returns LOCALISED_ENGLISH_CODE_NAME - assertEquals(LOCALISED_ENGLISH_CODE_NAME, track.localisedLanguage) + Assert.assertEquals(LOCALISED_ENGLISH_CODE_NAME, track.localisedLanguage) } private companion object { @@ -79,4 +78,4 @@ class TrackExtsTest { const val TEST_BLANK_STRING = " " } } -} +} \ No newline at end of file diff --git a/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt b/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt new file mode 100644 index 0000000..dfe1876 --- /dev/null +++ b/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt @@ -0,0 +1,90 @@ +package com.theoplayer.android.ui.util + +import com.theoplayer.android.api.THEOplayerGlobal +import io.mockk.every +import io.mockk.mockk +import io.mockk.mockkStatic +import io.mockk.verify +import org.junit.Before +import org.junit.Test +import org.junit.experimental.runners.Enclosed +import org.junit.runner.RunWith +import org.junit.runners.JUnit4 + +@RunWith(Enclosed::class) +class VersionUtilTest { + + @RunWith(JUnit4::class) + class RunForPlayerWithTest { + + private val actionAbove = mockk<() -> Unit>() + private val actionBelow = mockk<() -> Unit>() + + @Before + fun setUp() { + mockkStatic(THEOplayerGlobal::class) + + every { actionAbove.invoke() } returns Unit + every { actionBelow.invoke() } returns Unit + } + + @Test + fun `WHEN THEOplayerGlobal version is null THEN executes action for version below`() { + every { THEOplayerGlobal.getVersion() } returns null + + runForPlayerWith( + desiredMajorVersion = 2, + actionIfEqualOrAbove = actionAbove, + actionIfBelow = actionBelow, + ) + + verify { actionBelow() } + } + + @Test + fun `WHEN THEOplayerGlobal version is invalid THEN executes action for version below`() { + every { THEOplayerGlobal.getVersion() } returns TEST_PLAYER_VERSION_INVALID + + runForPlayerWith( + desiredMajorVersion = 2, + actionIfEqualOrAbove = actionAbove, + actionIfBelow = actionBelow, + ) + + verify { actionBelow() } + } + + @Test + fun `WHEN THEOplayerGlobal version is valid and old THEN executes action for version below`() { + every { THEOplayerGlobal.getVersion() } returns TEST_PLAYER_VERSION_OLD + + runForPlayerWith( + desiredMajorVersion = 2, + actionIfEqualOrAbove = actionAbove, + actionIfBelow = actionBelow, + ) + + verify { actionBelow() } + } + + @Test + fun `WHEN THEOplayerGlobal version is valid and new THEN executes action for version above`() { + every { THEOplayerGlobal.getVersion() } returns TEST_PLAYER_VERSION_NEW + + runForPlayerWith( + desiredMajorVersion = 2, + actionIfEqualOrAbove = actionAbove, + actionIfBelow = actionBelow, + ) + + verify { actionAbove() } + } + + private companion object { + const val TEST_PLAYER_VERSION_INVALID = "invalid version" + const val TEST_PLAYER_VERSION_NEW = "2.3.1" + const val TEST_PLAYER_VERSION_OLD = "1.1.5" + } + } + +} From c06453b05ecc572214b815d421d3ab7b53611345 Mon Sep 17 00:00:00 2001 From: Alex Dadukin Date: Mon, 16 Mar 2026 12:48:20 +0000 Subject: [PATCH 04/46] OPTI-1528: add CEA label formatting util --- .../com/theoplayer/android/ui/util/CeaUtil.kt | 20 +++++ .../theoplayer/android/ui/util/CeaUtilTest.kt | 88 +++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 ui/src/main/java/com/theoplayer/android/ui/util/CeaUtil.kt create mode 100644 ui/src/test/java/com/theoplayer/android/ui/util/CeaUtilTest.kt diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/CeaUtil.kt b/ui/src/main/java/com/theoplayer/android/ui/util/CeaUtil.kt new file mode 100644 index 0000000..36d91c3 --- /dev/null +++ b/ui/src/main/java/com/theoplayer/android/ui/util/CeaUtil.kt @@ -0,0 +1,20 @@ +package com.theoplayer.android.ui.util + +import androidx.annotation.IntRange + +/** + * Creates a text track label for CEA-608 and CEA-708 formats. + * + * @return an optional string composed of a [channelNumber] and a prepended + * "CC" suffix, or `null` if the channel number is invalid. + */ +internal fun getLabelForChannelNumber( + @IntRange(from = 0L, to = 63L) channelNumber: Int?, +): String? { + // CEA-608 only supports channel numbers in [1, 4], + // while CEA-708 support service numbers in [1, 63]. + if (channelNumber !in 1..63) { + return null + } + return "CC${channelNumber}" +} diff --git a/ui/src/test/java/com/theoplayer/android/ui/util/CeaUtilTest.kt b/ui/src/test/java/com/theoplayer/android/ui/util/CeaUtilTest.kt new file mode 100644 index 0000000..f5c58d7 --- /dev/null +++ b/ui/src/test/java/com/theoplayer/android/ui/util/CeaUtilTest.kt @@ -0,0 +1,88 @@ +package com.theoplayer.android.ui.util + +import org.junit.Assert.assertEquals +import org.junit.Test +import org.junit.experimental.runners.Enclosed +import org.junit.runner.RunWith +import org.junit.runners.Parameterized + +@RunWith(Enclosed::class) +class CeaUtilTest { + + @RunWith(Parameterized::class) + class GetLabelForChannelNumberTest( + private val args: Args, + ) { + + @Test + fun `WHEN provided with a channel number THEN returns an expected label`() { + assertEquals( + args.expectedLabel, + getLabelForChannelNumber(args.channelNumber), + ) + } + + data class Args( + val channelNumber: Int?, + val expectedLabel: String?, + ) + + private companion object { + @JvmStatic + @Parameterized.Parameters + fun data() = arrayOf( + // Boundary checks. + Args( + channelNumber = null, + expectedLabel = null, + ), + Args( + channelNumber = -1, + expectedLabel = null, + ), + Args( + channelNumber = -100, + expectedLabel = null, + ), + Args( + channelNumber = 100, + expectedLabel = null, + ), + Args( + channelNumber = 64, + expectedLabel = null, + ), + Args( + channelNumber = 0, + expectedLabel = null, + ), + + // Regular checks. + Args( + channelNumber = 1, + expectedLabel = "CC1", + ), + Args( + channelNumber = 2, + expectedLabel = "CC2", + ), + Args( + channelNumber = 3, + expectedLabel = "CC3", + ), + Args( + channelNumber = 4, + expectedLabel = "CC4", + ), + Args( + channelNumber = 22, + expectedLabel = "CC22", + ), + Args( + channelNumber = 63, + expectedLabel = "CC63", + ), + ) + } + } +} From 290530255cfc9e33e302cc61e2cbe63ace5b9c94 Mon Sep 17 00:00:00 2001 From: Alex Dadukin Date: Mon, 16 Mar 2026 13:17:56 +0000 Subject: [PATCH 05/46] OPTI-1528: add a CEA formatting checker with unit tests --- .../com/theoplayer/android/ui/util/CeaUtil.kt | 20 ++++ .../theoplayer/android/ui/util/CeaUtilTest.kt | 93 +++++++++++++++++++ 2 files changed, 113 insertions(+) diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/CeaUtil.kt b/ui/src/main/java/com/theoplayer/android/ui/util/CeaUtil.kt index 36d91c3..2dc8589 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/CeaUtil.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/CeaUtil.kt @@ -2,6 +2,26 @@ package com.theoplayer.android.ui.util import androidx.annotation.IntRange +private val CEA_FORMATTING_REGEX = "^CC(\\d+)$".toRegex() + +/** + * Checks whether a provided label is CEA-608 or CEA-708 formed. + */ +internal fun isLabelCeaFormatted(label: String): Boolean { + val matchResult = CEA_FORMATTING_REGEX.find(label) + val groupValues = matchResult?.groupValues + if (matchResult == null || + groupValues == null || + // There is one group we want to match with the channel number. + groupValues.size != 2) { + return false + } + + val rawChannelNumber = groupValues[1] + val channelNumber = rawChannelNumber.toIntOrNull() + return !rawChannelNumber.startsWith("0") && channelNumber in 1..63 +} + /** * Creates a text track label for CEA-608 and CEA-708 formats. * diff --git a/ui/src/test/java/com/theoplayer/android/ui/util/CeaUtilTest.kt b/ui/src/test/java/com/theoplayer/android/ui/util/CeaUtilTest.kt index f5c58d7..dac2eb9 100644 --- a/ui/src/test/java/com/theoplayer/android/ui/util/CeaUtilTest.kt +++ b/ui/src/test/java/com/theoplayer/android/ui/util/CeaUtilTest.kt @@ -9,6 +9,99 @@ import org.junit.runners.Parameterized @RunWith(Enclosed::class) class CeaUtilTest { + @RunWith(Parameterized::class) + class IsLabelCeaFormattedTest( + private val args: Args, + ) { + + @Test + fun `WHEN provided with a label THEN returns whether CEA formatted`() { + assertEquals( + args.expectedIsCeaFormatted, + isLabelCeaFormatted(args.label), + ) + } + + data class Args( + val label: String, + val expectedIsCeaFormatted: Boolean, + ) + + private companion object { + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data() = arrayOf( + // False. + Args( + label = "", + expectedIsCeaFormatted = false, + ), + Args( + label = "abc", + expectedIsCeaFormatted = false, + ), + Args( + label = "Some label", + expectedIsCeaFormatted = false, + ), + Args( + label = "Text with cc1 inlined", + expectedIsCeaFormatted = false, + ), + Args( + label = "cC1", + expectedIsCeaFormatted = false, + ), + Args( + label = "Cc1", + expectedIsCeaFormatted = false, + ), + Args( + label = "CC0", + expectedIsCeaFormatted = false, + ), + Args( + label = "CC01", + expectedIsCeaFormatted = false, + ), + Args( + label = "CC64", + expectedIsCeaFormatted = false, + ), + Args( + label = "CC128", + expectedIsCeaFormatted = false, + ), + + // True. + Args( + label = "CC1", + expectedIsCeaFormatted = true, + ), + Args( + label = "CC2", + expectedIsCeaFormatted = true, + ), + Args( + label = "CC3", + expectedIsCeaFormatted = true, + ), + Args( + label = "CC4", + expectedIsCeaFormatted = true, + ), + Args( + label = "CC22", + expectedIsCeaFormatted = true, + ), + Args( + label = "CC63", + expectedIsCeaFormatted = true, + ), + ) + } + } + @RunWith(Parameterized::class) class GetLabelForChannelNumberTest( private val args: Args, From 4c9b896cf0f7c7bf9282c4ac8c1cb7564bb4f8f9 Mon Sep 17 00:00:00 2001 From: Alex Dadukin Date: Mon, 16 Mar 2026 13:43:06 +0000 Subject: [PATCH 06/46] OPTI-1528: substitute label with channelNumber for CEA tracks --- .../theoplayer/android/ui/AudioTrackList.kt | 2 +- .../java/com/theoplayer/android/ui/Helper.kt | 46 ++++++++++++++++--- .../com/theoplayer/android/ui/LanguageMenu.kt | 4 +- .../android/ui/SubtitleTrackList.kt | 2 +- .../com/theoplayer/android/ui/util/CeaUtil.kt | 6 ++- .../theoplayer/android/ui/util/CeaUtilTest.kt | 6 ++- 6 files changed, 54 insertions(+), 12 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/AudioTrackList.kt b/ui/src/main/java/com/theoplayer/android/ui/AudioTrackList.kt index a430fbf..855fcb8 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/AudioTrackList.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/AudioTrackList.kt @@ -29,7 +29,7 @@ fun AudioTrackList( ) { val audioTrack = audioTracks[it] ListItem( - headlineContent = { Text(text = formatTrackLabel(audioTrack)) }, + headlineContent = { Text(text = rememberTrackLabel(audioTrack)) }, leadingContent = { RadioButton( selected = (activeAudioTrack == audioTrack), diff --git a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt index d85aefe..09fdae4 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt @@ -1,9 +1,16 @@ package com.theoplayer.android.ui +import android.content.res.Resources import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource +import androidx.compose.runtime.remember +import androidx.compose.ui.platform.LocalResources import com.theoplayer.android.api.player.track.Track +import com.theoplayer.android.api.player.track.texttrack.TextTrack +import com.theoplayer.android.api.player.track.texttrack.TextTrackType +import com.theoplayer.android.ui.util.getLabelForChannelNumber +import com.theoplayer.android.ui.util.isLabelCeaFormatted import com.theoplayer.android.ui.util.localisedLanguage +import com.theoplayer.android.ui.util.runForPlayerWith import kotlin.math.absoluteValue /** @@ -63,14 +70,41 @@ fun formatTime(time: Double, guide: Double = 0.0, preferNegative: Boolean = fals * @param track the media track or text track */ @Composable -fun formatTrackLabel(track: Track): String { - val label = track.label +fun rememberTrackLabel( + track: Track, + resources: Resources = LocalResources.current, +): String = remember(key1 = track.id, key2 = track.uid) { + val label: String? = runForPlayerWith( + // With 11 release, the player will no longer + // prefix text tracks with "CC" for CEA-608 and CEA-708, + // if [Track.label] is `null`. + desiredMajorVersion = 11, + actionIfEqualOrAbove = { track.label }, + actionIfBelow = { + if ((track is TextTrack) && isLabelCeaFormatted(track.label)) { + // If we are below 11th major release + // and the label is CEA-formatted we + // can safely assume it was the last resort + // option to produce a meaningful label, given + // we cannot localize the language code in the player. + null + } else { + track.label + } + }, + ) if (!label.isNullOrEmpty()) { - return label + return@remember label } val localisedLanguage = track.localisedLanguage if (localisedLanguage != null) { - return localisedLanguage + return@remember localisedLanguage } - return stringResource(R.string.theoplayer_ui_track_unknown) + if ((track is TextTrack) && track.type == TextTrackType.CEA608) { + val channelNumberLabel = getLabelForChannelNumber(track.channelNumber) + if (channelNumberLabel != null) { + return@remember channelNumberLabel + } + } + return@remember resources.getString(R.string.theoplayer_ui_track_unknown) } \ No newline at end of file diff --git a/ui/src/main/java/com/theoplayer/android/ui/LanguageMenu.kt b/ui/src/main/java/com/theoplayer/android/ui/LanguageMenu.kt index c3824d4..93d60b7 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/LanguageMenu.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/LanguageMenu.kt @@ -86,7 +86,7 @@ fun MenuScope.LanguageMenuCompact() { ) { Text( modifier = Modifier.weight(1f), - text = player?.activeAudioTrack?.let { formatTrackLabel(it) } + text = player?.activeAudioTrack?.let { rememberTrackLabel(it) } ?: stringResource( R.string.theoplayer_ui_audio_none ), @@ -115,7 +115,7 @@ fun MenuScope.LanguageMenuCompact() { ) { Text( modifier = Modifier.weight(1f), - text = player?.activeSubtitleTrack?.let { formatTrackLabel(it) } ?: stringResource( + text = player?.activeSubtitleTrack?.let { rememberTrackLabel(it) } ?: stringResource( R.string.theoplayer_ui_subtitles_off ), textAlign = TextAlign.Center diff --git a/ui/src/main/java/com/theoplayer/android/ui/SubtitleTrackList.kt b/ui/src/main/java/com/theoplayer/android/ui/SubtitleTrackList.kt index 8e70f19..7fbfdfd 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/SubtitleTrackList.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/SubtitleTrackList.kt @@ -45,7 +45,7 @@ fun SubtitleTrackList( ) { val audioTrack = subtitleTracks[it] ListItem( - headlineContent = { Text(text = formatTrackLabel(audioTrack)) }, + headlineContent = { Text(text = rememberTrackLabel(audioTrack)) }, leadingContent = { RadioButton( selected = (activeSubtitleTrack == audioTrack), diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/CeaUtil.kt b/ui/src/main/java/com/theoplayer/android/ui/util/CeaUtil.kt index 2dc8589..6a0c5cc 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/CeaUtil.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/CeaUtil.kt @@ -7,7 +7,11 @@ private val CEA_FORMATTING_REGEX = "^CC(\\d+)$".toRegex() /** * Checks whether a provided label is CEA-608 or CEA-708 formed. */ -internal fun isLabelCeaFormatted(label: String): Boolean { +internal fun isLabelCeaFormatted(label: String?): Boolean { + if (label == null) { + return false + } + val matchResult = CEA_FORMATTING_REGEX.find(label) val groupValues = matchResult?.groupValues if (matchResult == null || diff --git a/ui/src/test/java/com/theoplayer/android/ui/util/CeaUtilTest.kt b/ui/src/test/java/com/theoplayer/android/ui/util/CeaUtilTest.kt index dac2eb9..8e0a7f3 100644 --- a/ui/src/test/java/com/theoplayer/android/ui/util/CeaUtilTest.kt +++ b/ui/src/test/java/com/theoplayer/android/ui/util/CeaUtilTest.kt @@ -23,7 +23,7 @@ class CeaUtilTest { } data class Args( - val label: String, + val label: String?, val expectedIsCeaFormatted: Boolean, ) @@ -32,6 +32,10 @@ class CeaUtilTest { @Parameterized.Parameters(name = "{0}") fun data() = arrayOf( // False. + Args( + label = null, + expectedIsCeaFormatted = false, + ), Args( label = "", expectedIsCeaFormatted = false, From 8f724f7468745fde80de330c7dba1b3dbbdb4ed0 Mon Sep 17 00:00:00 2001 From: Alex Dadukin Date: Mon, 16 Mar 2026 13:44:35 +0000 Subject: [PATCH 07/46] OPTI-1528: add a DASH stream example with CEA text tracks --- .../main/java/com/theoplayer/android/ui/demo/Streams.kt | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/theoplayer/android/ui/demo/Streams.kt b/app/src/main/java/com/theoplayer/android/ui/demo/Streams.kt index aaf0cce..838b0ed 100644 --- a/app/src/main/java/com/theoplayer/android/ui/demo/Streams.kt +++ b/app/src/main/java/com/theoplayer/android/ui/demo/Streams.kt @@ -41,7 +41,14 @@ val streams by lazy { TypedSource.Builder("https://livesim.dashif.org/livesim/testpic_2s/Manifest.mpd") .build() ).build() - ) + ), + Stream( + title = "Test card (with CEA tracks)", + source = SourceDescription.Builder( + TypedSource.Builder("https://livesim2.dashif.org/vod/testpic_2s/cea608.mpd") + .build() + ).build() + ), ) } From 96b4176001372c49e46a77269d4d5a1d617a445e Mon Sep 17 00:00:00 2001 From: Alex Dadukin Date: Mon, 16 Mar 2026 13:46:44 +0000 Subject: [PATCH 08/46] OPTI-1528: check whether the label is null or blank --- ui/src/main/java/com/theoplayer/android/ui/Helper.kt | 2 +- ui/src/main/java/com/theoplayer/android/ui/util/CeaUtil.kt | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt index 09fdae4..0a9dce3 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt @@ -93,7 +93,7 @@ fun rememberTrackLabel( } }, ) - if (!label.isNullOrEmpty()) { + if (!label.isNullOrBlank()) { return@remember label } val localisedLanguage = track.localisedLanguage diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/CeaUtil.kt b/ui/src/main/java/com/theoplayer/android/ui/util/CeaUtil.kt index 6a0c5cc..923e426 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/CeaUtil.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/CeaUtil.kt @@ -1,5 +1,6 @@ package com.theoplayer.android.ui.util +import androidx.annotation.CheckResult import androidx.annotation.IntRange private val CEA_FORMATTING_REGEX = "^CC(\\d+)$".toRegex() @@ -7,6 +8,7 @@ private val CEA_FORMATTING_REGEX = "^CC(\\d+)$".toRegex() /** * Checks whether a provided label is CEA-608 or CEA-708 formed. */ +@CheckResult internal fun isLabelCeaFormatted(label: String?): Boolean { if (label == null) { return false @@ -32,6 +34,7 @@ internal fun isLabelCeaFormatted(label: String?): Boolean { * @return an optional string composed of a [channelNumber] and a prepended * "CC" suffix, or `null` if the channel number is invalid. */ +@CheckResult internal fun getLabelForChannelNumber( @IntRange(from = 0L, to = 63L) channelNumber: Int?, ): String? { From db63be85436ba837e818733f3d5897761e3c23ac Mon Sep 17 00:00:00 2001 From: Alex Dadukin Date: Mon, 16 Mar 2026 15:38:24 +0000 Subject: [PATCH 09/46] OPTI-1528: address the review feedback --- .../java/com/theoplayer/android/ui/Helper.kt | 41 +---- .../com/theoplayer/android/ui/util/CeaUtil.kt | 14 +- .../theoplayer/android/ui/util/TrackExts.kt | 76 ++++++++- .../theoplayer/android/ui/util/VersionUtil.kt | 30 ++-- .../theoplayer/android/ui/util/CeaUtilTest.kt | 8 +- .../android/ui/util/TrackExtsTest.kt | 144 +++++++++++++++++- .../android/ui/util/VersionUtilTest.kt | 126 +++++++-------- 7 files changed, 290 insertions(+), 149 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt index 0a9dce3..c29e708 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt @@ -5,12 +5,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalResources import com.theoplayer.android.api.player.track.Track -import com.theoplayer.android.api.player.track.texttrack.TextTrack -import com.theoplayer.android.api.player.track.texttrack.TextTrackType -import com.theoplayer.android.ui.util.getLabelForChannelNumber -import com.theoplayer.android.ui.util.isLabelCeaFormatted -import com.theoplayer.android.ui.util.localisedLanguage -import com.theoplayer.android.ui.util.runForPlayerWith +import com.theoplayer.android.ui.util.constructLabel import kotlin.math.absoluteValue /** @@ -74,37 +69,5 @@ fun rememberTrackLabel( track: Track, resources: Resources = LocalResources.current, ): String = remember(key1 = track.id, key2 = track.uid) { - val label: String? = runForPlayerWith( - // With 11 release, the player will no longer - // prefix text tracks with "CC" for CEA-608 and CEA-708, - // if [Track.label] is `null`. - desiredMajorVersion = 11, - actionIfEqualOrAbove = { track.label }, - actionIfBelow = { - if ((track is TextTrack) && isLabelCeaFormatted(track.label)) { - // If we are below 11th major release - // and the label is CEA-formatted we - // can safely assume it was the last resort - // option to produce a meaningful label, given - // we cannot localize the language code in the player. - null - } else { - track.label - } - }, - ) - if (!label.isNullOrBlank()) { - return@remember label - } - val localisedLanguage = track.localisedLanguage - if (localisedLanguage != null) { - return@remember localisedLanguage - } - if ((track is TextTrack) && track.type == TextTrackType.CEA608) { - val channelNumberLabel = getLabelForChannelNumber(track.channelNumber) - if (channelNumberLabel != null) { - return@remember channelNumberLabel - } - } - return@remember resources.getString(R.string.theoplayer_ui_track_unknown) + constructLabel(track) ?: resources.getString(R.string.theoplayer_ui_track_unknown) } \ No newline at end of file diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/CeaUtil.kt b/ui/src/main/java/com/theoplayer/android/ui/util/CeaUtil.kt index 923e426..a6fd685 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/CeaUtil.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/CeaUtil.kt @@ -10,16 +10,14 @@ private val CEA_FORMATTING_REGEX = "^CC(\\d+)$".toRegex() */ @CheckResult internal fun isLabelCeaFormatted(label: String?): Boolean { - if (label == null) { + if (label.isNullOrEmpty()) { return false } - val matchResult = CEA_FORMATTING_REGEX.find(label) - val groupValues = matchResult?.groupValues - if (matchResult == null || - groupValues == null || - // There is one group we want to match with the channel number. - groupValues.size != 2) { + val matchResult = CEA_FORMATTING_REGEX.find(label) ?: return false + val groupValues = matchResult.groupValues + // There is one group we want to match with the channel number. + if (groupValues.size != 2) { return false } @@ -36,7 +34,7 @@ internal fun isLabelCeaFormatted(label: String?): Boolean { */ @CheckResult internal fun getLabelForChannelNumber( - @IntRange(from = 0L, to = 63L) channelNumber: Int?, + @IntRange(from = 0L, to = 63L) channelNumber: Int, ): String? { // CEA-608 only supports channel numbers in [1, 4], // while CEA-708 support service numbers in [1, 63]. diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt index b4d6254..4c65930 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt @@ -1,7 +1,11 @@ package com.theoplayer.android.ui.util import androidx.annotation.CheckResult +import com.theoplayer.android.api.THEOplayerGlobal import com.theoplayer.android.api.player.track.Track +import com.theoplayer.android.api.player.track.texttrack.TextTrack +import com.theoplayer.android.api.player.track.texttrack.TextTrackType +import com.theoplayer.android.ui.R import java.util.Locale private const val LANGUAGE_UNDEFINED = "und" @@ -15,7 +19,7 @@ private const val LANGUAGE_UNDEFINED = "und" * returns `null`. */ @get:CheckResult -internal val Track.localisedLanguage: String? +internal val Track.localizedLanguage: String? get() { val languageCode = this.language if (languageCode.isNullOrBlank() || languageCode == LANGUAGE_UNDEFINED) { @@ -30,3 +34,73 @@ internal val Track.localisedLanguage: String? return localisedLanguage } + +/** + * Constructs a label for the given [Track] instance. + * The method works slightly different for different player version. + * + * On version 10 and below the logic checks the following and condition + * and the first not `null` entry from the list: + * 1. Track label if is not a language code + * or a CEA-prefixed string. + * 2. Track language display name + * 3. Track channel number if a text CEA-608 track + * 4. Track label if was either a language code or a CEA-prefixed string + * + * If none of the above is satisfied, returns `null`. + * + * On version 11 and later the logic has slightly changed as + * the player no longer constructs the [Track.getLabel] internally: + * 1. Track label + * 2. Track language display name + * 3. Track channel number + */ +internal fun constructLabel( + track: Track, +): String? { + val playerVersion = getPlayerMajorVersion(THEOplayerGlobal.getVersion()) + + val label: String? = if( + playerVersion != null && + playerVersion < 11 && + (track is TextTrack) && + ( + isLabelCeaFormatted(track.label) || + (track.label != null && track.language == track.label) + )) { + // If we are below 11th major release + // and the label is CEA-formatted we + // can safely assume it was the last resort + // option to produce a meaningful label, given + // we cannot localize the language code in the player. + null + } else { + // With 11 release, the player will no longer + // prefix text tracks with "CC" for CEA-608 and CEA-708, + // if [Track.label] is `null`. + track.label + } + + if (!label.isNullOrBlank()) { + return label + } + + val localisedLanguage = track.localizedLanguage + if (localisedLanguage != null) { + return localisedLanguage + } + + if ((track is TextTrack) && + track.channelNumber != null && + track.type == TextTrackType.CEA608) { + val channelNumberLabel = getLabelForChannelNumber(track.channelNumber) + if (channelNumberLabel != null) { + return channelNumberLabel + } + if (!track.label.isNullOrBlank()) { + return track.label + } + } + + return null +} diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt b/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt index 97eb1b5..2a7d625 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt @@ -1,26 +1,18 @@ package com.theoplayer.android.ui.util -import com.theoplayer.android.api.THEOplayerGlobal - -private const val VERSION_DELIMITER = "." +private const val DEFAULT_VERSION_DELIMITER = "." /** - * Performs a player version check and executes an appropriate action: - * if the major version is equal or above to the [desiredMajorVersion] - * then [actionIfEqualOrAbove] is triggered, otherwise [actionIfBelow]. + * Extracts a major version number from + * a semver-formatted string. */ -internal inline fun runForPlayerWith( - desiredMajorVersion: Int, - actionIfEqualOrAbove: () -> T, - actionIfBelow: () -> T, -): T { - val version: String? = THEOplayerGlobal.getVersion() - val versionSplits = version?.split(VERSION_DELIMITER) - val majorVersionNumber = versionSplits?.getOrNull(0)?.toIntOrNull() - - return if (majorVersionNumber == null || majorVersionNumber < desiredMajorVersion) { - actionIfBelow() - } else { - actionIfEqualOrAbove() +internal fun getPlayerMajorVersion(version: String): Int? { + val versionSplits = version.split( + DEFAULT_VERSION_DELIMITER, + limit = 3, + ) + if (versionSplits.size != 3) { + return null } + return versionSplits.getOrNull(0)?.toIntOrNull() } diff --git a/ui/src/test/java/com/theoplayer/android/ui/util/CeaUtilTest.kt b/ui/src/test/java/com/theoplayer/android/ui/util/CeaUtilTest.kt index 8e0a7f3..0f95e15 100644 --- a/ui/src/test/java/com/theoplayer/android/ui/util/CeaUtilTest.kt +++ b/ui/src/test/java/com/theoplayer/android/ui/util/CeaUtilTest.kt @@ -120,19 +120,15 @@ class CeaUtilTest { } data class Args( - val channelNumber: Int?, + val channelNumber: Int, val expectedLabel: String?, ) private companion object { @JvmStatic - @Parameterized.Parameters + @Parameterized.Parameters(name = "{0}") fun data() = arrayOf( // Boundary checks. - Args( - channelNumber = null, - expectedLabel = null, - ), Args( channelNumber = -1, expectedLabel = null, diff --git a/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt b/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt index 280d81f..d183285 100644 --- a/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt +++ b/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt @@ -1,15 +1,26 @@ package com.theoplayer.android.ui.util +import com.theoplayer.android.api.THEOplayerGlobal +import com.theoplayer.android.api.event.EventListener +import com.theoplayer.android.api.event.EventType +import com.theoplayer.android.api.event.track.TrackEvent import com.theoplayer.android.api.player.track.Track +import com.theoplayer.android.api.player.track.texttrack.TextTrack +import com.theoplayer.android.api.player.track.texttrack.TextTrackMode +import com.theoplayer.android.api.player.track.texttrack.TextTrackReadyState +import com.theoplayer.android.api.player.track.texttrack.TextTrackType +import com.theoplayer.android.api.player.track.texttrack.cue.TextTrackCueList import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic import org.junit.Assert +import org.junit.Assert.assertEquals import org.junit.Before import org.junit.Test import org.junit.experimental.runners.Enclosed import org.junit.runner.RunWith import org.junit.runners.JUnit4 +import org.junit.runners.Parameterized import java.util.Locale @RunWith(Enclosed::class) @@ -29,19 +40,19 @@ class TrackExtsTest { @Test fun `GIVEN language is null THEN localised language is also null`() { every { track.language } returns null - Assert.assertNull(track.localisedLanguage) + Assert.assertNull(track.localizedLanguage) } @Test fun `GIVEN language is und THEN localised language is null`() { every { track.language } returns LANGUAGE_CODE_UNDEFINED - Assert.assertNull(track.localisedLanguage) + Assert.assertNull(track.localizedLanguage) } @Test fun `GIVEN language is blank THEN localised language is null`() { every { track.language } returns TEST_BLANK_STRING - Assert.assertNull(track.localisedLanguage) + Assert.assertNull(track.localizedLanguage) } @Test @@ -50,7 +61,7 @@ class TrackExtsTest { every { Locale.forLanguageTag(eq(LANGUAGE_CODE_ENGLISH)) } returns locale every { locale.displayLanguage } returns null - Assert.assertNull(track.localisedLanguage) + Assert.assertNull(track.localizedLanguage) } @Test @@ -59,7 +70,7 @@ class TrackExtsTest { every { Locale.forLanguageTag(eq(LANGUAGE_CODE_ENGLISH)) } returns locale every { locale.displayLanguage } returns TEST_BLANK_STRING - Assert.assertNull(track.localisedLanguage) + Assert.assertNull(track.localizedLanguage) } @Test @@ -68,7 +79,7 @@ class TrackExtsTest { every { Locale.forLanguageTag(eq(LANGUAGE_CODE_ENGLISH)) } returns locale every { locale.displayLanguage } returns LOCALISED_ENGLISH_CODE_NAME - Assert.assertEquals(LOCALISED_ENGLISH_CODE_NAME, track.localisedLanguage) + Assert.assertEquals(LOCALISED_ENGLISH_CODE_NAME, track.localizedLanguage) } private companion object { @@ -78,4 +89,123 @@ class TrackExtsTest { const val TEST_BLANK_STRING = " " } } -} \ No newline at end of file + + @RunWith(Parameterized::class) + class ConstructLabelTest( + private val args: Args, + ) { + + private val track = mockk() + + @Before + fun setUp() { + mockkStatic(THEOplayerGlobal::class) + every { THEOplayerGlobal.getVersion() } returns args.playerVersion + + every { track.type } returns TextTrackType.CEA608 + every { track.label } returns args.label + every { track.language } returns args.language +// every { track.channelNumber } returns args.channelNumber + + mockkStatic(Track::localizedLanguage) + every { any().localizedLanguage } returns args.localizedLanguageName + } + + @Test + fun `WHEN a valid track provided THEN returns a correct label`() { + assertEquals( + args.expectedLabel, + constructLabel(track), + ) + } + + data class Args( + val label: String?, + val language: String?, + val localizedLanguageName: String?, + val channelNumber: String?, + val playerVersion: String, + val expectedLabel: String?, + ) + + private companion object { + + const val TEST_PLAYER_VERSION_10 = "10.1.1" + const val TEST_PLAYER_VERSION_11 = "11.0.10" + + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data() = arrayOf( + // Boundary checks. + Args( + label = null, + language = null, + localizedLanguageName = null, + channelNumber = null, + playerVersion = "", + expectedLabel = null, + ), + + // v10 checks. + Args( + label = "Hello world", + language = null, + localizedLanguageName = null, + channelNumber = null, + playerVersion = TEST_PLAYER_VERSION_10, + expectedLabel = "Hello world", + ), + Args( + label = null, + language = "en", + localizedLanguageName = "English", + channelNumber = null, + playerVersion = TEST_PLAYER_VERSION_10, + expectedLabel = "English", + ), + Args( + label = "en", + language = "en", + localizedLanguageName = "English", + channelNumber = null, + playerVersion = TEST_PLAYER_VERSION_10, + expectedLabel = "English", + ), + Args( + label = "en", + language = null, + localizedLanguageName = null, + channelNumber = null, + playerVersion = TEST_PLAYER_VERSION_10, + expectedLabel = "en", + ), + Args( + label = "CC1", + language = "en", + localizedLanguageName = "English", + channelNumber = null, + playerVersion = TEST_PLAYER_VERSION_10, + expectedLabel = "English", + ), + + // v11 checks. + Args( + label = "Hello world", + language = null, + localizedLanguageName = null, + channelNumber = null, + playerVersion = TEST_PLAYER_VERSION_11, + expectedLabel = "Hello world", + ), + Args( + label = "en", + language = "en", + localizedLanguageName = "English", + channelNumber = null, + playerVersion = TEST_PLAYER_VERSION_11, + expectedLabel = "en", + ), + ) + } + } +} diff --git a/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt b/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt index dfe1876..fb88c3f 100644 --- a/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt +++ b/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt @@ -1,89 +1,77 @@ package com.theoplayer.android.ui.util -import com.theoplayer.android.api.THEOplayerGlobal -import io.mockk.every -import io.mockk.mockk -import io.mockk.mockkStatic -import io.mockk.verify -import org.junit.Before +import org.junit.Assert.assertEquals import org.junit.Test import org.junit.experimental.runners.Enclosed import org.junit.runner.RunWith -import org.junit.runners.JUnit4 +import org.junit.runners.Parameterized @RunWith(Enclosed::class) class VersionUtilTest { - @RunWith(JUnit4::class) - class RunForPlayerWithTest { - - private val actionAbove = mockk<() -> Unit>() - private val actionBelow = mockk<() -> Unit>() - - @Before - fun setUp() { - mockkStatic(THEOplayerGlobal::class) - - every { actionAbove.invoke() } returns Unit - every { actionBelow.invoke() } returns Unit - } + @RunWith(Parameterized::class) + class RunForPlayerWithTest( + private val args: Args, + ) { @Test - fun `WHEN THEOplayerGlobal version is null THEN executes action for version below`() { - every { THEOplayerGlobal.getVersion() } returns null - - runForPlayerWith( - desiredMajorVersion = 2, - actionIfEqualOrAbove = actionAbove, - actionIfBelow = actionBelow, + fun `WHEN a version string provided THEN returns a correct major version`() { + assertEquals( + args.expectedMajorVersion, + getPlayerMajorVersion(args.version), ) - - verify { actionBelow() } } - @Test - fun `WHEN THEOplayerGlobal version is invalid THEN executes action for version below`() { - every { THEOplayerGlobal.getVersion() } returns TEST_PLAYER_VERSION_INVALID - - runForPlayerWith( - desiredMajorVersion = 2, - actionIfEqualOrAbove = actionAbove, - actionIfBelow = actionBelow, - ) + data class Args( + val version: String, + val expectedMajorVersion: Int?, + ) - verify { actionBelow() } - } - - @Test - fun `WHEN THEOplayerGlobal version is valid and old THEN executes action for version below`() { - every { THEOplayerGlobal.getVersion() } returns TEST_PLAYER_VERSION_OLD - - runForPlayerWith( - desiredMajorVersion = 2, - actionIfEqualOrAbove = actionAbove, - actionIfBelow = actionBelow, - ) - - verify { actionBelow() } - } - - @Test - fun `WHEN THEOplayerGlobal version is valid and new THEN executes action for version above`() { - every { THEOplayerGlobal.getVersion() } returns TEST_PLAYER_VERSION_NEW + private companion object { - runForPlayerWith( - desiredMajorVersion = 2, - actionIfEqualOrAbove = actionAbove, - actionIfBelow = actionBelow, + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data() = arrayOf( + // Boundary checks. + Args( + version = "", + expectedMajorVersion = null, + ), + Args( + version = "not a version string", + expectedMajorVersion = null, + ), + Args( + version = "1.00", + expectedMajorVersion = null, + ), + + // Regular checks. + Args( + version = "11.0.0", + expectedMajorVersion = 11, + ), + Args( + version = "1.2.3", + expectedMajorVersion = 1, + ), + Args( + version = "9.8.7", + expectedMajorVersion = 9, + ), + Args( + version = "1.1.0-beta01", + expectedMajorVersion = 1, + ), + Args( + version = "2.1.0-beta.1.0", + expectedMajorVersion = 2, + ), + Args( + version = "16.8.2+01", + expectedMajorVersion = 16, + ), ) - - verify { actionAbove() } - } - - private companion object { - const val TEST_PLAYER_VERSION_INVALID = "invalid version" - const val TEST_PLAYER_VERSION_NEW = "2.3.1" - const val TEST_PLAYER_VERSION_OLD = "1.1.5" } } From ec229de8eec82d98a000e847da49f1d9cfeb1a6a Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 16 Mar 2026 18:33:19 +0100 Subject: [PATCH 10/46] Read `Track.channelNumber` using reflection --- .../theoplayer/android/ui/util/TrackExts.kt | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt index 4c65930..e2e3d1c 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt @@ -5,7 +5,7 @@ import com.theoplayer.android.api.THEOplayerGlobal import com.theoplayer.android.api.player.track.Track import com.theoplayer.android.api.player.track.texttrack.TextTrack import com.theoplayer.android.api.player.track.texttrack.TextTrackType -import com.theoplayer.android.ui.R +import java.lang.reflect.Method import java.util.Locale private const val LANGUAGE_UNDEFINED = "und" @@ -90,10 +90,8 @@ internal fun constructLabel( return localisedLanguage } - if ((track is TextTrack) && - track.channelNumber != null && - track.type == TextTrackType.CEA608) { - val channelNumberLabel = getLabelForChannelNumber(track.channelNumber) + if ((track is TextTrack) && track.type == TextTrackType.CEA608) { + val channelNumberLabel = track.channelNumberCompat?.let { getLabelForChannelNumber(it) } if (channelNumberLabel != null) { return channelNumberLabel } @@ -104,3 +102,19 @@ internal fun constructLabel( return null } + +/** + * Returns [TextTrack.channelNumber], if available. + */ +private val TextTrack.channelNumberCompat: Int? + get() = textTrackChannelNumberGetter?.invoke(this) as? Int + +private val textTrackChannelNumberGetter: Method? by lazy { + try { + TextTrack::class.java.getDeclaredMethod("getChannelNumber").also { + check(it.returnType == Int::class.java) + } + } catch (_: Throwable) { + null + } +} From 8f5d777dbf1ef8dd793aff43e85e87f3127b0a49 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 16 Mar 2026 18:34:34 +0100 Subject: [PATCH 11/46] Fix indentation --- .../main/java/com/theoplayer/android/ui/util/TrackExts.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt index e2e3d1c..3f6d2d7 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt @@ -60,14 +60,12 @@ internal fun constructLabel( ): String? { val playerVersion = getPlayerMajorVersion(THEOplayerGlobal.getVersion()) - val label: String? = if( + val label: String? = if ( playerVersion != null && playerVersion < 11 && (track is TextTrack) && - ( - isLabelCeaFormatted(track.label) || - (track.label != null && track.language == track.label) - )) { + (isLabelCeaFormatted(track.label) || (track.label != null && track.language == track.label)) + ) { // If we are below 11th major release // and the label is CEA-formatted we // can safely assume it was the last resort From b6a03dccc0a0624747610e718b1052a4ffcbaf73 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 16 Mar 2026 18:39:02 +0100 Subject: [PATCH 12/46] Simplify --- .../theoplayer/android/ui/util/TrackExts.kt | 37 ++++++------------- 1 file changed, 12 insertions(+), 25 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt index 3f6d2d7..f3c5228 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt @@ -22,17 +22,10 @@ private const val LANGUAGE_UNDEFINED = "und" internal val Track.localizedLanguage: String? get() { val languageCode = this.language - if (languageCode.isNullOrBlank() || languageCode == LANGUAGE_UNDEFINED) { - return null - } - - val localisedLanguage = - Locale.forLanguageTag(languageCode).displayLanguage - if (localisedLanguage.isNullOrBlank()) { - return null - } - - return localisedLanguage + ?.takeUnless { it.isBlank() || it == LANGUAGE_UNDEFINED } + ?: return null + val localisedLanguage = Locale.forLanguageTag(languageCode).displayLanguage + return localisedLanguage.takeUnless { it.isBlank() } } /** @@ -79,23 +72,17 @@ internal fun constructLabel( track.label } - if (!label.isNullOrBlank()) { - return label - } + if (!label.isNullOrBlank()) return label - val localisedLanguage = track.localizedLanguage - if (localisedLanguage != null) { - return localisedLanguage - } + track.localizedLanguage?.let { return it } if ((track is TextTrack) && track.type == TextTrackType.CEA608) { - val channelNumberLabel = track.channelNumberCompat?.let { getLabelForChannelNumber(it) } - if (channelNumberLabel != null) { - return channelNumberLabel - } - if (!track.label.isNullOrBlank()) { - return track.label - } + track.channelNumberCompat + ?.let { getLabelForChannelNumber(it) } + ?.let { return it } + track.label + ?.takeUnless { it.isBlank() } + ?.let { return it } } return null From 6a74318cf8106217547b2a2ced120fe48cc05247 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 16 Mar 2026 18:41:00 +0100 Subject: [PATCH 13/46] Tweak checks --- ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt index f3c5228..61c7e97 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt @@ -51,12 +51,11 @@ internal val Track.localizedLanguage: String? internal fun constructLabel( track: Track, ): String? { - val playerVersion = getPlayerMajorVersion(THEOplayerGlobal.getVersion()) + val playerVersion = getPlayerMajorVersion(THEOplayerGlobal.getVersion()) ?: 0 val label: String? = if ( - playerVersion != null && - playerVersion < 11 && (track is TextTrack) && + playerVersion < 11 && (isLabelCeaFormatted(track.label) || (track.label != null && track.language == track.label)) ) { // If we are below 11th major release From 5b9fe038aad6e296a36c6905767ced21cd612c7c Mon Sep 17 00:00:00 2001 From: Alex Dadukin Date: Mon, 16 Mar 2026 18:47:51 +0000 Subject: [PATCH 14/46] OPTI-1528: return locale full display name in its own locale --- ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt index 61c7e97..b37e35f 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt @@ -24,7 +24,8 @@ internal val Track.localizedLanguage: String? val languageCode = this.language ?.takeUnless { it.isBlank() || it == LANGUAGE_UNDEFINED } ?: return null - val localisedLanguage = Locale.forLanguageTag(languageCode).displayLanguage + val locale = Locale.forLanguageTag(languageCode) + val localisedLanguage = locale.getDisplayName(locale) return localisedLanguage.takeUnless { it.isBlank() } } From 9940a86486b683a74bb871de754a624a7c20fd87 Mon Sep 17 00:00:00 2001 From: Alex Dadukin Date: Tue, 17 Mar 2026 11:03:07 +0000 Subject: [PATCH 15/46] OPTI-1528: fix reflection call --- gradle/libs.versions.toml | 2 +- .../theoplayer/android/ui/util/TrackExts.kt | 37 +++++---- .../android/ui/util/TrackExtsTest.kt | 79 ++++++++++++------- 3 files changed, 74 insertions(+), 44 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 22062b9..dd74eb4 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,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 = "[7.6.0, 11.0)" } +theoplayer = { prefer="10.13.0", strictly = "[7.6.0, 11.0)" } theoplayer-min = { strictly = "7.6.0" } core = "1.18.0" core-pip = "1.0.0-alpha02" diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt index b37e35f..7763185 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt @@ -19,14 +19,14 @@ private const val LANGUAGE_UNDEFINED = "und" * returns `null`. */ @get:CheckResult -internal val Track.localizedLanguage: String? +internal val Track.localizedLanguageName: String? get() { val languageCode = this.language ?.takeUnless { it.isBlank() || it == LANGUAGE_UNDEFINED } ?: return null val locale = Locale.forLanguageTag(languageCode) - val localisedLanguage = locale.getDisplayName(locale) - return localisedLanguage.takeUnless { it.isBlank() } + val localisedLanguage: String? = locale.getDisplayName(locale) + return localisedLanguage?.takeUnless { it.isBlank() } } /** @@ -38,7 +38,7 @@ internal val Track.localizedLanguage: String? * 1. Track label if is not a language code * or a CEA-prefixed string. * 2. Track language display name - * 3. Track channel number if a text CEA-608 track + * 3. Track caption channel if a text CEA-608 track * 4. Track label if was either a language code or a CEA-prefixed string * * If none of the above is satisfied, returns `null`. @@ -47,7 +47,7 @@ internal val Track.localizedLanguage: String? * the player no longer constructs the [Track.getLabel] internally: * 1. Track label * 2. Track language display name - * 3. Track channel number + * 3. Track caption channel */ internal fun constructLabel( track: Track, @@ -72,14 +72,17 @@ internal fun constructLabel( track.label } - if (!label.isNullOrBlank()) return label + if (!label.isNullOrBlank()) { + return label + } - track.localizedLanguage?.let { return it } + track.localizedLanguageName?.let { return it } if ((track is TextTrack) && track.type == TextTrackType.CEA608) { - track.channelNumberCompat + track.captionChannelCompat ?.let { getLabelForChannelNumber(it) } ?.let { return it } + track.label ?.takeUnless { it.isBlank() } ?.let { return it } @@ -89,17 +92,21 @@ internal fun constructLabel( } /** - * Returns [TextTrack.channelNumber], if available. + * Returns [TextTrack.getCaptionChannel], if available. */ -private val TextTrack.channelNumberCompat: Int? - get() = textTrackChannelNumberGetter?.invoke(this) as? Int +private val TextTrack.captionChannelCompat: Int? + get() = textTrackCaptionChannelGetter?.invoke(this) as? Int -private val textTrackChannelNumberGetter: Method? by lazy { +private val textTrackCaptionChannelGetter: Method? by lazy { try { - TextTrack::class.java.getDeclaredMethod("getChannelNumber").also { - check(it.returnType == Int::class.java) + TextTrack::class.java.getMethod("getCaptionChannel").also { + check(it.returnType.kotlin == Int::class) } - } catch (_: Throwable) { + } catch (_: NoSuchMethodException) { + null + } catch (_: SecurityException) { + null + } catch (_: IllegalStateException) { null } } diff --git a/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt b/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt index d183285..1d580cc 100644 --- a/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt +++ b/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt @@ -1,18 +1,14 @@ package com.theoplayer.android.ui.util import com.theoplayer.android.api.THEOplayerGlobal -import com.theoplayer.android.api.event.EventListener -import com.theoplayer.android.api.event.EventType -import com.theoplayer.android.api.event.track.TrackEvent import com.theoplayer.android.api.player.track.Track import com.theoplayer.android.api.player.track.texttrack.TextTrack -import com.theoplayer.android.api.player.track.texttrack.TextTrackMode -import com.theoplayer.android.api.player.track.texttrack.TextTrackReadyState import com.theoplayer.android.api.player.track.texttrack.TextTrackType -import com.theoplayer.android.api.player.track.texttrack.cue.TextTrackCueList +import io.mockk.clearStaticMockk import io.mockk.every import io.mockk.mockk import io.mockk.mockkStatic +import org.junit.After import org.junit.Assert import org.junit.Assert.assertEquals import org.junit.Before @@ -27,7 +23,7 @@ import java.util.Locale class TrackExtsTest { @RunWith(JUnit4::class) - class LocalisedLanguageTest { + class LocalisedLanguageNameTest { private val track = mockk() private val locale = mockk() @@ -37,49 +33,54 @@ class TrackExtsTest { mockkStatic(Locale::class) } + @After + fun tearDown() { + clearStaticMockk(Locale::class) + } + @Test fun `GIVEN language is null THEN localised language is also null`() { every { track.language } returns null - Assert.assertNull(track.localizedLanguage) + Assert.assertNull(track.localizedLanguageName) } @Test fun `GIVEN language is und THEN localised language is null`() { every { track.language } returns LANGUAGE_CODE_UNDEFINED - Assert.assertNull(track.localizedLanguage) + Assert.assertNull(track.localizedLanguageName) } @Test fun `GIVEN language is blank THEN localised language is null`() { every { track.language } returns TEST_BLANK_STRING - Assert.assertNull(track.localizedLanguage) + Assert.assertNull(track.localizedLanguageName) } @Test fun `GIVEN locale returns null as displayLanguage THEN localised language is null`() { every { track.language } returns LANGUAGE_CODE_ENGLISH every { Locale.forLanguageTag(eq(LANGUAGE_CODE_ENGLISH)) } returns locale - every { locale.displayLanguage } returns null + every { locale.getDisplayName(any()) } returns null - Assert.assertNull(track.localizedLanguage) + Assert.assertNull(track.localizedLanguageName) } @Test fun `GIVEN locale returns a blank string as displayLanguage THEN localised language is null`() { every { track.language } returns LANGUAGE_CODE_ENGLISH every { Locale.forLanguageTag(eq(LANGUAGE_CODE_ENGLISH)) } returns locale - every { locale.displayLanguage } returns TEST_BLANK_STRING + every { locale.getDisplayName(any()) } returns TEST_BLANK_STRING - Assert.assertNull(track.localizedLanguage) + Assert.assertNull(track.localizedLanguageName) } @Test fun `GIVEN locale returns a valid display name THEN returns localised name`() { every { track.language } returns LANGUAGE_CODE_ENGLISH every { Locale.forLanguageTag(eq(LANGUAGE_CODE_ENGLISH)) } returns locale - every { locale.displayLanguage } returns LOCALISED_ENGLISH_CODE_NAME + every { locale.getDisplayName(any()) } returns LOCALISED_ENGLISH_CODE_NAME - Assert.assertEquals(LOCALISED_ENGLISH_CODE_NAME, track.localizedLanguage) + assertEquals(LOCALISED_ENGLISH_CODE_NAME, track.localizedLanguageName) } private companion object { @@ -105,10 +106,16 @@ class TrackExtsTest { every { track.type } returns TextTrackType.CEA608 every { track.label } returns args.label every { track.language } returns args.language -// every { track.channelNumber } returns args.channelNumber + every { track.captionChannel } returns args.captionChannel + + mockkStatic(Track::localizedLanguageName) + every { any().localizedLanguageName } returns args.localizedLanguageName + } - mockkStatic(Track::localizedLanguage) - every { any().localizedLanguage } returns args.localizedLanguageName + @After + fun tearDown() { + clearStaticMockk(THEOplayerGlobal::class) + clearStaticMockk(Track::localizedLanguageName) } @Test @@ -123,7 +130,7 @@ class TrackExtsTest { val label: String?, val language: String?, val localizedLanguageName: String?, - val channelNumber: String?, + val captionChannel: Int?, val playerVersion: String, val expectedLabel: String?, ) @@ -141,7 +148,7 @@ class TrackExtsTest { label = null, language = null, localizedLanguageName = null, - channelNumber = null, + captionChannel = null, playerVersion = "", expectedLabel = null, ), @@ -151,7 +158,7 @@ class TrackExtsTest { label = "Hello world", language = null, localizedLanguageName = null, - channelNumber = null, + captionChannel = null, playerVersion = TEST_PLAYER_VERSION_10, expectedLabel = "Hello world", ), @@ -159,7 +166,7 @@ class TrackExtsTest { label = null, language = "en", localizedLanguageName = "English", - channelNumber = null, + captionChannel = null, playerVersion = TEST_PLAYER_VERSION_10, expectedLabel = "English", ), @@ -167,7 +174,7 @@ class TrackExtsTest { label = "en", language = "en", localizedLanguageName = "English", - channelNumber = null, + captionChannel = null, playerVersion = TEST_PLAYER_VERSION_10, expectedLabel = "English", ), @@ -175,7 +182,7 @@ class TrackExtsTest { label = "en", language = null, localizedLanguageName = null, - channelNumber = null, + captionChannel = null, playerVersion = TEST_PLAYER_VERSION_10, expectedLabel = "en", ), @@ -183,17 +190,25 @@ class TrackExtsTest { label = "CC1", language = "en", localizedLanguageName = "English", - channelNumber = null, + captionChannel = null, playerVersion = TEST_PLAYER_VERSION_10, expectedLabel = "English", ), + Args( + label = null, + language = null, + localizedLanguageName = null, + captionChannel = 1, + playerVersion = TEST_PLAYER_VERSION_10, + expectedLabel = "CC1", + ), // v11 checks. Args( label = "Hello world", language = null, localizedLanguageName = null, - channelNumber = null, + captionChannel = null, playerVersion = TEST_PLAYER_VERSION_11, expectedLabel = "Hello world", ), @@ -201,10 +216,18 @@ class TrackExtsTest { label = "en", language = "en", localizedLanguageName = "English", - channelNumber = null, + captionChannel = null, playerVersion = TEST_PLAYER_VERSION_11, expectedLabel = "en", ), + Args( + label = null, + language = null, + localizedLanguageName = null, + captionChannel = 4, + playerVersion = TEST_PLAYER_VERSION_11, + expectedLabel = "CC4", + ), ) } } From e35cec6b06f622323009f58e970e679336bd66bc Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 17 Mar 2026 15:44:58 +0100 Subject: [PATCH 16/46] Add `Version` class --- .../theoplayer/android/ui/util/TrackExts.kt | 4 +- .../theoplayer/android/ui/util/VersionUtil.kt | 48 ++++++++++++++----- .../android/ui/util/VersionUtilTest.kt | 4 +- 3 files changed, 41 insertions(+), 15 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt index 7763185..f9aa371 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt @@ -52,11 +52,11 @@ internal val Track.localizedLanguageName: String? internal fun constructLabel( track: Track, ): String? { - val playerVersion = getPlayerMajorVersion(THEOplayerGlobal.getVersion()) ?: 0 + val playerVersion = Version.parse(THEOplayerGlobal.getVersion()) ?: Version.ZERO val label: String? = if ( (track is TextTrack) && - playerVersion < 11 && + playerVersion.major < 11 && (isLabelCeaFormatted(track.label) || (track.label != null && track.language == track.label)) ) { // If we are below 11th major release diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt b/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt index 2a7d625..a3dfbe1 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt @@ -1,18 +1,44 @@ package com.theoplayer.android.ui.util -private const val DEFAULT_VERSION_DELIMITER = "." +private const val VERSION_DELIMITER = '.' /** - * Extracts a major version number from - * a semver-formatted string. + * A [semver](https://semver.org/) version. */ -internal fun getPlayerMajorVersion(version: String): Int? { - val versionSplits = version.split( - DEFAULT_VERSION_DELIMITER, - limit = 3, - ) - if (versionSplits.size != 3) { - return null +internal data class Version( + /** + * The major version. + */ + val major: Int, + /** + * The minor version. + */ + val minor: Int, + /** + * The patch (and prerelease) version. + */ + val patch: String, +) { + override fun toString() = buildString { + append(major) + append(VERSION_DELIMITER) + append(minor) + append(VERSION_DELIMITER) + append(patch) + } + + companion object { + val ZERO = Version(major = 0, minor = 0, patch = "0") + + fun parse(version: String): Version? { + val versionParts = version.split(VERSION_DELIMITER, limit = 3) + if (versionParts.size != 3) return null + val (major, minor, patch) = versionParts + return Version( + major = major.toIntOrNull() ?: return null, + minor = minor.toIntOrNull() ?: return null, + patch = patch + ) + } } - return versionSplits.getOrNull(0)?.toIntOrNull() } diff --git a/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt b/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt index fb88c3f..27bbccb 100644 --- a/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt +++ b/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt @@ -10,7 +10,7 @@ import org.junit.runners.Parameterized class VersionUtilTest { @RunWith(Parameterized::class) - class RunForPlayerWithTest( + class ParseVersionTest( private val args: Args, ) { @@ -18,7 +18,7 @@ class VersionUtilTest { fun `WHEN a version string provided THEN returns a correct major version`() { assertEquals( args.expectedMajorVersion, - getPlayerMajorVersion(args.version), + Version.parse(args.version)?.major, ) } From e61f50fda347778efa002b600330cb4a00efd104 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 17 Mar 2026 15:46:09 +0100 Subject: [PATCH 17/46] Cache parsed THEOplayer version --- .../java/com/theoplayer/android/ui/Helper.kt | 20 +++++++++++++++++++ .../theoplayer/android/ui/util/TrackExts.kt | 5 +---- .../theoplayer/android/ui/util/VersionUtil.kt | 11 ++++++++++ 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt index c29e708..25fd1c6 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt @@ -70,4 +70,24 @@ fun rememberTrackLabel( resources: Resources = LocalResources.current, ): String = remember(key1 = track.id, key2 = track.uid) { constructLabel(track) ?: resources.getString(R.string.theoplayer_ui_track_unknown) +} + +/** + * Memoize the most recent call. + */ +internal inline fun memoizeLast(crossinline transform: (P) -> R): (P) -> R { + return object : (P) -> R { + private var lastCall: Pair? = null + + override fun invoke(input: P): R { + val lastCall = this.lastCall + return if (lastCall != null && lastCall.first == input) { + lastCall.second + } else { + transform(input).also { output -> + this.lastCall = input to output + } + } + } + } } \ No newline at end of file diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt index f9aa371..f88ed70 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt @@ -1,7 +1,6 @@ package com.theoplayer.android.ui.util import androidx.annotation.CheckResult -import com.theoplayer.android.api.THEOplayerGlobal import com.theoplayer.android.api.player.track.Track import com.theoplayer.android.api.player.track.texttrack.TextTrack import com.theoplayer.android.api.player.track.texttrack.TextTrackType @@ -52,11 +51,9 @@ internal val Track.localizedLanguageName: String? internal fun constructLabel( track: Track, ): String? { - val playerVersion = Version.parse(THEOplayerGlobal.getVersion()) ?: Version.ZERO - val label: String? = if ( (track is TextTrack) && - playerVersion.major < 11 && + theoplayerVersion.major < 11 && (isLabelCeaFormatted(track.label) || (track.label != null && track.language == track.label)) ) { // If we are below 11th major release diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt b/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt index a3dfbe1..8a9bc67 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt @@ -1,5 +1,8 @@ package com.theoplayer.android.ui.util +import com.theoplayer.android.api.THEOplayerGlobal +import com.theoplayer.android.ui.memoizeLast + private const val VERSION_DELIMITER = '.' /** @@ -42,3 +45,11 @@ internal data class Version( } } } + +private val getCachedTheoplayerVersion = memoizeLast(Version::parse) + +/** + * Returns the major version of THEOplayer. + */ +internal val theoplayerVersion: Version + get() = getCachedTheoplayerVersion(THEOplayerGlobal.getVersion()) ?: Version.ZERO From ba3da70bca5b2b8abb175a84a2445a6aa4460851 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 17 Mar 2026 15:54:47 +0100 Subject: [PATCH 18/46] Throw if player version cannot be parsed --- .../theoplayer/android/ui/util/VersionUtil.kt | 26 +++++------ .../android/ui/util/TrackExtsTest.kt | 2 +- .../android/ui/util/VersionUtilTest.kt | 43 +++++++++++-------- 3 files changed, 41 insertions(+), 30 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt b/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt index 8a9bc67..15baa57 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt @@ -31,17 +31,19 @@ internal data class Version( } companion object { - val ZERO = Version(major = 0, minor = 0, patch = "0") - - fun parse(version: String): Version? { - val versionParts = version.split(VERSION_DELIMITER, limit = 3) - if (versionParts.size != 3) return null - val (major, minor, patch) = versionParts - return Version( - major = major.toIntOrNull() ?: return null, - minor = minor.toIntOrNull() ?: return null, - patch = patch - ) + fun parse(version: String): Version { + try { + val versionParts = version.split(VERSION_DELIMITER, limit = 3) + require(versionParts.size == 3) + val (major, minor, patch) = versionParts + return Version( + major = major.toInt(), + minor = minor.toInt(), + patch = patch + ) + } catch (e: IllegalArgumentException) { + throw IllegalArgumentException("Invalid version", e) + } } } } @@ -52,4 +54,4 @@ private val getCachedTheoplayerVersion = memoizeLast(Version::parse) * Returns the major version of THEOplayer. */ internal val theoplayerVersion: Version - get() = getCachedTheoplayerVersion(THEOplayerGlobal.getVersion()) ?: Version.ZERO + get() = getCachedTheoplayerVersion(THEOplayerGlobal.getVersion()) diff --git a/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt b/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt index 1d580cc..236ca2e 100644 --- a/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt +++ b/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt @@ -149,7 +149,7 @@ class TrackExtsTest { language = null, localizedLanguageName = null, captionChannel = null, - playerVersion = "", + playerVersion = "0.0.0", expectedLabel = null, ), diff --git a/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt b/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt index 27bbccb..12624aa 100644 --- a/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt +++ b/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt @@ -1,6 +1,7 @@ package com.theoplayer.android.ui.util import org.junit.Assert.assertEquals +import org.junit.Assert.assertThrows import org.junit.Test import org.junit.experimental.runners.Enclosed import org.junit.runner.RunWith @@ -18,13 +19,13 @@ class VersionUtilTest { fun `WHEN a version string provided THEN returns a correct major version`() { assertEquals( args.expectedMajorVersion, - Version.parse(args.version)?.major, + Version.parse(args.version).major, ) } data class Args( val version: String, - val expectedMajorVersion: Int?, + val expectedMajorVersion: Int, ) private companion object { @@ -32,21 +33,6 @@ class VersionUtilTest { @JvmStatic @Parameterized.Parameters(name = "{0}") fun data() = arrayOf( - // Boundary checks. - Args( - version = "", - expectedMajorVersion = null, - ), - Args( - version = "not a version string", - expectedMajorVersion = null, - ), - Args( - version = "1.00", - expectedMajorVersion = null, - ), - - // Regular checks. Args( version = "11.0.0", expectedMajorVersion = 11, @@ -75,4 +61,27 @@ class VersionUtilTest { } } + @RunWith(Parameterized::class) + class InvalidVersionTest( + private val version: String + ) { + + @Test + fun `WHEN an invalid version string provided THEN throws an error`() { + assertThrows(IllegalArgumentException::class.java) { + Version.parse(version) + } + } + + private companion object { + @JvmStatic + @Parameterized.Parameters(name = "{0}") + fun data() = arrayOf( + "", + "not a version string", + "1.00" + ) + } + } + } From 1dc74662a9817e8ed032cc6075f1800ef3fc67af Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 17 Mar 2026 15:55:46 +0100 Subject: [PATCH 19/46] Rename --- .../java/com/theoplayer/android/ui/util/VersionUtil.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt b/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt index 15baa57..92a7ebe 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt @@ -20,14 +20,14 @@ internal data class Version( /** * The patch (and prerelease) version. */ - val patch: String, + val patchAndPrerelease: String, ) { override fun toString() = buildString { append(major) append(VERSION_DELIMITER) append(minor) append(VERSION_DELIMITER) - append(patch) + append(patchAndPrerelease) } companion object { @@ -35,11 +35,11 @@ internal data class Version( try { val versionParts = version.split(VERSION_DELIMITER, limit = 3) require(versionParts.size == 3) - val (major, minor, patch) = versionParts + val (major, minor, patchAndPrerelease) = versionParts return Version( major = major.toInt(), minor = minor.toInt(), - patch = patch + patchAndPrerelease = patchAndPrerelease ) } catch (e: IllegalArgumentException) { throw IllegalArgumentException("Invalid version", e) From 1fdfcfc1df2f8ca0712eab62e7cf4c28aa0edba2 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 17 Mar 2026 15:58:29 +0100 Subject: [PATCH 20/46] Test the whole version --- .../android/ui/util/VersionUtilTest.kt | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt b/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt index 12624aa..4141ba6 100644 --- a/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt +++ b/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt @@ -8,7 +8,7 @@ import org.junit.runner.RunWith import org.junit.runners.Parameterized @RunWith(Enclosed::class) -class VersionUtilTest { +internal class VersionUtilTest { @RunWith(Parameterized::class) class ParseVersionTest( @@ -18,14 +18,14 @@ class VersionUtilTest { @Test fun `WHEN a version string provided THEN returns a correct major version`() { assertEquals( - args.expectedMajorVersion, - Version.parse(args.version).major, + args.expected, + Version.parse(args.version), ) } data class Args( val version: String, - val expectedMajorVersion: Int, + val expected: Version, ) private companion object { @@ -35,27 +35,27 @@ class VersionUtilTest { fun data() = arrayOf( Args( version = "11.0.0", - expectedMajorVersion = 11, + expected = Version(major = 11, minor = 0, patchAndPrerelease = "0"), ), Args( version = "1.2.3", - expectedMajorVersion = 1, + expected = Version(major = 1, minor = 2, patchAndPrerelease = "3"), ), Args( version = "9.8.7", - expectedMajorVersion = 9, + expected = Version(major = 9, minor = 8, patchAndPrerelease = "7"), ), Args( version = "1.1.0-beta01", - expectedMajorVersion = 1, + expected = Version(major = 1, minor = 1, patchAndPrerelease = "0-beta01"), ), Args( version = "2.1.0-beta.1.0", - expectedMajorVersion = 2, + expected = Version(major = 2, minor = 1, patchAndPrerelease = "0-beta.1.0"), ), Args( version = "16.8.2+01", - expectedMajorVersion = 16, + expected = Version(major = 16, minor = 8, patchAndPrerelease = "2+01"), ), ) } From 63bba7876bbfc4ce6482d0ab02d3253ab5df790b Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 17 Mar 2026 16:01:26 +0100 Subject: [PATCH 21/46] Improve test titles --- .../java/com/theoplayer/android/ui/util/VersionUtilTest.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt b/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt index 4141ba6..b402abe 100644 --- a/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt +++ b/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt @@ -26,7 +26,9 @@ internal class VersionUtilTest { data class Args( val version: String, val expected: Version, - ) + ) { + override fun toString(): String = version + } private companion object { From 650244704cb893bebf5cd813be7095e22617c5d5 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 17 Mar 2026 17:15:54 +0100 Subject: [PATCH 22/46] Make `Version` comparable --- .../theoplayer/android/ui/util/VersionUtil.kt | 9 +++++- .../android/ui/util/VersionUtilTest.kt | 30 +++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt b/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt index 92a7ebe..8c562c6 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt @@ -21,7 +21,7 @@ internal data class Version( * The patch (and prerelease) version. */ val patchAndPrerelease: String, -) { +) : Comparable { override fun toString() = buildString { append(major) append(VERSION_DELIMITER) @@ -30,6 +30,13 @@ internal data class Version( append(patchAndPrerelease) } + override fun compareTo(other: Version): Int { + return compareBy { it.major } + .thenBy { it.minor } + .thenBy { it.patchAndPrerelease } + .compare(this, other) + } + companion object { fun parse(version: String): Version { try { diff --git a/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt b/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt index b402abe..621fdcd 100644 --- a/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt +++ b/ui/src/test/java/com/theoplayer/android/ui/util/VersionUtilTest.kt @@ -86,4 +86,34 @@ internal class VersionUtilTest { } } + class CompareVersionTest { + @Test + fun `WHEN left major is less than right major THEN left is less than right`() { + assertEquals(Version.parse("3.0.0") compareTo Version.parse("4.0.0"), -1) + assertEquals(Version.parse("3.9.1") compareTo Version.parse("4.0.0"), -1) + assertEquals(Version.parse("10.5.3") compareTo Version.parse("11.0.0"), -1) + } + + @Test + fun `WHEN majors are equal and left minor is less than right major THEN left is less than right`() { + assertEquals(Version.parse("3.0.0") compareTo Version.parse("3.1.0"), -1) + assertEquals(Version.parse("3.9.1") compareTo Version.parse("3.10.0"), -1) + assertEquals(Version.parse("10.5.3") compareTo Version.parse("10.6.0"), -1) + } + + @Test + fun `WHEN majors and minors are equal and left patch is less than right patch THEN left is less than right`() { + assertEquals(Version.parse("3.0.0") compareTo Version.parse("3.0.1"), -1) + assertEquals(Version.parse("3.9.1") compareTo Version.parse("3.9.10"), -1) + assertEquals(Version.parse("10.5.3") compareTo Version.parse("10.5.4"), -1) + } + + @Test + fun `WHEN majors, minors and patches are equal THEN left is equal to right`() { + assertEquals(Version.parse("3.0.0") compareTo Version.parse("3.0.0"), 0) + assertEquals(Version.parse("3.9.1") compareTo Version.parse("3.9.1"), 0) + assertEquals(Version.parse("10.5.3") compareTo Version.parse("10.5.3"), 0) + } + } + } From 785cb6df3f9339dfea3b1e64050fd57bee445c14 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 17 Mar 2026 17:17:08 +0100 Subject: [PATCH 23/46] Move to `Compat` --- .../com/theoplayer/android/ui/util/Compat.kt | 24 +++++++++++++++++++ .../theoplayer/android/ui/util/TrackExts.kt | 21 ---------------- 2 files changed, 24 insertions(+), 21 deletions(-) create mode 100644 ui/src/main/java/com/theoplayer/android/ui/util/Compat.kt diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/Compat.kt b/ui/src/main/java/com/theoplayer/android/ui/util/Compat.kt new file mode 100644 index 0000000..cf240e7 --- /dev/null +++ b/ui/src/main/java/com/theoplayer/android/ui/util/Compat.kt @@ -0,0 +1,24 @@ +package com.theoplayer.android.ui.util + +import com.theoplayer.android.api.player.track.texttrack.TextTrack +import java.lang.reflect.Method + +/** + * Returns [TextTrack.getCaptionChannel], if available. + */ +internal val TextTrack.captionChannelCompat: Int? + get() = textTrackCaptionChannelGetter?.invoke(this) as? Int + +private val textTrackCaptionChannelGetter: Method? by lazy { + try { + TextTrack::class.java.getMethod("getCaptionChannel").also { + check(it.returnType.kotlin == Int::class) + } + } catch (_: NoSuchMethodException) { + null + } catch (_: SecurityException) { + null + } catch (_: IllegalStateException) { + null + } +} diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt index f88ed70..a1cb19c 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt @@ -4,7 +4,6 @@ import androidx.annotation.CheckResult import com.theoplayer.android.api.player.track.Track import com.theoplayer.android.api.player.track.texttrack.TextTrack import com.theoplayer.android.api.player.track.texttrack.TextTrackType -import java.lang.reflect.Method import java.util.Locale private const val LANGUAGE_UNDEFINED = "und" @@ -87,23 +86,3 @@ internal fun constructLabel( return null } - -/** - * Returns [TextTrack.getCaptionChannel], if available. - */ -private val TextTrack.captionChannelCompat: Int? - get() = textTrackCaptionChannelGetter?.invoke(this) as? Int - -private val textTrackCaptionChannelGetter: Method? by lazy { - try { - TextTrack::class.java.getMethod("getCaptionChannel").also { - check(it.returnType.kotlin == Int::class) - } - } catch (_: NoSuchMethodException) { - null - } catch (_: SecurityException) { - null - } catch (_: IllegalStateException) { - null - } -} From 834a6089b7e1d36c4d063acbb3965c809cfb73f8 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 17 Mar 2026 17:26:25 +0100 Subject: [PATCH 24/46] Use helper class instead of reflection --- .../com/theoplayer/android/ui/util/Compat.kt | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/Compat.kt b/ui/src/main/java/com/theoplayer/android/ui/util/Compat.kt index cf240e7..9823550 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/Compat.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/Compat.kt @@ -1,24 +1,30 @@ package com.theoplayer.android.ui.util +import androidx.annotation.DoNotInline import com.theoplayer.android.api.player.track.texttrack.TextTrack -import java.lang.reflect.Method /** * Returns [TextTrack.getCaptionChannel], if available. */ internal val TextTrack.captionChannelCompat: Int? - get() = textTrackCaptionChannelGetter?.invoke(this) as? Int + get() { + // TextTrack.getCaptionChannel was added in THEOplayer 10.13.0. + return if (theoplayerVersion >= version1013) { + TheoPlayer1013Impl.getTextTrackCaptionChannel(this) + } else null + } + +private val version1013 = Version(major = 10, minor = 13, patchAndPrerelease = "0") -private val textTrackCaptionChannelGetter: Method? by lazy { - try { - TextTrack::class.java.getMethod("getCaptionChannel").also { - check(it.returnType.kotlin == Int::class) - } - } catch (_: NoSuchMethodException) { - null - } catch (_: SecurityException) { - null - } catch (_: IllegalStateException) { - null +/** + * This class must be loaded **only** with THEOplayer 10.13.0 or higher. + * + * This uses the same pattern as AndroidX AppCompat, + * see e.g. [androidx.appcompat.app.AppCompatDelegate.Api33Impl] + */ +private class TheoPlayer1013Impl private constructor() { + companion object { + @DoNotInline + fun getTextTrackCaptionChannel(track: TextTrack): Int? = track.captionChannel } } From 1fa3e797be70fbe69519946b24e0f31c16456e6a Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 18 Mar 2026 11:32:58 +0100 Subject: [PATCH 25/46] Link to AndroidX docs --- ui/src/main/java/com/theoplayer/android/ui/util/Compat.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/Compat.kt b/ui/src/main/java/com/theoplayer/android/ui/util/Compat.kt index 9823550..cb6d0a7 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/Compat.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/Compat.kt @@ -21,6 +21,8 @@ private val version1013 = Version(major = 10, minor = 13, patchAndPrerelease = " * * This uses the same pattern as AndroidX AppCompat, * see e.g. [androidx.appcompat.app.AppCompatDelegate.Api33Impl] + * and the docs about [API-specific implementations](https://github.com/androidx/androidx/blob/androidx-main/docs/api_guidelines/compat.md#delegating-to-api-specific-implementations-delegating-to-api-specific-implementations) + * and [static shims](https://github.com/androidx/androidx/blob/androidx-main/docs/api_guidelines/platform_compat.md#static-shims-ex-viewcompat-static-shim). */ private class TheoPlayer1013Impl private constructor() { companion object { From 9316f4e130093265186f0ccfdf8a7fcfb7481e84 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Wed, 18 Mar 2026 13:14:59 +0100 Subject: [PATCH 26/46] Simplify mocking THEOplayer version --- .../java/com/theoplayer/android/ui/Helper.kt | 20 ------------------- .../com/theoplayer/android/ui/util/Compat.kt | 2 +- .../theoplayer/android/ui/util/TrackExts.kt | 2 +- .../theoplayer/android/ui/util/VersionUtil.kt | 14 ++++++------- .../android/ui/util/TrackExtsTest.kt | 9 +++++---- 5 files changed, 13 insertions(+), 34 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt index 25fd1c6..16e8c55 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt @@ -71,23 +71,3 @@ fun rememberTrackLabel( ): String = remember(key1 = track.id, key2 = track.uid) { constructLabel(track) ?: resources.getString(R.string.theoplayer_ui_track_unknown) } - -/** - * Memoize the most recent call. - */ -internal inline fun memoizeLast(crossinline transform: (P) -> R): (P) -> R { - return object : (P) -> R { - private var lastCall: Pair? = null - - override fun invoke(input: P): R { - val lastCall = this.lastCall - return if (lastCall != null && lastCall.first == input) { - lastCall.second - } else { - transform(input).also { output -> - this.lastCall = input to output - } - } - } - } -} \ No newline at end of file diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/Compat.kt b/ui/src/main/java/com/theoplayer/android/ui/util/Compat.kt index cb6d0a7..164ed51 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/Compat.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/Compat.kt @@ -9,7 +9,7 @@ import com.theoplayer.android.api.player.track.texttrack.TextTrack internal val TextTrack.captionChannelCompat: Int? get() { // TextTrack.getCaptionChannel was added in THEOplayer 10.13.0. - return if (theoplayerVersion >= version1013) { + return if (THEOplayerGlobalExt.version >= version1013) { TheoPlayer1013Impl.getTextTrackCaptionChannel(this) } else null } diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt index a1cb19c..54bb6c9 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt @@ -52,7 +52,7 @@ internal fun constructLabel( ): String? { val label: String? = if ( (track is TextTrack) && - theoplayerVersion.major < 11 && + THEOplayerGlobalExt.version.major < 11 && (isLabelCeaFormatted(track.label) || (track.label != null && track.language == track.label)) ) { // If we are below 11th major release diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt b/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt index 8c562c6..d654e98 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/VersionUtil.kt @@ -1,7 +1,6 @@ package com.theoplayer.android.ui.util import com.theoplayer.android.api.THEOplayerGlobal -import com.theoplayer.android.ui.memoizeLast private const val VERSION_DELIMITER = '.' @@ -55,10 +54,9 @@ internal data class Version( } } -private val getCachedTheoplayerVersion = memoizeLast(Version::parse) - -/** - * Returns the major version of THEOplayer. - */ -internal val theoplayerVersion: Version - get() = getCachedTheoplayerVersion(THEOplayerGlobal.getVersion()) +internal object THEOplayerGlobalExt { + /** + * Returns the version of THEOplayer, as a [Version]. + */ + val version: Version by lazy { Version.parse(THEOplayerGlobal.getVersion()) } +} diff --git a/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt b/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt index 236ca2e..a4befda 100644 --- a/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt +++ b/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt @@ -1,12 +1,13 @@ package com.theoplayer.android.ui.util -import com.theoplayer.android.api.THEOplayerGlobal import com.theoplayer.android.api.player.track.Track import com.theoplayer.android.api.player.track.texttrack.TextTrack import com.theoplayer.android.api.player.track.texttrack.TextTrackType +import io.mockk.clearMocks import io.mockk.clearStaticMockk import io.mockk.every import io.mockk.mockk +import io.mockk.mockkObject import io.mockk.mockkStatic import org.junit.After import org.junit.Assert @@ -100,8 +101,8 @@ class TrackExtsTest { @Before fun setUp() { - mockkStatic(THEOplayerGlobal::class) - every { THEOplayerGlobal.getVersion() } returns args.playerVersion + mockkObject(THEOplayerGlobalExt) + every { THEOplayerGlobalExt.version } returns Version.parse(args.playerVersion) every { track.type } returns TextTrackType.CEA608 every { track.label } returns args.label @@ -114,7 +115,7 @@ class TrackExtsTest { @After fun tearDown() { - clearStaticMockk(THEOplayerGlobal::class) + clearMocks(THEOplayerGlobalExt) clearStaticMockk(Track::localizedLanguageName) } From c07dbb2e96306280637bf74590c941d29c099a3d Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Fri, 27 Mar 2026 13:43:01 +0100 Subject: [PATCH 27/46] Require THEOplayer 10.13.0 for compiling --- gradle/libs.versions.toml | 2 ++ ui/build.gradle.kts | 2 ++ 2 files changed, 4 insertions(+) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index dd74eb4..935679d 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -18,6 +18,7 @@ androidx-espresso = "3.7.0" androidx-mediarouter = "1.8.1" dokka = "2.0.0" theoplayer = { prefer="10.13.0", strictly = "[7.6.0, 11.0)" } +theoplayer-compile = { prefer = "10.+", strictly = "[10.13.0, 11.0)" } theoplayer-min = { strictly = "7.6.0" } core = "1.18.0" core-pip = "1.0.0-alpha02" @@ -54,6 +55,7 @@ theoplayer-cast = { group = "com.theoplayer.theoplayer-sdk-android", name = "int 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" } +theoplayer-compile = { group = "com.theoplayer.theoplayer-sdk-android", name = "core", version.ref = "theoplayer-compile" } [plugins] android-application = { id = "com.android.application", version.ref = "gradle" } diff --git a/ui/build.gradle.kts b/ui/build.gradle.kts index 918d0db..96fdcf2 100644 --- a/ui/build.gradle.kts +++ b/ui/build.gradle.kts @@ -87,7 +87,9 @@ dependencies { debugImplementation(libs.androidx.compose.ui.tooling) debugImplementation(libs.androidx.compose.ui.testManifest) + compileOnly(libs.theoplayer.compile) api(libs.theoplayer) + testImplementation(libs.theoplayer) dokkaPlugin(libs.dokka.plugin) } From 88121b455d12ceb642f4eeefab9f1a6628a26e00 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Fri, 27 Mar 2026 13:43:24 +0100 Subject: [PATCH 28/46] Prefer latest THEOplayer 10.x version --- 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 935679d..0840087 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -17,7 +17,7 @@ androidx-junit = "1.3.0" androidx-espresso = "3.7.0" androidx-mediarouter = "1.8.1" dokka = "2.0.0" -theoplayer = { prefer="10.13.0", strictly = "[7.6.0, 11.0)" } +theoplayer = { prefer = "10.+", strictly = "[7.6.0, 11.0)" } theoplayer-compile = { prefer = "10.+", strictly = "[10.13.0, 11.0)" } theoplayer-min = { strictly = "7.6.0" } core = "1.18.0" From 52e5efc0dbbae570f8868df7bf37335e7be7580f Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Fri, 27 Mar 2026 13:47:39 +0100 Subject: [PATCH 29/46] Don't test with null `Locale.getDisplayName()` --- .../java/com/theoplayer/android/ui/util/TrackExts.kt | 4 ++-- .../java/com/theoplayer/android/ui/util/TrackExtsTest.kt | 9 --------- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt index 54bb6c9..33f1557 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt @@ -23,8 +23,8 @@ internal val Track.localizedLanguageName: String? ?.takeUnless { it.isBlank() || it == LANGUAGE_UNDEFINED } ?: return null val locale = Locale.forLanguageTag(languageCode) - val localisedLanguage: String? = locale.getDisplayName(locale) - return localisedLanguage?.takeUnless { it.isBlank() } + val localisedLanguage = locale.getDisplayName(locale).orEmpty() + return localisedLanguage.takeUnless { it.isBlank() } } /** diff --git a/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt b/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt index a4befda..d9ff2a0 100644 --- a/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt +++ b/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt @@ -57,15 +57,6 @@ class TrackExtsTest { Assert.assertNull(track.localizedLanguageName) } - @Test - fun `GIVEN locale returns null as displayLanguage THEN localised language is null`() { - every { track.language } returns LANGUAGE_CODE_ENGLISH - every { Locale.forLanguageTag(eq(LANGUAGE_CODE_ENGLISH)) } returns locale - every { locale.getDisplayName(any()) } returns null - - Assert.assertNull(track.localizedLanguageName) - } - @Test fun `GIVEN locale returns a blank string as displayLanguage THEN localised language is null`() { every { track.language } returns LANGUAGE_CODE_ENGLISH From f601bcb0a7ae2b89785026cd54535c564a6d901b Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Fri, 27 Mar 2026 13:57:09 +0100 Subject: [PATCH 30/46] Rename --- .../android/ui/util/TrackExtsTest.kt | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt b/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt index d9ff2a0..a753fc5 100644 --- a/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt +++ b/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt @@ -129,8 +129,8 @@ class TrackExtsTest { private companion object { - const val TEST_PLAYER_VERSION_10 = "10.1.1" - const val TEST_PLAYER_VERSION_11 = "11.0.10" + const val TEST_PLAYER_VERSION_10_0 = "10.0.0" + const val TEST_PLAYER_VERSION_11_0 = "11.0.0" @JvmStatic @Parameterized.Parameters(name = "{0}") @@ -145,13 +145,13 @@ class TrackExtsTest { expectedLabel = null, ), - // v10 checks. + // v10.0 checks. Args( label = "Hello world", language = null, localizedLanguageName = null, captionChannel = null, - playerVersion = TEST_PLAYER_VERSION_10, + playerVersion = TEST_PLAYER_VERSION_10_0, expectedLabel = "Hello world", ), Args( @@ -159,7 +159,7 @@ class TrackExtsTest { language = "en", localizedLanguageName = "English", captionChannel = null, - playerVersion = TEST_PLAYER_VERSION_10, + playerVersion = TEST_PLAYER_VERSION_10_0, expectedLabel = "English", ), Args( @@ -167,7 +167,7 @@ class TrackExtsTest { language = "en", localizedLanguageName = "English", captionChannel = null, - playerVersion = TEST_PLAYER_VERSION_10, + playerVersion = TEST_PLAYER_VERSION_10_0, expectedLabel = "English", ), Args( @@ -175,7 +175,7 @@ class TrackExtsTest { language = null, localizedLanguageName = null, captionChannel = null, - playerVersion = TEST_PLAYER_VERSION_10, + playerVersion = TEST_PLAYER_VERSION_10_0, expectedLabel = "en", ), Args( @@ -183,7 +183,7 @@ class TrackExtsTest { language = "en", localizedLanguageName = "English", captionChannel = null, - playerVersion = TEST_PLAYER_VERSION_10, + playerVersion = TEST_PLAYER_VERSION_10_0, expectedLabel = "English", ), Args( @@ -191,17 +191,17 @@ class TrackExtsTest { language = null, localizedLanguageName = null, captionChannel = 1, - playerVersion = TEST_PLAYER_VERSION_10, + playerVersion = TEST_PLAYER_VERSION_10_0, expectedLabel = "CC1", ), - // v11 checks. + // v11.0 checks. Args( label = "Hello world", language = null, localizedLanguageName = null, captionChannel = null, - playerVersion = TEST_PLAYER_VERSION_11, + playerVersion = TEST_PLAYER_VERSION_11_0, expectedLabel = "Hello world", ), Args( @@ -209,7 +209,7 @@ class TrackExtsTest { language = "en", localizedLanguageName = "English", captionChannel = null, - playerVersion = TEST_PLAYER_VERSION_11, + playerVersion = TEST_PLAYER_VERSION_11_0, expectedLabel = "en", ), Args( @@ -217,7 +217,7 @@ class TrackExtsTest { language = null, localizedLanguageName = null, captionChannel = 4, - playerVersion = TEST_PLAYER_VERSION_11, + playerVersion = TEST_PLAYER_VERSION_11_0, expectedLabel = "CC4", ), ) From 475dd6e4ac66848e06f25f840a9cf3b1201ac546 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Fri, 27 Mar 2026 14:05:12 +0100 Subject: [PATCH 31/46] Fix tests --- .../android/ui/util/TrackExtsTest.kt | 65 +++++++++++++++++-- 1 file changed, 61 insertions(+), 4 deletions(-) diff --git a/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt b/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt index a753fc5..88d9961 100644 --- a/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt +++ b/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt @@ -130,6 +130,7 @@ class TrackExtsTest { private companion object { const val TEST_PLAYER_VERSION_10_0 = "10.0.0" + const val TEST_PLAYER_VERSION_10_13 = "10.13.0" const val TEST_PLAYER_VERSION_11_0 = "11.0.0" @JvmStatic @@ -146,6 +147,8 @@ class TrackExtsTest { ), // v10.0 checks. + // - Track.captionChannel is never set + // - Track.label can be "CC1" or "CC2" Args( label = "Hello world", language = null, @@ -187,20 +190,74 @@ class TrackExtsTest { expectedLabel = "English", ), Args( - label = null, + label = "CC1", language = null, localizedLanguageName = null, - captionChannel = 1, + captionChannel = null, playerVersion = TEST_PLAYER_VERSION_10_0, expectedLabel = "CC1", ), + // v10.13 checks. + // - Track.captionChannel is always set + // - Track.label can be "CC1" or "CC2" + Args( + label = "Hello world", + language = null, + localizedLanguageName = null, + captionChannel = 1, + playerVersion = TEST_PLAYER_VERSION_10_13, + expectedLabel = "Hello world", + ), + Args( + label = "CC1", + language = "en", + localizedLanguageName = "English", + captionChannel = 1, + playerVersion = TEST_PLAYER_VERSION_10_13, + expectedLabel = "English", + ), + Args( + label = "en", + language = "en", + localizedLanguageName = "English", + captionChannel = 1, + playerVersion = TEST_PLAYER_VERSION_10_13, + expectedLabel = "English", + ), + Args( + label = "en", + language = null, + localizedLanguageName = null, + captionChannel = 1, + playerVersion = TEST_PLAYER_VERSION_10_13, + expectedLabel = "en", + ), + Args( + label = "CC1", + language = "en", + localizedLanguageName = "English", + captionChannel = 1, + playerVersion = TEST_PLAYER_VERSION_10_13, + expectedLabel = "English", + ), + Args( + label = "CC4", + language = null, + localizedLanguageName = null, + captionChannel = 4, + playerVersion = TEST_PLAYER_VERSION_10_13, + expectedLabel = "CC4", + ), + // v11.0 checks. + // - Track.captionChannel is always set + // - Track.label must not be "CC1" or "CC2" Args( label = "Hello world", language = null, localizedLanguageName = null, - captionChannel = null, + captionChannel = 1, playerVersion = TEST_PLAYER_VERSION_11_0, expectedLabel = "Hello world", ), @@ -208,7 +265,7 @@ class TrackExtsTest { label = "en", language = "en", localizedLanguageName = "English", - captionChannel = null, + captionChannel = 1, playerVersion = TEST_PLAYER_VERSION_11_0, expectedLabel = "en", ), From 38a83be57ce941da1e9ed255a3482ab98cab9654 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Fri, 27 Mar 2026 14:12:26 +0100 Subject: [PATCH 32/46] Update changelog --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ab01b18..afcd5ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,10 @@ > - 🏠 Internal > - 💅 Polish +## Unreleased + +* 🐛 The language menu now shows CEA-608/708 closed caption tracks with their localized language name (if available) instead of their channel number (e.g. "CC1"). ([#84](https://github.com/THEOplayer/android-ui/pull/84)) + ## 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)) From 2a818cf65f5e643cd5de1e2de5866fa1732e22a3 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Fri, 27 Mar 2026 14:15:05 +0100 Subject: [PATCH 33/46] Tweak --- ui/src/main/java/com/theoplayer/android/ui/Helper.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt index 16e8c55..7099a82 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt @@ -68,6 +68,6 @@ fun formatTime(time: Double, guide: Double = 0.0, preferNegative: Boolean = fals fun rememberTrackLabel( track: Track, resources: Resources = LocalResources.current, -): String = remember(key1 = track.id, key2 = track.uid) { +): String = remember(track.id, track.uid) { constructLabel(track) ?: resources.getString(R.string.theoplayer_ui_track_unknown) } From d575f556109cc6eb58ff42e91f03339184d1d664 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Fri, 27 Mar 2026 14:22:29 +0100 Subject: [PATCH 34/46] Add overloads for MediaTrack and TextTrack --- .../java/com/theoplayer/android/ui/Helper.kt | 33 +++++++++++++++++++ .../theoplayer/android/ui/util/TrackExts.kt | 22 +++++++++---- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt index 7099a82..03b4903 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt @@ -5,6 +5,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalResources import com.theoplayer.android.api.player.track.Track +import com.theoplayer.android.api.player.track.mediatrack.MediaTrack +import com.theoplayer.android.api.player.track.texttrack.TextTrack import com.theoplayer.android.ui.util.constructLabel import kotlin.math.absoluteValue @@ -68,6 +70,37 @@ fun formatTime(time: Double, guide: Double = 0.0, preferNegative: Boolean = fals fun rememberTrackLabel( track: Track, resources: Resources = LocalResources.current, +): String = remember(track.id, track.uid) { + val label = when (track) { + is TextTrack -> constructLabel(track) + is MediaTrack<*> -> constructLabel(track) + else -> null + } + label ?: resources.getString(R.string.theoplayer_ui_track_unknown) +} + +/** + * Return a human-readable label for the given media track. + * + * @param track the media track + */ +@Composable +fun rememberTrackLabel( + track: MediaTrack<*>, + resources: Resources = LocalResources.current, +): String = remember(track.id, track.uid) { + constructLabel(track) ?: resources.getString(R.string.theoplayer_ui_track_unknown) +} + +/** + * Return a human-readable label for the given text track. + * + * @param track the text track + */ +@Composable +fun rememberTrackLabel( + track: TextTrack, + resources: Resources = LocalResources.current, ): String = remember(track.id, track.uid) { constructLabel(track) ?: resources.getString(R.string.theoplayer_ui_track_unknown) } diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt index 33f1557..be4d80d 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt @@ -2,6 +2,7 @@ package com.theoplayer.android.ui.util import androidx.annotation.CheckResult import com.theoplayer.android.api.player.track.Track +import com.theoplayer.android.api.player.track.mediatrack.MediaTrack import com.theoplayer.android.api.player.track.texttrack.TextTrack import com.theoplayer.android.api.player.track.texttrack.TextTrackType import java.util.Locale @@ -28,7 +29,19 @@ internal val Track.localizedLanguageName: String? } /** - * Constructs a label for the given [Track] instance. + * Constructs a label for the given [MediaTrack] instance. + * + * This returns the first non-empty entry from the list: + * 1. Track label + * 2. Track language display name + */ +internal fun constructLabel(track: MediaTrack<*>): String? { + return track.label?.takeUnless { it.isBlank() } + ?: track.localizedLanguageName +} + +/** + * Constructs a label for the given [TextTrack] instance. * The method works slightly different for different player version. * * On version 10 and below the logic checks the following and condition @@ -47,11 +60,8 @@ internal val Track.localizedLanguageName: String? * 2. Track language display name * 3. Track caption channel */ -internal fun constructLabel( - track: Track, -): String? { +internal fun constructLabel(track: TextTrack): String? { val label: String? = if ( - (track is TextTrack) && THEOplayerGlobalExt.version.major < 11 && (isLabelCeaFormatted(track.label) || (track.label != null && track.language == track.label)) ) { @@ -74,7 +84,7 @@ internal fun constructLabel( track.localizedLanguageName?.let { return it } - if ((track is TextTrack) && track.type == TextTrackType.CEA608) { + if (track.type == TextTrackType.CEA608) { track.captionChannelCompat ?.let { getLabelForChannelNumber(it) } ?.let { return it } From d08ac128bd754d45dbaa027e298a0a962be242c6 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Fri, 27 Mar 2026 14:31:01 +0100 Subject: [PATCH 35/46] Rename --- .../java/com/theoplayer/android/ui/SubtitleTrackList.kt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/SubtitleTrackList.kt b/ui/src/main/java/com/theoplayer/android/ui/SubtitleTrackList.kt index 7fbfdfd..9d3bb55 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/SubtitleTrackList.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/SubtitleTrackList.kt @@ -43,17 +43,17 @@ fun SubtitleTrackList( count = subtitleTracks.size, key = { subtitleTracks[it].uid } ) { - val audioTrack = subtitleTracks[it] + val subtitleTrack = subtitleTracks[it] ListItem( - headlineContent = { Text(text = rememberTrackLabel(audioTrack)) }, + headlineContent = { Text(text = rememberTrackLabel(subtitleTrack)) }, leadingContent = { RadioButton( - selected = (activeSubtitleTrack == audioTrack), + selected = (activeSubtitleTrack == subtitleTrack), onClick = null ) }, modifier = Modifier.clickable(onClick = { - player?.activeSubtitleTrack = audioTrack + player?.activeSubtitleTrack = subtitleTrack onClick?.let { it() } }) ) From 010c53f9735ffda6b4619048ab958e8d0a21a304 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Fri, 27 Mar 2026 14:39:35 +0100 Subject: [PATCH 36/46] Simplify --- .../java/com/theoplayer/android/ui/Helper.kt | 37 ++++++++----------- 1 file changed, 15 insertions(+), 22 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt index 03b4903..d09f729 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt @@ -1,9 +1,8 @@ package com.theoplayer.android.ui -import android.content.res.Resources import androidx.compose.runtime.Composable import androidx.compose.runtime.remember -import androidx.compose.ui.platform.LocalResources +import androidx.compose.ui.res.stringResource import com.theoplayer.android.api.player.track.Track import com.theoplayer.android.api.player.track.mediatrack.MediaTrack import com.theoplayer.android.api.player.track.texttrack.TextTrack @@ -67,16 +66,14 @@ fun formatTime(time: Double, guide: Double = 0.0, preferNegative: Boolean = fals * @param track the media track or text track */ @Composable -fun rememberTrackLabel( - track: Track, - resources: Resources = LocalResources.current, -): String = remember(track.id, track.uid) { - val label = when (track) { - is TextTrack -> constructLabel(track) - is MediaTrack<*> -> constructLabel(track) - else -> null - } - label ?: resources.getString(R.string.theoplayer_ui_track_unknown) +fun rememberTrackLabel(track: Track): String { + return remember(track.id, track.uid) { + when (track) { + is TextTrack -> constructLabel(track) + is MediaTrack<*> -> constructLabel(track) + else -> null + } + } ?: stringResource(R.string.theoplayer_ui_track_unknown) } /** @@ -85,11 +82,9 @@ fun rememberTrackLabel( * @param track the media track */ @Composable -fun rememberTrackLabel( - track: MediaTrack<*>, - resources: Resources = LocalResources.current, -): String = remember(track.id, track.uid) { - constructLabel(track) ?: resources.getString(R.string.theoplayer_ui_track_unknown) +fun rememberTrackLabel(track: MediaTrack<*>): String { + return remember(track.id, track.uid) { constructLabel(track) } + ?: stringResource(R.string.theoplayer_ui_track_unknown) } /** @@ -98,9 +93,7 @@ fun rememberTrackLabel( * @param track the text track */ @Composable -fun rememberTrackLabel( - track: TextTrack, - resources: Resources = LocalResources.current, -): String = remember(track.id, track.uid) { - constructLabel(track) ?: resources.getString(R.string.theoplayer_ui_track_unknown) +fun rememberTrackLabel(track: TextTrack): String { + return remember(track.id, track.uid) { constructLabel(track) } + ?: stringResource(R.string.theoplayer_ui_track_unknown) } From ba3170e26dde5ec39144467d4666b005bd189566 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Fri, 27 Mar 2026 14:43:15 +0100 Subject: [PATCH 37/46] Rename back to formatTrackLabel --- .../main/java/com/theoplayer/android/ui/AudioTrackList.kt | 2 +- ui/src/main/java/com/theoplayer/android/ui/Helper.kt | 6 +++--- ui/src/main/java/com/theoplayer/android/ui/LanguageMenu.kt | 4 ++-- .../java/com/theoplayer/android/ui/SubtitleTrackList.kt | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/AudioTrackList.kt b/ui/src/main/java/com/theoplayer/android/ui/AudioTrackList.kt index 855fcb8..a430fbf 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/AudioTrackList.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/AudioTrackList.kt @@ -29,7 +29,7 @@ fun AudioTrackList( ) { val audioTrack = audioTracks[it] ListItem( - headlineContent = { Text(text = rememberTrackLabel(audioTrack)) }, + headlineContent = { Text(text = formatTrackLabel(audioTrack)) }, leadingContent = { RadioButton( selected = (activeAudioTrack == audioTrack), diff --git a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt index d09f729..aa6a485 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Helper.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Helper.kt @@ -66,7 +66,7 @@ fun formatTime(time: Double, guide: Double = 0.0, preferNegative: Boolean = fals * @param track the media track or text track */ @Composable -fun rememberTrackLabel(track: Track): String { +fun formatTrackLabel(track: Track): String { return remember(track.id, track.uid) { when (track) { is TextTrack -> constructLabel(track) @@ -82,7 +82,7 @@ fun rememberTrackLabel(track: Track): String { * @param track the media track */ @Composable -fun rememberTrackLabel(track: MediaTrack<*>): String { +fun formatTrackLabel(track: MediaTrack<*>): String { return remember(track.id, track.uid) { constructLabel(track) } ?: stringResource(R.string.theoplayer_ui_track_unknown) } @@ -93,7 +93,7 @@ fun rememberTrackLabel(track: MediaTrack<*>): String { * @param track the text track */ @Composable -fun rememberTrackLabel(track: TextTrack): String { +fun formatTrackLabel(track: TextTrack): String { return remember(track.id, track.uid) { constructLabel(track) } ?: stringResource(R.string.theoplayer_ui_track_unknown) } diff --git a/ui/src/main/java/com/theoplayer/android/ui/LanguageMenu.kt b/ui/src/main/java/com/theoplayer/android/ui/LanguageMenu.kt index 93d60b7..c3824d4 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/LanguageMenu.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/LanguageMenu.kt @@ -86,7 +86,7 @@ fun MenuScope.LanguageMenuCompact() { ) { Text( modifier = Modifier.weight(1f), - text = player?.activeAudioTrack?.let { rememberTrackLabel(it) } + text = player?.activeAudioTrack?.let { formatTrackLabel(it) } ?: stringResource( R.string.theoplayer_ui_audio_none ), @@ -115,7 +115,7 @@ fun MenuScope.LanguageMenuCompact() { ) { Text( modifier = Modifier.weight(1f), - text = player?.activeSubtitleTrack?.let { rememberTrackLabel(it) } ?: stringResource( + text = player?.activeSubtitleTrack?.let { formatTrackLabel(it) } ?: stringResource( R.string.theoplayer_ui_subtitles_off ), textAlign = TextAlign.Center diff --git a/ui/src/main/java/com/theoplayer/android/ui/SubtitleTrackList.kt b/ui/src/main/java/com/theoplayer/android/ui/SubtitleTrackList.kt index 9d3bb55..c29b3a9 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/SubtitleTrackList.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/SubtitleTrackList.kt @@ -45,7 +45,7 @@ fun SubtitleTrackList( ) { val subtitleTrack = subtitleTracks[it] ListItem( - headlineContent = { Text(text = rememberTrackLabel(subtitleTrack)) }, + headlineContent = { Text(text = formatTrackLabel(subtitleTrack)) }, leadingContent = { RadioButton( selected = (activeSubtitleTrack == subtitleTrack), From 2c389a22d2abba45abd3e68cf5ac4b7058b44615 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Fri, 27 Mar 2026 14:45:14 +0100 Subject: [PATCH 38/46] Tweak changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index afcd5ac..118a2e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ ## Unreleased -* 🐛 The language menu now shows CEA-608/708 closed caption tracks with their localized language name (if available) instead of their channel number (e.g. "CC1"). ([#84](https://github.com/THEOplayer/android-ui/pull/84)) +* 🐛 The language menu now prefers to show CEA-608/708 closed caption tracks with their localized language name (if available) instead of their language code (e.g. "en") or channel number (e.g. "CC1"). ([#84](https://github.com/THEOplayer/android-ui/pull/84)) ## v1.13.3 (2026-03-23) From 485696793649a44ca488428646a9eec7007acc54 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 07:35:39 +0200 Subject: [PATCH 39/46] Bump actions/configure-pages from 5 to 6 (#93) Bumps [actions/configure-pages](https://github.com/actions/configure-pages) from 5 to 6. - [Release notes](https://github.com/actions/configure-pages/releases) - [Commits](https://github.com/actions/configure-pages/compare/v5...v6) --- updated-dependencies: - dependency-name: actions/configure-pages dependency-version: '6' 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/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 59f8ee5..6456cd4 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -27,7 +27,7 @@ jobs: - name: Checkout uses: actions/checkout@v6 - name: Setup Pages - uses: actions/configure-pages@v5 + uses: actions/configure-pages@v6 - name: Setup Java uses: actions/setup-java@v5 with: From 6827084bc47b716dfc3fa417ba1345bef716ad63 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 1 Apr 2026 07:35:57 +0200 Subject: [PATCH 40/46] Bump actions/deploy-pages from 4 to 5 (#94) Bumps [actions/deploy-pages](https://github.com/actions/deploy-pages) from 4 to 5. - [Release notes](https://github.com/actions/deploy-pages/releases) - [Commits](https://github.com/actions/deploy-pages/compare/v4...v5) --- updated-dependencies: - dependency-name: actions/deploy-pages dependency-version: '5' 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/docs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 6456cd4..4b9c9dd 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -58,4 +58,4 @@ jobs: steps: - name: Deploy to GitHub Pages id: deployment - uses: actions/deploy-pages@v4 + uses: actions/deploy-pages@v5 From 52d6db4593832b449a0a584fd4e3d84fce9e8982 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 7 Apr 2026 14:48:50 +0200 Subject: [PATCH 41/46] Remove check for version 11 --- .../theoplayer/android/ui/util/TrackExts.kt | 22 --------------- .../android/ui/util/TrackExtsTest.kt | 28 ------------------- 2 files changed, 50 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt index be4d80d..105d06c 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt @@ -30,10 +30,6 @@ internal val Track.localizedLanguageName: String? /** * Constructs a label for the given [MediaTrack] instance. - * - * This returns the first non-empty entry from the list: - * 1. Track label - * 2. Track language display name */ internal fun constructLabel(track: MediaTrack<*>): String? { return track.label?.takeUnless { it.isBlank() } @@ -42,27 +38,9 @@ internal fun constructLabel(track: MediaTrack<*>): String? { /** * Constructs a label for the given [TextTrack] instance. - * The method works slightly different for different player version. - * - * On version 10 and below the logic checks the following and condition - * and the first not `null` entry from the list: - * 1. Track label if is not a language code - * or a CEA-prefixed string. - * 2. Track language display name - * 3. Track caption channel if a text CEA-608 track - * 4. Track label if was either a language code or a CEA-prefixed string - * - * If none of the above is satisfied, returns `null`. - * - * On version 11 and later the logic has slightly changed as - * the player no longer constructs the [Track.getLabel] internally: - * 1. Track label - * 2. Track language display name - * 3. Track caption channel */ internal fun constructLabel(track: TextTrack): String? { val label: String? = if ( - THEOplayerGlobalExt.version.major < 11 && (isLabelCeaFormatted(track.label) || (track.label != null && track.language == track.label)) ) { // If we are below 11th major release diff --git a/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt b/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt index 88d9961..d916845 100644 --- a/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt +++ b/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt @@ -249,34 +249,6 @@ class TrackExtsTest { playerVersion = TEST_PLAYER_VERSION_10_13, expectedLabel = "CC4", ), - - // v11.0 checks. - // - Track.captionChannel is always set - // - Track.label must not be "CC1" or "CC2" - Args( - label = "Hello world", - language = null, - localizedLanguageName = null, - captionChannel = 1, - playerVersion = TEST_PLAYER_VERSION_11_0, - expectedLabel = "Hello world", - ), - Args( - label = "en", - language = "en", - localizedLanguageName = "English", - captionChannel = 1, - playerVersion = TEST_PLAYER_VERSION_11_0, - expectedLabel = "en", - ), - Args( - label = null, - language = null, - localizedLanguageName = null, - captionChannel = 4, - playerVersion = TEST_PLAYER_VERSION_11_0, - expectedLabel = "CC4", - ), ) } } From 35779c2ef46d4dc1d438e156d050cfceec775122 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 7 Apr 2026 14:49:41 +0200 Subject: [PATCH 42/46] Refactor --- .../theoplayer/android/ui/util/TrackExts.kt | 28 ++++++++----------- 1 file changed, 11 insertions(+), 17 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt index 105d06c..f7f85eb 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt @@ -40,25 +40,19 @@ internal fun constructLabel(track: MediaTrack<*>): String? { * Constructs a label for the given [TextTrack] instance. */ internal fun constructLabel(track: TextTrack): String? { - val label: String? = if ( - (isLabelCeaFormatted(track.label) || (track.label != null && track.language == track.label)) - ) { - // If we are below 11th major release - // and the label is CEA-formatted we - // can safely assume it was the last resort - // option to produce a meaningful label, given - // we cannot localize the language code in the player. - null - } else { - // With 11 release, the player will no longer - // prefix text tracks with "CC" for CEA-608 and CEA-708, - // if [Track.label] is `null`. - track.label + val label = track.label?.takeIf { + when { + // Ignore empty labels. + it.isBlank() -> false + // Ignore default label with just the language code. + it == track.language -> false + // Ignore default label with just the caption channel. + isLabelCeaFormatted(it) -> false + else -> true + } } - if (!label.isNullOrBlank()) { - return label - } + label?.let { return it } track.localizedLanguageName?.let { return it } From 57a637d89a5a2d254e406a7b5daa9efddc0c7441 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 7 Apr 2026 14:50:11 +0200 Subject: [PATCH 43/46] Optimize slightly --- ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt index f7f85eb..b2bed13 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/util/TrackExts.kt @@ -47,7 +47,7 @@ internal fun constructLabel(track: TextTrack): String? { // Ignore default label with just the language code. it == track.language -> false // Ignore default label with just the caption channel. - isLabelCeaFormatted(it) -> false + (track.type == TextTrackType.CEA608 && isLabelCeaFormatted(it)) -> false else -> true } } From aea04ea82fa89cd62491420d7bd5073d726c12f3 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 7 Apr 2026 14:53:27 +0200 Subject: [PATCH 44/46] Update changelog --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 118a2e5..603be8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ ## Unreleased -* 🐛 The language menu now prefers to show CEA-608/708 closed caption tracks with their localized language name (if available) instead of their language code (e.g. "en") or channel number (e.g. "CC1"). ([#84](https://github.com/THEOplayer/android-ui/pull/84)) +* 🐛 The language menu now prefers to show CEA-608/708 closed caption tracks with their localized language name (if available) instead of their language code (e.g. "en") or channel number (e.g. "CC1"). ([#84](https://github.com/THEOplayer/android-ui/pull/84), [#95](https://github.com/THEOplayer/android-ui/pull/95)) ## v1.13.3 (2026-03-23) From 725d27b5970ea3d92dc82a849317d649001dda9e Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 7 Apr 2026 14:54:21 +0200 Subject: [PATCH 45/46] Remove unused constant --- ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt b/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt index d916845..22a76b0 100644 --- a/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt +++ b/ui/src/test/java/com/theoplayer/android/ui/util/TrackExtsTest.kt @@ -131,7 +131,6 @@ class TrackExtsTest { const val TEST_PLAYER_VERSION_10_0 = "10.0.0" const val TEST_PLAYER_VERSION_10_13 = "10.13.0" - const val TEST_PLAYER_VERSION_11_0 = "11.0.0" @JvmStatic @Parameterized.Parameters(name = "{0}") From 90c2bcb8699a8dcf9f3e4413b2694bec54d5d908 Mon Sep 17 00:00:00 2001 From: "theoplayer-bot[bot]" <873105+theoplayer-bot[bot]@users.noreply.github.com> Date: Tue, 7 Apr 2026 14:43:15 +0000 Subject: [PATCH 46/46] 1.13.4 --- CHANGELOG.md | 2 +- gradle.properties | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 603be8b..28098ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ > - 🏠 Internal > - 💅 Polish -## Unreleased +## v1.13.4 (2026-04-07) * 🐛 The language menu now prefers to show CEA-608/708 closed caption tracks with their localized language name (if available) instead of their language code (e.g. "en") or channel number (e.g. "CC1"). ([#84](https://github.com/THEOplayer/android-ui/pull/84), [#95](https://github.com/THEOplayer/android-ui/pull/95)) diff --git a/gradle.properties b/gradle.properties index 62543ed..f6c4344 100644 --- a/gradle.properties +++ b/gradle.properties @@ -27,4 +27,4 @@ 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.3 +version=1.13.4