From edd71ccc399c7e24fffb6c080c31eeb9ca2c3946 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Fri, 18 Aug 2023 12:37:57 +0200 Subject: [PATCH 01/15] Add IMA integration dependencies --- app/build.gradle.kts | 2 ++ gradle/libs.versions.toml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 85f747a..405c40f 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -77,4 +77,6 @@ dependencies { "mavenImplementation"("com.theoplayer.android-ui:android-ui:1.+") implementation(libs.theoplayer) + implementation(libs.theoplayer.ads) + implementation(libs.theoplayer.ads.ima) } \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 63295c5..b5e1eea 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -35,6 +35,8 @@ dokka-plugin = { group = "org.jetbrains.dokka", name = "android-documentation-pl kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin-gradle-plugin" } junit4 = { group = "junit", name = "junit", version.ref = "junit4" } theoplayer = { group = "com.theoplayer.theoplayer-sdk-android", name = "core", version.ref = "theoplayer" } +theoplayer-ads = { group = "com.theoplayer.theoplayer-sdk-android", name = "integration-ads", version.ref = "theoplayer" } +theoplayer-ads-ima = { group = "com.theoplayer.theoplayer-sdk-android", name = "integration-ads-ima", version.ref = "theoplayer" } [plugins] android-application = { id = "com.android.application", version.ref = "gradle" } From fa9e5847b81960ca42428c9fbe9a651ae27cc9b3 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 29 Jul 2024 17:44:55 +0200 Subject: [PATCH 02/15] Make UI ad aware --- .../java/com/theoplayer/android/ui/Player.kt | 27 +++++++++++++++++++ .../com/theoplayer/android/ui/UIController.kt | 4 +++ 2 files changed, 31 insertions(+) diff --git a/ui/src/main/java/com/theoplayer/android/ui/Player.kt b/ui/src/main/java/com/theoplayer/android/ui/Player.kt index be32c17..cf5e7f0 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Player.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Player.kt @@ -11,10 +11,13 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.compose.runtime.staticCompositionLocalOf import com.theoplayer.android.api.THEOplayerView +import com.theoplayer.android.api.ads.Ads import com.theoplayer.android.api.cast.Cast import com.theoplayer.android.api.cast.chromecast.PlayerCastState import com.theoplayer.android.api.error.THEOplayerException import com.theoplayer.android.api.event.EventListener +import com.theoplayer.android.api.event.ads.AdEvent +import com.theoplayer.android.api.event.ads.AdsEventTypes import com.theoplayer.android.api.event.chromecast.CastErrorEvent import com.theoplayer.android.api.event.chromecast.CastStateChangeEvent import com.theoplayer.android.api.event.chromecast.ChromecastEventTypes @@ -92,6 +95,11 @@ interface Player { */ val cast: Cast? + /** + * Returns the raw [Ads] API of the backing THEOplayer instance. + */ + val ads: Ads? + /** * Returns the current playback position of the media, in seconds. */ @@ -178,6 +186,11 @@ interface Player { */ val loading: Boolean + /** + * Returns whether the player is currently playing an ad. + */ + val playingAd: Boolean + /** * Returns the [StreamType] of the media. * @@ -282,6 +295,7 @@ enum class StreamType { internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player { override val player = theoplayerView?.player + override val ads = theoplayerView?.player?.ads override val cast = theoplayerView?.cast override var currentTime by mutableStateOf(0.0) private set @@ -291,6 +305,8 @@ internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player private set override var paused by mutableStateOf(true) private set + override var playingAd by mutableStateOf(false) + private set override var ended by mutableStateOf(false) private set override var seeking by mutableStateOf(false) @@ -331,6 +347,10 @@ internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player videoHeight = player?.videoHeight ?: 0 } + private fun updatePlayingAd() { + playingAd = player?.ads?.isPlaying ?: false + } + private val playListener = EventListener { updateCurrentTimeAndPlaybackState() } private val pauseListener = EventListener { updateCurrentTimeAndPlaybackState() } private val endedListener = EventListener { updateCurrentTimeAndPlaybackState() } @@ -342,6 +362,7 @@ internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player private val readyStateChangeListener = EventListener { updateCurrentTimeAndPlaybackState() } private val resizeListener = EventListener { updateVideoWidthAndHeight() } + private val adListener = EventListener> { updatePlayingAd() } private val sourceChangeListener = EventListener { _source = player?.source error = null @@ -350,6 +371,7 @@ internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player updateDuration() updateVideoWidthAndHeight() updateActiveVideoTrack() + updatePlayingAd() } private val errorListener = EventListener { event -> _source = player?.source @@ -358,6 +380,7 @@ internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player updateDuration() updateVideoWidthAndHeight() updateActiveVideoTrack() + updatePlayingAd() } override fun play() { @@ -657,6 +680,8 @@ internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player TextTrackListEventTypes.TRACKLISTCHANGE, textTrackListChangeListener ) + ads?.addEventListener(AdsEventTypes.AD_BEGIN, adListener) + ads?.addEventListener(AdsEventTypes.AD_END, adListener) cast?.chromecast?.addEventListener( ChromecastEventTypes.STATECHANGE, chromecastStateChangeListener @@ -720,6 +745,8 @@ internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player TextTrackListEventTypes.TRACKLISTCHANGE, textTrackListChangeListener ) + ads?.removeEventListener(AdsEventTypes.AD_BEGIN, adListener) + ads?.removeEventListener(AdsEventTypes.AD_END, adListener) cast?.chromecast?.removeEventListener( ChromecastEventTypes.STATECHANGE, chromecastStateChangeListener diff --git a/ui/src/main/java/com/theoplayer/android/ui/UIController.kt b/ui/src/main/java/com/theoplayer/android/ui/UIController.kt index fbe9cfa..8cd738c 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/UIController.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/UIController.kt @@ -187,6 +187,10 @@ fun UIController( PlayerContainer(modifier = modifier, player = player) { CompositionLocalProvider(LocalPlayer provides player) { + if (player.playingAd) { + // Do not overlay controls in case of ad play-out. + return@CompositionLocalProvider + } AnimatedContent( label = "ContentAnimation", modifier = Modifier From 93612e14a49eff0d78d4ebdd2f0a860b217b56a3 Mon Sep 17 00:00:00 2001 From: Tom Van Laerhoven Date: Wed, 6 Sep 2023 16:33:30 +0200 Subject: [PATCH 03/15] Optionally resume ad when foregrounding app --- .../com/theoplayer/android/ui/UIController.kt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/UIController.kt b/ui/src/main/java/com/theoplayer/android/ui/UIController.kt index 8cd738c..8229bfd 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/UIController.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/UIController.kt @@ -445,6 +445,7 @@ fun rememberPlayer(config: THEOplayerConfig? = null): Player { internal fun rememberTHEOplayerView(config: THEOplayerConfig? = null): THEOplayerView { val context = LocalContext.current val theoplayerView = remember { THEOplayerView(context, config) } + var wasPlayingAd by remember { mutableStateOf(false) } DisposableEffect(theoplayerView) { onDispose { @@ -456,8 +457,19 @@ internal fun rememberTHEOplayerView(config: THEOplayerConfig? = null): THEOplaye DisposableEffect(lifecycle, theoplayerView) { val lifecycleObserver = LifecycleEventObserver { _, event -> when (event) { - Lifecycle.Event.ON_RESUME -> theoplayerView.onResume() - Lifecycle.Event.ON_PAUSE -> theoplayerView.onPause() + Lifecycle.Event.ON_RESUME -> { + theoplayerView.onResume() + if (wasPlayingAd) { + theoplayerView.player.play() + wasPlayingAd = false + } + } + + Lifecycle.Event.ON_PAUSE -> { + wasPlayingAd = theoplayerView.player.ads.isPlaying + theoplayerView.onPause() + } + Lifecycle.Event.ON_DESTROY -> theoplayerView.onDestroy() else -> {} } From 699e109e93dc61dc7dcc6b57ea352f2a1b0f4d76 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 29 Jul 2024 17:53:10 +0200 Subject: [PATCH 04/15] Rework --- .../main/java/com/theoplayer/android/ui/UIController.kt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/UIController.kt b/ui/src/main/java/com/theoplayer/android/ui/UIController.kt index 8229bfd..6d90d62 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/UIController.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/UIController.kt @@ -141,6 +141,8 @@ fun UIController( false } else if (!player.firstPlay) { true + } else if (player.playingAd) { + false } else if (forceControlsHidden) { false } else { @@ -187,10 +189,6 @@ fun UIController( PlayerContainer(modifier = modifier, player = player) { CompositionLocalProvider(LocalPlayer provides player) { - if (player.playingAd) { - // Do not overlay controls in case of ad play-out. - return@CompositionLocalProvider - } AnimatedContent( label = "ContentAnimation", modifier = Modifier @@ -244,7 +242,7 @@ fun UIController( is UIState.Controls -> { scope.PlayerControls( controlsVisible = controlsVisible.value, - animationsActive = isReady, + animationsActive = isReady && !player.playingAd, centerOverlay = centerOverlay, topChrome = topChrome, centerChrome = centerChrome, From 8c9e54395869bdb4b317a4c3f303eb530166b457 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 29 Jul 2024 18:26:39 +0200 Subject: [PATCH 05/15] Add "Select a stream" dialog to demo --- .../android/ui/demo/MainActivity.kt | 80 ++++++++++++++++--- .../com/theoplayer/android/ui/demo/Streams.kt | 18 +++++ 2 files changed, 87 insertions(+), 11 deletions(-) create mode 100644 app/src/main/java/com/theoplayer/android/ui/demo/Streams.kt diff --git a/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt b/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt index 6618204..8d02e76 100644 --- a/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt +++ b/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt @@ -3,19 +3,26 @@ package com.theoplayer.android.ui.demo import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Brush +import androidx.compose.material.icons.rounded.Movie import androidx.compose.material.icons.rounded.Refresh import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog import com.theoplayer.android.api.THEOplayerConfig -import com.theoplayer.android.api.source.SourceDescription -import com.theoplayer.android.api.source.TypedSource import com.theoplayer.android.ui.DefaultUI import com.theoplayer.android.ui.demo.nitflex.NitflexUI import com.theoplayer.android.ui.demo.nitflex.theme.NitflexTheme @@ -37,14 +44,12 @@ class MainActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3Api::class) @Composable fun MainContent() { - val source = SourceDescription.Builder( - TypedSource.Builder("https://cdn.theoplayer.com/video/elephants-dream/playlist.m3u8") - .build() - ).build() + var stream by remember { mutableStateOf(streams.first()) } + var streamMenuOpen by remember { mutableStateOf(false) } val player = rememberPlayer() - LaunchedEffect(player, source) { - player.source = source + LaunchedEffect(player, stream) { + player.source = stream.source } var themeMenuOpen by remember { mutableStateOf(false) } @@ -61,11 +66,14 @@ fun MainContent() { }, actions = { IconButton(onClick = { - player.source = source + player.source = stream.source player.play() }) { Icon(Icons.Rounded.Refresh, contentDescription = "Reload") } + IconButton(onClick = { streamMenuOpen = true }) { + Icon(Icons.Rounded.Movie, contentDescription = "Stream") + } IconButton(onClick = { themeMenuOpen = true }) { Icon(Icons.Rounded.Brush, contentDescription = "Theme") } @@ -90,7 +98,7 @@ fun MainContent() { DefaultUI( modifier = playerModifier, player = player, - title = "Elephant's Dream" + title = stream.title ) } @@ -99,11 +107,23 @@ fun MainContent() { NitflexUI( modifier = playerModifier, player = player, - title = "Elephant's Dream" + title = stream.title ) } } } + + if (streamMenuOpen) { + SelectStreamDialog( + streams = streams, + currentStream = stream, + onSelectStream = { + stream = it + streamMenuOpen = false + }, + onDismissRequest = { streamMenuOpen = false } + ) + } }) } } @@ -113,6 +133,44 @@ enum class PlayerTheme { Nitflex } +@Composable +fun SelectStreamDialog( + streams: List, + currentStream: Stream, + onSelectStream: (Stream) -> Unit, + onDismissRequest: () -> Unit, +) { + Dialog(onDismissRequest = onDismissRequest) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = "Select a stream", + style = MaterialTheme.typography.headlineSmall + ) + LazyColumn { + items(items = streams) { + ListItem( + headlineContent = { Text(text = it.title) }, + leadingContent = { + RadioButton( + selected = (it == currentStream), + onClick = null + ) + }, + modifier = Modifier.clickable(onClick = { + onSelectStream(it) + }) + ) + } + } + } + } + } +} + @Preview(showBackground = true) @Composable fun DefaultPreview() { 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 new file mode 100644 index 0000000..8c85857 --- /dev/null +++ b/app/src/main/java/com/theoplayer/android/ui/demo/Streams.kt @@ -0,0 +1,18 @@ +package com.theoplayer.android.ui.demo + +import com.theoplayer.android.api.source.SourceDescription +import com.theoplayer.android.api.source.TypedSource + +data class Stream(val title: String, val source: SourceDescription) + +val streams by lazy { + listOf( + Stream( + title = "Elephant's Dream", + source = SourceDescription.Builder( + TypedSource.Builder("https://cdn.theoplayer.com/video/elephants-dream/playlist.m3u8") + .build() + ).build() + ) + ) +} \ No newline at end of file From a2f70e21eb5f6a18cd570f20a68b33bbd0aefef1 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 29 Jul 2024 18:35:39 +0200 Subject: [PATCH 06/15] Add stream with preroll ad to demo --- .../com/theoplayer/android/ui/demo/MainActivity.kt | 6 ++++++ .../java/com/theoplayer/android/ui/demo/Streams.kt | 14 +++++++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt b/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt index 8d02e76..e0773c0 100644 --- a/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt +++ b/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt @@ -23,6 +23,7 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import androidx.compose.ui.window.Dialog import com.theoplayer.android.api.THEOplayerConfig +import com.theoplayer.android.api.ads.ima.GoogleImaIntegrationFactory import com.theoplayer.android.ui.DefaultUI import com.theoplayer.android.ui.demo.nitflex.NitflexUI import com.theoplayer.android.ui.demo.nitflex.theme.NitflexTheme @@ -48,6 +49,11 @@ fun MainContent() { var streamMenuOpen by remember { mutableStateOf(false) } val player = rememberPlayer() + LaunchedEffect(player) { + player.theoplayerView?.let { theoplayerView -> + theoplayerView.player.addIntegration(GoogleImaIntegrationFactory.createGoogleImaIntegration(theoplayerView)) + } + } LaunchedEffect(player, stream) { player.source = stream.source } 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 8c85857..93e7c8b 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 @@ -2,17 +2,29 @@ package com.theoplayer.android.ui.demo import com.theoplayer.android.api.source.SourceDescription import com.theoplayer.android.api.source.TypedSource +import com.theoplayer.android.api.source.addescription.GoogleImaAdDescription data class Stream(val title: String, val source: SourceDescription) val streams by lazy { listOf( Stream( - title = "Elephant's Dream", + title = "Elephant's Dream (HLS)", source = SourceDescription.Builder( TypedSource.Builder("https://cdn.theoplayer.com/video/elephants-dream/playlist.m3u8") .build() ).build() + ), + Stream( + title = "Sintel (DASH) with preroll ad", + source = SourceDescription.Builder( + TypedSource.Builder("https://cdn.theoplayer.com/video/dash/webvtt-embedded-in-isobmff/Manifest.mpd") + .build() + ).ads( + GoogleImaAdDescription.Builder("https://cdn.theoplayer.com/demos/ads/vast/dfp-preroll-no-skip.xml") + .timeOffset("start") + .build() + ).build() ) ) } \ No newline at end of file From adf37ce5ce6aabc38b1527cd4958fbbac7479201 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 29 Jul 2024 18:59:02 +0200 Subject: [PATCH 07/15] Use dialog for selecting a theme --- .../android/ui/demo/MainActivity.kt | 63 +++++++++++++++---- 1 file changed, 50 insertions(+), 13 deletions(-) diff --git a/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt b/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt index e0773c0..810e3a8 100644 --- a/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt +++ b/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt @@ -83,16 +83,6 @@ fun MainContent() { IconButton(onClick = { themeMenuOpen = true }) { Icon(Icons.Rounded.Brush, contentDescription = "Theme") } - DropdownMenu( - expanded = themeMenuOpen, - onDismissRequest = { themeMenuOpen = false }) { - DropdownMenuItem( - text = { Text(text = "Default theme") }, - onClick = { theme = PlayerTheme.Default }) - DropdownMenuItem( - text = { Text(text = "Nitflex theme") }, - onClick = { theme = PlayerTheme.Nitflex }) - } } ) }, content = { padding -> @@ -130,13 +120,23 @@ fun MainContent() { onDismissRequest = { streamMenuOpen = false } ) } + if (themeMenuOpen) { + SelectThemeDialog( + currentTheme = theme, + onSelectTheme = { + theme = it + themeMenuOpen = false + }, + onDismissRequest = { themeMenuOpen = false } + ) + } }) } } -enum class PlayerTheme { - Default, - Nitflex +enum class PlayerTheme(val title: String) { + Default(title = "Default theme"), + Nitflex(title = "Nitflex theme") } @Composable @@ -177,6 +177,43 @@ fun SelectStreamDialog( } } +@Composable +fun SelectThemeDialog( + currentTheme: PlayerTheme, + onSelectTheme: (PlayerTheme) -> Unit, + onDismissRequest: () -> Unit, +) { + Dialog(onDismissRequest = onDismissRequest) { + Surface( + modifier = Modifier.fillMaxWidth(), + shape = RoundedCornerShape(16.dp), + ) { + Column(modifier = Modifier.padding(16.dp)) { + Text( + text = "Select a theme", + style = MaterialTheme.typography.headlineSmall + ) + LazyColumn { + items(items = PlayerTheme.values()) { + ListItem( + headlineContent = { Text(text = it.title) }, + leadingContent = { + RadioButton( + selected = (it == currentTheme), + onClick = null + ) + }, + modifier = Modifier.clickable(onClick = { + onSelectTheme(it) + }) + ) + } + } + } + } + } +} + @Preview(showBackground = true) @Composable fun DefaultPreview() { From e39af92fbcdf947d6e234cf0a44ffedcbd78d031 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 29 Jul 2024 19:00:27 +0200 Subject: [PATCH 08/15] Tweaks --- .../java/com/theoplayer/android/ui/demo/MainActivity.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt b/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt index 810e3a8..6310abd 100644 --- a/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt +++ b/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt @@ -45,7 +45,7 @@ class MainActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3Api::class) @Composable fun MainContent() { - var stream by remember { mutableStateOf(streams.first()) } + var stream by rememberSaveable { mutableStateOf(streams.first()) } var streamMenuOpen by remember { mutableStateOf(false) } val player = rememberPlayer() @@ -85,7 +85,7 @@ fun MainContent() { } } ) - }, content = { padding -> + }) { padding -> val playerModifier = Modifier .padding(padding) .fillMaxSize(1f) @@ -130,7 +130,7 @@ fun MainContent() { onDismissRequest = { themeMenuOpen = false } ) } - }) + } } } From b0d92bf05fc2a205d5279f2ba7b9d9005177bb5a Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 29 Jul 2024 19:11:32 +0200 Subject: [PATCH 09/15] Remember selected stream across activity recreations --- .../java/com/theoplayer/android/ui/demo/MainActivity.kt | 2 +- .../main/java/com/theoplayer/android/ui/demo/Streams.kt | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt b/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt index 6310abd..48dbae3 100644 --- a/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt +++ b/app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt @@ -45,7 +45,7 @@ class MainActivity : ComponentActivity() { @OptIn(ExperimentalMaterial3Api::class) @Composable fun MainContent() { - var stream by rememberSaveable { mutableStateOf(streams.first()) } + var stream by rememberSaveable(stateSaver = StreamSaver) { mutableStateOf(streams.first()) } var streamMenuOpen by remember { mutableStateOf(false) } val player = rememberPlayer() 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 93e7c8b..a00ac32 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 @@ -1,5 +1,7 @@ package com.theoplayer.android.ui.demo +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.saveable.SaverScope import com.theoplayer.android.api.source.SourceDescription import com.theoplayer.android.api.source.TypedSource import com.theoplayer.android.api.source.addescription.GoogleImaAdDescription @@ -27,4 +29,9 @@ val streams by lazy { ).build() ) ) +} + +object StreamSaver : Saver { + override fun restore(value: Int): Stream? = streams.getOrNull(value) + override fun SaverScope.save(value: Stream): Int = streams.indexOf(value) } \ No newline at end of file From 0543365da156339ab987f7d0d0bca0db35345d2a Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 30 Jul 2024 13:37:56 +0200 Subject: [PATCH 10/15] Add TimeRanges.empty() --- ui/src/main/java/com/theoplayer/android/ui/Player.kt | 4 ++-- ui/src/main/java/com/theoplayer/android/ui/SeekBar.kt | 2 +- ui/src/main/java/com/theoplayer/android/ui/TimeRanges.kt | 8 ++++++++ 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/Player.kt b/ui/src/main/java/com/theoplayer/android/ui/Player.kt index cf5e7f0..3c6fa8e 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/Player.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/Player.kt @@ -301,7 +301,7 @@ internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player private set override var duration by mutableStateOf(Double.NaN) private set - override var seekable by mutableStateOf(TimeRanges(listOf())) + override var seekable by mutableStateOf(TimeRanges.empty()) private set override var paused by mutableStateOf(true) private set @@ -324,7 +324,7 @@ internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player private fun updateCurrentTime() { currentTime = player?.currentTime ?: 0.0 - seekable = player?.seekable?.let { TimeRanges.fromTHEOplayer(it) } ?: TimeRanges(listOf()) + seekable = player?.seekable?.let { TimeRanges.fromTHEOplayer(it) } ?: TimeRanges.empty() } private fun updateDuration() { diff --git a/ui/src/main/java/com/theoplayer/android/ui/SeekBar.kt b/ui/src/main/java/com/theoplayer/android/ui/SeekBar.kt index 39fe984..dbb1735 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/SeekBar.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/SeekBar.kt @@ -26,7 +26,7 @@ fun SeekBar( ) { val player = Player.current val currentTime = player?.currentTime?.toFloat() ?: 0.0f - val seekable = player?.seekable ?: TimeRanges(listOf()) + val seekable = player?.seekable ?: TimeRanges.empty() val valueRange = remember(seekable) { val bounds = seekable.bounds ?: 0.0..0.0 diff --git a/ui/src/main/java/com/theoplayer/android/ui/TimeRanges.kt b/ui/src/main/java/com/theoplayer/android/ui/TimeRanges.kt index 7320bf5..39fe28d 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/TimeRanges.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/TimeRanges.kt @@ -31,6 +31,14 @@ data class TimeRanges(private val ranges: List> } companion object { + private val _empty by lazy(mode = LazyThreadSafetyMode.NONE) { TimeRanges(listOf()) } + + /** + * Returns an empty [TimeRanges]. + */ + @JvmStatic + fun empty(): TimeRanges = _empty + /** * Converts a [com.theoplayer.android.api.timerange.TimeRanges] to a [TimeRanges]. */ From 8ae0733adcc20f1961b21c968bc4ec00d6b4eab4 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 30 Jul 2024 13:57:35 +0200 Subject: [PATCH 11/15] Update dependencies --- gradle/libs.versions.toml | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index b5e1eea..a7d4a47 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,15 +1,15 @@ [versions] gradle = "8.3.2" kotlin-gradle-plugin = "1.8.10" -ktx = "1.12.0" -lifecycle-runtime = "2.7.0" -activity-compose = "1.8.2" -appcompat = "1.6.1" -compose-bom = "2024.04.00" +ktx = "1.13.1" +lifecycle-runtime = "2.8.4" +activity-compose = "1.9.1" +appcompat = "1.7.0" +compose-bom = "2024.06.00" junit4 = "4.13.2" -ui-test-junit4 = "1.6.5" # ...not in BOM for some reason? -androidx-junit = "1.1.5" -androidx-espresso = "3.5.1" +ui-test-junit4 = "1.6.8" # ...not in BOM for some reason? +androidx-junit = "1.2.1" +androidx-espresso = "3.6.1" dokka = "1.9.20" theoplayer = "7.8.0" From 8f789d0ca12449d4b04cd4529cff64950735603e Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 30 Jul 2024 16:08:40 +0200 Subject: [PATCH 12/15] Remove obsolete @OptIn --- ui/src/main/java/com/theoplayer/android/ui/UIController.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/UIController.kt b/ui/src/main/java/com/theoplayer/android/ui/UIController.kt index 6d90d62..20d5ea0 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/UIController.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/UIController.kt @@ -102,7 +102,6 @@ fun UIController( * @param bottomChrome controls to show at the bottom of the player, for example a [SeekBar] * or a [Row] containing a [MuteButton] and a [FullscreenButton]. */ -@OptIn(ExperimentalAnimationApi::class) @Composable fun UIController( modifier: Modifier = Modifier, From 7aea6a8bca4eae0fa611dce8126d6fe4659d095e Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 30 Jul 2024 16:16:31 +0200 Subject: [PATCH 13/15] Disable seekbar while playing an ad --- ui/src/main/java/com/theoplayer/android/ui/SeekBar.kt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/SeekBar.kt b/ui/src/main/java/com/theoplayer/android/ui/SeekBar.kt index dbb1735..7b14218 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/SeekBar.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/SeekBar.kt @@ -27,6 +27,8 @@ fun SeekBar( val player = Player.current val currentTime = player?.currentTime?.toFloat() ?: 0.0f val seekable = player?.seekable ?: TimeRanges.empty() + val playingAd = player?.playingAd ?: false + val enabled = seekable.isNotEmpty() && !playingAd val valueRange = remember(seekable) { val bounds = seekable.bounds ?: 0.0..0.0 @@ -40,7 +42,7 @@ fun SeekBar( colors = colors, value = seekTime ?: currentTime, valueRange = valueRange, - enabled = seekable.isNotEmpty(), + enabled = enabled, onValueChange = remember { { time -> seekTime = time From 08d9ac1966bd60a0aa2774b0091de4e607df8f3c Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Tue, 30 Jul 2024 16:16:48 +0200 Subject: [PATCH 14/15] Use duration when seekable is empty --- ui/src/main/java/com/theoplayer/android/ui/SeekBar.kt | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ui/src/main/java/com/theoplayer/android/ui/SeekBar.kt b/ui/src/main/java/com/theoplayer/android/ui/SeekBar.kt index 7b14218..012e686 100644 --- a/ui/src/main/java/com/theoplayer/android/ui/SeekBar.kt +++ b/ui/src/main/java/com/theoplayer/android/ui/SeekBar.kt @@ -27,12 +27,16 @@ fun SeekBar( val player = Player.current val currentTime = player?.currentTime?.toFloat() ?: 0.0f val seekable = player?.seekable ?: TimeRanges.empty() + val duration = player?.duration ?: Double.NaN val playingAd = player?.playingAd ?: false val enabled = seekable.isNotEmpty() && !playingAd - val valueRange = remember(seekable) { - val bounds = seekable.bounds ?: 0.0..0.0 - bounds.start.toFloat()..bounds.endInclusive.toFloat() + val valueRange = remember(seekable, duration) { + seekable.bounds?.let { bounds -> + bounds.start.toFloat()..bounds.endInclusive.toFloat() + } ?: run { + 0f..(if (duration.isFinite()) duration.toFloat() else 0f) + } } var seekTime by remember { mutableStateOf(null) } var wasPlayingBeforeSeek by remember { mutableStateOf(false) } From 83b7b54cd7d796cc4b9d0962f4e9ac22fd2221e7 Mon Sep 17 00:00:00 2001 From: Mattias Buelens Date: Mon, 12 Aug 2024 11:20:27 +0200 Subject: [PATCH 15/15] Update changelog --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index dc2b4d9..ba2bcb9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ > - 🏠 Internal > - 💅 Polish +## Unreleased + +* 💥 Updated to Jetpack Compose version 1.6.8 ([BOM](https://developer.android.com/jetpack/compose/bom) 2024.06.00). +* 🚀 Added basic support for advertisements. (Requires THEOplayer SDK version 7.10.0 or higher.) + ## v1.6.0 (2024-04-16) * 🚀 Added support for THEOplayer Android SDK version 7.