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)
+ }
+}