Skip to content

Commit 83db468

Browse files
committed
Add websupport via new WebElementApi
1 parent a396981 commit 83db468

21 files changed

Lines changed: 1093 additions & 1105 deletions

File tree

gradle/libs.versions.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,11 @@ kermit = "2.0.8"
88
kotlin = "2.3.0"
99
agp = "8.12.3"
1010
kotlinx-coroutines = "1.10.2"
11-
kotlinxBrowserWasmJs = "0.3"
11+
kotlinxBrowserWasmJs = "0.5.0"
1212
kotlinxDatetime = "0.7.1-0.6.x-compat"
1313
compose = "1.9.3"
1414
androidx-activityCompose = "1.12.2"
15+
androidx-core = "1.17.0"
1516
media3Exoplayer = "1.9.0"
1617
jna = "5.18.1"
1718
platformtoolsDarkmodedetector = "0.7.4"
@@ -28,12 +29,14 @@ filekit-dialogs-compose = { module = "io.github.vinceglb:filekit-dialogs-compose
2829
gst1-java-core = { module = "org.freedesktop.gstreamer:gst1-java-core", version.ref = "gst1JavaCore" }
2930
kermit = { module = "co.touchlab:kermit", version.ref = "kermit" }
3031
kotlinx-browser-wasm-js = { module = "org.jetbrains.kotlinx:kotlinx-browser-wasm-js", version.ref = "kotlinxBrowserWasmJs" }
32+
kotlinx-browser = { module = "org.jetbrains.kotlinx:kotlinx-browser", version.ref = "kotlinxBrowserWasmJs" }
3133
kotlinx-coroutines-core = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-core", version.ref = "kotlinx-coroutines" }
3234
kotlinx-coroutines-android = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-android", version.ref = "kotlinx-coroutines" }
3335
kotlinx-coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" }
3436
kotlinx-coroutines-test = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-test", version.ref = "kotlinx-coroutines" }
3537
kotlinx-datetime = { module = "org.jetbrains.kotlinx:kotlinx-datetime", version.ref = "kotlinxDatetime" }
3638
androidx-activityCompose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activityCompose" }
39+
androidx-core = { module = "androidx.core:core-ktx", version.ref = "androidx-core" }
3740
jna = { module = "net.java.dev.jna:jna", version.ref = "jna" }
3841
jna-jpms = { module = "net.java.dev.jna:jna-jpms", version.ref = "jna" }
3942
jna-platform = { module = "net.java.dev.jna:jna-platform", version.ref = "jna" }

mediaplayer/ComposeMediaPlayer.podspec

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,38 +8,29 @@ Pod::Spec.new do |spec|
88
spec.summary = 'A multiplatform video player library for Compose applications'
99
spec.vendored_frameworks = 'build/cocoapods/framework/ComposeMediaPlayer.framework'
1010
spec.libraries = 'c++'
11-
12-
13-
1411
if !Dir.exist?('build/cocoapods/framework/ComposeMediaPlayer.framework') || Dir.empty?('build/cocoapods/framework/ComposeMediaPlayer.framework')
1512
raise "
16-
1713
Kotlin framework 'ComposeMediaPlayer' doesn't exist yet, so a proper Xcode project can't be generated.
1814
'pod install' should be executed after running ':generateDummyFramework' Gradle task:
19-
2015
./gradlew :mediaplayer:generateDummyFramework
21-
2216
Alternatively, proper pod installation is performed during Gradle sync in the IDE (if Podfile location is set)"
2317
end
24-
2518
spec.xcconfig = {
2619
'ENABLE_USER_SCRIPT_SANDBOXING' => 'NO',
2720
}
28-
2921
spec.pod_target_xcconfig = {
3022
'KOTLIN_PROJECT_PATH' => ':mediaplayer',
3123
'PRODUCT_MODULE_NAME' => 'ComposeMediaPlayer',
3224
}
33-
3425
spec.script_phases = [
3526
{
3627
:name => 'Build ComposeMediaPlayer',
3728
:execution_position => :before_compile,
3829
:shell_path => '/bin/sh',
3930
:script => <<-SCRIPT
4031
if [ "YES" = "$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED" ]; then
41-
echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
42-
exit 0
32+
echo "Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \"YES\""
33+
exit 0
4334
fi
4435
set -ev
4536
REPO_ROOT="$PODS_TARGET_SRCROOT"
@@ -51,4 +42,4 @@ Pod::Spec.new do |spec|
5142
}
5243
]
5344
spec.resources = ['build/compose/cocoapods/compose-resources']
54-
end
45+
end

mediaplayer/build.gradle.kts

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,10 +34,17 @@ kotlin {
3434
jvmToolchain(17)
3535
androidTarget { publishLibraryVariants("release") }
3636
jvm()
37+
js {
38+
browser()
39+
binaries.executable()
40+
}
41+
42+
@OptIn(ExperimentalWasmDsl::class)
3743
wasmJs {
3844
browser()
3945
binaries.executable()
4046
}
47+
4148
iosX64()
4249
iosArm64()
4350
iosSimulatorArm64()
@@ -81,6 +88,7 @@ kotlin {
8188
implementation(libs.androidx.media3.exoplayer)
8289
implementation(libs.androidx.media3.ui)
8390
implementation(libs.androidx.activityCompose)
91+
implementation(libs.androidx.core)
8492
}
8593

8694
androidUnitTest.dependencies {
@@ -111,8 +119,10 @@ kotlin {
111119
implementation(libs.kotlinx.coroutines.test)
112120
}
113121

114-
wasmJsMain.dependencies {
115-
implementation(libs.kotlinx.browser.wasm.js)
122+
webMain.dependencies {
123+
implementation(libs.kotlinx.browser)
124+
implementation(compose.ui)
125+
116126
}
117127

118128
wasmJsTest.dependencies {
@@ -138,28 +148,32 @@ android {
138148
compileSdk = 36
139149

140150
defaultConfig {
141-
minSdk = 21
151+
minSdk = 23
142152
}
143153
}
144154

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

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

165179
val buildWin: TaskProvider<Exec> = tasks.register<Exec>("buildNativeWin") {
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
@file:OptIn(ExperimentalComposeUiApi::class)
2+
3+
package io.github.kdroidfilter.composemediaplayer
4+
5+
import androidx.compose.foundation.layout.fillMaxSize
6+
import androidx.compose.runtime.*
7+
import androidx.compose.ui.ExperimentalComposeUiApi
8+
import androidx.compose.ui.Modifier
9+
import androidx.compose.ui.layout.ContentScale
10+
import androidx.compose.ui.viewinterop.WebElementView
11+
import org.w3c.dom.HTMLVideoElement
12+
13+
@Composable
14+
actual fun VideoPlayerSurface(
15+
playerState: VideoPlayerState,
16+
modifier: Modifier,
17+
contentScale: ContentScale,
18+
overlay: @Composable () -> Unit
19+
) {
20+
if (playerState.hasMedia) {
21+
var videoElement by remember { mutableStateOf<HTMLVideoElement?>(null) }
22+
var videoRatio by remember { mutableStateOf<Float?>(null) }
23+
var useCors by remember { mutableStateOf(true) }
24+
val scope = rememberCoroutineScope()
25+
26+
// State for CORS mode changes
27+
var lastPosition by remember { mutableStateOf(0.0) }
28+
var wasPlaying by remember { mutableStateOf(false) }
29+
var lastPlaybackSpeed by remember { mutableStateOf(1.0f) }
30+
31+
// Shared effects
32+
VideoPlayerEffects(
33+
playerState = playerState,
34+
videoElement = videoElement,
35+
scope = scope,
36+
useCors = useCors,
37+
onLastPositionChange = { lastPosition = it },
38+
onWasPlayingChange = { wasPlaying = it },
39+
onLastPlaybackSpeedChange = { lastPlaybackSpeed = it },
40+
lastPosition = lastPosition,
41+
wasPlaying = wasPlaying,
42+
lastPlaybackSpeed = lastPlaybackSpeed
43+
)
44+
45+
VideoVolumeAndSpeedEffects(
46+
playerState = playerState,
47+
videoElement = videoElement
48+
)
49+
50+
// Video content layout with WebElementView
51+
VideoContentLayout(
52+
playerState = playerState,
53+
modifier = modifier,
54+
videoRatio = videoRatio,
55+
contentScale = contentScale,
56+
overlay = overlay
57+
) {
58+
key(useCors) {
59+
WebElementView(
60+
factory = {
61+
createVideoElement(useCors).apply {
62+
setupMetadataListener(playerState) { ratio ->
63+
videoRatio = ratio
64+
}
65+
setupVideoElement(
66+
video = this,
67+
playerState = playerState,
68+
scope = scope,
69+
enableAudioDetection = true,
70+
useCors = useCors,
71+
onCorsError = { useCors = false }
72+
)
73+
}
74+
},
75+
modifier = if (playerState.isFullscreen) Modifier.fillMaxSize() else modifier,
76+
update = { video ->
77+
videoElement = video
78+
video.applyInteropBehindCanvas()
79+
video.applyContentScale(contentScale, videoRatio)
80+
},
81+
onRelease = { video ->
82+
video.safePause()
83+
videoElement = null
84+
}
85+
)
86+
}
87+
}
88+
}
89+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package io.github.kdroidfilter.composemediaplayer.util
2+
3+
import io.github.vinceglb.filekit.PlatformFile
4+
import org.w3c.dom.url.URL
5+
6+
actual fun PlatformFile.getUri(): String {
7+
return URL.createObjectURL(this.file)
8+
}
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
@file:OptIn(ExperimentalComposeUiApi::class)
2+
3+
package io.github.kdroidfilter.composemediaplayer
4+
5+
import androidx.compose.foundation.layout.fillMaxSize
6+
import androidx.compose.runtime.*
7+
import androidx.compose.ui.ExperimentalComposeUiApi
8+
import androidx.compose.ui.Modifier
9+
import androidx.compose.ui.layout.ContentScale
10+
import androidx.compose.ui.viewinterop.WebElementView
11+
import org.w3c.dom.HTMLVideoElement
12+
13+
@Composable
14+
actual fun VideoPlayerSurface(
15+
playerState: VideoPlayerState,
16+
modifier: Modifier,
17+
contentScale: ContentScale,
18+
overlay: @Composable () -> Unit
19+
) {
20+
if (playerState.hasMedia) {
21+
var videoElement by remember { mutableStateOf<HTMLVideoElement?>(null) }
22+
var videoRatio by remember { mutableStateOf<Float?>(null) }
23+
var useCors by remember { mutableStateOf(true) }
24+
val scope = rememberCoroutineScope()
25+
26+
// State for CORS mode changes
27+
var lastPosition by remember { mutableStateOf(0.0) }
28+
var wasPlaying by remember { mutableStateOf(false) }
29+
var lastPlaybackSpeed by remember { mutableStateOf(1.0f) }
30+
31+
// Shared effects
32+
VideoPlayerEffects(
33+
playerState = playerState,
34+
videoElement = videoElement,
35+
scope = scope,
36+
useCors = useCors,
37+
onLastPositionChange = { lastPosition = it },
38+
onWasPlayingChange = { wasPlaying = it },
39+
onLastPlaybackSpeedChange = { lastPlaybackSpeed = it },
40+
lastPosition = lastPosition,
41+
wasPlaying = wasPlaying,
42+
lastPlaybackSpeed = lastPlaybackSpeed
43+
)
44+
45+
VideoVolumeAndSpeedEffects(
46+
playerState = playerState,
47+
videoElement = videoElement
48+
)
49+
50+
// Video content layout with WebElementView
51+
VideoContentLayout(
52+
playerState = playerState,
53+
modifier = modifier,
54+
videoRatio = videoRatio,
55+
contentScale = contentScale,
56+
overlay = overlay
57+
) {
58+
key(useCors) {
59+
WebElementView(
60+
factory = {
61+
createVideoElement(useCors).apply {
62+
setupMetadataListener(playerState) { ratio ->
63+
videoRatio = ratio
64+
}
65+
setupVideoElement(
66+
video = this,
67+
playerState = playerState,
68+
scope = scope,
69+
enableAudioDetection = true,
70+
useCors = useCors,
71+
onCorsError = { useCors = false }
72+
)
73+
}
74+
},
75+
modifier = if (playerState.isFullscreen) Modifier.fillMaxSize() else modifier,
76+
update = { video ->
77+
videoElement = video
78+
video.applyInteropBehindCanvas()
79+
video.applyContentScale(contentScale, videoRatio)
80+
},
81+
onRelease = { video ->
82+
video.safePause()
83+
videoElement = null
84+
}
85+
)
86+
}
87+
}
88+
}
89+
}

0 commit comments

Comments
 (0)