Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
> - 🏠 Internal
> - 💅 Polish

## v1.12.0 (2025-09-08)

* 🚀 Added `PictureInPictureButton`. ([#19](https://github.com/THEOplayer/android-ui/issues/19), [#70](https://github.com/THEOplayer/android-ui/pull/70))
* 🚀 The default UI now shows a minimal set of controls while playing an ad. ([#71](https://github.com/THEOplayer/android-ui/pull/71))
* 🚀 `UIController` no longer hides all controls while playing an ad. ([#71](https://github.com/THEOplayer/android-ui/pull/71))

## v1.11.1 (2025-08-01)

* 🐛 Fixed clicking on overlays from OptiView Ads not working. ([#68](https://github.com/THEOplayer/android-ui/pull/68))
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
android:supportsPictureInPicture="true"
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboardHidden"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.THEOplayerAndroidUI">
Expand Down
109 changes: 63 additions & 46 deletions app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,23 @@ 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.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.ListItem
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.RadioButton
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBar
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
Expand All @@ -30,6 +44,7 @@ import com.theoplayer.android.api.ads.ima.GoogleImaIntegrationFactory
import com.theoplayer.android.api.cast.CastConfiguration
import com.theoplayer.android.api.cast.CastIntegrationFactory
import com.theoplayer.android.api.cast.CastStrategy
import com.theoplayer.android.api.pip.PipConfiguration
import com.theoplayer.android.ui.DefaultUI
import com.theoplayer.android.ui.demo.nitflex.NitflexUI
import com.theoplayer.android.ui.demo.nitflex.theme.NitflexTheme
Expand Down Expand Up @@ -59,7 +74,10 @@ fun MainContent() {

val context = LocalContext.current
val theoplayerView = remember(context) {
THEOplayerView(context).apply {
val config = THEOplayerConfig.Builder().apply {
pipConfiguration(PipConfiguration.Builder().build())
}.build()
THEOplayerView(context, config).apply {
// Add ads integration through Google IMA
player.addIntegration(
GoogleImaIntegrationFactory.createGoogleImaIntegration(this)
Expand All @@ -81,11 +99,10 @@ fun MainContent() {
var themeMenuOpen by remember { mutableStateOf(false) }
var theme by rememberSaveable { mutableStateOf(PlayerTheme.Default) }

Surface(
Scaffold(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
Scaffold(topBar = {
containerColor = MaterialTheme.colorScheme.background,
topBar = {
TopAppBar(
title = {
Text(text = "Demo")
Expand All @@ -105,51 +122,51 @@ fun MainContent() {
}
}
)
}) { padding ->
val playerModifier = Modifier
.padding(padding)
.fillMaxSize(1f)
when (theme) {
PlayerTheme.Default -> {
DefaultUI(
}
) { padding ->
val playerModifier = Modifier
.padding(padding)
.fillMaxSize(1f)
when (theme) {
PlayerTheme.Default -> {
DefaultUI(
modifier = playerModifier,
player = player,
title = stream.title
)
}

PlayerTheme.Nitflex -> {
NitflexTheme(useDarkTheme = true) {
NitflexUI(
modifier = playerModifier,
player = player,
title = stream.title
)
}

PlayerTheme.Nitflex -> {
NitflexTheme(useDarkTheme = true) {
NitflexUI(
modifier = playerModifier,
player = player,
title = stream.title
)
}
}
}
}

if (streamMenuOpen) {
SelectStreamDialog(
streams = streams,
currentStream = stream,
onSelectStream = {
stream = it
streamMenuOpen = false
},
onDismissRequest = { streamMenuOpen = false }
)
}
if (themeMenuOpen) {
SelectThemeDialog(
currentTheme = theme,
onSelectTheme = {
theme = it
themeMenuOpen = false
},
onDismissRequest = { themeMenuOpen = false }
)
}
if (streamMenuOpen) {
SelectStreamDialog(
streams = streams,
currentStream = stream,
onSelectStream = {
stream = it
streamMenuOpen = false
},
onDismissRequest = { streamMenuOpen = false }
)
}
if (themeMenuOpen) {
SelectThemeDialog(
currentTheme = theme,
onSelectTheme = {
theme = it
themeMenuOpen = false
},
onDismissRequest = { themeMenuOpen = false }
)
}
}
}
Expand Down Expand Up @@ -214,7 +231,7 @@ fun SelectThemeDialog(
style = MaterialTheme.typography.headlineSmall
)
LazyColumn {
items(items = PlayerTheme.values()) {
items(items = PlayerTheme.entries) {
ListItem(
headlineContent = { Text(text = it.title) },
leadingContent = {
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/java/com/theoplayer/android/ui/demo/Streams.kt
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,13 @@ data class Stream(val title: String, val source: SourceDescription)

val streams by lazy {
listOf(
Stream(
title = "Bip Bop (HLS)",
source = SourceDescription.Builder(
TypedSource.Builder("https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8")
.build()
).build()
),
Stream(
title = "Elephant's Dream (HLS)",
source = SourceDescription.Builder(
Expand Down
3 changes: 3 additions & 0 deletions app/src/main/res/values-nl/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -39,4 +39,7 @@
<string name="theoplayer_ui_quality_automatic_with_height">Automatisch (%1$dp)</string>
<string name="theoplayer_ui_track_unknown">Onbekend</string>
<string name="theoplayer_ui_error_title">Fout</string>
<string name="theoplayer_ui_pip_enter">Start picture-in-picture</string>
<string name="theoplayer_ui_pip_exit">Stop picture-in-picture</string>
<string name="theo_pip_placeholder">Video speelt in PiP.</string>
</resources>
3 changes: 3 additions & 0 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -50,4 +50,7 @@
<string name="theoplayer_ui_bandwidth_format_kbps" translatable="false">#kbps</string>
<string name="theoplayer_ui_track_unknown">Unknown</string>
<string name="theoplayer_ui_error_title">An error occurred</string>
<string name="theoplayer_ui_pip_enter">Enter picture-in-picture</string>
<string name="theoplayer_ui_pip_exit">Exit picture-in-picture</string>
<string name="theo_pip_placeholder">Video playing in PiP mode.</string>
</resources>
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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.11.1
version=1.12.0
33 changes: 21 additions & 12 deletions ui/src/main/java/com/theoplayer/android/ui/DefaultUI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ fun DefaultUI(
}
},
topChrome = {
if (player.firstPlay) {
if (player.firstPlay && !player.pictureInPicture) {
Row(verticalAlignment = Alignment.CenterVertically) {
title?.let {
Text(
Expand All @@ -89,37 +89,46 @@ fun DefaultUI(
)
}
Spacer(modifier = Modifier.weight(1f))
LanguageMenuButton()
SettingsMenuButton()
if (!player.playingAd) {
LanguageMenuButton()
SettingsMenuButton()
}
ChromecastButton()
}
}
},
centerChrome = {
if (player.firstPlay) {
if (player.firstPlay && !player.playingAd) {
SeekButton(seekOffset = -10, iconSize = 48.dp, contentPadding = PaddingValues(8.dp))
}
PlayButton(iconModifier = Modifier.size(96.dp), contentPadding = PaddingValues(8.dp))
if (player.firstPlay) {
if (player.firstPlay && !player.playingAd) {
SeekButton(seekOffset = 10, iconSize = 48.dp, contentPadding = PaddingValues(8.dp))
}
},
bottomChrome = {
if (player.firstPlay) {
ChromecastDisplay(modifier = Modifier.padding(8.dp))
if (player.streamType != StreamType.Live) {
if (!player.playingAd && player.streamType != StreamType.Live) {
SeekBar()
}
Row(verticalAlignment = Alignment.CenterVertically) {
MuteButton()
LiveButton()
if (player.streamType != StreamType.Live) {
CurrentTimeDisplay(
showRemaining = player.streamType == StreamType.Dvr,
showDuration = player.streamType == StreamType.Vod
)
if (player.playingAd) {
if (player.streamType != StreamType.Live) {
SeekBar()
}
} else {
LiveButton()
if (player.streamType != StreamType.Live) {
CurrentTimeDisplay(
showRemaining = player.streamType == StreamType.Dvr,
showDuration = player.streamType == StreamType.Vod
)
}
}
Spacer(modifier = Modifier.weight(1f))
PictureInPictureButton()
FullscreenButton()
}
}
Expand Down
41 changes: 28 additions & 13 deletions ui/src/main/java/com/theoplayer/android/ui/FullscreenHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ import android.view.ViewGroup
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine

internal interface FullscreenHandler {
val fullscreen: Boolean
Expand All @@ -29,8 +34,13 @@ internal class FullscreenHandlerImpl(private val view: View) : FullscreenHandler
private var previousViewParent: ViewGroup? = null
private var previousViewIndex: Int = 0
private var previousViewLayoutParams: ViewGroup.LayoutParams? = null
private val scope = CoroutineScope(Dispatchers.Main)

override fun requestFullscreen() {
scope.launch { requestFullscreenAsync() }
}

suspend fun requestFullscreenAsync() {
val activity = view.context as? Activity ?: return
val window = activity.window

Expand All @@ -53,22 +63,25 @@ internal class FullscreenHandlerImpl(private val view: View) : FullscreenHandler
previousViewIndex = parent.indexOfChild(view)
previousViewLayoutParams = view.layoutParams
parent.removeView(view)
rootView.post {
rootView.addView(
view,
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
rootView.postAsync()
rootView.addView(
view,
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
ViewGroup.LayoutParams.MATCH_PARENT
)
}
)
}

fullscreen = true
onFullscreenChangeListener?.onFullscreenChange(fullscreen)
}

override fun exitFullscreen() {
scope.launch { exitFullscreenAsync() }
}

private suspend fun exitFullscreenAsync() {
val activity = view.context as? Activity ?: return
val window = activity.window

Expand All @@ -85,15 +98,17 @@ internal class FullscreenHandlerImpl(private val view: View) : FullscreenHandler
val rootView = activity.findViewById<ViewGroup>(android.R.id.content)
previousViewParent?.let { parent ->
rootView.removeView(view)
parent.post {
parent.addView(view, previousViewIndex, previousViewLayoutParams)
view.layout(view.left, view.top, view.right, view.bottom)
}
parent.postAsync()
parent.addView(view, previousViewIndex, previousViewLayoutParams)
view.layout(view.left, view.top, view.right, view.bottom)
}
previousViewParent = null
previousViewIndex = 0

fullscreen = false
onFullscreenChangeListener?.onFullscreenChange(fullscreen)
}
}
}

private suspend fun View.postAsync() =
suspendCoroutine { continuation -> post { continuation.resume(Unit) } }
Loading
Loading