Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,11 @@ kermit = "2.0.8"
kotlin = "2.3.0"
agp = "8.12.3"
kotlinx-coroutines = "1.10.2"
kotlinxBrowserWasmJs = "0.3"
kotlinxBrowserWasmJs = "0.5.0"
kotlinxDatetime = "0.7.1-0.6.x-compat"
compose = "1.9.3"
androidx-activityCompose = "1.12.2"
androidx-core = "1.17.0"
media3Exoplayer = "1.9.0"
jna = "5.18.1"
platformtoolsDarkmodedetector = "0.7.4"
Expand All @@ -28,12 +29,14 @@ filekit-dialogs-compose = { module = "io.github.vinceglb:filekit-dialogs-compose
gst1-java-core = { module = "org.freedesktop.gstreamer:gst1-java-core", version.ref = "gst1JavaCore" }
kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
kotlinx-browser-wasm-js = { module = "org.jetbrains.kotlinx:kotlinx-browser-wasm-js", version.ref = "kotlinxBrowserWasmJs" }
kotlinx-browser = { module = "org.jetbrains.kotlinx:kotlinx-browser", version.ref = "kotlinxBrowserWasmJs" }
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
androidx-activityCompose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
jna-jpms = { module = "net.java.dev.jna:jna-jpms", version.ref = "jna" }
jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }
Expand Down
15 changes: 3 additions & 12 deletions mediaplayer/ComposeMediaPlayer.podspec
Original file line number Diff line number Diff line change
Expand Up @@ -8,38 +8,29 @@ Pod::Spec.new do |spec|
spec.summary = 'A multiplatform video player library for Compose applications'
spec.vendored_frameworks = 'build/cocoapods/framework/ComposeMediaPlayer.framework'
spec.libraries = 'c++'



if !Dir.exist?('build/cocoapods/framework/ComposeMediaPlayer.framework') || Dir.empty?('build/cocoapods/framework/ComposeMediaPlayer.framework')
raise "

Kotlin framework 'ComposeMediaPlayer' doesn't exist yet, so a proper Xcode project can't be generated.
'pod install' should be executed after running ':generateDummyFramework' Gradle task:

./gradlew :mediaplayer:generateDummyFramework

Alternatively, proper pod installation is performed during Gradle sync in the IDE (if Podfile location is set)"
end

spec.xcconfig = {
'ENABLE_USER_SCRIPT_SANDBOXING' => 'NO',
}

spec.pod_target_xcconfig = {
'KOTLIN_PROJECT_PATH' => ':mediaplayer',
'PRODUCT_MODULE_NAME' => 'ComposeMediaPlayer',
}

spec.script_phases = [
{
:name => 'Build ComposeMediaPlayer',
:execution_position => :before_compile,
:shell_path => '/bin/sh',
:script => <<-SCRIPT
if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
exit 0
echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
exit 0
fi
set -ev
REPO_ROOT="$PODS_TARGET_SRCROOT"
Expand All @@ -51,4 +42,4 @@ Pod::Spec.new do |spec|
}
]
spec.resources = ['build/compose/cocoapods/compose-resources']
end
end
28 changes: 21 additions & 7 deletions mediaplayer/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,17 @@ kotlin {
jvmToolchain(17)
androidTarget { publishLibraryVariants("release") }
jvm()
js {
browser()
binaries.executable()
}

@OptIn(ExperimentalWasmDsl::class)
wasmJs {
browser()
binaries.executable()
}

iosX64()
iosArm64()
iosSimulatorArm64()
Expand Down Expand Up @@ -81,6 +88,7 @@ kotlin {
implementation(libs.androidx.media3.exoplayer)
implementation(libs.androidx.media3.ui)
implementation(libs.androidx.activityCompose)
implementation(libs.androidx.core)
}

androidUnitTest.dependencies {
Expand Down Expand Up @@ -111,8 +119,10 @@ kotlin {
implementation(libs.kotlinx.coroutines.test)
}

wasmJsMain.dependencies {
implementation(libs.kotlinx.browser.wasm.js)
webMain.dependencies {
implementation(libs.kotlinx.browser)
implementation(compose.ui)

}

wasmJsTest.dependencies {
Expand All @@ -138,28 +148,32 @@ android {
compileSdk = 36

defaultConfig {
minSdk = 21
minSdk = 23
}
}

val buildMacArm: TaskProvider<Exec> = tasks.register<Exec>("buildNativeMacArm") {
onlyIf { System.getProperty("os.name").startsWith("Mac") }
workingDir(rootDir)
commandLine("swiftc", "-emit-library", "-emit-module", "-module-name", "NativeVideoPlayer",
commandLine(
"swiftc", "-emit-library", "-emit-module", "-module-name", "NativeVideoPlayer",
"-target", "arm64-apple-macosx14.0",
"-o", "mediaplayer/src/jvmMain/resources/darwin-aarch64/libNativeVideoPlayer.dylib",
"mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/native/NativeVideoPlayer.swift",
"-O", "-whole-module-optimization")
"-O", "-whole-module-optimization"
)
}

val buildMacX64: TaskProvider<Exec> = tasks.register<Exec>("buildNativeMacX64") {
onlyIf { System.getProperty("os.name").startsWith("Mac") }
workingDir(rootDir)
commandLine("swiftc", "-emit-library", "-emit-module", "-module-name", "NativeVideoPlayer",
commandLine(
"swiftc", "-emit-library", "-emit-module", "-module-name", "NativeVideoPlayer",
"-target", "x86_64-apple-macosx14.0",
"-o", "mediaplayer/src/jvmMain/resources/darwin-x86-64/libNativeVideoPlayer.dylib",
"mediaplayer/src/jvmMain/kotlin/io/github/kdroidfilter/composemediaplayer/mac/native/NativeVideoPlayer.swift",
"-O", "-whole-module-optimization")
"-O", "-whole-module-optimization"
)
}

val buildWin: TaskProvider<Exec> = tasks.register<Exec>("buildNativeWin") {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.Uri
import androidx.annotation.OptIn
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableDoubleStateOf
Expand Down Expand Up @@ -798,5 +799,6 @@ actual open class VideoPlayerState internal constructor(isInPreview: Boolean) {
}
}

@OptIn(UnstableApi::class)
internal actual fun createVideoPlayerState(isInPreview: Boolean): VideoPlayerState =
VideoPlayerState(isInPreview)
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
@file:OptIn(ExperimentalComposeUiApi::class)

package io.github.kdroidfilter.composemediaplayer

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.viewinterop.WebElementView
import org.w3c.dom.HTMLVideoElement

@Composable
actual fun VideoPlayerSurface(
playerState: VideoPlayerState,
modifier: Modifier,
contentScale: ContentScale,
overlay: @Composable () -> Unit
) {
if (playerState.hasMedia) {
var videoElement by remember { mutableStateOf<HTMLVideoElement?>(null) }
var videoRatio by remember { mutableStateOf<Float?>(null) }
var useCors by remember { mutableStateOf(true) }
val scope = rememberCoroutineScope()

// State for CORS mode changes
var lastPosition by remember { mutableStateOf(0.0) }
var wasPlaying by remember { mutableStateOf(false) }
var lastPlaybackSpeed by remember { mutableStateOf(1.0f) }

// Shared effects
VideoPlayerEffects(
playerState = playerState,
videoElement = videoElement,
scope = scope,
useCors = useCors,
onLastPositionChange = { lastPosition = it },
onWasPlayingChange = { wasPlaying = it },
onLastPlaybackSpeedChange = { lastPlaybackSpeed = it },
lastPosition = lastPosition,
wasPlaying = wasPlaying,
lastPlaybackSpeed = lastPlaybackSpeed
)

VideoVolumeAndSpeedEffects(
playerState = playerState,
videoElement = videoElement
)

// Video content layout with WebElementView
VideoContentLayout(
playerState = playerState,
modifier = modifier,
videoRatio = videoRatio,
contentScale = contentScale,
overlay = overlay
) {
key(useCors) {
WebElementView(
factory = {
createVideoElement(useCors).apply {
setupMetadataListener(playerState) { ratio ->
videoRatio = ratio
}
setupVideoElement(
video = this,
playerState = playerState,
scope = scope,
enableAudioDetection = true,
useCors = useCors,
onCorsError = { useCors = false }
)
}
},
modifier = if (playerState.isFullscreen) Modifier.fillMaxSize() else modifier,
update = { video ->
videoElement = video
video.applyInteropBehindCanvas()
video.applyContentScale(contentScale, videoRatio)
},
onRelease = { video ->
video.safePause()
videoElement = null
}
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.github.kdroidfilter.composemediaplayer.util

import io.github.vinceglb.filekit.PlatformFile
import org.w3c.dom.url.URL

actual fun PlatformFile.getUri(): String {
return URL.createObjectURL(this.file)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
@file:OptIn(ExperimentalComposeUiApi::class)

package io.github.kdroidfilter.composemediaplayer

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.*
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.viewinterop.WebElementView
import org.w3c.dom.HTMLVideoElement

@Composable
actual fun VideoPlayerSurface(
playerState: VideoPlayerState,
modifier: Modifier,
contentScale: ContentScale,
overlay: @Composable () -> Unit
) {
if (playerState.hasMedia) {
var videoElement by remember { mutableStateOf<HTMLVideoElement?>(null) }
var videoRatio by remember { mutableStateOf<Float?>(null) }
var useCors by remember { mutableStateOf(true) }
val scope = rememberCoroutineScope()

// State for CORS mode changes
var lastPosition by remember { mutableStateOf(0.0) }
var wasPlaying by remember { mutableStateOf(false) }
var lastPlaybackSpeed by remember { mutableStateOf(1.0f) }

// Shared effects
VideoPlayerEffects(
playerState = playerState,
videoElement = videoElement,
scope = scope,
useCors = useCors,
onLastPositionChange = { lastPosition = it },
onWasPlayingChange = { wasPlaying = it },
onLastPlaybackSpeedChange = { lastPlaybackSpeed = it },
lastPosition = lastPosition,
wasPlaying = wasPlaying,
lastPlaybackSpeed = lastPlaybackSpeed
)

VideoVolumeAndSpeedEffects(
playerState = playerState,
videoElement = videoElement
)

// Video content layout with WebElementView
VideoContentLayout(
playerState = playerState,
modifier = modifier,
videoRatio = videoRatio,
contentScale = contentScale,
overlay = overlay
) {
key(useCors) {
WebElementView(
factory = {
createVideoElement(useCors).apply {
setupMetadataListener(playerState) { ratio ->
videoRatio = ratio
}
setupVideoElement(
video = this,
playerState = playerState,
scope = scope,
enableAudioDetection = true,
useCors = useCors,
onCorsError = { useCors = false }
)
}
},
modifier = if (playerState.isFullscreen) Modifier.fillMaxSize() else modifier,
update = { video ->
videoElement = video
video.applyInteropBehindCanvas()
video.applyContentScale(contentScale, videoRatio)
},
onRelease = { video ->
video.safePause()
videoElement = null
}
)
}
}
}
}
Loading