diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index 405c40f..50f952d 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -65,6 +65,8 @@ dependencies {
implementation(libs.androidx.compose.ui.toolingPreview)
implementation(libs.androidx.compose.material3)
implementation(libs.androidx.compose.material.iconsExtended)
+ implementation(libs.androidx.mediarouter)
+ implementation(libs.playServices.castFramework)
testImplementation(libs.junit4)
androidTestImplementation(libs.androidx.junit)
androidTestImplementation(libs.androidx.espresso)
@@ -79,4 +81,5 @@ dependencies {
implementation(libs.theoplayer)
implementation(libs.theoplayer.ads)
implementation(libs.theoplayer.ads.ima)
+ implementation(libs.theoplayer.cast)
}
\ No newline at end of file
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 75a35e2..33139e7 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -28,6 +28,10 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/java/com/theoplayer/android/ui/demo/CastOptionsProvider.kt b/app/src/main/java/com/theoplayer/android/ui/demo/CastOptionsProvider.kt
new file mode 100644
index 0000000..e6d5e33
--- /dev/null
+++ b/app/src/main/java/com/theoplayer/android/ui/demo/CastOptionsProvider.kt
@@ -0,0 +1,30 @@
+package com.theoplayer.android.ui.demo
+
+import android.content.Context
+import com.google.android.gms.cast.CastMediaControlIntent
+import com.google.android.gms.cast.framework.CastOptions
+import com.google.android.gms.cast.framework.OptionsProvider
+import com.google.android.gms.cast.framework.SessionProvider
+import com.google.android.gms.cast.framework.media.CastMediaOptions
+import com.google.android.gms.cast.framework.media.NotificationOptions
+
+class CastOptionsProvider : OptionsProvider {
+ override fun getCastOptions(context: Context): CastOptions {
+ val notificationOptions = NotificationOptions.Builder()
+ .setTargetActivityClassName(MainActivity::class.java.name)
+ .build()
+
+ val castMediaOptions = CastMediaOptions.Builder()
+ .setNotificationOptions(notificationOptions)
+ .build()
+
+ return CastOptions.Builder()
+ .setReceiverApplicationId(CastMediaControlIntent.DEFAULT_MEDIA_RECEIVER_APPLICATION_ID)
+ .setCastMediaOptions(castMediaOptions)
+ .build()
+ }
+
+ override fun getAdditionalSessionProviders(context: Context): List? {
+ return null
+ }
+}
\ No newline at end of file
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 48dbae3..cc4b606 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
@@ -22,8 +22,12 @@ 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.google.android.gms.cast.framework.CastContext
import com.theoplayer.android.api.THEOplayerConfig
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.ui.DefaultUI
import com.theoplayer.android.ui.demo.nitflex.NitflexUI
import com.theoplayer.android.ui.demo.nitflex.theme.NitflexTheme
@@ -34,6 +38,9 @@ class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
+ // Initialize Chromecast immediately, for automatic receiver discovery to work correctly.
+ CastContext.getSharedInstance(this)
+
setContent {
THEOplayerTheme(useDarkTheme = true) {
MainContent()
@@ -51,7 +58,17 @@ fun MainContent() {
val player = rememberPlayer()
LaunchedEffect(player) {
player.theoplayerView?.let { theoplayerView ->
- theoplayerView.player.addIntegration(GoogleImaIntegrationFactory.createGoogleImaIntegration(theoplayerView))
+ // Add ads integration through Google IMA
+ theoplayerView.player.addIntegration(
+ GoogleImaIntegrationFactory.createGoogleImaIntegration(theoplayerView)
+ )
+ // Add Chromecast integration
+ val castConfiguration = CastConfiguration.Builder().apply {
+ castStrategy(CastStrategy.AUTO)
+ }.build()
+ theoplayerView.player.addIntegration(
+ CastIntegrationFactory.createCastIntegration(theoplayerView, castConfiguration)
+ )
}
}
LaunchedEffect(player, stream) {
diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml
index 3497fd4..3f07931 100644
--- a/app/src/main/res/values/themes.xml
+++ b/app/src/main/res/values/themes.xml
@@ -1,7 +1,8 @@
-
\ No newline at end of file
diff --git a/docs/guides/chromecast.md b/docs/guides/chromecast.md
new file mode 100644
index 0000000..e79bee6
--- /dev/null
+++ b/docs/guides/chromecast.md
@@ -0,0 +1,105 @@
+---
+sidebar_position: 2
+---
+
+# Setting up Chromecast
+
+The Open Video UI for Android has integrated support for Chromecast.
+
+- When using the `DefaultUI`, the Chromecast button is automatically added in the top right corner
+ of the player.
+- When creating a custom UI using a `UIController`, you can add a `ChromecastButton` component
+ anywhere you like.
+ You should also add a `ChromecastDisplay` to show a "Playing on Chromecast" message while casting.
+
+However, you need to perform some additional setup to get it fully working.
+
+## Install the THEOplayer Cast integration
+
+The Chromecast support requires the THEOplayer Cast integration to be included in your app.
+We'll also need to interact with the Cast Application Framework directly, so we'll include that too:
+
+```groovy title="build.gradle"
+dependencies {
+ implementation "com.theoplayer.theoplayer-sdk-android:core:7.+"
+ // highlight-next-line
+ implementation "com.theoplayer.theoplayer-sdk-android:integration-cast:7.+"
+ // highlight-next-line
+ implementation "com.google.android.gms:play-services-cast-framework:21.5.0"
+ implementation "com.theoplayer.android-ui:android-ui:1.+"
+}
+```
+
+Create the player manually using `rememberPlayer()`, and then create and add the cast integration:
+
+```kotlin title="MainActivity.kt"
+import com.theoplayer.android.api.cast.CastConfiguration
+import com.theoplayer.android.api.cast.CastIntegrationFactory
+import com.theoplayer.android.api.cast.CastStrategy
+
+setContent {
+ val player = rememberPlayer()
+ LaunchedEffect(player) {
+ player.theoplayerView?.let { theoplayerView ->
+ // Add Chromecast integration
+ val castConfiguration = CastConfiguration.Builder().apply {
+ castStrategy(CastStrategy.AUTO)
+ }.build()
+ theoplayerView.player.addIntegration(
+ CastIntegrationFactory.createCastIntegration(theoplayerView, castConfiguration)
+ )
+ }
+ }
+
+ DefaultUI(
+ player = player
+ )
+}
+```
+
+## Initialize the `CastContext` during activity creation
+
+The Cast Application Framework handles automatic discovery of Chromecast receivers.
+However, this only works correctly if the `CastContext` is initialized immediately when
+your app's activity is constructed.
+
+Therefore, make sure to call `CastContext.getSharedInstance(this)` inside `Activity.onCreate()`:
+
+```kotlin title="MainActivity.kt"
+import com.google.android.gms.cast.framework.CastContext
+
+class MainActivity : ComponentActivity() {
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+
+ // Initialize Chromecast immediately, for automatic receiver discovery to work correctly.
+ CastContext.getSharedInstance(this)
+
+ setContent {
+ // ...
+ }
+ }
+}
+```
+
+## Use an `AppCompat` theme
+
+The Cast Application Framework creates dialogs such as
+[MediaRouteChooserDialog](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteChooserDialog)
+and [MediaRouteControllerDialog](https://developer.android.com/reference/androidx/mediarouter/app/MediaRouteControllerDialog)
+to start and control a cast session. However, because these dialogs inherit
+from [AppCompatDialog](https://developer.android.com/reference/androidx/appcompat/app/AppCompatDialog),
+you need to use theme based on `Theme.AppCompat` in your app:
+
+```xml title="src/main/res/values/themes.xml"
+
+
+
+
+
+```
diff --git a/docs/guides/custom-ui.md b/docs/guides/custom-ui.md
index a6687d6..0b28342 100644
--- a/docs/guides/custom-ui.md
+++ b/docs/guides/custom-ui.md
@@ -1,3 +1,7 @@
+---
+sidebar_position: 1
+---
+
# Making a custom UI
Although the default UI was designed to support a variety of usage scenarios, you may still run into a case that it doesn't handle very well. Perhaps you want to move some buttons around, or add like and dislike buttons to the control bar, or perhaps integrate a text chat component inside your player. In these situations, you may want to build a custom player UI to create a truly unique experience for your viewers.
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 12b5501..a7352f5 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -7,9 +7,11 @@ activity-compose = "1.9.1"
appcompat = "1.7.0"
compose-bom = "2024.06.00"
junit4 = "4.13.2"
+playServices-castFramework = "21.5.0"
ui-test-junit4 = "1.6.8" # ...not in BOM for some reason?
androidx-junit = "1.2.1"
androidx-espresso = "3.6.1"
+androidx-mediarouter = "1.7.0"
dokka = "1.9.20"
theoplayer = "7.10.0"
@@ -29,6 +31,8 @@ androidx-compose-ui-tooling = { group = "androidx.compose.ui", name = "ui-toolin
androidx-compose-ui-toolingPreview = { group = "androidx.compose.ui", name = "ui-tooling-preview" }
androidx-junit = { group = "androidx.test.ext", name = "junit", version.ref = "androidx-junit" }
androidx-espresso = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "androidx-espresso" }
+androidx-mediarouter = { group = "androidx.mediarouter", name = "mediarouter", version.ref = "androidx-mediarouter" }
+playServices-castFramework = { group = "com.google.android.gms", name = "play-services-cast-framework", version.ref = "playServices-castFramework" }
gradle-plugin = { group = "com.android.tools.build", name = "gradle", version.ref = "gradle" }
dokka-base = { group = "org.jetbrains.dokka", name = "dokka-base", version.ref = "dokka" }
dokka-plugin = { group = "org.jetbrains.dokka", name = "android-documentation-plugin", version.ref = "dokka" }
@@ -37,6 +41,7 @@ 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" }
+theoplayer-cast = { group = "com.theoplayer.theoplayer-sdk-android", name = "integration-cast", version.ref = "theoplayer" }
[plugins]
android-application = { id = "com.android.application", version.ref = "gradle" }
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 a019ea6..6b14e26 100644
--- a/ui/src/main/java/com/theoplayer/android/ui/Player.kt
+++ b/ui/src/main/java/com/theoplayer/android/ui/Player.kt
@@ -296,7 +296,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 cast by mutableStateOf(null)
override var currentTime by mutableStateOf(0.0)
private set
override var duration by mutableStateOf(Double.NaN)
@@ -367,6 +367,9 @@ internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player
_source = player?.source
error = null
firstPlay = false
+ // The cast integration is only registered *after* rememberPlayer() is called,
+ // so it's not available at construction time. Check if it's available now.
+ updateCast(theoplayerView?.cast)
updateCurrentTimeAndPlaybackState()
updateDuration()
updateVideoWidthAndHeight()
@@ -611,6 +614,29 @@ internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player
override var castState: PlayerCastState by mutableStateOf(PlayerCastState.UNAVAILABLE)
override var castReceiverName: String? by mutableStateOf(null)
+ private fun updateCast(cast: Cast?) {
+ if (this.cast == cast) return
+ this.cast?.let { oldCast ->
+ oldCast.chromecast.removeEventListener(
+ ChromecastEventTypes.STATECHANGE,
+ chromecastStateChangeListener
+ )
+ oldCast.chromecast.removeEventListener(
+ ChromecastEventTypes.ERROR,
+ chromecastErrorListener
+ )
+ }
+ this.cast = cast
+ cast?.let {
+ cast.chromecast.addEventListener(
+ ChromecastEventTypes.STATECHANGE,
+ chromecastStateChangeListener
+ )
+ cast.chromecast.addEventListener(ChromecastEventTypes.ERROR, chromecastErrorListener)
+ }
+ updateCastState()
+ }
+
private fun updateCastState() {
castState = cast?.chromecast?.state ?: PlayerCastState.UNAVAILABLE
castReceiverName = cast?.chromecast?.receiverName
@@ -683,11 +709,7 @@ internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player
ads?.addEventListener(AdsEventTypes.AD_BREAK_BEGIN, adListener)
ads?.addEventListener(AdsEventTypes.AD_SKIP, adListener)
ads?.addEventListener(AdsEventTypes.AD_BREAK_END, adListener)
- cast?.chromecast?.addEventListener(
- ChromecastEventTypes.STATECHANGE,
- chromecastStateChangeListener
- )
- cast?.chromecast?.addEventListener(ChromecastEventTypes.ERROR, chromecastErrorListener)
+ updateCast(theoplayerView?.cast)
fullscreenHandler?.onFullscreenChangeListener = fullscreenListener
}
@@ -749,11 +771,7 @@ internal class PlayerImpl(override val theoplayerView: THEOplayerView?) : Player
ads?.removeEventListener(AdsEventTypes.AD_BREAK_BEGIN, adListener)
ads?.removeEventListener(AdsEventTypes.AD_SKIP, adListener)
ads?.removeEventListener(AdsEventTypes.AD_BREAK_END, adListener)
- cast?.chromecast?.removeEventListener(
- ChromecastEventTypes.STATECHANGE,
- chromecastStateChangeListener
- )
- cast?.chromecast?.removeEventListener(ChromecastEventTypes.ERROR, chromecastErrorListener)
+ updateCast(null)
fullscreenHandler?.onFullscreenChangeListener = null
}
}
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 464a034..1f78ef1 100644
--- a/ui/src/main/java/com/theoplayer/android/ui/UIController.kt
+++ b/ui/src/main/java/com/theoplayer/android/ui/UIController.kt
@@ -167,14 +167,14 @@ fun UIController(
derivedStateOf {
if (!isReady) {
false
- } else if (!player.firstPlay) {
+ } else if (!player.firstPlay || player.castState == PlayerCastState.CONNECTED) {
true
} else if (player.playingAd) {
false
} else if (forceControlsHidden) {
false
} else {
- isRecentlyTapped || isPressed || player.paused || player.castState == PlayerCastState.CONNECTED
+ isRecentlyTapped || isPressed || player.paused
}
}
}