From 8716effd8f654b898eacf10359113a234d008b0a Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Tue, 27 Aug 2024 11:16:38 +0300 Subject: [PATCH 01/25] [ZEUS-4737] Updated tutorial with background audio init --- .../tutorials/playbacksdk/getstarted.json | 39 ++++++++++++++----- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/docs/data/tutorials/playbacksdk/getstarted.json b/docs/data/tutorials/playbacksdk/getstarted.json index 49477f7..cc41dfb 100644 --- a/docs/data/tutorials/playbacksdk/getstarted.json +++ b/docs/data/tutorials/playbacksdk/getstarted.json @@ -507,12 +507,19 @@ "fun PlayerContent(entryId: String, authorizationToken: String) {", " val apiKey = \"API_KEY\",", " // Initialize SDK with the settings", - " PlaybackSDKManager.initialize(apiKey) { license, error ->", - " error?.let {", - " errorMessage = it.toString()", - " }", - " customPlugin = BitmovinVideoPlayerPlugin()", - " VideoPlayerPluginManager.registerPlugin(customPlugin!!)", + " PlaybackSDKManager.initialize(settingsManager.apiKey, userAgent = userAgentHeader) { license, error ->", + " error?.let {", + " errorMessage = it.toString()", + " }", + "", + " val customPlugin = BitmovinVideoPlayerPlugin()", + "", + " val config = VideoPlayerConfig()", + " config.playbackConfig.autoplayEnabled = true", + " config.playbackConfig.backgroundPlaybackEnabled = false", + " customPlugin.setup(config)", + "", + " VideoPlayerPluginManager.registerPlugin(customPlugin)", " }", " if (authorizationToken.isEmpty()) {", " playerLoaded = true", @@ -646,8 +653,14 @@ " errorMessage = it.toString()", " }", "", - " customPlugin = BitmovinVideoPlayerPlugin()", - " VideoPlayerPluginManager.registerPlugin(customPlugin!!)", + " val customPlugin = BitmovinVideoPlayerPlugin()", + "", + " val config = VideoPlayerConfig()", + " config.playbackConfig.autoplayEnabled = true", + " config.playbackConfig.backgroundPlaybackEnabled = false", + " customPlugin.setup(config)", + "", + " VideoPlayerPluginManager.registerPlugin(customPlugin)", " }", "}" ] @@ -727,8 +740,14 @@ " errorMessage = it.toString()", " }", "", - " customPlugin = BitmovinVideoPlayerPlugin()", - " VideoPlayerPluginManager.registerPlugin(customPlugin!!)", + " val customPlugin = BitmovinVideoPlayerPlugin()", + "", + " val config = VideoPlayerConfig()", + " config.playbackConfig.autoplayEnabled = true", + " config.playbackConfig.backgroundPlaybackEnabled = false", + " customPlugin.setup(config)", + "", + " VideoPlayerPluginManager.registerPlugin(customPlugin)", " }", "}" ] From 8966e00599b204a3f6cb49888f44d591e3840fba Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Tue, 27 Aug 2024 11:32:32 +0300 Subject: [PATCH 02/25] [ZEUS-4737] Updated tutorial with background audio init --- docs/data/tutorials/playbacksdk/getstarted.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/data/tutorials/playbacksdk/getstarted.json b/docs/data/tutorials/playbacksdk/getstarted.json index cc41dfb..9a202c6 100644 --- a/docs/data/tutorials/playbacksdk/getstarted.json +++ b/docs/data/tutorials/playbacksdk/getstarted.json @@ -163,7 +163,7 @@ }, { "type": "text", - "text": " Initialize the Playback SDK by providing your API key and register the default player plugin." + "text": "Create an instance of the custom video player plugin and configure it by setting playback options such as autoplay and background playback. To do this, create a `VideoPlayerConfig` object, configure its playback settings to enable autoplay and disable background playback, and then pass this configuration to the plugin." }, { "type": "text", @@ -172,7 +172,7 @@ { "inlineContent": [ { - "text": "Make sure this step is done when the app starts.", + "text": "Ensure that the plugin setup and registration occur during the app's initialization process.", "type": "text" } ], From 19f1f92f22c9349d146e89a765007e97a5c0c108 Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Tue, 27 Aug 2024 22:07:43 +0300 Subject: [PATCH 03/25] [ZEUS-4737] Changes requested by @artem-y-pamediagroup --- docs/data/tutorials/playbacksdk/getstarted.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/data/tutorials/playbacksdk/getstarted.json b/docs/data/tutorials/playbacksdk/getstarted.json index 9a202c6..9f422f8 100644 --- a/docs/data/tutorials/playbacksdk/getstarted.json +++ b/docs/data/tutorials/playbacksdk/getstarted.json @@ -132,7 +132,7 @@ { "inlineContent": [ { - "text": "Make sure this step is done before proceed to the next steps", + "text": "Ensure that the plugin setup and registration occur during the app's initialization process.", "type": "text" } ], @@ -163,7 +163,7 @@ }, { "type": "text", - "text": "Create an instance of the custom video player plugin and configure it by setting playback options such as autoplay and background playback. To do this, create a `VideoPlayerConfig` object, configure its playback settings to enable autoplay and disable background playback, and then pass this configuration to the plugin." + "text": "Create an instance of the custom video player plugin and configure it by setting playback options such as autoplay and background playback. To do this, create a `VideoPlayerConfig` object, update its autoplay and background playback settings, and then pass this configuration object to the plugin." }, { "type": "text", From a35c5946f2e4ee7b0463014de6bba15918738c27 Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Thu, 29 Aug 2024 11:15:07 +0300 Subject: [PATCH 04/25] [ZEUS-4737] Sample code changes requested by @artem-y-pamediagroup --- .../tutorials/playbacksdk/getstarted.json | 159 ++++++++++-------- 1 file changed, 90 insertions(+), 69 deletions(-) diff --git a/docs/data/tutorials/playbacksdk/getstarted.json b/docs/data/tutorials/playbacksdk/getstarted.json index 9f422f8..61d3687 100644 --- a/docs/data/tutorials/playbacksdk/getstarted.json +++ b/docs/data/tutorials/playbacksdk/getstarted.json @@ -484,9 +484,33 @@ "import com.streamamg.player.plugin.VideoPlayerPluginManager", "import com.streamamg.player.plugin.bitmovin.BitmovinVideoPlayerPlugin", "", - "class PlayerViewActivity : ComponentActivity() {", + "", " override fun onCreate(savedInstanceState: Bundle?) {", " super.onCreate(savedInstanceState)", + "", + " val apiKey = \"API_KEY\"", + " val userAgentHeader = \"USER_AGENT\"", + "", + " PlaybackSDKManager.updateCastContext(this)", + "", + " // Initialize SDK with the settings", + " PlaybackSDKManager.initialize(apiKey, userAgent = userAgentHeader) { license, error ->", + "", + " error?.let {", + " Log.e(this::class.simpleName, it.toString())", + " }", + "", + " val customPlugin = BitmovinVideoPlayerPlugin()", + " // Enable background playback", + " val playerConfig = VideoPlayerConfig()", + " playerConfig.playbackConfig.autoplayEnabled = true", + " playerConfig.playbackConfig.backgroundPlaybackEnabled = true", + " // Inject the player config to the plugin", + " customPlugin.setup(playerConfig)", + "", + " VideoPlayerPluginManager.registerPlugin(customPlugin)", + " }", + "", " setContent {", " val navController = rememberNavController()", " PlaybackDemoAndroidTheme {", @@ -494,35 +518,24 @@ " modifier = Modifier.fillMaxSize(),", " color = Color.White", " ) {", - " PlayerContent(entryId = \"\", authorizationToken = \"\")", + " // Custom view to show player UI", + " PlayerContent(entryId = \"ENTRY ID\", authorizationToken = \"AUTH TOKEN or null or empty string\")", " }", " }", " }", " }", "}", "", - "var customPlugin: BitmovinVideoPlayerPlugin? = null", - "", "@Composable", "fun PlayerContent(entryId: String, authorizationToken: String) {", - " val apiKey = \"API_KEY\",", - " // Initialize SDK with the settings", - " PlaybackSDKManager.initialize(settingsManager.apiKey, userAgent = userAgentHeader) { license, error ->", - " error?.let {", - " errorMessage = it.toString()", - " }", - "", - " val customPlugin = BitmovinVideoPlayerPlugin()", - "", - " val config = VideoPlayerConfig()", - " config.playbackConfig.autoplayEnabled = true", - " config.playbackConfig.backgroundPlaybackEnabled = false", - " customPlugin.setup(config)", - "", - " VideoPlayerPluginManager.registerPlugin(customPlugin)", - " }", + + " val context = LocalContext.current", + " PlaybackSDKManager.updateCastContext(context)", + " if (authorizationToken.isEmpty()) {", - " playerLoaded = true", + " LaunchedEffect(Unit) {", + " playerLoaded = true", + " }", " } else {", " // Need to start the SSO before using the playback API", " LaunchedEffect(Unit) {", @@ -537,6 +550,7 @@ " }", " }", " }", + " if (playerLoaded) {", " Box(", " modifier = Modifier.fillMaxSize()", @@ -547,7 +561,7 @@ " .aspectRatio(16f / 9f)", " .align(Alignment.TopCenter)", " ) {", - " loadPlayer(entryId, authorizationToken) { error ->", + " PlaybackSDKManager.loadPlayer(entryId, authorizationToken) { error ->", " Log.e(\"PlayerActivity\", error.message.toString())", " when (error) {", " is PlaybackAPIError.ApiError -> {", @@ -625,6 +639,30 @@ "", " override fun onCreate(savedInstanceState: Bundle?) {", " super.onCreate(savedInstanceState)", + "", + " val apiKey = \"API_KEY\"", + " val userAgentHeader = \"USER_AGENT\"", + "", + " PlaybackSDKManager.updateCastContext(this)", + "", + " // Initialize SDK with the settings", + " PlaybackSDKManager.initialize(apiKey, userAgent = userAgentHeader) { license, error ->", + "", + " error?.let {", + " Log.e(this::class.simpleName, it.toString())", + " }", + "", + " val customPlugin = BitmovinVideoPlayerPlugin()", + " // Enable background playback", + " val playerConfig = VideoPlayerConfig()", + " playerConfig.playbackConfig.autoplayEnabled = true", + " playerConfig.playbackConfig.backgroundPlaybackEnabled = true", + " // Inject the player config to the plugin", + " customPlugin.setup(playerConfig)", + "", + " VideoPlayerPluginManager.registerPlugin(customPlugin)", + " }", + "", " setContent {", " val navController = rememberNavController()", " PlaybackDemoAndroidTheme {", @@ -632,7 +670,8 @@ " modifier = Modifier.fillMaxSize(),", " color = Color.White", " ) {", - " PlayerContent(entryId = \"\", authorizationToken = \"\")", + " // Custom view to show player UI", + " PlayerContent(entryId = \"ENTRY ID\", authorizationToken = \"AUTH TOKEN or null or empty string\")", " }", " }", " }", @@ -643,25 +682,7 @@ "", "@Composable", "fun PlayerContent(entryId: String, authorizationToken: String) {", - " ", - " val apiKey = \"API_KEY\",", - "", - " // Initialize SDK with the settings", - " PlaybackSDKManager.initialize(apiKey) { license, error ->", - "", - " error?.let {", - " errorMessage = it.toString()", - " }", - "", - " val customPlugin = BitmovinVideoPlayerPlugin()", - "", - " val config = VideoPlayerConfig()", - " config.playbackConfig.autoplayEnabled = true", - " config.playbackConfig.backgroundPlaybackEnabled = false", - " customPlugin.setup(config)", - "", - " VideoPlayerPluginManager.registerPlugin(customPlugin)", - " }", + " // Yours UI for the screen ", "}" ] }, @@ -702,10 +723,33 @@ "import com.streamamg.player.plugin.VideoPlayerPluginManager", "import com.streamamg.player.plugin.bitmovin.BitmovinVideoPlayerPlugin", "", - "class PlayerViewActivity : ComponentActivity() {", "", " override fun onCreate(savedInstanceState: Bundle?) {", " super.onCreate(savedInstanceState)", + "", + " val apiKey = \"API_KEY\"", + " val userAgentHeader = \"USER_AGENT\"", + "", + " PlaybackSDKManager.updateCastContext(this)", + "", + " // Initialize SDK with the settings", + " PlaybackSDKManager.initialize(apiKey, userAgent = userAgentHeader) { license, error ->", + "", + " error?.let {", + " Log.e(this::class.simpleName, it.toString())", + " }", + "", + " val customPlugin = BitmovinVideoPlayerPlugin()", + " // Enable background playback", + " val playerConfig = VideoPlayerConfig()", + " playerConfig.playbackConfig.autoplayEnabled = true", + " playerConfig.playbackConfig.backgroundPlaybackEnabled = true", + " // Inject the player config to the plugin", + " customPlugin.setup(playerConfig)", + "", + " VideoPlayerPluginManager.registerPlugin(customPlugin)", + " }", + "", " setContent {", " val navController = rememberNavController()", " PlaybackDemoAndroidTheme {", @@ -713,7 +757,8 @@ " modifier = Modifier.fillMaxSize(),", " color = Color.White", " ) {", - " PlayerContent(entryId = \"\", authorizationToken = \"\")", + " // Custom view to show player UI", + " PlayerContent(entryId = \"ENTRY ID\", authorizationToken = \"AUTH TOKEN or null or empty string\")", " }", " }", " }", @@ -724,31 +769,7 @@ "", "@Composable", "fun PlayerContent(entryId: String, authorizationToken: String) {", - " ", - " val apiKey = \"API_KEY\",", - "", - " // Should match the user agent of the HTTP client used in the app", - " val customUserAgent = \"okhttp/${okhttp3.OkHttp.VERSION}\"", - "", - " // Initialize SDK with the settings", - " PlaybackSDKManager.initialize(", - " apiKey = apiKey,", - " userAgent = customUserAgent,", - " ) { license, error ->", - "", - " error?.let {", - " errorMessage = it.toString()", - " }", - "", - " val customPlugin = BitmovinVideoPlayerPlugin()", - "", - " val config = VideoPlayerConfig()", - " config.playbackConfig.autoplayEnabled = true", - " config.playbackConfig.backgroundPlaybackEnabled = false", - " customPlugin.setup(config)", - "", - " VideoPlayerPluginManager.registerPlugin(customPlugin)", - " }", + " // Yours UI for the screen ", "}" ] }, From f35855533f199acbb06371b75b5b079241958c2a Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Thu, 29 Aug 2024 11:28:21 +0300 Subject: [PATCH 05/25] [ZEUS-4737] Updated code to retrieve http agent --- docs/data/tutorials/playbacksdk/getstarted.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/data/tutorials/playbacksdk/getstarted.json b/docs/data/tutorials/playbacksdk/getstarted.json index 61d3687..ff5d592 100644 --- a/docs/data/tutorials/playbacksdk/getstarted.json +++ b/docs/data/tutorials/playbacksdk/getstarted.json @@ -489,7 +489,7 @@ " super.onCreate(savedInstanceState)", "", " val apiKey = \"API_KEY\"", - " val userAgentHeader = \"USER_AGENT\"", + " val customUserAgent = \"okhttp/${okhttp3.OkHttp.VERSION}\"", "", " PlaybackSDKManager.updateCastContext(this)", "", @@ -641,7 +641,7 @@ " super.onCreate(savedInstanceState)", "", " val apiKey = \"API_KEY\"", - " val userAgentHeader = \"USER_AGENT\"", + " val customUserAgent = \"okhttp/${okhttp3.OkHttp.VERSION}\"", "", " PlaybackSDKManager.updateCastContext(this)", "", @@ -728,7 +728,7 @@ " super.onCreate(savedInstanceState)", "", " val apiKey = \"API_KEY\"", - " val userAgentHeader = \"USER_AGENT\"", + " val customUserAgent = \"okhttp/${okhttp3.OkHttp.VERSION}\"", "", " PlaybackSDKManager.updateCastContext(this)", "", From 903910d56c7a76c1a3cb8030b73a7fcff330f511 Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Thu, 29 Aug 2024 11:41:18 +0300 Subject: [PATCH 06/25] [ZEUS-4737] Removed unwanted code from tutorial --- docs/data/tutorials/playbacksdk/getstarted.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/data/tutorials/playbacksdk/getstarted.json b/docs/data/tutorials/playbacksdk/getstarted.json index ff5d592..34ba8f9 100644 --- a/docs/data/tutorials/playbacksdk/getstarted.json +++ b/docs/data/tutorials/playbacksdk/getstarted.json @@ -678,7 +678,6 @@ " }", "}", "", - "var customPlugin: BitmovinVideoPlayerPlugin? = null", "", "@Composable", "fun PlayerContent(entryId: String, authorizationToken: String) {", @@ -765,8 +764,6 @@ " }", "}", "", - "var customPlugin: BitmovinVideoPlayerPlugin? = null", - "", "@Composable", "fun PlayerContent(entryId: String, authorizationToken: String) {", " // Yours UI for the screen ", From 9572531a940d7474b502236588b769a3fce4a9e3 Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Thu, 29 Aug 2024 17:21:16 +0300 Subject: [PATCH 07/25] [ZEUS-4737] Updated higlights for code --- .../tutorials/playbacksdk/getstarted.json | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/docs/data/tutorials/playbacksdk/getstarted.json b/docs/data/tutorials/playbacksdk/getstarted.json index 34ba8f9..6bcf244 100644 --- a/docs/data/tutorials/playbacksdk/getstarted.json +++ b/docs/data/tutorials/playbacksdk/getstarted.json @@ -641,12 +641,11 @@ " super.onCreate(savedInstanceState)", "", " val apiKey = \"API_KEY\"", - " val customUserAgent = \"okhttp/${okhttp3.OkHttp.VERSION}\"", "", " PlaybackSDKManager.updateCastContext(this)", "", " // Initialize SDK with the settings", - " PlaybackSDKManager.initialize(apiKey, userAgent = userAgentHeader) { license, error ->", + " PlaybackSDKManager.initialize(apiKey) { license, error ->", "", " error?.let {", " Log.e(this::class.simpleName, it.toString())", @@ -690,22 +689,22 @@ "fileName": "PlayBackDemoAppWithUserAgent.kt", "highlights": [ { - "line": 36 + "line": 17 }, { - "line": 37 + "line": 18 }, { - "line": 40 + "line": 22 }, { - "line": 41 + "line": 23 }, { - "line": 42 + "line": 24 }, { - "line": 43 + "line": 25 } ], "identifier": "PlayBackDemoAppWithUserAgent.kt", @@ -727,12 +726,16 @@ " super.onCreate(savedInstanceState)", "", " val apiKey = \"API_KEY\"", + " // Should match the user agent of the HTTP client used in the app", " val customUserAgent = \"okhttp/${okhttp3.OkHttp.VERSION}\"", "", " PlaybackSDKManager.updateCastContext(this)", "", " // Initialize SDK with the settings", - " PlaybackSDKManager.initialize(apiKey, userAgent = userAgentHeader) { license, error ->", + " PlaybackSDKManager.initialize(", + " apiKey = apiKey,", + " userAgent = userAgentHeader", + " ) { license, error ->", "", " error?.let {", " Log.e(this::class.simpleName, it.toString())", From 204c8b47047001c695f5f6f18b464921187763ba Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Thu, 29 Aug 2024 17:23:52 +0300 Subject: [PATCH 08/25] [ZEUS-4737] Updated highlights for code --- docs/data/tutorials/playbacksdk/getstarted.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/data/tutorials/playbacksdk/getstarted.json b/docs/data/tutorials/playbacksdk/getstarted.json index 6bcf244..a662c5a 100644 --- a/docs/data/tutorials/playbacksdk/getstarted.json +++ b/docs/data/tutorials/playbacksdk/getstarted.json @@ -689,10 +689,10 @@ "fileName": "PlayBackDemoAppWithUserAgent.kt", "highlights": [ { - "line": 17 + "line": 16 }, { - "line": 18 + "line": 17 }, { "line": 22 From 1d605127111f6a2797a7f2efd8e3b0769252e886 Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Mon, 16 Sep 2024 07:29:54 +0300 Subject: [PATCH 09/25] [ZEUS-4926] Updated tutorial with new explanations for analytics --- docs/data/tutorials/playbacksdk/getstarted.json | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/data/tutorials/playbacksdk/getstarted.json b/docs/data/tutorials/playbacksdk/getstarted.json index a662c5a..6456d4a 100644 --- a/docs/data/tutorials/playbacksdk/getstarted.json +++ b/docs/data/tutorials/playbacksdk/getstarted.json @@ -293,7 +293,7 @@ }, { "type": "text", - "text": " function provided by the Playback SDK to initialize and load the video player. The function takes the entry ID and authorization token as parameters. Additionally, it includes a closure to handle any potential playback errors that may occur during the loading process." + "text": " function provided by the Playback SDK to initialize and load the video player. The function takes three parameters: the entry ID, viewer ID, and authorization token. The viewer ID is the unique ID from CloudPay (CustomerID). It will not be present for free videos, as there is no session for those users. If the viewer ID is null, analytics tracking will be disabled. Additionally, it includes a closure to handle any potential playback errors that may occur during the loading process." }, { "type": "text", @@ -474,6 +474,14 @@ "references": { "PlayerTestView.kt": { "identifier": "PlayerTestView.kt", + "highlights": [ + { + "line": 84 + }, + { + "line": 86 + } + ], "content": [ "// Others imports", "import com.streamamg.PlaybackAPIError", @@ -561,7 +569,9 @@ " .aspectRatio(16f / 9f)", " .align(Alignment.TopCenter)", " ) {", - " PlaybackSDKManager.loadPlayer(entryId, authorizationToken) { error ->", + " val viewerId = \"viewer id\"", + " ", + " PlaybackSDKManager.loadPlayer(entryId, viewerId, authorizationToken) { error ->", " Log.e(\"PlayerActivity\", error.message.toString())", " when (error) {", " is PlaybackAPIError.ApiError -> {", @@ -600,7 +610,6 @@ ], "syntax": "swift", "fileType": "kotlin", - "highlights": [], "fileName": "PlayerTestView.kt", "type": "file" }, From 942ad811d359cd21171f64f1d192b3b3e5c7f5b7 Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Wed, 25 Sep 2024 17:12:35 +0300 Subject: [PATCH 10/25] [CORE-4969] Added ViewModel for player. Updated SDK logic --- playback-sdk-android/build.gradle.kts | 2 + .../bitmovin/BitmovinVideoPlayerPlugin.kt | 274 +++++++----------- .../plugin/bitmovin/VideoPlayerViewModel.kt | 130 +++++++++ .../player/ui/BackgroundPlaybackService.kt | 13 +- 4 files changed, 244 insertions(+), 175 deletions(-) create mode 100644 playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt diff --git a/playback-sdk-android/build.gradle.kts b/playback-sdk-android/build.gradle.kts index aec4178..e6c0296 100644 --- a/playback-sdk-android/build.gradle.kts +++ b/playback-sdk-android/build.gradle.kts @@ -165,4 +165,6 @@ dependencies { implementation("com.google.accompanist:accompanist-permissions:0.28.0") testImplementation("org.jetbrains.kotlinx:kotlinx-coroutines-test:1.9.0-RC") + + implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.8.6") } \ No newline at end of file diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt index 883d2fc..f3a6368 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt @@ -2,21 +2,17 @@ package com.streamamg.player.plugin.bitmovin import android.Manifest import android.app.Activity -import android.app.ActivityManager -import android.content.ComponentName import android.content.Context import android.content.ContextWrapper -import android.content.Intent -import android.content.ServiceConnection import android.content.pm.ActivityInfo import android.os.Build -import android.os.IBinder -import android.view.ViewGroup +import android.util.Log import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.collectAsState import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -27,22 +23,18 @@ import androidx.compose.ui.viewinterop.AndroidView import androidx.core.view.WindowCompat import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.viewmodel.compose.viewModel import com.bitmovin.player.PlayerView import com.bitmovin.player.api.Player -import com.bitmovin.player.api.PlayerConfig -import com.bitmovin.player.api.event.Event -import com.bitmovin.player.api.event.PlayerEvent -import com.bitmovin.player.api.source.SourceConfig import com.bitmovin.player.api.ui.FullscreenHandler import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState -import com.streamamg.PlaybackSDKManager import com.streamamg.player.plugin.VideoPlayerConfig import com.streamamg.player.plugin.VideoPlayerPlugin -import com.streamamg.player.ui.BackgroundPlaybackService +import java.net.URL class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { @@ -50,11 +42,10 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { override val version: String = "1.0" private var hlsUrl: String = "" - private var playerView: PlayerView? = null private var playerConfig = VideoPlayerConfig() private var playerBind: Player? = null private val fullscreen = mutableStateOf(false) - private var isServiceBound = false + private var changedSource = false override fun setup(config: VideoPlayerConfig) { playerConfig.playbackConfig.autoplayEnabled = config.playbackConfig.autoplayEnabled @@ -63,94 +54,86 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { @Composable override fun PlayerView(hlsUrl: String): Unit { + changedSource = !urlsAreEqualExcludingKs(this.hlsUrl, hlsUrl) + if (changedSource) { + playerBind = null + } + val playerViewModel: VideoPlayerViewModel = viewModel() + this.hlsUrl = hlsUrl + val context = LocalContext.current val currentLifecycle = LocalLifecycleOwner.current - val observers = remember { mutableListOf() } val lastHlsUrl = remember { mutableStateOf(hlsUrl) } if (playerConfig.playbackConfig.backgroundPlaybackEnabled) { if (Build.VERSION.SDK_INT >= 33) { // Managing new permissions for the Background service notifications - RequestMissingPermissions() + RequestMissingPermissions { granted -> + playerViewModel.updatePermissionsState(granted, context) + } } else { // Bind and start the Background service without permissions - bindAndStartBackgroundService(LocalContext.current) + playerViewModel.updatePermissionsState(true, context) } } + DisposableEffect(hlsUrl) { + playerViewModel.initializePlayer(context, playerConfig, hlsUrl) + playerBind = playerViewModel.player + if (changedSource) { + playerViewModel.loadVideo(hlsUrl) + } + onDispose { + playerViewModel.handleAppInForeground(context) + } + } + + DisposableEffect(currentLifecycle) { + val observer = LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_STOP -> { + playerViewModel.handleAppInBackground(context) + } + Lifecycle.Event.ON_START -> { + playerViewModel.handleAppInForeground(context) + } + Lifecycle.Event.ON_PAUSE -> { + playerViewModel.handleAppInBackground(context) + } + Lifecycle.Event.ON_RESUME -> { + playerViewModel.handleAppInForeground(context) + } + else -> {} + } + } + currentLifecycle.lifecycle.addObserver(observer) + onDispose { + currentLifecycle.lifecycle.removeObserver(observer) + } + } + + val isReady = playerViewModel.isPlayerReady.collectAsState() + key(lastHlsUrl.value) { // Force recomposition when the HLS URL changes AndroidView( modifier = Modifier.fillMaxSize(), factory = { context -> - if (!playerConfig.playbackConfig.backgroundPlaybackEnabled) { - // Init the Player without the Background service - val playerConfig = - PlayerConfig(key = PlaybackSDKManager.bitmovinLicense) - playerBind = Player(context, playerConfig) - playerView = PlayerView(context, playerBind) - initializePlayer(lastHlsUrl.value) - } else { - // Init the Player with the Background service later in the BackgroundPlaybackService - if (playerView == null) { - playerView = PlayerView(context, playerBind) - } - } - - val observer = object : DefaultLifecycleObserver { - override fun onStart(owner: LifecycleOwner) { - if (!playerConfig.playbackConfig.backgroundPlaybackEnabled) { - if (playerConfig.playbackConfig.autoplayEnabled) { - playerBind?.play() - } - } - } - override fun onResume(owner: LifecycleOwner) { - if (!playerConfig.playbackConfig.backgroundPlaybackEnabled) { - if (playerConfig.playbackConfig.autoplayEnabled) { - playerBind?.play() - } - } - } - override fun onPause(owner: LifecycleOwner) { - if (!playerConfig.playbackConfig.backgroundPlaybackEnabled) { - playerBind?.pause() - } - } - override fun onStop(owner: LifecycleOwner) { - if (!playerConfig.playbackConfig.backgroundPlaybackEnabled) { - playerBind?.pause() - } - } - override fun onDestroy(owner: LifecycleOwner) { - if (playerConfig.playbackConfig.backgroundPlaybackEnabled) { - unbindAndStopBackgroundService(context) - } - } + Log.d("SDK", "-------- player View created") + PlayerView(context, playerViewModel.player).apply { + setFullscreenHandler(fullscreenHandler) + keepScreenOn = true + player = playerViewModel.player } - - // Remove previous observer - if (observers.isNotEmpty()) { - observers.forEach { currentLifecycle.lifecycle.removeObserver(it) } - observers.clear() - } - // Add new observer - currentLifecycle.lifecycle.addObserver(observer) - observers.add(observer) - - playerView!! // Directly return the PlayerView }, update = { view -> - // Update the PlayerView with the new HLS URL - if (lastHlsUrl.value != hlsUrl) { - observers.firstOrNull()?.onStop(currentLifecycle) - observers.firstOrNull()?.onDestroy(currentLifecycle) - lastHlsUrl.value = hlsUrl - - if (view.parent != null) { - (view.parent as? ViewGroup)?.removeView(view) - playerView?.setFullscreenHandler(null) - } + if (isReady.value) { + Log.d("SDK", "-------- player View update") + view.player = playerViewModel.player + } + if (changedSource) { + playerViewModel.loadVideo(hlsUrl) + playerViewModel.playVideo() } } ) @@ -170,18 +153,13 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { val context = LocalContext.current DisposableEffect(orientation) { val activity = context.findActivity() ?: return@DisposableEffect onDispose {} - val originalOrientation = activity.requestedOrientation activity.requestedOrientation = orientation - onDispose { - // restore original orientation when view disappears - activity.requestedOrientation = originalOrientation - } + onDispose {} } } @Composable fun SystemBars(show: Boolean) { - val context = LocalContext.current val activity = context.findActivity() ?: return val window = activity.window @@ -202,12 +180,9 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { @RequiresApi(Build.VERSION_CODES.TIRAMISU) @OptIn(ExperimentalPermissionsApi::class) @Composable - private fun RequestMissingPermissions() { - val context = LocalContext.current + private fun RequestMissingPermissions(callback: ((Boolean) -> Unit)) { val permissionState = rememberPermissionState(permission = Manifest.permission.POST_NOTIFICATIONS) { granted -> - if (granted) { - bindAndStartBackgroundService(context) - } + callback(granted) } if (!permissionState.status.isGranted) { LaunchedEffect( @@ -215,91 +190,44 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { block = { permissionState.launchPermissionRequest() } ) } else { - bindAndStartBackgroundService(context) + callback(true) } } - fun isServiceRunning(context: Context, serviceClass: Class<*>): Boolean { - val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager - val services: List = activityManager.getRunningServices(Int.MAX_VALUE) - - for (runningServiceInfo in services) { - if (runningServiceInfo.service.getClassName().equals(serviceClass.name)) { - return true - } - } - return false + private fun urlsAreEqualExcludingKs(url1: String, url2: String): Boolean { + if (url1.isEmpty()) return false + if (url2.isEmpty()) return false + val normalizedUrl1 = normalizeUrl(url1) + val normalizedUrl2 = normalizeUrl(url2) + return normalizedUrl1 == normalizedUrl2 } - private fun bindAndStartBackgroundService(context: Context) { - val intent = Intent(context, BackgroundPlaybackService::class.java) - - try { - if (isServiceRunning(context, BackgroundPlaybackService::class.java)) { - context.unbindService(mConnection) - context.stopService(intent) - } - context.bindService(intent, mConnection, Context.BIND_AUTO_CREATE) - context.startService(intent) - } catch (e: Exception) { - e.printStackTrace() + private fun normalizeUrl(urlString: String): String { + val url = URL(urlString) + val protocol = url.protocol + val host = url.host + val port = url.port + val path = url.path + + // Parse query parameters excluding 'ks' and sort them for consistent comparison + val queryParams = url.query?.split("&")?.mapNotNull { + val parts = it.split("=", limit = 2) + if (parts[0] != "ks") { + it + } else null + }?.sorted()?.joinToString("&") ?: "" + + // Reconstruct the URL without the 'ks' parameter + val normalizedUrl = StringBuilder() + normalizedUrl.append(protocol).append("://").append(host) + if (port != -1) { + normalizedUrl.append(":").append(port) } - } - - private fun unbindAndStopBackgroundService(context: Context) { - if (!isServiceBound) return - - val intent = Intent(context, BackgroundPlaybackService::class.java) - - try { - context.unbindService(mConnection) - context.stopService(intent) - isServiceBound = false - } catch (e: Exception) { - e.printStackTrace() - } - } - - /** - * Defines callbacks for service binding, passed to bindService() - */ - private val mConnection = object : ServiceConnection { - - override fun onServiceConnected(className: ComponentName, service: IBinder) { - // We've bound to the Service, cast the IBinder and get the Player instance - val binder = service as BackgroundPlaybackService.BackgroundBinder - playerBind = binder.player - - if (playerView == null) { - playerView = PlayerView(binder.getService(), playerBind) - } - - playerView?.player = playerBind - - initializePlayer(this@BitmovinVideoPlayerPlugin.hlsUrl) - isServiceBound = true - } - - override fun onServiceDisconnected(arg0: ComponentName) { - isServiceBound = false - } - } - - private fun initializePlayer(hlsUrl: String) { - playerBind?.next(PlayerEvent.Ready::class.java, this::checkEvent) -// playerBind?.next(SourceEvent.Loaded::class.java, this::checkEvent) - - playerBind?.load(SourceConfig.fromUrl(hlsUrl)) - } - - private fun checkEvent(event: Event) { - if (event is PlayerEvent.Ready) { - playerView?.setFullscreenHandler(fullscreenHandler) - playerView?.keepScreenOn = true - if (playerConfig.playbackConfig.autoplayEnabled) { - playerBind?.play() - } + normalizedUrl.append(path) + if (queryParams.isNotEmpty()) { + normalizedUrl.append("?").append(queryParams) } + return normalizedUrl.toString() } private fun Context.findActivity(): Activity? = when (this) { diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt new file mode 100644 index 0000000..d25bb5a --- /dev/null +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt @@ -0,0 +1,130 @@ +package com.streamamg.player.plugin.bitmovin + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.IBinder +import android.util.Log +import androidx.lifecycle.ViewModel +import com.bitmovin.player.api.Player +import com.bitmovin.player.api.PlayerConfig +import com.bitmovin.player.api.event.PlayerEvent +import com.bitmovin.player.api.source.SourceConfig +import com.streamamg.PlaybackSDKManager +import com.streamamg.player.plugin.VideoPlayerConfig +import com.streamamg.player.ui.BackgroundPlaybackService +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class VideoPlayerViewModel : ViewModel() { + var player: Player? = null + private var currentVideoUrl: String? = null + private var config: VideoPlayerConfig? = null + private var isServiceBound = false + private var backgroundPlaybackEnabled = false + private var autoplayEnabled = false + private var isPermissionsGranted = false + private var _isPlayerReady = MutableStateFlow(false) + val isPlayerReady: StateFlow + get() = _isPlayerReady + + private val serviceConnection = object : ServiceConnection { + override fun onServiceConnected(className: ComponentName, service: IBinder) { + val binder = service as BackgroundPlaybackService.BackgroundBinder + val playbackService = binder.getService() + playbackService.setPlayer(player!!) // Pass player to the service + isServiceBound = true + } + + override fun onServiceDisconnected(arg0: ComponentName) { + isServiceBound = false + } + } + + fun initializePlayer(context: Context, config: VideoPlayerConfig, videoUrl: String) { + this.config = config + backgroundPlaybackEnabled = config.playbackConfig.backgroundPlaybackEnabled + autoplayEnabled = config.playbackConfig.autoplayEnabled + if (player == null || videoUrl != currentVideoUrl) { + // Initialize player if not already initialized + val playerConfig = + PlayerConfig(key = PlaybackSDKManager.bitmovinLicense) + player = Player(context, playerConfig) + } + loadVideo(videoUrl) + + updateBackgroundService(context) + } + + private fun updateBackgroundService(context: Context) { + if (backgroundPlaybackEnabled && isPermissionsGranted) { + bindToBackgroundPlaybackService(context) + } + } + + fun updatePermissionsState(isGranted: Boolean, context: Context) { + isPermissionsGranted = isGranted + updateBackgroundService(context) + } + + fun loadVideo(videoUrl: String) { + currentVideoUrl = videoUrl + val sourceConfig = SourceConfig.fromUrl(videoUrl) + player?.pause() + player?.load(sourceConfig) + player?.next(PlayerEvent.Ready::class.java) { + _isPlayerReady.value = true + } + player?.next(PlayerEvent.Error::class.java) { + Log.d("SDK", "Player error") + } + if (autoplayEnabled) { + player?.play() + } + + } + + private fun bindToBackgroundPlaybackService(context: Context) { + // Bind to the background service + if (_isPlayerReady.value && !isServiceBound) { + val intent = Intent(context, BackgroundPlaybackService::class.java) + context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE) + } + } + + private fun unbindFromService(context: Context) { + if (isServiceBound) { + context.unbindService(serviceConnection) + isServiceBound = false + } + } + + fun handleAppInBackground(context: Context) { + if (backgroundPlaybackEnabled) { + bindToBackgroundPlaybackService(context) + } else { + player?.pause() + } + } + + fun handleAppInForeground(context: Context) { + if (backgroundPlaybackEnabled) { + unbindFromService(context) + } else if (autoplayEnabled) { + player?.play() + } + } + + fun playVideo() { + if (isPlayerReady.value) { + player?.play() + } + } + + override fun onCleared() { + super.onCleared() + player?.destroy() + player = null + } +} \ No newline at end of file diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/ui/BackgroundPlaybackService.kt b/playback-sdk-android/src/main/java/com/streamamg/player/ui/BackgroundPlaybackService.kt index 900e0ac..3ea5aa0 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/ui/BackgroundPlaybackService.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/ui/BackgroundPlaybackService.kt @@ -35,6 +35,10 @@ class BackgroundPlaybackService : Service() { } } + fun setPlayer(player: Player) { + this.player = player + } + fun releasePlayer() { player?.destroy() player = null @@ -78,7 +82,7 @@ class BackgroundPlaybackService : Service() { override fun onCreate() { super.onCreate() - setPlayer(this) +// setPlayer(this) // Create a PlayerNotificationManager with the static create method // By passing null for the mediaDescriptionAdapter, a DefaultMediaDescriptionAdapter will be used internally. @@ -105,7 +109,7 @@ class BackgroundPlaybackService : Service() { }) // Attaching the Player to the PlayerNotificationManager - setPlayer(sharedPlayer) +// setPlayer(sharedPlayer) } } @@ -137,4 +141,9 @@ class BackgroundPlaybackService : Service() { isRunning = true return START_STICKY } + + fun setPlayer(playerBind: Player?) { + this.player = playerBind + playerNotificationManager.setPlayer(playerBind) + } } From 963e7ce9e6a0f80ce9468e8b6149562533f9d5ca Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Wed, 25 Sep 2024 17:33:58 +0300 Subject: [PATCH 11/25] [CORE-4969] Updated logic load new video from url --- .../bitmovin/BitmovinVideoPlayerPlugin.kt | 59 ++----------------- .../plugin/bitmovin/VideoPlayerViewModel.kt | 46 +++++++++++++-- 2 files changed, 48 insertions(+), 57 deletions(-) diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt index f3a6368..807943f 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt @@ -54,10 +54,6 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { @Composable override fun PlayerView(hlsUrl: String): Unit { - changedSource = !urlsAreEqualExcludingKs(this.hlsUrl, hlsUrl) - if (changedSource) { - playerBind = null - } val playerViewModel: VideoPlayerViewModel = viewModel() this.hlsUrl = hlsUrl @@ -80,9 +76,6 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { DisposableEffect(hlsUrl) { playerViewModel.initializePlayer(context, playerConfig, hlsUrl) playerBind = playerViewModel.player - if (changedSource) { - playerViewModel.loadVideo(hlsUrl) - } onDispose { playerViewModel.handleAppInForeground(context) } @@ -97,12 +90,12 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { Lifecycle.Event.ON_START -> { playerViewModel.handleAppInForeground(context) } - Lifecycle.Event.ON_PAUSE -> { - playerViewModel.handleAppInBackground(context) - } - Lifecycle.Event.ON_RESUME -> { - playerViewModel.handleAppInForeground(context) - } +// Lifecycle.Event.ON_PAUSE -> { +// playerViewModel.handleAppInBackground(context) +// } +// Lifecycle.Event.ON_RESUME -> { +// playerViewModel.handleAppInForeground(context) +// } else -> {} } } @@ -131,10 +124,6 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { Log.d("SDK", "-------- player View update") view.player = playerViewModel.player } - if (changedSource) { - playerViewModel.loadVideo(hlsUrl) - playerViewModel.playVideo() - } } ) } @@ -194,42 +183,6 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { } } - private fun urlsAreEqualExcludingKs(url1: String, url2: String): Boolean { - if (url1.isEmpty()) return false - if (url2.isEmpty()) return false - val normalizedUrl1 = normalizeUrl(url1) - val normalizedUrl2 = normalizeUrl(url2) - return normalizedUrl1 == normalizedUrl2 - } - - private fun normalizeUrl(urlString: String): String { - val url = URL(urlString) - val protocol = url.protocol - val host = url.host - val port = url.port - val path = url.path - - // Parse query parameters excluding 'ks' and sort them for consistent comparison - val queryParams = url.query?.split("&")?.mapNotNull { - val parts = it.split("=", limit = 2) - if (parts[0] != "ks") { - it - } else null - }?.sorted()?.joinToString("&") ?: "" - - // Reconstruct the URL without the 'ks' parameter - val normalizedUrl = StringBuilder() - normalizedUrl.append(protocol).append("://").append(host) - if (port != -1) { - normalizedUrl.append(":").append(port) - } - normalizedUrl.append(path) - if (queryParams.isNotEmpty()) { - normalizedUrl.append("?").append(queryParams) - } - return normalizedUrl.toString() - } - private fun Context.findActivity(): Activity? = when (this) { is Activity -> this is ContextWrapper -> baseContext.findActivity() diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt index d25bb5a..7b5a7e5 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt @@ -16,6 +16,7 @@ import com.streamamg.player.plugin.VideoPlayerConfig import com.streamamg.player.ui.BackgroundPlaybackService import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import java.net.URL class VideoPlayerViewModel : ViewModel() { var player: Player? = null @@ -46,7 +47,7 @@ class VideoPlayerViewModel : ViewModel() { this.config = config backgroundPlaybackEnabled = config.playbackConfig.backgroundPlaybackEnabled autoplayEnabled = config.playbackConfig.autoplayEnabled - if (player == null || videoUrl != currentVideoUrl) { + if (player == null) { // Initialize player if not already initialized val playerConfig = PlayerConfig(key = PlaybackSDKManager.bitmovinLicense) @@ -69,10 +70,11 @@ class VideoPlayerViewModel : ViewModel() { } fun loadVideo(videoUrl: String) { + if (!urlsAreEqualExcludingKs(currentVideoUrl ?: "", videoUrl)) { + val sourceConfig = SourceConfig.fromUrl(videoUrl) + player?.load(sourceConfig) + } currentVideoUrl = videoUrl - val sourceConfig = SourceConfig.fromUrl(videoUrl) - player?.pause() - player?.load(sourceConfig) player?.next(PlayerEvent.Ready::class.java) { _isPlayerReady.value = true } @@ -127,4 +129,40 @@ class VideoPlayerViewModel : ViewModel() { player?.destroy() player = null } + + private fun urlsAreEqualExcludingKs(url1: String, url2: String): Boolean { + if (url1.isEmpty()) return false + if (url2.isEmpty()) return false + val normalizedUrl1 = normalizeUrl(url1) + val normalizedUrl2 = normalizeUrl(url2) + return normalizedUrl1 == normalizedUrl2 + } + + private fun normalizeUrl(urlString: String): String { + val url = URL(urlString) + val protocol = url.protocol + val host = url.host + val port = url.port + val path = url.path + + // Parse query parameters excluding 'ks' and sort them for consistent comparison + val queryParams = url.query?.split("&")?.mapNotNull { + val parts = it.split("=", limit = 2) + if (parts[0] != "ks") { + it + } else null + }?.sorted()?.joinToString("&") ?: "" + + // Reconstruct the URL without the 'ks' parameter + val normalizedUrl = StringBuilder() + normalizedUrl.append(protocol).append("://").append(host) + if (port != -1) { + normalizedUrl.append(":").append(port) + } + normalizedUrl.append(path) + if (queryParams.isNotEmpty()) { + normalizedUrl.append("?").append(queryParams) + } + return normalizedUrl.toString() + } } \ No newline at end of file From 17975b77a2c89868c729f291dad475fa3c547fab Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Thu, 26 Sep 2024 11:20:14 +0300 Subject: [PATCH 12/25] [CORE-4969] Refactor code --- .../bitmovin/BitmovinVideoPlayerPlugin.kt | 19 ++++++------------- .../plugin/bitmovin/VideoPlayerViewModel.kt | 4 +--- 2 files changed, 7 insertions(+), 16 deletions(-) diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt index 807943f..e55e9a1 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt @@ -6,7 +6,6 @@ import android.content.Context import android.content.ContextWrapper import android.content.pm.ActivityInfo import android.os.Build -import android.util.Log import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable @@ -34,7 +33,6 @@ import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import com.streamamg.player.plugin.VideoPlayerConfig import com.streamamg.player.plugin.VideoPlayerPlugin -import java.net.URL class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { @@ -45,7 +43,6 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { private var playerConfig = VideoPlayerConfig() private var playerBind: Player? = null private val fullscreen = mutableStateOf(false) - private var changedSource = false override fun setup(config: VideoPlayerConfig) { playerConfig.playbackConfig.autoplayEnabled = config.playbackConfig.autoplayEnabled @@ -63,12 +60,10 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { if (playerConfig.playbackConfig.backgroundPlaybackEnabled) { if (Build.VERSION.SDK_INT >= 33) { - // Managing new permissions for the Background service notifications RequestMissingPermissions { granted -> playerViewModel.updatePermissionsState(granted, context) } } else { - // Bind and start the Background service without permissions playerViewModel.updatePermissionsState(true, context) } } @@ -90,12 +85,12 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { Lifecycle.Event.ON_START -> { playerViewModel.handleAppInForeground(context) } -// Lifecycle.Event.ON_PAUSE -> { -// playerViewModel.handleAppInBackground(context) -// } -// Lifecycle.Event.ON_RESUME -> { -// playerViewModel.handleAppInForeground(context) -// } + Lifecycle.Event.ON_PAUSE -> { + playerViewModel.handleAppInBackground(context) + } + Lifecycle.Event.ON_RESUME -> { + playerViewModel.handleAppInForeground(context) + } else -> {} } } @@ -112,7 +107,6 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { AndroidView( modifier = Modifier.fillMaxSize(), factory = { context -> - Log.d("SDK", "-------- player View created") PlayerView(context, playerViewModel.player).apply { setFullscreenHandler(fullscreenHandler) keepScreenOn = true @@ -121,7 +115,6 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { }, update = { view -> if (isReady.value) { - Log.d("SDK", "-------- player View update") view.player = playerViewModel.player } } diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt index 7b5a7e5..428967f 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt @@ -34,7 +34,7 @@ class VideoPlayerViewModel : ViewModel() { override fun onServiceConnected(className: ComponentName, service: IBinder) { val binder = service as BackgroundPlaybackService.BackgroundBinder val playbackService = binder.getService() - playbackService.setPlayer(player!!) // Pass player to the service + playbackService.setPlayer(player!!) isServiceBound = true } @@ -48,7 +48,6 @@ class VideoPlayerViewModel : ViewModel() { backgroundPlaybackEnabled = config.playbackConfig.backgroundPlaybackEnabled autoplayEnabled = config.playbackConfig.autoplayEnabled if (player == null) { - // Initialize player if not already initialized val playerConfig = PlayerConfig(key = PlaybackSDKManager.bitmovinLicense) player = Player(context, playerConfig) @@ -88,7 +87,6 @@ class VideoPlayerViewModel : ViewModel() { } private fun bindToBackgroundPlaybackService(context: Context) { - // Bind to the background service if (_isPlayerReady.value && !isServiceBound) { val intent = Intent(context, BackgroundPlaybackService::class.java) context.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE) From a809762ecd61dad2925f25ed339569bb6e3d40f2 Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Thu, 26 Sep 2024 12:39:35 +0300 Subject: [PATCH 13/25] [CORE-4969] Fixed small issue with starting background service --- .../bitmovin/BitmovinVideoPlayerPlugin.kt | 49 ++++++++++--------- .../plugin/bitmovin/VideoPlayerViewModel.kt | 8 +-- .../player/ui/BackgroundPlaybackService.kt | 16 ------ 3 files changed, 29 insertions(+), 44 deletions(-) diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt index e55e9a1..4a8bdaf 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt @@ -76,30 +76,6 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { } } - DisposableEffect(currentLifecycle) { - val observer = LifecycleEventObserver { _, event -> - when (event) { - Lifecycle.Event.ON_STOP -> { - playerViewModel.handleAppInBackground(context) - } - Lifecycle.Event.ON_START -> { - playerViewModel.handleAppInForeground(context) - } - Lifecycle.Event.ON_PAUSE -> { - playerViewModel.handleAppInBackground(context) - } - Lifecycle.Event.ON_RESUME -> { - playerViewModel.handleAppInForeground(context) - } - else -> {} - } - } - currentLifecycle.lifecycle.addObserver(observer) - onDispose { - currentLifecycle.lifecycle.removeObserver(observer) - } - } - val isReady = playerViewModel.isPlayerReady.collectAsState() key(lastHlsUrl.value) { @@ -116,9 +92,34 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { update = { view -> if (isReady.value) { view.player = playerViewModel.player + playerViewModel.updateBackgroundService(context) } } ) + + DisposableEffect(currentLifecycle) { + val observer = LifecycleEventObserver { _, event -> + when (event) { + Lifecycle.Event.ON_STOP -> { + playerViewModel.handleAppInBackground(context) + } + Lifecycle.Event.ON_START -> { + playerViewModel.handleAppInForeground(context) + } + Lifecycle.Event.ON_PAUSE -> { + playerViewModel.handleAppInBackground(context) + } + Lifecycle.Event.ON_RESUME -> { + playerViewModel.handleAppInForeground(context) + } + else -> {} + } + } + currentLifecycle.lifecycle.addObserver(observer) + onDispose { + currentLifecycle.lifecycle.removeObserver(observer) + } + } } if (fullscreen.value) { diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt index 428967f..7172253 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt @@ -57,7 +57,7 @@ class VideoPlayerViewModel : ViewModel() { updateBackgroundService(context) } - private fun updateBackgroundService(context: Context) { + fun updateBackgroundService(context: Context) { if (backgroundPlaybackEnabled && isPermissionsGranted) { bindToBackgroundPlaybackService(context) } @@ -68,7 +68,7 @@ class VideoPlayerViewModel : ViewModel() { updateBackgroundService(context) } - fun loadVideo(videoUrl: String) { + private fun loadVideo(videoUrl: String) { if (!urlsAreEqualExcludingKs(currentVideoUrl ?: "", videoUrl)) { val sourceConfig = SourceConfig.fromUrl(videoUrl) player?.load(sourceConfig) @@ -101,7 +101,7 @@ class VideoPlayerViewModel : ViewModel() { } fun handleAppInBackground(context: Context) { - if (backgroundPlaybackEnabled) { + if (backgroundPlaybackEnabled && _isPlayerReady.value) { bindToBackgroundPlaybackService(context) } else { player?.pause() @@ -109,7 +109,7 @@ class VideoPlayerViewModel : ViewModel() { } fun handleAppInForeground(context: Context) { - if (backgroundPlaybackEnabled) { + if (backgroundPlaybackEnabled && _isPlayerReady.value) { unbindFromService(context) } else if (autoplayEnabled) { player?.play() diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/ui/BackgroundPlaybackService.kt b/playback-sdk-android/src/main/java/com/streamamg/player/ui/BackgroundPlaybackService.kt index 3ea5aa0..83094ab 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/ui/BackgroundPlaybackService.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/ui/BackgroundPlaybackService.kt @@ -28,17 +28,6 @@ class BackgroundPlaybackService : Service() { private var player: Player? = null - fun setPlayer(context: Context) { - if (player == null) { - val playerConfig = PlayerConfig(key = PlaybackSDKManager.bitmovinLicense) - player = Player(context, playerConfig) - } - } - - fun setPlayer(player: Player) { - this.player = player - } - fun releasePlayer() { player?.destroy() player = null @@ -82,8 +71,6 @@ class BackgroundPlaybackService : Service() { override fun onCreate() { super.onCreate() -// setPlayer(this) - // Create a PlayerNotificationManager with the static create method // By passing null for the mediaDescriptionAdapter, a DefaultMediaDescriptionAdapter will be used internally. playerNotificationManager = PlayerNotificationManager.createWithNotificationChannel( @@ -107,9 +94,6 @@ class BackgroundPlaybackService : Service() { stopSelf() } }) - - // Attaching the Player to the PlayerNotificationManager -// setPlayer(sharedPlayer) } } From 80784af382d8d53623879578a0b2c14cce1796f8 Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Sun, 6 Oct 2024 22:26:47 +0300 Subject: [PATCH 14/25] Updated SDK logic for ViewModelProvider. Fixed some Fragment issues --- playback-sdk-android/build.gradle.kts | 1 + .../player/plugin/VideoPlayerConfig.kt | 3 +- .../bitmovin/BitmovinVideoPlayerPlugin.kt | 30 +++++-- .../bitmovin/DetectRotationAndFullscreen.kt | 59 ++++++++++++ .../plugin/bitmovin/VideoPlayerViewModel.kt | 89 ++++++++++++------- 5 files changed, 143 insertions(+), 39 deletions(-) create mode 100644 playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/DetectRotationAndFullscreen.kt diff --git a/playback-sdk-android/build.gradle.kts b/playback-sdk-android/build.gradle.kts index e6c0296..4324037 100644 --- a/playback-sdk-android/build.gradle.kts +++ b/playback-sdk-android/build.gradle.kts @@ -148,6 +148,7 @@ tasks.create("releaseSourcesJar") { dependencies { implementation("androidx.compose.runtime:runtime:1.6.2") implementation("androidx.core:core-ktx:1.12.0") + implementation("androidx.fragment:fragment-ktx:1.6.1") implementation("androidx.appcompat:appcompat:1.6.1") implementation("com.google.android.material:material:1.11.0") implementation("androidx.compose.foundation:foundation-layout-android:1.6.2") diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/VideoPlayerConfig.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/VideoPlayerConfig.kt index 4e4cfa9..e19c4cc 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/VideoPlayerConfig.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/VideoPlayerConfig.kt @@ -12,5 +12,6 @@ data class VideoPlayerConfig( @Serializable data class PlaybackConfig( var autoplayEnabled: Boolean = true, - var backgroundPlaybackEnabled: Boolean = true + var backgroundPlaybackEnabled: Boolean = true, + var fullscreenRotationEnabled: Boolean = true ) diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt index 4a8bdaf..4e90064 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt @@ -6,6 +6,8 @@ import android.content.Context import android.content.ContextWrapper import android.content.pm.ActivityInfo import android.os.Build +import android.util.Log +import androidx.activity.ComponentActivity import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable @@ -16,6 +18,7 @@ import androidx.compose.runtime.key import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.viewinterop.AndroidView @@ -24,6 +27,9 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver +import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.ViewModelStoreOwner +import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.viewModel import com.bitmovin.player.PlayerView import com.bitmovin.player.api.Player @@ -36,6 +42,7 @@ import com.streamamg.player.plugin.VideoPlayerPlugin class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { + private var playerView: PlayerView? = null override val name: String = "Bitmovin" override val version: String = "1.0" @@ -47,16 +54,22 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { override fun setup(config: VideoPlayerConfig) { playerConfig.playbackConfig.autoplayEnabled = config.playbackConfig.autoplayEnabled playerConfig.playbackConfig.backgroundPlaybackEnabled = config.playbackConfig.backgroundPlaybackEnabled + playerConfig.playbackConfig.fullscreenRotationEnabled = config.playbackConfig.fullscreenRotationEnabled } @Composable override fun PlayerView(hlsUrl: String): Unit { - val playerViewModel: VideoPlayerViewModel = viewModel() + val context = LocalContext.current + val activity = context.findActivity() as? ComponentActivity + val playerViewModel: VideoPlayerViewModel = activity?.let { + ViewModelProvider(it)[VideoPlayerViewModel::class.java] + } ?: viewModel() this.hlsUrl = hlsUrl - val context = LocalContext.current val currentLifecycle = LocalLifecycleOwner.current val lastHlsUrl = remember { mutableStateOf(hlsUrl) } + val configuration = LocalConfiguration.current + if (playerConfig.playbackConfig.backgroundPlaybackEnabled) { if (Build.VERSION.SDK_INT >= 33) { @@ -83,21 +96,27 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { AndroidView( modifier = Modifier.fillMaxSize(), factory = { context -> - PlayerView(context, playerViewModel.player).apply { - setFullscreenHandler(fullscreenHandler) + playerView = PlayerView(context, playerViewModel.player).apply { keepScreenOn = true player = playerViewModel.player } + playerView!! }, update = { view -> if (isReady.value) { + playerView?.setFullscreenHandler(fullscreenHandler) view.player = playerViewModel.player playerViewModel.updateBackgroundService(context) } } ) - DisposableEffect(currentLifecycle) { + playerView?.setFullscreenHandler(fullscreenHandler) + + if (playerConfig.playbackConfig.fullscreenRotationEnabled) + DetectRotationAndFullscreen(playerView) + + DisposableEffect(currentLifecycle, configuration) { val observer = LifecycleEventObserver { _, event -> when (event) { Lifecycle.Event.ON_STOP -> { @@ -116,6 +135,7 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { } } currentLifecycle.lifecycle.addObserver(observer) + onDispose { currentLifecycle.lifecycle.removeObserver(observer) } diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/DetectRotationAndFullscreen.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/DetectRotationAndFullscreen.kt new file mode 100644 index 0000000..25abff0 --- /dev/null +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/DetectRotationAndFullscreen.kt @@ -0,0 +1,59 @@ +package com.streamamg.player.plugin.bitmovin + +import android.content.Context +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.util.Log +import androidx.compose.runtime.* +import androidx.compose.ui.platform.LocalContext +import com.bitmovin.player.PlayerView +import kotlin.math.absoluteValue + +@Composable +fun DetectRotationAndFullscreen(playerView: PlayerView?) { + val context = LocalContext.current + var isLandscape by remember { mutableStateOf(false) } + + DisposableEffect(Unit) { + val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager + val accelerometer = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER) + + val sensorEventListener = object : SensorEventListener { + override fun onSensorChanged(event: SensorEvent?) { + event?.let { + val x = event.values[0] + val y = event.values[1] + + // Determine if the device is in landscape or portrait orientation + if (x.absoluteValue > y.absoluteValue) { + // Landscape mode + if (!isLandscape) { + isLandscape = true + Log.d("Orientation", "Landscape mode detected") + playerView?.enterFullscreen() + } + } else { + // Portrait mode + if (isLandscape) { + isLandscape = false + Log.d("Orientation", "Portrait mode detected") + playerView?.exitFullscreen() + } + } + } + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} + } + + // Register the sensor listener + sensorManager.registerListener(sensorEventListener, accelerometer, SensorManager.SENSOR_DELAY_NORMAL) + + onDispose { + // Unregister the sensor listener to avoid memory leaks + sensorManager.unregisterListener(sensorEventListener) + } + } +} diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt index 7172253..32a4195 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt @@ -16,6 +16,7 @@ import com.streamamg.player.plugin.VideoPlayerConfig import com.streamamg.player.ui.BackgroundPlaybackService import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import java.net.MalformedURLException import java.net.URL class VideoPlayerViewModel : ViewModel() { @@ -52,6 +53,8 @@ class VideoPlayerViewModel : ViewModel() { PlayerConfig(key = PlaybackSDKManager.bitmovinLicense) player = Player(context, playerConfig) } + unbindFromService(context) + loadVideo(videoUrl) updateBackgroundService(context) @@ -94,10 +97,12 @@ class VideoPlayerViewModel : ViewModel() { } private fun unbindFromService(context: Context) { - if (isServiceBound) { - context.unbindService(serviceConnection) - isServiceBound = false - } + try { + if (isServiceBound) { + context.unbindService(serviceConnection) + isServiceBound = false + } + } catch (_: IllegalArgumentException) {} } fun handleAppInBackground(context: Context) { @@ -129,38 +134,56 @@ class VideoPlayerViewModel : ViewModel() { } private fun urlsAreEqualExcludingKs(url1: String, url2: String): Boolean { - if (url1.isEmpty()) return false - if (url2.isEmpty()) return false - val normalizedUrl1 = normalizeUrl(url1) - val normalizedUrl2 = normalizeUrl(url2) - return normalizedUrl1 == normalizedUrl2 + if (url1.isEmpty() || url2.isEmpty()) return false + + return try { + val normalizedUrl1 = normalizeUrl(url1) + val normalizedUrl2 = normalizeUrl(url2) + normalizedUrl1 == normalizedUrl2 + } catch (e: MalformedURLException) { + // Якщо URL некоректний, повертаємо false + e.printStackTrace() + false + } catch (e: Exception) { + // Обробляємо будь-які інші непередбачені винятки + e.printStackTrace() + false + } } private fun normalizeUrl(urlString: String): String { - val url = URL(urlString) - val protocol = url.protocol - val host = url.host - val port = url.port - val path = url.path - - // Parse query parameters excluding 'ks' and sort them for consistent comparison - val queryParams = url.query?.split("&")?.mapNotNull { - val parts = it.split("=", limit = 2) - if (parts[0] != "ks") { - it - } else null - }?.sorted()?.joinToString("&") ?: "" - - // Reconstruct the URL without the 'ks' parameter - val normalizedUrl = StringBuilder() - normalizedUrl.append(protocol).append("://").append(host) - if (port != -1) { - normalizedUrl.append(":").append(port) - } - normalizedUrl.append(path) - if (queryParams.isNotEmpty()) { - normalizedUrl.append("?").append(queryParams) + return try { + val url = URL(urlString) + + val protocol = url.protocol ?: return urlString + val host = url.host ?: return urlString + val port = url.port + val path = url.path ?: "" + + val queryParams = url.query?.split("&")?.mapNotNull { + val parts = it.split("=", limit = 2) + if (parts[0] != "ks") { + it + } else null + }?.sorted()?.joinToString("&") ?: "" + + val normalizedUrl = StringBuilder() + normalizedUrl.append(protocol).append("://").append(host) + if (port != -1) { + normalizedUrl.append(":").append(port) + } + normalizedUrl.append(path) + if (queryParams.isNotEmpty()) { + normalizedUrl.append("?").append(queryParams) + } + normalizedUrl.toString() + + } catch (e: MalformedURLException) { + e.printStackTrace() + urlString + } catch (e: Exception) { + e.printStackTrace() + urlString } - return normalizedUrl.toString() } } \ No newline at end of file From c0c84fcdf0877227d0097359a551683851123da6 Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Sun, 6 Oct 2024 23:11:32 +0300 Subject: [PATCH 15/25] Added additional logic to handle Jetpack compose lifecycle --- .../bitmovin/BitmovinVideoPlayerPlugin.kt | 18 +++++++++++++----- .../plugin/bitmovin/VideoPlayerViewModel.kt | 17 +++++++++++++++++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt index 4e90064..cce5f68 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt @@ -60,8 +60,10 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { @Composable override fun PlayerView(hlsUrl: String): Unit { val context = LocalContext.current + val isJetpackCompose = LocalViewModelStoreOwner.current != null + val activity = context.findActivity() as? ComponentActivity - val playerViewModel: VideoPlayerViewModel = activity?.let { + val playerViewModel: VideoPlayerViewModel = if (isJetpackCompose) viewModel() else activity?.let { ViewModelProvider(it)[VideoPlayerViewModel::class.java] } ?: viewModel() @@ -85,7 +87,11 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { playerViewModel.initializePlayer(context, playerConfig, hlsUrl) playerBind = playerViewModel.player onDispose { - playerViewModel.handleAppInForeground(context) + if (isJetpackCompose) { + playerViewModel.unbindAndStopService(context) + } else { + playerViewModel.handleAppInBackground(context) + } } } @@ -96,6 +102,7 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { AndroidView( modifier = Modifier.fillMaxSize(), factory = { context -> + Log.d("SDK", "New PlayerView factory called") playerView = PlayerView(context, playerViewModel.player).apply { keepScreenOn = true player = playerViewModel.player @@ -103,16 +110,17 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { playerView!! }, update = { view -> + Log.d("SDK", "View updated ${isReady.value}") if (isReady.value) { - playerView?.setFullscreenHandler(fullscreenHandler) + Log.d("SDK", "Player ready") view.player = playerViewModel.player + playerView?.setFullscreenHandler(fullscreenHandler) + playerView?.invalidate() playerViewModel.updateBackgroundService(context) } } ) - playerView?.setFullscreenHandler(fullscreenHandler) - if (playerConfig.playbackConfig.fullscreenRotationEnabled) DetectRotationAndFullscreen(playerView) diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt index 32a4195..6893019 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt @@ -105,6 +105,17 @@ class VideoPlayerViewModel : ViewModel() { } catch (_: IllegalArgumentException) {} } + fun unbindAndStopService(context: Context) { + val intent = Intent(context, BackgroundPlaybackService::class.java) + try { + if (isServiceBound) { + context.unbindService(serviceConnection) + isServiceBound = false + } + context.stopService(intent) + } catch (_: IllegalArgumentException) {} + } + fun handleAppInBackground(context: Context) { if (backgroundPlaybackEnabled && _isPlayerReady.value) { bindToBackgroundPlaybackService(context) @@ -127,6 +138,12 @@ class VideoPlayerViewModel : ViewModel() { } } + fun pauseVideo() { + if (isPlayerReady.value) { + player?.pause() + } + } + override fun onCleared() { super.onCleared() player?.destroy() From 878f5113f3a1e87f315d6f8dd30a8cd033791108 Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Sun, 6 Oct 2024 23:12:09 +0300 Subject: [PATCH 16/25] Added additional logic to handle Jetpack compose lifecycle --- .idea/misc.xml | 3 ++- .../player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt | 3 --- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.idea/misc.xml b/.idea/misc.xml index 8978d23..6e4b8a0 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,6 +1,7 @@ + - + diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt index cce5f68..5e290b1 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt @@ -102,7 +102,6 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { AndroidView( modifier = Modifier.fillMaxSize(), factory = { context -> - Log.d("SDK", "New PlayerView factory called") playerView = PlayerView(context, playerViewModel.player).apply { keepScreenOn = true player = playerViewModel.player @@ -110,9 +109,7 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { playerView!! }, update = { view -> - Log.d("SDK", "View updated ${isReady.value}") if (isReady.value) { - Log.d("SDK", "Player ready") view.player = playerViewModel.player playerView?.setFullscreenHandler(fullscreenHandler) playerView?.invalidate() From be0cb5601647b5eab84febdcb009611a8278656f Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Mon, 7 Oct 2024 14:35:55 +0300 Subject: [PATCH 17/25] Added Fixed edge case for url comparison --- .../streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt index 6893019..2535cc0 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt @@ -179,7 +179,7 @@ class VideoPlayerViewModel : ViewModel() { val queryParams = url.query?.split("&")?.mapNotNull { val parts = it.split("=", limit = 2) - if (parts[0] != "ks") { + if (parts.size == 2 && parts[0] != "ks") { it } else null }?.sorted()?.joinToString("&") ?: "" From 917ffd4e95731dd23cff22755826f2e41090e5b9 Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Tue, 8 Oct 2024 17:28:29 +0300 Subject: [PATCH 18/25] [CORE-4969] Fixed issue for Fragments and jetpack compose detection --- .../player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt index 5e290b1..8258957 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt @@ -60,7 +60,10 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { @Composable override fun PlayerView(hlsUrl: String): Unit { val context = LocalContext.current - val isJetpackCompose = LocalViewModelStoreOwner.current != null + val isJetpackCompose = when (context) { + is ComponentActivity -> true + else -> false + } val activity = context.findActivity() as? ComponentActivity val playerViewModel: VideoPlayerViewModel = if (isJetpackCompose) viewModel() else activity?.let { From 38a77a552a991469da9e613078dbfaa5fc42ab8c Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Tue, 8 Oct 2024 18:13:00 +0300 Subject: [PATCH 19/25] [CORE-4969] Added ability to disable fullscreen --- .../java/com/streamamg/player/plugin/VideoPlayerConfig.kt | 3 ++- .../player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt | 7 +++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/VideoPlayerConfig.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/VideoPlayerConfig.kt index e19c4cc..d74973d 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/VideoPlayerConfig.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/VideoPlayerConfig.kt @@ -13,5 +13,6 @@ data class VideoPlayerConfig( data class PlaybackConfig( var autoplayEnabled: Boolean = true, var backgroundPlaybackEnabled: Boolean = true, - var fullscreenRotationEnabled: Boolean = true + var fullscreenRotationEnabled: Boolean = true, + var fullscreenEnabled: Boolean = true ) diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt index 8258957..2e1c65d 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt @@ -6,7 +6,6 @@ import android.content.Context import android.content.ContextWrapper import android.content.pm.ActivityInfo import android.os.Build -import android.util.Log import androidx.activity.ComponentActivity import androidx.annotation.RequiresApi import androidx.compose.foundation.layout.fillMaxSize @@ -28,8 +27,6 @@ import androidx.core.view.WindowInsetsControllerCompat import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleEventObserver import androidx.lifecycle.ViewModelProvider -import androidx.lifecycle.ViewModelStoreOwner -import androidx.lifecycle.viewmodel.compose.LocalViewModelStoreOwner import androidx.lifecycle.viewmodel.compose.viewModel import com.bitmovin.player.PlayerView import com.bitmovin.player.api.Player @@ -55,6 +52,7 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { playerConfig.playbackConfig.autoplayEnabled = config.playbackConfig.autoplayEnabled playerConfig.playbackConfig.backgroundPlaybackEnabled = config.playbackConfig.backgroundPlaybackEnabled playerConfig.playbackConfig.fullscreenRotationEnabled = config.playbackConfig.fullscreenRotationEnabled + playerConfig.playbackConfig.fullscreenEnabled = config.playbackConfig.fullscreenEnabled } @Composable @@ -114,7 +112,8 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { update = { view -> if (isReady.value) { view.player = playerViewModel.player - playerView?.setFullscreenHandler(fullscreenHandler) + if (playerConfig.playbackConfig.fullscreenEnabled) + playerView?.setFullscreenHandler(fullscreenHandler) playerView?.invalidate() playerViewModel.updateBackgroundService(context) } From 2df6ecfe8e7af657663859e743ff00009cf73066 Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Mon, 14 Oct 2024 19:24:55 +0300 Subject: [PATCH 20/25] [CORE-4969] Added logic for handling errors --- .../bitmovin/BitmovinVideoPlayerPlugin.kt | 42 +++++++++++-------- .../plugin/bitmovin/VideoPlayerViewModel.kt | 11 +++++ .../com/streamamg/player/ui/PlaybackUIView.kt | 6 +++ 3 files changed, 41 insertions(+), 18 deletions(-) diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt index 2e1c65d..e9b5ad7 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt @@ -38,7 +38,7 @@ import com.streamamg.player.plugin.VideoPlayerConfig import com.streamamg.player.plugin.VideoPlayerPlugin -class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { +class BitmovinVideoPlayerPlugin : VideoPlayerPlugin, LifecycleCleaner { private var playerView: PlayerView? = null override val name: String = "Bitmovin" override val version: String = "1.0" @@ -47,6 +47,7 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { private var playerConfig = VideoPlayerConfig() private var playerBind: Player? = null private val fullscreen = mutableStateOf(false) + private var playerViewModel: VideoPlayerViewModel? = null override fun setup(config: VideoPlayerConfig) { playerConfig.playbackConfig.autoplayEnabled = config.playbackConfig.autoplayEnabled @@ -64,7 +65,7 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { } val activity = context.findActivity() as? ComponentActivity - val playerViewModel: VideoPlayerViewModel = if (isJetpackCompose) viewModel() else activity?.let { + playerViewModel = if (isJetpackCompose) viewModel() else activity?.let { ViewModelProvider(it)[VideoPlayerViewModel::class.java] } ?: viewModel() @@ -77,45 +78,45 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { if (playerConfig.playbackConfig.backgroundPlaybackEnabled) { if (Build.VERSION.SDK_INT >= 33) { RequestMissingPermissions { granted -> - playerViewModel.updatePermissionsState(granted, context) + playerViewModel?.updatePermissionsState(granted, context) } } else { - playerViewModel.updatePermissionsState(true, context) + playerViewModel?.updatePermissionsState(true, context) } } DisposableEffect(hlsUrl) { - playerViewModel.initializePlayer(context, playerConfig, hlsUrl) - playerBind = playerViewModel.player + playerViewModel?.initializePlayer(context, playerConfig, hlsUrl) + playerBind = playerViewModel?.player onDispose { if (isJetpackCompose) { - playerViewModel.unbindAndStopService(context) + playerViewModel?.unbindAndStopService(context) } else { - playerViewModel.handleAppInBackground(context) + playerViewModel?.handleAppInBackground(context) } } } - val isReady = playerViewModel.isPlayerReady.collectAsState() + val isReady = playerViewModel?.isPlayerReady?.collectAsState() key(lastHlsUrl.value) { // Force recomposition when the HLS URL changes AndroidView( modifier = Modifier.fillMaxSize(), factory = { context -> - playerView = PlayerView(context, playerViewModel.player).apply { + playerView = PlayerView(context, playerViewModel?.player).apply { keepScreenOn = true - player = playerViewModel.player + player = playerViewModel?.player } playerView!! }, update = { view -> - if (isReady.value) { - view.player = playerViewModel.player + if (isReady?.value == true) { + view.player = playerViewModel?.player if (playerConfig.playbackConfig.fullscreenEnabled) playerView?.setFullscreenHandler(fullscreenHandler) playerView?.invalidate() - playerViewModel.updateBackgroundService(context) + playerViewModel?.updateBackgroundService(context) } } ) @@ -127,16 +128,16 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { val observer = LifecycleEventObserver { _, event -> when (event) { Lifecycle.Event.ON_STOP -> { - playerViewModel.handleAppInBackground(context) + playerViewModel?.handleAppInBackground(context) } Lifecycle.Event.ON_START -> { - playerViewModel.handleAppInForeground(context) + playerViewModel?.handleAppInForeground(context) } Lifecycle.Event.ON_PAUSE -> { - playerViewModel.handleAppInBackground(context) + playerViewModel?.handleAppInBackground(context) } Lifecycle.Event.ON_RESUME -> { - playerViewModel.handleAppInForeground(context) + playerViewModel?.handleAppInForeground(context) } else -> {} } @@ -222,6 +223,11 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { playerBind?.destroy() } + override fun clean(context: Context) { + playerViewModel?.unbindAndStopService(context=context) + playerViewModel?.clean() + } + private val fullscreenHandler = object : FullscreenHandler { override fun onFullscreenRequested() { fullscreen.value = true diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt index 2535cc0..ecc5f1b 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt @@ -7,6 +7,8 @@ import android.content.ServiceConnection import android.os.IBinder import android.util.Log import androidx.lifecycle.ViewModel +import androidx.lifecycle.findViewTreeLifecycleOwner +import androidx.lifecycle.viewModelScope import com.bitmovin.player.api.Player import com.bitmovin.player.api.PlayerConfig import com.bitmovin.player.api.event.PlayerEvent @@ -16,6 +18,7 @@ import com.streamamg.player.plugin.VideoPlayerConfig import com.streamamg.player.ui.BackgroundPlaybackService import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.launch import java.net.MalformedURLException import java.net.URL @@ -74,8 +77,10 @@ class VideoPlayerViewModel : ViewModel() { private fun loadVideo(videoUrl: String) { if (!urlsAreEqualExcludingKs(currentVideoUrl ?: "", videoUrl)) { val sourceConfig = SourceConfig.fromUrl(videoUrl) + Log.d("SDK", "Loading new Source") player?.load(sourceConfig) } + Log.d("SDK", "Loading existing source") currentVideoUrl = videoUrl player?.next(PlayerEvent.Ready::class.java) { _isPlayerReady.value = true @@ -144,6 +149,12 @@ class VideoPlayerViewModel : ViewModel() { } } + fun clean() { + // call this in main thread if it was called from background + pauseVideo() + currentVideoUrl = null + } + override fun onCleared() { super.onCleared() player?.destroy() diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/ui/PlaybackUIView.kt b/playback-sdk-android/src/main/java/com/streamamg/player/ui/PlaybackUIView.kt index 9d2b985..adcad49 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/ui/PlaybackUIView.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/ui/PlaybackUIView.kt @@ -12,9 +12,11 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import com.streamamg.PlaybackAPIError import com.streamamg.PlaybackSDKManager import com.streamamg.player.plugin.VideoPlayerPluginManager +import com.streamamg.player.plugin.bitmovin.LifecycleCleaner object PlaybackUIView { @@ -27,11 +29,15 @@ object PlaybackUIView { ) { var hasFetchedVideoDetails by remember { mutableStateOf(false) } var videoURL: String? by remember { mutableStateOf(null) } + val context = LocalContext.current LaunchedEffect(entryId) { PlaybackSDKManager.loadHLSStream(entryId, authorizationToken, userAgent) { hlsURL, error -> if (error != null) { // Handle error + VideoPlayerPluginManager.selectedPlugin?.let { plugin -> + (plugin as? LifecycleCleaner)?.clean(context) + } onError?.invoke(error) } else { // Update video URL From ccc058e1f6fa87ed602f28edf2d3599a8f2df68b Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Mon, 14 Oct 2024 19:25:04 +0300 Subject: [PATCH 21/25] [CORE-4969] Added logic for handling errors --- .../streamamg/player/plugin/bitmovin/LifecycleCleaner.kt | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/LifecycleCleaner.kt diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/LifecycleCleaner.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/LifecycleCleaner.kt new file mode 100644 index 0000000..e920923 --- /dev/null +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/LifecycleCleaner.kt @@ -0,0 +1,7 @@ +package com.streamamg.player.plugin.bitmovin + +import android.content.Context + +internal interface LifecycleCleaner { + fun clean(context: Context) +} \ No newline at end of file From e2c9ca5e5a6f970420e26d71c9efa152b81e49e8 Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Thu, 17 Oct 2024 18:23:48 +0300 Subject: [PATCH 22/25] [CORE-4969] Updated SDK for device rotation listener --- .../plugin/bitmovin/BitmovinVideoPlayerPlugin.kt | 13 ++++++++++++- .../plugin/bitmovin/DetectRotationAndFullscreen.kt | 8 +++++--- 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt index e9b5ad7..2181827 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/BitmovinVideoPlayerPlugin.kt @@ -38,6 +38,11 @@ import com.streamamg.player.plugin.VideoPlayerConfig import com.streamamg.player.plugin.VideoPlayerPlugin +internal interface FullscreenListener { + fun enterFullscreen() + fun exitFullscreen() +} + class BitmovinVideoPlayerPlugin : VideoPlayerPlugin, LifecycleCleaner { private var playerView: PlayerView? = null override val name: String = "Bitmovin" @@ -122,7 +127,13 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin, LifecycleCleaner { ) if (playerConfig.playbackConfig.fullscreenRotationEnabled) - DetectRotationAndFullscreen(playerView) + DetectRotationAndFullscreen(playerView) { isFullscreen -> + if (isFullscreen) { + playerView?.enterFullscreen() + } else { + playerView?.exitFullscreen() + } + } DisposableEffect(currentLifecycle, configuration) { val observer = LifecycleEventObserver { _, event -> diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/DetectRotationAndFullscreen.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/DetectRotationAndFullscreen.kt index 25abff0..a1e049c 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/DetectRotationAndFullscreen.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/DetectRotationAndFullscreen.kt @@ -12,7 +12,7 @@ import com.bitmovin.player.PlayerView import kotlin.math.absoluteValue @Composable -fun DetectRotationAndFullscreen(playerView: PlayerView?) { +fun DetectRotationAndFullscreen(playerView: PlayerView?, callback: (isFullscreen: Boolean) -> Unit) { val context = LocalContext.current var isLandscape by remember { mutableStateOf(false) } @@ -32,14 +32,16 @@ fun DetectRotationAndFullscreen(playerView: PlayerView?) { if (!isLandscape) { isLandscape = true Log.d("Orientation", "Landscape mode detected") - playerView?.enterFullscreen() +// playerView?.enterFullscreen() + callback.invoke(true) } } else { // Portrait mode if (isLandscape) { isLandscape = false Log.d("Orientation", "Portrait mode detected") - playerView?.exitFullscreen() +// playerView?.exitFullscreen() + callback.invoke(false) } } } From 619c1e12ec7346e9056ae7a319c25fb34c7d39db Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Wed, 23 Oct 2024 12:31:06 +0300 Subject: [PATCH 23/25] [CORE-4969] Fixed crash with lateinit. Fixed play/pause state --- .../main/java/com/streamamg/PlaybackSDKManager.kt | 8 ++++---- .../player/plugin/bitmovin/VideoPlayerViewModel.kt | 13 +++++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/playback-sdk-android/src/main/java/com/streamamg/PlaybackSDKManager.kt b/playback-sdk-android/src/main/java/com/streamamg/PlaybackSDKManager.kt index 28f7566..6f8e851 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/PlaybackSDKManager.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/PlaybackSDKManager.kt @@ -27,7 +27,7 @@ object PlaybackSDKManager { //region Private Properties - private lateinit var playBackAPI: PlaybackAPI + private var playBackAPI: PlaybackAPI? = null private lateinit var playerInformationAPI: PlayerInformationAPI private lateinit var amgAPIKey: String @@ -164,15 +164,15 @@ object PlaybackSDKManager { completion: (URL?, PlaybackAPIError?) -> Unit ) { coroutineScope.launch(Dispatchers.IO) { - playBackAPI.getVideoDetails(entryId, authorizationToken, userAgent) - .catch { e -> + playBackAPI?.getVideoDetails(entryId, authorizationToken, userAgent) + ?.catch { e -> // Handle the PlaybackAPIError or any other Throwable as a PlaybackAPIError when (e) { is PlaybackAPIError -> completion(null, e) else -> completion(null, PlaybackAPIError.NetworkError(e)) } } - .collect { videoDetails -> + ?.collect { videoDetails -> // Successfully retrieved video details, now check for the HLS URL val hlsURLString = videoDetails.media?.hls if (!hlsURLString.isNullOrEmpty()) { diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt index ecc5f1b..cf131b0 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt @@ -11,7 +11,9 @@ import androidx.lifecycle.findViewTreeLifecycleOwner import androidx.lifecycle.viewModelScope import com.bitmovin.player.api.Player import com.bitmovin.player.api.PlayerConfig +import com.bitmovin.player.api.event.Event import com.bitmovin.player.api.event.PlayerEvent +import com.bitmovin.player.api.event.on import com.bitmovin.player.api.source.SourceConfig import com.streamamg.PlaybackSDKManager import com.streamamg.player.plugin.VideoPlayerConfig @@ -30,6 +32,7 @@ class VideoPlayerViewModel : ViewModel() { private var backgroundPlaybackEnabled = false private var autoplayEnabled = false private var isPermissionsGranted = false + private var isPlayerPaused = false private var _isPlayerReady = MutableStateFlow(false) val isPlayerReady: StateFlow get() = _isPlayerReady @@ -77,10 +80,9 @@ class VideoPlayerViewModel : ViewModel() { private fun loadVideo(videoUrl: String) { if (!urlsAreEqualExcludingKs(currentVideoUrl ?: "", videoUrl)) { val sourceConfig = SourceConfig.fromUrl(videoUrl) - Log.d("SDK", "Loading new Source") + isPlayerPaused = false player?.load(sourceConfig) } - Log.d("SDK", "Loading existing source") currentVideoUrl = videoUrl player?.next(PlayerEvent.Ready::class.java) { _isPlayerReady.value = true @@ -88,7 +90,10 @@ class VideoPlayerViewModel : ViewModel() { player?.next(PlayerEvent.Error::class.java) { Log.d("SDK", "Player error") } - if (autoplayEnabled) { + player?.next(PlayerEvent.Paused::class) { + isPlayerPaused = player?.isPaused == true + } + if (autoplayEnabled && !isPlayerPaused) { player?.play() } @@ -132,7 +137,7 @@ class VideoPlayerViewModel : ViewModel() { fun handleAppInForeground(context: Context) { if (backgroundPlaybackEnabled && _isPlayerReady.value) { unbindFromService(context) - } else if (autoplayEnabled) { + } else if (autoplayEnabled && !isPlayerPaused) { player?.play() } } From ce01da9893a54fcdce1e4dd5b822719d5bdde289 Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Wed, 23 Oct 2024 14:32:06 +0300 Subject: [PATCH 24/25] [CORE-4969] Fixed play/pause state --- .../streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt index cf131b0..6d3e9ef 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/VideoPlayerViewModel.kt @@ -88,11 +88,14 @@ class VideoPlayerViewModel : ViewModel() { _isPlayerReady.value = true } player?.next(PlayerEvent.Error::class.java) { - Log.d("SDK", "Player error") + Log.d("SDK", "Player error ${it.message}") } player?.next(PlayerEvent.Paused::class) { isPlayerPaused = player?.isPaused == true } + player?.next(PlayerEvent.Play::class) { + isPlayerPaused = player?.isPaused == true + } if (autoplayEnabled && !isPlayerPaused) { player?.play() } From 297daff9341fd96438030791726156d2edd1757d Mon Sep 17 00:00:00 2001 From: Oleksandr Kharchenko Date: Mon, 4 Nov 2024 09:11:35 +0200 Subject: [PATCH 25/25] [CORE-4969] Improved detector for device rotation --- .../bitmovin/DetectRotationAndFullscreen.kt | 34 +++++++++---------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/DetectRotationAndFullscreen.kt b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/DetectRotationAndFullscreen.kt index a1e049c..9e1737b 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/DetectRotationAndFullscreen.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/player/plugin/bitmovin/DetectRotationAndFullscreen.kt @@ -9,12 +9,15 @@ import android.util.Log import androidx.compose.runtime.* import androidx.compose.ui.platform.LocalContext import com.bitmovin.player.PlayerView +import kotlin.math.abs import kotlin.math.absoluteValue @Composable fun DetectRotationAndFullscreen(playerView: PlayerView?, callback: (isFullscreen: Boolean) -> Unit) { val context = LocalContext.current var isLandscape by remember { mutableStateOf(false) } + var lastOrientation by remember { mutableStateOf(false) } + val threshold = 5.0f DisposableEffect(Unit) { val sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager @@ -26,35 +29,30 @@ fun DetectRotationAndFullscreen(playerView: PlayerView?, callback: (isFullscreen val x = event.values[0] val y = event.values[1] - // Determine if the device is in landscape or portrait orientation - if (x.absoluteValue > y.absoluteValue) { - // Landscape mode - if (!isLandscape) { - isLandscape = true - Log.d("Orientation", "Landscape mode detected") -// playerView?.enterFullscreen() - callback.invoke(true) - } - } else { - // Portrait mode - if (isLandscape) { - isLandscape = false - Log.d("Orientation", "Portrait mode detected") -// playerView?.exitFullscreen() - callback.invoke(false) + val currentOrientation = x.absoluteValue > y.absoluteValue + if (abs(x) > threshold || abs(y) > threshold) { + if (currentOrientation != lastOrientation) { + if (currentOrientation && !isLandscape) { + isLandscape = true + Log.d("Orientation", "Landscape mode detected") + callback.invoke(true) + } else if (!currentOrientation && isLandscape) { + isLandscape = false + Log.d("Orientation", "Portrait mode detected") + callback.invoke(false) + } } } + lastOrientation = currentOrientation } } override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {} } - // Register the sensor listener sensorManager.registerListener(sensorEventListener, accelerometer, SensorManager.SENSOR_DELAY_NORMAL) onDispose { - // Unregister the sensor listener to avoid memory leaks sensorManager.unregisterListener(sensorEventListener) } }