Skip to content

Commit fe67834

Browse files
Merge pull request #73 from THEOplayer/release/1.12.0
Release 1.12.0
2 parents 33044ad + bc672cf commit fe67834

16 files changed

Lines changed: 317 additions & 85 deletions

File tree

CHANGELOG.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,12 @@
99
> - 🏠 Internal
1010
> - 💅 Polish
1111
12+
## v1.12.0 (2025-09-08)
13+
14+
* 🚀 Added `PictureInPictureButton`. ([#19](https://github.com/THEOplayer/android-ui/issues/19), [#70](https://github.com/THEOplayer/android-ui/pull/70))
15+
* 🚀 The default UI now shows a minimal set of controls while playing an ad. ([#71](https://github.com/THEOplayer/android-ui/pull/71))
16+
* 🚀 `UIController` no longer hides all controls while playing an ad. ([#71](https://github.com/THEOplayer/android-ui/pull/71))
17+
1218
## v1.11.1 (2025-08-01)
1319

1420
* 🐛 Fixed clicking on overlays from OptiView Ads not working. ([#68](https://github.com/THEOplayer/android-ui/pull/68))

app/src/main/AndroidManifest.xml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
tools:targetApi="31">
1919
<activity
2020
android:name=".MainActivity"
21-
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"
21+
android:supportsPictureInPicture="true"
22+
android:configChanges="screenSize|smallestScreenSize|screenLayout|orientation|keyboardHidden"
2223
android:exported="true"
2324
android:label="@string/app_name"
2425
android:theme="@style/Theme.THEOplayerAndroidUI">

app/src/main/java/com/theoplayer/android/ui/demo/MainActivity.kt

Lines changed: 63 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,23 @@ import androidx.compose.material.icons.Icons
1515
import androidx.compose.material.icons.rounded.Brush
1616
import androidx.compose.material.icons.rounded.Movie
1717
import androidx.compose.material.icons.rounded.Refresh
18-
import androidx.compose.material3.*
19-
import androidx.compose.runtime.*
18+
import androidx.compose.material3.ExperimentalMaterial3Api
19+
import androidx.compose.material3.Icon
20+
import androidx.compose.material3.IconButton
21+
import androidx.compose.material3.ListItem
22+
import androidx.compose.material3.MaterialTheme
23+
import androidx.compose.material3.RadioButton
24+
import androidx.compose.material3.Scaffold
25+
import androidx.compose.material3.Surface
26+
import androidx.compose.material3.Text
27+
import androidx.compose.material3.TopAppBar
28+
import androidx.compose.runtime.Composable
29+
import androidx.compose.runtime.LaunchedEffect
30+
import androidx.compose.runtime.getValue
31+
import androidx.compose.runtime.mutableStateOf
32+
import androidx.compose.runtime.remember
2033
import androidx.compose.runtime.saveable.rememberSaveable
34+
import androidx.compose.runtime.setValue
2135
import androidx.compose.ui.Modifier
2236
import androidx.compose.ui.platform.LocalContext
2337
import androidx.compose.ui.tooling.preview.Preview
@@ -30,6 +44,7 @@ import com.theoplayer.android.api.ads.ima.GoogleImaIntegrationFactory
3044
import com.theoplayer.android.api.cast.CastConfiguration
3145
import com.theoplayer.android.api.cast.CastIntegrationFactory
3246
import com.theoplayer.android.api.cast.CastStrategy
47+
import com.theoplayer.android.api.pip.PipConfiguration
3348
import com.theoplayer.android.ui.DefaultUI
3449
import com.theoplayer.android.ui.demo.nitflex.NitflexUI
3550
import com.theoplayer.android.ui.demo.nitflex.theme.NitflexTheme
@@ -59,7 +74,10 @@ fun MainContent() {
5974

6075
val context = LocalContext.current
6176
val theoplayerView = remember(context) {
62-
THEOplayerView(context).apply {
77+
val config = THEOplayerConfig.Builder().apply {
78+
pipConfiguration(PipConfiguration.Builder().build())
79+
}.build()
80+
THEOplayerView(context, config).apply {
6381
// Add ads integration through Google IMA
6482
player.addIntegration(
6583
GoogleImaIntegrationFactory.createGoogleImaIntegration(this)
@@ -81,11 +99,10 @@ fun MainContent() {
8199
var themeMenuOpen by remember { mutableStateOf(false) }
82100
var theme by rememberSaveable { mutableStateOf(PlayerTheme.Default) }
83101

84-
Surface(
102+
Scaffold(
85103
modifier = Modifier.fillMaxSize(),
86-
color = MaterialTheme.colorScheme.background
87-
) {
88-
Scaffold(topBar = {
104+
containerColor = MaterialTheme.colorScheme.background,
105+
topBar = {
89106
TopAppBar(
90107
title = {
91108
Text(text = "Demo")
@@ -105,51 +122,51 @@ fun MainContent() {
105122
}
106123
}
107124
)
108-
}) { padding ->
109-
val playerModifier = Modifier
110-
.padding(padding)
111-
.fillMaxSize(1f)
112-
when (theme) {
113-
PlayerTheme.Default -> {
114-
DefaultUI(
125+
}
126+
) { padding ->
127+
val playerModifier = Modifier
128+
.padding(padding)
129+
.fillMaxSize(1f)
130+
when (theme) {
131+
PlayerTheme.Default -> {
132+
DefaultUI(
133+
modifier = playerModifier,
134+
player = player,
135+
title = stream.title
136+
)
137+
}
138+
139+
PlayerTheme.Nitflex -> {
140+
NitflexTheme(useDarkTheme = true) {
141+
NitflexUI(
115142
modifier = playerModifier,
116143
player = player,
117144
title = stream.title
118145
)
119146
}
120-
121-
PlayerTheme.Nitflex -> {
122-
NitflexTheme(useDarkTheme = true) {
123-
NitflexUI(
124-
modifier = playerModifier,
125-
player = player,
126-
title = stream.title
127-
)
128-
}
129-
}
130147
}
148+
}
131149

132-
if (streamMenuOpen) {
133-
SelectStreamDialog(
134-
streams = streams,
135-
currentStream = stream,
136-
onSelectStream = {
137-
stream = it
138-
streamMenuOpen = false
139-
},
140-
onDismissRequest = { streamMenuOpen = false }
141-
)
142-
}
143-
if (themeMenuOpen) {
144-
SelectThemeDialog(
145-
currentTheme = theme,
146-
onSelectTheme = {
147-
theme = it
148-
themeMenuOpen = false
149-
},
150-
onDismissRequest = { themeMenuOpen = false }
151-
)
152-
}
150+
if (streamMenuOpen) {
151+
SelectStreamDialog(
152+
streams = streams,
153+
currentStream = stream,
154+
onSelectStream = {
155+
stream = it
156+
streamMenuOpen = false
157+
},
158+
onDismissRequest = { streamMenuOpen = false }
159+
)
160+
}
161+
if (themeMenuOpen) {
162+
SelectThemeDialog(
163+
currentTheme = theme,
164+
onSelectTheme = {
165+
theme = it
166+
themeMenuOpen = false
167+
},
168+
onDismissRequest = { themeMenuOpen = false }
169+
)
153170
}
154171
}
155172
}
@@ -214,7 +231,7 @@ fun SelectThemeDialog(
214231
style = MaterialTheme.typography.headlineSmall
215232
)
216233
LazyColumn {
217-
items(items = PlayerTheme.values()) {
234+
items(items = PlayerTheme.entries) {
218235
ListItem(
219236
headlineContent = { Text(text = it.title) },
220237
leadingContent = {

app/src/main/java/com/theoplayer/android/ui/demo/Streams.kt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,13 @@ data class Stream(val title: String, val source: SourceDescription)
1010

1111
val streams by lazy {
1212
listOf(
13+
Stream(
14+
title = "Bip Bop (HLS)",
15+
source = SourceDescription.Builder(
16+
TypedSource.Builder("https://devstreaming-cdn.apple.com/videos/streaming/examples/bipbop_16x9/bipbop_16x9_variant.m3u8")
17+
.build()
18+
).build()
19+
),
1320
Stream(
1421
title = "Elephant's Dream (HLS)",
1522
source = SourceDescription.Builder(

app/src/main/res/values-nl/strings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,7 @@
3939
<string name="theoplayer_ui_quality_automatic_with_height">Automatisch (%1$dp)</string>
4040
<string name="theoplayer_ui_track_unknown">Onbekend</string>
4141
<string name="theoplayer_ui_error_title">Fout</string>
42+
<string name="theoplayer_ui_pip_enter">Start picture-in-picture</string>
43+
<string name="theoplayer_ui_pip_exit">Stop picture-in-picture</string>
44+
<string name="theo_pip_placeholder">Video speelt in PiP.</string>
4245
</resources>

app/src/main/res/values/strings.xml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,4 +50,7 @@
5050
<string name="theoplayer_ui_bandwidth_format_kbps" translatable="false">#kbps</string>
5151
<string name="theoplayer_ui_track_unknown">Unknown</string>
5252
<string name="theoplayer_ui_error_title">An error occurred</string>
53+
<string name="theoplayer_ui_pip_enter">Enter picture-in-picture</string>
54+
<string name="theoplayer_ui_pip_exit">Exit picture-in-picture</string>
55+
<string name="theo_pip_placeholder">Video playing in PiP mode.</string>
5356
</resources>

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,4 @@ org.gradle.configuration-cache=true
2727
org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled
2828
org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true
2929
# The version of the THEOplayer Open Video UI for Android.
30-
version=1.11.1
30+
version=1.12.0

ui/src/main/java/com/theoplayer/android/ui/DefaultUI.kt

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ fun DefaultUI(
8080
}
8181
},
8282
topChrome = {
83-
if (player.firstPlay) {
83+
if (player.firstPlay && !player.pictureInPicture) {
8484
Row(verticalAlignment = Alignment.CenterVertically) {
8585
title?.let {
8686
Text(
@@ -89,37 +89,46 @@ fun DefaultUI(
8989
)
9090
}
9191
Spacer(modifier = Modifier.weight(1f))
92-
LanguageMenuButton()
93-
SettingsMenuButton()
92+
if (!player.playingAd) {
93+
LanguageMenuButton()
94+
SettingsMenuButton()
95+
}
9496
ChromecastButton()
9597
}
9698
}
9799
},
98100
centerChrome = {
99-
if (player.firstPlay) {
101+
if (player.firstPlay && !player.playingAd) {
100102
SeekButton(seekOffset = -10, iconSize = 48.dp, contentPadding = PaddingValues(8.dp))
101103
}
102104
PlayButton(iconModifier = Modifier.size(96.dp), contentPadding = PaddingValues(8.dp))
103-
if (player.firstPlay) {
105+
if (player.firstPlay && !player.playingAd) {
104106
SeekButton(seekOffset = 10, iconSize = 48.dp, contentPadding = PaddingValues(8.dp))
105107
}
106108
},
107109
bottomChrome = {
108110
if (player.firstPlay) {
109111
ChromecastDisplay(modifier = Modifier.padding(8.dp))
110-
if (player.streamType != StreamType.Live) {
112+
if (!player.playingAd && player.streamType != StreamType.Live) {
111113
SeekBar()
112114
}
113115
Row(verticalAlignment = Alignment.CenterVertically) {
114116
MuteButton()
115-
LiveButton()
116-
if (player.streamType != StreamType.Live) {
117-
CurrentTimeDisplay(
118-
showRemaining = player.streamType == StreamType.Dvr,
119-
showDuration = player.streamType == StreamType.Vod
120-
)
117+
if (player.playingAd) {
118+
if (player.streamType != StreamType.Live) {
119+
SeekBar()
120+
}
121+
} else {
122+
LiveButton()
123+
if (player.streamType != StreamType.Live) {
124+
CurrentTimeDisplay(
125+
showRemaining = player.streamType == StreamType.Dvr,
126+
showDuration = player.streamType == StreamType.Vod
127+
)
128+
}
121129
}
122130
Spacer(modifier = Modifier.weight(1f))
131+
PictureInPictureButton()
123132
FullscreenButton()
124133
}
125134
}

ui/src/main/java/com/theoplayer/android/ui/FullscreenHandler.kt

Lines changed: 28 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ import android.view.ViewGroup
77
import androidx.core.view.WindowCompat
88
import androidx.core.view.WindowInsetsCompat
99
import androidx.core.view.WindowInsetsControllerCompat
10+
import kotlinx.coroutines.CoroutineScope
11+
import kotlinx.coroutines.Dispatchers
12+
import kotlinx.coroutines.launch
13+
import kotlin.coroutines.resume
14+
import kotlin.coroutines.suspendCoroutine
1015

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

3339
override fun requestFullscreen() {
40+
scope.launch { requestFullscreenAsync() }
41+
}
42+
43+
suspend fun requestFullscreenAsync() {
3444
val activity = view.context as? Activity ?: return
3545
val window = activity.window
3646

@@ -53,22 +63,25 @@ internal class FullscreenHandlerImpl(private val view: View) : FullscreenHandler
5363
previousViewIndex = parent.indexOfChild(view)
5464
previousViewLayoutParams = view.layoutParams
5565
parent.removeView(view)
56-
rootView.post {
57-
rootView.addView(
58-
view,
59-
ViewGroup.LayoutParams(
60-
ViewGroup.LayoutParams.MATCH_PARENT,
61-
ViewGroup.LayoutParams.MATCH_PARENT
62-
)
66+
rootView.postAsync()
67+
rootView.addView(
68+
view,
69+
ViewGroup.LayoutParams(
70+
ViewGroup.LayoutParams.MATCH_PARENT,
71+
ViewGroup.LayoutParams.MATCH_PARENT
6372
)
64-
}
73+
)
6574
}
6675

6776
fullscreen = true
6877
onFullscreenChangeListener?.onFullscreenChange(fullscreen)
6978
}
7079

7180
override fun exitFullscreen() {
81+
scope.launch { exitFullscreenAsync() }
82+
}
83+
84+
private suspend fun exitFullscreenAsync() {
7285
val activity = view.context as? Activity ?: return
7386
val window = activity.window
7487

@@ -85,15 +98,17 @@ internal class FullscreenHandlerImpl(private val view: View) : FullscreenHandler
8598
val rootView = activity.findViewById<ViewGroup>(android.R.id.content)
8699
previousViewParent?.let { parent ->
87100
rootView.removeView(view)
88-
parent.post {
89-
parent.addView(view, previousViewIndex, previousViewLayoutParams)
90-
view.layout(view.left, view.top, view.right, view.bottom)
91-
}
101+
parent.postAsync()
102+
parent.addView(view, previousViewIndex, previousViewLayoutParams)
103+
view.layout(view.left, view.top, view.right, view.bottom)
92104
}
93105
previousViewParent = null
94106
previousViewIndex = 0
95107

96108
fullscreen = false
97109
onFullscreenChangeListener?.onFullscreenChange(fullscreen)
98110
}
99-
}
111+
}
112+
113+
private suspend fun View.postAsync() =
114+
suspendCoroutine { continuation -> post { continuation.resume(Unit) } }

0 commit comments

Comments
 (0)