diff --git a/compose-player/build.gradle b/compose-player/build.gradle index 8923de8..ad11e00 100644 --- a/compose-player/build.gradle +++ b/compose-player/build.gradle @@ -24,6 +24,7 @@ android { } dependencies { + implementation 'androidx.appcompat:appcompat:1.6.1' def composeBom = platform('androidx.compose:compose-bom:2023.10.00') implementation(composeBom) androidTestImplementation(composeBom) diff --git a/compose-player/src/main/java/video/api/compose/player/ApiVideoPlayer.kt b/compose-player/src/main/java/video/api/compose/player/ApiVideoPlayer.kt index 1f08e60..42a257c 100644 --- a/compose-player/src/main/java/video/api/compose/player/ApiVideoPlayer.kt +++ b/compose-player/src/main/java/video/api/compose/player/ApiVideoPlayer.kt @@ -1,28 +1,42 @@ package video.api.compose.player +import android.annotation.SuppressLint +import android.content.pm.ActivityInfo +import android.widget.ImageButton +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.SideEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.composed +import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.viewinterop.AndroidView +import androidx.compose.ui.window.SecureFlagPolicy +import video.api.compose.player.extensions.findActivity import video.api.player.ApiVideoPlayerController +import video.api.player.extensions.showSystemUI import video.api.player.models.VideoOptions import video.api.player.models.VideoType import video.api.player.notifications.ApiVideoPlayerNotificationController import video.api.player.views.ApiVideoExoPlayerView /** - * [ApiVideoPlayer] is a composable that displays an api.video video. + * [ApiVideoPlayer] is a composable that displays an api.video video from the video options. * * @param videoOptions The video options * @param modifier The modifier to be applied to the view * @param viewFit Sets how the video is fitted in its parent view * @param showControls Shows or hides the control buttons - * @param showSubtitles Shows or hides the subtitles and the subtitle button + * @param showFullScreenButton Shows or hides the full screen button + * @param showSubtitleButton Shows or hides the subtitles and the subtitle button * @param autoplay True to play the video immediately, false otherwise * @param notificationController The notification controller. Set to null to disable the notification. */ @@ -32,7 +46,8 @@ fun ApiVideoPlayer( modifier: Modifier = Modifier, viewFit: ApiVideoExoPlayerView.ViewFit = ApiVideoExoPlayerView.ViewFit.Contains, showControls: Boolean = true, - showSubtitles: Boolean = true, + showFullScreenButton: Boolean = true, + showSubtitleButton: Boolean = true, autoplay: Boolean = true, notificationController: ApiVideoPlayerNotificationController? = ApiVideoPlayerNotificationController( LocalContext.current @@ -53,7 +68,8 @@ fun ApiVideoPlayer( modifier = modifier, viewFit = viewFit, showControls = showControls, - showSubtitles = showSubtitles + showFullScreenButton = showFullScreenButton, + showSubtitleButton = showSubtitleButton ) } @@ -65,40 +81,88 @@ fun ApiVideoPlayer( * @param modifier The modifier to be applied to the view * @param viewFit Sets how the video is fitted in its parent view * @param showControls Shows or hides the control buttons - * @param showSubtitles Shows or hides the subtitles and the subtitle button + * @param showFullScreenButton Shows or hides the full screen button + * @param showSubtitleButton Shows or hides the subtitles and the subtitle button */ +@SuppressLint("OpaqueUnitKey") @Composable fun ApiVideoPlayer( controller: ApiVideoPlayerController, modifier: Modifier = Modifier, viewFit: ApiVideoExoPlayerView.ViewFit = ApiVideoExoPlayerView.ViewFit.Contains, showControls: Boolean = true, - showSubtitles: Boolean = true + showFullScreenButton: Boolean = true, + showSubtitleButton: Boolean = true ) { val context = LocalContext.current + var isFullScreenModeEntered by remember { mutableStateOf(false) } + // player view - DisposableEffect( - AndroidView( - modifier = modifier, - factory = { - ApiVideoExoPlayerView(context).apply { - this.fullScreenListener = null - this.viewFit = viewFit - this.showControls = showControls - this.showSubtitles = showSubtitles - - controller.setPlayerView(this) + val playerView = remember { + ApiVideoExoPlayerView(context).apply { + this.viewFit = viewFit + this.showControls = showControls + this.showSubtitles = showSubtitleButton + + if (showFullScreenButton) { + this.fullScreenListener = object : ApiVideoExoPlayerView.FullScreenListener { + override fun onFullScreenModeChanged(isFullScreen: Boolean) { + isFullScreenModeEntered = isFullScreen + } } } - ) - ) { + + controller.setPlayerView(this) + } + } + + DisposableEffect( + if (isFullScreenModeEntered) { + FullScreenPlayer( + originalView = playerView, + playerController = controller, + securePolicy = SecureFlagPolicy.Inherit, + onDismissRequest = { + isFullScreenModeEntered = false + }, + ) + } else { + ApiVideoPlayer( + view = playerView, + modifier = modifier + ) + }, + + ) { onDispose { controller.release() } } } +/** + * [ApiVideoPlayer] is a composable that displays the api.video player view. + * + * @param view The Android player view + * @param modifier The modifier to be applied to the view + */ +@SuppressLint("OpaqueUnitKey") +@Composable +private fun ApiVideoPlayer( + view: ApiVideoExoPlayerView, + modifier: Modifier = Modifier, +) { + AndroidView( + modifier = modifier, + factory = { + view.apply { + setBackgroundColor(android.graphics.Color.BLACK) + } + } + ) +} + @Preview @Composable private fun ApiVideoPlayerPreview() { @@ -106,3 +170,66 @@ private fun ApiVideoPlayerPreview() { ApiVideoPlayer(VideoOptions("vi77Dgk0F8eLwaFOtC5870yn", VideoType.VOD)) } } + +@Composable +private fun FullScreenPlayer( + originalView: ApiVideoExoPlayerView, + playerController: ApiVideoPlayerController, + securePolicy: SecureFlagPolicy, + onDismissRequest: () -> Unit, +) { + val context = LocalContext.current + val currentActivity = context.findActivity() + + val originalOrientation = remember { + currentActivity!!.requestedOrientation + } + val fullScreenPlayerView = remember { + originalView.duplicate() + } + + val internalOnDismissRequest = { + // Going back to normal screen + playerController.switchTargetView(fullScreenPlayerView, originalView) + originalView.findViewById(androidx.media3.ui.R.id.exo_fullscreen) + .performClick() + + currentActivity?.requestedOrientation = originalOrientation + currentActivity?.window?.showSystemUI() + + onDismissRequest() + } + + SideEffect { + fullScreenPlayerView.fullScreenListener = + object : ApiVideoExoPlayerView.FullScreenListener { + override fun onFullScreenModeChanged(isFullScreen: Boolean) { + if (!isFullScreen) { + internalOnDismissRequest() + } + } + } + } + SideEffect { + // Going to full screen + currentActivity!!.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE + playerController.switchTargetView(originalView, fullScreenPlayerView) + fullScreenPlayerView.findViewById(androidx.media3.ui.R.id.exo_fullscreen) + .performClick() + } + + FullScreenDialog({ + internalOnDismissRequest() + }, securePolicy) { + Box( + modifier = Modifier + .fillMaxSize() + .background(Color.Black), + ) { + ApiVideoPlayer( + view = fullScreenPlayerView, + modifier = Modifier.fillMaxSize() + ) + } + } +} diff --git a/compose-player/src/main/java/video/api/compose/player/FullScreenDialog.kt b/compose-player/src/main/java/video/api/compose/player/FullScreenDialog.kt new file mode 100644 index 0000000..0f4b614 --- /dev/null +++ b/compose-player/src/main/java/video/api/compose/player/FullScreenDialog.kt @@ -0,0 +1,58 @@ +package video.api.compose.player + +import android.annotation.SuppressLint +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.runtime.SideEffect +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.window.Dialog +import androidx.compose.ui.window.DialogProperties +import androidx.compose.ui.window.SecureFlagPolicy +import video.api.compose.player.extensions.findActivity +import video.api.player.extensions.hideSystemUI +import video.api.player.models.VideoOptions +import video.api.player.models.VideoType + +@SuppressLint("UnsafeOptInUsageError") +@Composable +fun FullScreenDialog( + onDismissRequest: () -> Unit = {}, + securePolicy: SecureFlagPolicy = SecureFlagPolicy.Inherit, + content: @Composable () -> Unit, +) { + val context = LocalContext.current + + Dialog( + onDismissRequest = onDismissRequest, + properties = DialogProperties( + dismissOnClickOutside = false, + usePlatformDefaultWidth = false, + securePolicy = securePolicy, + decorFitsSystemWindows = false, + ), + ) { + SideEffect { + val currentActivity = context.findActivity() + currentActivity!!.window.hideSystemUI() + } + + content() + } +} + +@Preview +@Composable +private fun FullScreenDialogPreview() { + MaterialTheme { + FullScreenDialog { + ApiVideoPlayer( + VideoOptions("vi77Dgk0F8eLwaFOtC5870yn", VideoType.VOD), + modifier = Modifier + .fillMaxSize(), + ) + } + } +} \ No newline at end of file diff --git a/compose-player/src/main/java/video/api/compose/player/extensions/ContextExtensions.kt b/compose-player/src/main/java/video/api/compose/player/extensions/ContextExtensions.kt new file mode 100644 index 0000000..258f257 --- /dev/null +++ b/compose-player/src/main/java/video/api/compose/player/extensions/ContextExtensions.kt @@ -0,0 +1,11 @@ +package video.api.compose.player.extensions + +import android.app.Activity +import android.content.Context +import android.content.ContextWrapper + +fun Context.findActivity(): Activity? = when (this) { + is Activity -> this + is ContextWrapper -> baseContext.findActivity() + else -> null +} \ No newline at end of file diff --git a/examples/compose/src/main/java/video/api/compose/player/example/MainActivity.kt b/examples/compose/src/main/java/video/api/compose/player/example/MainActivity.kt index 20cd681..53fea53 100644 --- a/examples/compose/src/main/java/video/api/compose/player/example/MainActivity.kt +++ b/examples/compose/src/main/java/video/api/compose/player/example/MainActivity.kt @@ -3,6 +3,11 @@ package video.api.compose.player.example import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color import video.api.compose.player.ApiVideoPlayer import video.api.player.models.VideoOptions import video.api.player.models.VideoType @@ -12,11 +17,19 @@ class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { - ApiVideoPlayer( - videoOptions = VideoOptions("vi77Dgk0F8eLwaFOtC5870yn", VideoType.VOD), - viewFit = ApiVideoExoPlayerView.ViewFit.FitHeight, - autoplay = true - ) + /** + * Use a [Surface] to set the background color of the player in fullscreen mode. + */ + Surface( + modifier = Modifier.fillMaxSize(), + color = Color.Black, + ) { + ApiVideoPlayer( + videoOptions = VideoOptions("vi77Dgk0F8eLwaFOtC5870yn", VideoType.VOD), + viewFit = ApiVideoExoPlayerView.ViewFit.FitHeight, + autoplay = true + ) + } } } } diff --git a/player/src/main/java/video/api/player/extensions/WindowExtensions.kt b/player/src/main/java/video/api/player/extensions/WindowExtensions.kt index 2484678..79fc995 100644 --- a/player/src/main/java/video/api/player/extensions/WindowExtensions.kt +++ b/player/src/main/java/video/api/player/extensions/WindowExtensions.kt @@ -17,4 +17,18 @@ fun Window.hideSystemUI() { controller.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE } +} + +/** + * Hides the system UI: status bar, navigation bar and system bars. + */ +fun Window.showSystemUI() { + WindowCompat.setDecorFitsSystemWindows(this, true) + WindowInsetsControllerCompat(this, this.decorView).let { controller -> + controller.show(WindowInsetsCompat.Type.systemBars()) + controller.show(WindowInsetsCompat.Type.navigationBars()) + controller.show(WindowInsetsCompat.Type.statusBars()) + controller.systemBarsBehavior = + WindowInsetsControllerCompat.BEHAVIOR_DEFAULT + } } \ No newline at end of file