Skip to content

Commit

Permalink
Merge pull request #9 from alsterverse/feature/robustify-player
Browse files Browse the repository at this point in the history
Robustify player
  • Loading branch information
Mats-Hjalmar authored Oct 19, 2024
2 parents 8dfb0f9 + fd1cddc commit f57e253
Show file tree
Hide file tree
Showing 13 changed files with 215 additions and 5 deletions.
7 changes: 4 additions & 3 deletions build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
plugins {
//trick: for the same plugin versions in all sub-modules
alias(libs.plugins.androidLibrary).apply(false)
alias(libs.plugins.kotlinMultiplatform).apply(false)
id("com.vanniktech.maven.publish") version "0.25.3" apply false
alias(libs.plugins.androidLibrary) apply false
alias(libs.plugins.kotlinMultiplatform) apply false
alias(libs.plugins.compose.compiler) apply false
alias(libs.plugins.vanniktechPublish) apply false
}
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
[versions]
agp = "8.5.2"
kotlin = "2.0.20"
compose-multiplatform = "1.7.0"
compose = "1.5.4"
compose-material3 = "1.1.2"
androidx-activityCompose = "1.8.0"
media3Exoplayer = "1.4.1"
kotlinxCoroutinesCore = "1.8.1"
vanniktechPublish = "0.25.3"

[libraries]
kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" }
Expand All @@ -31,3 +33,5 @@ kotlinAndroid = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" }
kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
kotlinCocoapods = { id = "org.jetbrains.kotlin.native.cocoapods", version.ref = "kotlin" }
compose-compiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "compose-multiplatform" }
vanniktechPublish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktechPublish" }
5 changes: 5 additions & 0 deletions player/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ val githubProperties = Properties().apply {
plugins {
alias(libs.plugins.kotlinMultiplatform)
alias(libs.plugins.androidLibrary)
alias(libs.plugins.compose.compiler)
alias(libs.plugins.composeMultiplatform)

id("com.vanniktech.maven.publish")
}

Expand Down Expand Up @@ -43,9 +46,11 @@ kotlin {
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.exoplayer.hls)
implementation(libs.androidx.media3.exoplayer.dash)
implementation(libs.androidx.media3.ui)
}
commonMain.dependencies {
implementation(libs.kotlinx.coroutines.core)
implementation(compose.foundation)
}
iosMain.dependencies {
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package se.alster.player

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.platform.LocalContext

@Composable
actual fun rememberPlayerController(): PlayerController {
val context = LocalContext.current
val playerController = remember { PlayerControllerAndroid(context) }
return playerController
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package se.alster.player.ui

import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.util.UnstableApi
import androidx.media3.ui.AspectRatioFrameLayout
import androidx.media3.ui.PlayerView
import se.alster.player.PlayerProvider
import se.alster.player.R

@OptIn(UnstableApi::class)
@Composable
internal actual fun InternalVideoView(
modifier: Modifier,
showControls: Boolean,
aspectRatio: AspectRatio,
playerProvider: PlayerProvider
) {
val player = playerProvider.player
val currentView = LocalView.current as ViewGroup

AndroidView(
modifier = modifier.clipToBounds(),
factory = { ctx ->
(LayoutInflater.from(ctx).inflate(
R.layout.video_texture_view,
currentView,
false
) as PlayerView)
.also {
it.resizeMode = aspectRatio.toResizeMode()
it.useController = showControls
it.player = player
}
},
)
}

@OptIn(UnstableApi::class)
private fun AspectRatio.toResizeMode(): Int = when (this) {
AspectRatio.ScaleToFit -> AspectRatioFrameLayout.RESIZE_MODE_FIT
AspectRatio.ScaleToFill -> AspectRatioFrameLayout.RESIZE_MODE_ZOOM
AspectRatio.FillStretch -> AspectRatioFrameLayout.RESIZE_MODE_FILL
}
9 changes: 9 additions & 0 deletions player/src/androidMain/res/layout/video_texture_view.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.media3.ui.PlayerView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:keep_content_on_player_reset="true"
app:resize_mode="zoom"
app:surface_type="texture_view"
app:use_controller="false" />
Original file line number Diff line number Diff line change
@@ -1,8 +1,15 @@
package se.alster.player

import androidx.compose.runtime.Composable
import kotlinx.coroutines.flow.Flow
import kotlin.time.Duration
import se.alster.player.ui.VideoView

/**
* A controller for a video player. The controller is responsible for loading and playing videos.
* The controller also provides information about the current state of the video player. The controller
* is not responsible for rendering the video player. The rendering is done by the [VideoView].
*/
interface PlayerController {
val playerProvider: PlayerProvider
val currentProgress: Flow<Duration>
Expand All @@ -18,4 +25,7 @@ interface PlayerController {
fun pause()
fun scrub(delta: Duration)
fun seekTo(position: Duration)
}
}

@Composable
expect fun rememberPlayerController(): PlayerController
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
package se.alster.player

/**
* Provides a player instance.
*/
expect interface PlayerProvider
25 changes: 25 additions & 0 deletions player/src/commonMain/kotlin/se/alster/player/ui/AspectRatio.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package se.alster.player.ui

/**
* The aspect ratio of the video.
*
* [ScaleToFit] - The video is scaled to fit inside the view. The aspect ratio is preserved.
* [ScaleToFill] - The video is scaled to fill the view. The aspect ratio is preserved.
* [FillStretch] - The video is stretched to fill the view. The aspect ratio is not preserved.
*/
enum class AspectRatio {
/**
* The video is scaled to fit inside the view. The aspect ratio is preserved.
*/
ScaleToFit,

/**
* The video is scaled to fill the view. The aspect ratio is preserved.
*/
ScaleToFill,

/**
* The video is stretched to fill the view. The aspect ratio is not preserved.
*/
FillStretch,
}
41 changes: 41 additions & 0 deletions player/src/commonMain/kotlin/se/alster/player/ui/VideoView.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package se.alster.player.ui

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import se.alster.player.PlayerProvider

/**
* A composable that displays a video player. The player is provided by the [playerProvider].
* Note: The [VideoView] does not handle the lifecycle of the player. The player should be
* released when it is no longer needed.
*
* Example usage:
* ```kotlin
* val playerProvider = rememberPlayerProvider()
* LaunchedEffect(playerProvider) {
* playerProvider.loadVideo("https://example.com/video.mp4")
* playerProvider.play()
* }
* VideoView(
* modifier = Modifier.fillMaxSize(),
* playerProvider = playerProvider
* showControls = true
* )
* ```
*/
@Composable
fun VideoView(
modifier: Modifier = Modifier.fillMaxSize(),
showControls: Boolean = true,
aspectRatio: AspectRatio = AspectRatio.ScaleToFill,
playerProvider: PlayerProvider,
) = InternalVideoView(modifier, showControls, aspectRatio, playerProvider)

@Composable
internal expect fun InternalVideoView(
modifier: Modifier,
showControls: Boolean,
aspectRatio: AspectRatio,
playerProvider: PlayerProvider
)
10 changes: 10 additions & 0 deletions player/src/iosMain/kotlin/se/alster/player/PlayerController.ios.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package se.alster.player

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember

@Composable
actual fun rememberPlayerController(): PlayerController {
val playerController = remember { PlayerControllerIOS() }
return playerController
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ import platform.AVFoundation.AVPlayer

actual interface PlayerProvider {
val player: AVPlayer
}
}
40 changes: 40 additions & 0 deletions player/src/iosMain/kotlin/se/alster/player/ui/VideoView.ios.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package se.alster.player.ui

import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.viewinterop.UIKitView
import platform.AVFoundation.AVLayerVideoGravity
import platform.AVFoundation.AVLayerVideoGravityResize
import platform.AVFoundation.AVLayerVideoGravityResizeAspect
import platform.AVFoundation.AVLayerVideoGravityResizeAspectFill
import platform.AVKit.AVPlayerViewController
import se.alster.player.PlayerProvider

@Composable
internal actual fun InternalVideoView(
modifier: Modifier,
showControls: Boolean,
aspectRatio: AspectRatio,
playerProvider: PlayerProvider
) {
val player = playerProvider.player

val avPlayerViewController = remember { AVPlayerViewController() }
UIKitView(
factory = {
avPlayerViewController.player = player
avPlayerViewController.videoGravity = aspectRatio.toVideoGravity()
avPlayerViewController.showsPlaybackControls = showControls

avPlayerViewController.view
},
modifier = modifier,
)
}

private fun AspectRatio.toVideoGravity(): AVLayerVideoGravity = when (this) {
AspectRatio.ScaleToFit -> AVLayerVideoGravityResizeAspect
AspectRatio.ScaleToFill -> AVLayerVideoGravityResizeAspectFill
AspectRatio.FillStretch -> AVLayerVideoGravityResize
}

0 comments on commit f57e253

Please sign in to comment.