diff --git a/.idea/kotlinc.xml b/.idea/kotlinc.xml index 8d81632..fdf8d99 100644 --- a/.idea/kotlinc.xml +++ b/.idea/kotlinc.xml @@ -1,6 +1,6 @@ - \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 1e9b3ee..a18d992 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -41,7 +41,7 @@ android { viewBinding = true } composeOptions { - kotlinCompilerExtensionVersion = "1.5.10" + kotlinCompilerExtensionVersion = "1.5.1" } packaging { resources { diff --git a/build.gradle.kts b/build.gradle.kts index 6940b14..843e941 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,11 +1,12 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. plugins { id("com.android.application") version "8.2.2" apply false - id("org.jetbrains.kotlin.android") version "1.9.22" apply false - id("com.android.library") version "8.2.2" apply false + id("org.jetbrains.kotlin.android") version "1.9.0" apply false + // id("com.android.library") version "8.2.2" apply false - id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10" apply true - kotlin("jvm") version "1.9.22" + id("org.jetbrains.kotlin.plugin.serialization") version "1.8.10" apply false + kotlin("jvm") version "1.9.0" + // id("org.jetbrains.kotlin.jvm") version "1.9.23" apply false } dependencies { diff --git a/playback-sdk-android/build.gradle.kts b/playback-sdk-android/build.gradle.kts index b9e57f4..d57fbaa 100644 --- a/playback-sdk-android/build.gradle.kts +++ b/playback-sdk-android/build.gradle.kts @@ -13,7 +13,7 @@ android { } composeOptions { - kotlinCompilerExtensionVersion = "1.5.10" + kotlinCompilerExtensionVersion = "1.5.2" } kotlinOptions { diff --git a/playback-sdk-android/src/main/java/com/streamamg/PlayBackAPIError.kt b/playback-sdk-android/src/main/java/com/streamamg/PlayBackAPIError.kt index 7a0071d..8bb9201 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/PlayBackAPIError.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/PlayBackAPIError.kt @@ -4,6 +4,9 @@ sealed class SDKError : Throwable() { object InitializationError : SDKError() object MissingLicense : SDKError() object LoadHLSStreamError : SDKError() + + object FetchBitmovinLicenseError : SDKError() + } sealed class PlayBackAPIError : Throwable() { 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 7175cfd..f059106 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/PlayBackSDKManager.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/PlayBackSDKManager.kt @@ -7,26 +7,53 @@ import androidx.compose.runtime.Composable import com.streamamg.api.player.PlayBackAPI import com.streamamg.api.player.PlayBackAPIService import com.streamamg.player.ui.PlaybackUIView -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.flow.first -import kotlinx.coroutines.launch +import kotlinx.coroutines.* +import kotlinx.coroutines.flow.firstOrNull import java.net.URL +/** + * Singleton object to manage playback SDK functionalities. + */ object PlayBackSDKManager { - private var playBackAPI: PlayBackAPI? = null - private var playerInformationAPI: PlayerInformationAPI? = null - private var bitmovinLicense: String? = null - private var amgAPIKey: String? = null + + //region Properties + + //region Private Properties + + private lateinit var playBackAPI: PlayBackAPI + private lateinit var playerInformationAPI: PlayerInformationAPI + + private lateinit var amgAPIKey: String + private val coroutineScope = CoroutineScope(SupervisorJob() + Dispatchers.Main) + private lateinit var playBackAPIService: PlayBackAPIService + + //endregion + + //region Internal Properties + + /** + * Base URL for the playback API. + */ internal var baseURL = "https://api.playback.streamamg.com/v1" - private val coroutineScope = CoroutineScope(Dispatchers.Main) - private var playBackAPIService: PlayBackAPIService? = null + internal lateinit var bitmovinLicense: String + //endregion + //endregion + + //region Public methods + + //region Initialization + + /** + * Initializes the playback SDK. + * @param apiKey The API key for authentication. + * @param baseURL The base URL for the playback API. Default is null. + * @param completion Callback to be invoked upon completion of initialization. + */ fun initialize( apiKey: String, baseURL: String? = null, - completion: (String?, Throwable?) -> Unit + completion: (String?, SDKError?) -> Unit ) { if (apiKey.isEmpty()) { completion(null, SDKError.InitializationError) @@ -39,94 +66,113 @@ object PlayBackSDKManager { playBackAPIService = PlayBackAPIService(apiKey) this.playBackAPI = playBackAPIService - // Fetching Bitmovin license + // Fetching player information fetchPlayerInfo(completion) } - private fun fetchPlayerInfo(completion: (String?, Throwable?) -> Unit) { - val playerInformationAPIExist = playerInformationAPI ?: run { - completion(null, SDKError.InitializationError) - return + //endregion + + //region Load Player + + /** + * Loads the player UI. + * @param entryID The ID of the entry. + * @param authorizationToken The authorization token. + * @param onError Callback for handling errors. Default is null. + * @return Composable function to render the player UI. + */ + @Composable + fun loadPlayer( + entryID: String, + authorizationToken: String, + onError: ((PlayBackAPIError) -> Unit)? + ): @Composable () -> Unit { + val playbackUIView = PlaybackUIView(entryID, authorizationToken, onError) + + return { + playbackUIView.Render() } + } + + //endregion + + //endregion + + //region Internal methods + + //region Player Information + + /** + * Fetches player information including Bitmovin license. + * @param completion Callback to be invoked upon completion of fetching player information. + */ + private fun fetchPlayerInfo(completion: (String?, SDKError?) -> Unit) { coroutineScope.launch { try { - val playerInfo = playerInformationAPIExist.getPlayerInformation().first() + val playerInfo = playerInformationAPI.getPlayerInformation().firstOrNull() - // Check if playerInfo is null before accessing its properties if (playerInfo?.player?.bitmovin?.license.isNullOrEmpty()) { completion(null, SDKError.MissingLicense) return@launch } - // Extract the Bitmovin license val bitmovinLicense = playerInfo?.player?.bitmovin?.license - // Set the received Bitmovin license - this@PlayBackSDKManager.bitmovinLicense = bitmovinLicense + this@PlayBackSDKManager.bitmovinLicense = bitmovinLicense ?: run { + completion(null, SDKError.MissingLicense) + return@launch + } - // Log success message Log.d( "PlayBackSDKManager", "Bitmovin license fetched successfully: $bitmovinLicense" ) - // Call the completion handler with success completion(bitmovinLicense, null) } catch (e: Throwable) { Log.e("PlayBackSDKManager", "Error fetching Bitmovin license: $e") - completion(null, e) + completion(null, SDKError.FetchBitmovinLicenseError) } } } + //endregion + + //region HLS Stream + + /** + * Loads the HLS stream. + * @param entryId The ID of the entry. + * @param authorizationToken The authorization token. + * @param completion Callback to be invoked upon completion of loading the HLS stream. + */ fun loadHLSStream( entryId: String, authorizationToken: String?, completion: (URL?, SDKError?) -> Unit ) { - val playBackAPIExist = playBackAPI ?: run { - completion(null, SDKError.InitializationError) - return - } - GlobalScope.launch(Dispatchers.IO) { + coroutineScope.launch(Dispatchers.IO) { try { val videoDetails = - playBackAPIExist.getVideoDetails(entryId, authorizationToken).first() - - // Log received video details - Log.d("PlayBackSDKManager", "Received video details: $videoDetails") + playBackAPI.getVideoDetails(entryId, authorizationToken).firstOrNull() - // Extract the HLS stream URL from video details - val hlsURLString = videoDetails.media?.hls + val hlsURLString = videoDetails?.media?.hls val hlsURL = hlsURLString?.let { URL(it) } if (hlsURL != null) { - // Call the completion handler with the HLS stream URL completion(hlsURL, null) } else { completion(null, SDKError.LoadHLSStreamError) } } catch (e: Throwable) { Log.e("PlayBackSDKManager", "Error loading HLS stream: $e") - completion(null, SDKError.InitializationError) //TODO: add correct error + completion(null, SDKError.LoadHLSStreamError) } } } + //endregion - @Composable - fun loadPlayer( - entryID: String, - authorizationToken: String, - onError: ((PlayBackAPIError) -> Unit)? - ): @Composable () -> Unit { + //endregion - val playbackUIView = PlaybackUIView(entryID, authorizationToken, onError) - - // Return a Composable function that renders the playback UI - return { - playbackUIView.Render() - } - - } -} \ No newline at end of file +} diff --git a/playback-sdk-android/src/main/java/com/streamamg/api/player/PlayBackAPIService.kt b/playback-sdk-android/src/main/java/com/streamamg/api/player/PlayBackAPIService.kt index 78f153d..03a3d6e 100644 --- a/playback-sdk-android/src/main/java/com/streamamg/api/player/PlayBackAPIService.kt +++ b/playback-sdk-android/src/main/java/com/streamamg/api/player/PlayBackAPIService.kt @@ -57,11 +57,11 @@ internal class PlayBackAPIService(private val apiKey: String) : PlayBackAPI { emit(responseModel) } else { // TODO: handle error -// val errorResponse = connection.errorStream?.let { -// Json.decodeFromString(it.reader().readText()) -// } -// val errorMessage = errorResponse?.message ?: "Unknown authentication error" -// throw PlayBackAPIError.apiError(connection.responseCode, errorMessage) + val errorResponse = connection.errorStream?.let { + Json.decodeFromString(it.reader().readText()) + } + val errorMessage = errorResponse?.message ?: "Unknown authentication error" + throw PlayBackAPIError.apiError(connection.responseCode, errorMessage) } } catch (e: IOException) { throw PlayBackAPIError.NetworkError(e) 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 51c1ffc..9520d1f 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 @@ -1,15 +1,27 @@ package com.streamamg.player.plugin.bitmovin import android.view.View +import android.widget.LinearLayout import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.DisposableEffectScope +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.viewinterop.AndroidView +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner import com.bitmovin.player.PlayerView +import com.bitmovin.player.api.Player +import com.bitmovin.player.api.PlayerConfig import com.bitmovin.player.api.source.Source import com.bitmovin.player.api.source.SourceConfig import com.bitmovin.player.api.source.SourceType +import com.bitmovin.player.api.ui.PlayerViewConfig +import com.bitmovin.player.ui.CustomMessageHandler +import com.streamamg.PlayBackSDKManager import com.streamamg.playback_sdk_android.R import com.streamamg.player.plugin.VideoPlayerPlugin @@ -18,45 +30,88 @@ class BitmovinVideoPlayerPlugin : VideoPlayerPlugin { override val name: String = "Bitmovin" override val version: String = "1.0" - private var hlsUrl: String = "" + private var playerViewLifecycleHandler: PlayerViewLifecycleHandler = PlayerViewLifecycleHandler() + private lateinit var hlsUrl: String + private lateinit var playerView: PlayerView override fun setup() { - // You can perform any setup required for the Bitmovin player here + // TODO: Add here setup actions. } @Composable override fun playerView(hlsUrl: String): Unit { - return Box(modifier = Modifier.fillMaxSize()) { - AndroidView( - factory = { context -> - View.inflate(context, R.layout.player_view, null) - }, - modifier = Modifier.fillMaxSize(), - update = { view -> - val playerView = view.findViewById(R.id.playerView) - initPlayer(playerView, hlsUrl) + this.hlsUrl = hlsUrl + val lifecycle = LocalLifecycleOwner.current.lifecycle + val observers = remember { mutableListOf() } + // Access context within the AndroidView composable + AndroidView( + modifier = Modifier.fillMaxSize(), + factory = { context -> + + + + val playerConfig = PlayerConfig(key = PlayBackSDKManager.bitmovinLicense) + val player = Player(context, playerConfig) + playerView = PlayerView(context, player) // Use context provided here + playerView.player?.load(SourceConfig.fromUrl(hlsUrl)) + + + val observer = object : DefaultLifecycleObserver { + override fun onStart(owner: LifecycleOwner) = playerViewLifecycleHandler.onStart(playerView) + override fun onResume(owner: LifecycleOwner) = playerViewLifecycleHandler.onResume(playerView) + override fun onPause(owner: LifecycleOwner) = playerViewLifecycleHandler.onPause(playerView) + override fun onStop(owner: LifecycleOwner) = playerViewLifecycleHandler.onStop(playerView) + override fun onDestroy(owner: LifecycleOwner) { + // Do not destroy the player in `onDestroy` as the player lifecycle is handled outside + // of the composable. This is achieved by setting the player to `null` before destroying. + playerView.player = null + playerViewLifecycleHandler.onDestroy(playerView) + } } - ) - } + + lifecycle.addObserver(observer) + observers.add(observer) + + playerView // Directly return the PlayerView + } + ) } + override fun play() { - // Implement play functionality for Bitmovin player if needed + playerView.player?.play() } override fun pause() { - // Implement pause functionality for Bitmovin player if needed + playerView.player?.pause() } override fun removePlayer() { - // Implement player removal for Bitmovin player if needed + playerView.player = null + ///playerView.player?.release + } +} + +// TODO: This might need to go in the VideoPlayerPlugin protocol. +private class PlayerViewLifecycleHandler { + + fun onStart(playerView: PlayerView) { + playerView.player?.play() // Start playback when the composable starts } - private fun initPlayer(playerView: PlayerView, sourceUrl: String) { - val sourceConfig = SourceConfig(sourceUrl, SourceType.Hls) - val source = Source(sourceConfig) - playerView.player?.load(source) + fun onResume(playerView: PlayerView) { + playerView.player?.play() // Resume playback when the composable resumes } -} + fun onPause(playerView: PlayerView) { + playerView.player?.pause() // Pause playback when the composable is paused + } + fun onStop(playerView: PlayerView) { + playerView.player?.pause() // Pause playback when the composable stops + } + + fun onDestroy(playerView: PlayerView) { + // Do nothing here as the player lifecycle is managed outside the composable + } +} diff --git a/playback-sdk-android/src/test/java/com/streamamg/playback_sdk_android_app/PlayBackSDKManagerTests.kt b/playback-sdk-android/src/test/java/com/streamamg/playback_sdk_android_app/PlayBackSDKManagerTests.kt new file mode 100644 index 0000000..d2b5f4c --- /dev/null +++ b/playback-sdk-android/src/test/java/com/streamamg/playback_sdk_android_app/PlayBackSDKManagerTests.kt @@ -0,0 +1,65 @@ +package com.streamamg.playback_sdk_android_app + +import androidx.compose.runtime.Composable +import com.streamamg.* +import kotlinx.coroutines.runBlocking +import org.junit.Assert.* +import org.junit.Before +import org.junit.Test +import java.net.URL + +class PlayBackSDKManagerTests { + + private lateinit var manager: PlayBackSDKManager + private val apiKey = "f3Beljhmlz2ea7M9TfErE6mKPsAcY3BrasMMEG24" + private val entryID = "0_k3mz0mf8" + + @Before + fun setUp() { + manager = PlayBackSDKManager + } + + @Test + fun testInitialization() { + assertNotNull(manager) + } + + @Test + fun testInitializeWithValidAPIKey() { + val completion: (String?, SDKError?) -> Unit = { _, _ -> } + runBlocking { + manager.initialize(apiKey) { license, error -> + assertNotNull(license) + assertNull(error) + } + } + } + + @Test + fun testLoadHLSStream() { + val completion: (URL?, SDKError?) -> Unit = { _, _ -> } + runBlocking { + manager.loadHLSStream(entryID, null) { hlsURL, error -> + assertNotNull(hlsURL) + assertNull(error) + } + } + } + + @Test + fun testInitializeWithEmptyAPIKey() { + val completion: (String?, SDKError?) -> Unit = { _, _ -> } + runBlocking { + manager.initialize("") { _, error -> + assertNull(error) + } + } + } + + @Composable + @Test + fun testLoadPlayer() { + val player = manager.loadPlayer(entryID, "authToken", null) + assertNotNull(player) + } +}