Skip to content

Commit 6b3bcb6

Browse files
authored
Merge pull request #1209 from DimensionDev/feature/native_media_viewer
switch to native media viewer
2 parents 9437a77 + 7767f27 commit 6b3bcb6

File tree

19 files changed

+506
-391
lines changed

19 files changed

+506
-391
lines changed

compose-ui/build.gradle.kts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,6 @@ kotlin {
8787
implementation(project.dependencies.platform(libs.koin.bom))
8888
implementation(libs.fluent.ui)
8989
implementation(libs.koin.compose)
90-
implementation(libs.composemediaplayer)
9190
implementation(libs.androidx.collection)
9291
}
9392
}
Lines changed: 0 additions & 207 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,10 @@
11
package dev.dimension.flare.ui.component.platform
22

3-
import androidx.collection.lruCache
43
import androidx.compose.foundation.ExperimentalFoundationApi
5-
import androidx.compose.foundation.combinedClickable
6-
import androidx.compose.foundation.layout.Box
74
import androidx.compose.foundation.layout.BoxScope
8-
import androidx.compose.foundation.layout.aspectRatio
9-
import androidx.compose.foundation.layout.fillMaxSize
10-
import androidx.compose.foundation.layout.wrapContentSize
115
import androidx.compose.runtime.Composable
12-
import androidx.compose.runtime.DisposableEffect
13-
import androidx.compose.runtime.LaunchedEffect
14-
import androidx.compose.runtime.getValue
15-
import androidx.compose.runtime.mutableLongStateOf
16-
import androidx.compose.runtime.remember
17-
import androidx.compose.runtime.setValue
186
import androidx.compose.ui.Modifier
19-
import androidx.compose.ui.draw.clipToBounds
20-
import androidx.compose.ui.geometry.Size
217
import androidx.compose.ui.layout.ContentScale
22-
import androidx.compose.ui.layout.layout
23-
import androidx.compose.ui.platform.LocalDensity
24-
import androidx.compose.ui.unit.Density
25-
import androidx.compose.ui.unit.Dp
26-
import io.github.kdroidfilter.composemediaplayer.VideoPlayerState
27-
import io.github.kdroidfilter.composemediaplayer.VideoPlayerSurface
28-
import kotlinx.coroutines.delay
29-
import kotlin.concurrent.timer
30-
import kotlin.math.roundToInt
31-
import kotlin.time.Duration.Companion.minutes
328

339
@OptIn(ExperimentalFoundationApi::class)
3410
@Composable
@@ -49,187 +25,4 @@ internal actual fun PlatformVideoPlayer(
4925
errorContent: @Composable BoxScope.() -> Unit,
5026
loadingPlaceholder: @Composable BoxScope.() -> Unit,
5127
) {
52-
val playerPool: VideoPlayerPool = org.koin.compose.koinInject()
53-
val player =
54-
remember(uri) {
55-
playerPool
56-
.get(uri)
57-
.apply {
58-
if (autoPlay) {
59-
play()
60-
}
61-
// volume = if (muted) 0f else 1f
62-
}
63-
}
64-
DisposableEffect(uri) {
65-
onDispose {
66-
playerPool.release(uri)
67-
}
68-
}
69-
var remainingTime by remember { mutableLongStateOf(0L) }
70-
val density = LocalDensity.current
71-
val size =
72-
remember(player.metadata) {
73-
val height = player.metadata.height
74-
val width = player.metadata.width
75-
if (height != null && width != null) {
76-
with(density) {
77-
Size(
78-
width = width.toFloat(),
79-
height = height.toFloat(),
80-
)
81-
}
82-
} else {
83-
null
84-
}
85-
}
86-
LaunchedEffect(player.isPlaying) {
87-
if (player.isPlaying) {
88-
player.volume = 0f
89-
}
90-
}
91-
LaunchedEffect(player) {
92-
while (true) {
93-
val duration = player.metadata.duration
94-
if (remainingTimeContent != null && duration != null) {
95-
val position = player.sliderPos / 1000f
96-
remainingTime = (duration * 1000 * (1f - position)).toLong()
97-
}
98-
delay(500)
99-
}
100-
}
101-
102-
Box(modifier) {
103-
VideoPlayerSurface(
104-
playerState = player,
105-
modifier =
106-
Modifier
107-
.clipToBounds()
108-
.resizeWithContentScale(
109-
contentScale = contentScale,
110-
sourceSizeDp = size,
111-
).let {
112-
if (aspectRatio != null) {
113-
it.aspectRatio(
114-
aspectRatio,
115-
)
116-
} else {
117-
it
118-
}
119-
}.let {
120-
if (onClick != null) {
121-
it.combinedClickable(
122-
onClick = onClick,
123-
onLongClick = onLongClick,
124-
)
125-
} else {
126-
it
127-
}
128-
}.matchParentSize(),
129-
)
130-
if (player.error != null) {
131-
errorContent.invoke(this)
132-
} else if (player.isLoading) {
133-
loadingPlaceholder()
134-
} else {
135-
remainingTimeContent?.invoke(this, remainingTime)
136-
}
137-
}
138-
}
139-
140-
@Composable
141-
private fun Modifier.resizeWithContentScale(
142-
contentScale: ContentScale,
143-
sourceSizeDp: Size?,
144-
density: Density = LocalDensity.current,
145-
): Modifier =
146-
then(
147-
Modifier
148-
.fillMaxSize()
149-
.wrapContentSize()
150-
.then(
151-
sourceSizeDp?.let { srcSizeDp ->
152-
Modifier.layout { measurable, constraints ->
153-
val srcSizePx =
154-
with(density) { Size(Dp(srcSizeDp.width).toPx(), Dp(srcSizeDp.height).toPx()) }
155-
val dstSizePx = Size(constraints.maxWidth.toFloat(), constraints.maxHeight.toFloat())
156-
val scaleFactor = contentScale.computeScaleFactor(srcSizePx, dstSizePx)
157-
val placeable =
158-
measurable.measure(
159-
constraints.copy(
160-
maxWidth =
161-
(srcSizePx.width * scaleFactor.scaleX)
162-
.takeIf { !it.isNaN() }
163-
?.roundToInt()
164-
?: constraints.maxWidth,
165-
maxHeight =
166-
(srcSizePx.height * scaleFactor.scaleY)
167-
.takeIf { !it.isNaN() }
168-
?.roundToInt()
169-
// or keep 16:9
170-
?: (constraints.maxWidth * 9 / 16),
171-
),
172-
)
173-
layout(placeable.width, placeable.height) { placeable.place(0, 0) }
174-
}
175-
} ?: Modifier,
176-
),
177-
)
178-
179-
public class VideoPlayerPool {
180-
private val positionPool = mutableMapOf<String, Float>()
181-
private val lockCount = linkedMapOf<String, Long>()
182-
private val pool =
183-
lruCache<String, VideoPlayerState>(
184-
maxSize = 10,
185-
create = { uri ->
186-
VideoPlayerState()
187-
.apply {
188-
loop = true
189-
openUri(uri)
190-
}
191-
},
192-
onEntryRemoved = { evicted, key, oldValue, newValue ->
193-
if (evicted) {
194-
positionPool.put(key, oldValue.sliderPos)
195-
oldValue.dispose()
196-
} else if (newValue != null) {
197-
val position = positionPool.get(key)
198-
if (position != null) {
199-
newValue.seekTo(position)
200-
positionPool.remove(key)
201-
}
202-
}
203-
},
204-
)
205-
206-
private val clearTimer =
207-
timer(period = 1.minutes.inWholeMilliseconds) {
208-
pool.snapshot().forEach { (uri, _) ->
209-
if (lockCount.getOrElse(uri) { 0 } == 0L) {
210-
pool.remove(uri)?.dispose()
211-
}
212-
}
213-
}
214-
215-
public fun peek(uri: String): VideoPlayerState? = pool.get(uri)
216-
217-
public fun get(uri: String): VideoPlayerState {
218-
lock(uri)
219-
return pool.get(uri)!!
220-
}
221-
222-
public fun lock(uri: String) {
223-
lockCount.put(uri, lockCount.getOrElse(uri) { 0 } + 1)
224-
}
225-
226-
public fun release(uri: String): Boolean {
227-
lockCount.put(uri, lockCount.getOrElse(uri) { 0 } - 1)
228-
val count = lockCount.getOrElse(uri) { 0 }
229-
if (count == 0L) {
230-
pool.get(uri)?.pause()
231-
}
232-
233-
return count == 0L
234-
}
23528
}

desktopApp/build-swift.gradle.kts

Lines changed: 9 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
import java.nio.file.Files
22

3-
val swiftSource = layout.projectDirectory.dir("src/main/swift/macosBridge.xcodeproj").asFile
3+
val swiftSource = layout.projectDirectory.dir("src/main/swift/macosBridge").asFile
4+
val xcodeproj = layout.projectDirectory.dir("src/main/swift/macosBridge.xcodeproj").asFile
45
val buildOutput = layout.projectDirectory.file("src/main/swift/build/Release/libmacosBridge.dylib")
56
val targetLib = layout.projectDirectory.dir("resources/macos-arm64").file("libmacosBridge.dylib").asFile
67
val isMac = org.gradle.internal.os.OperatingSystem.current().isMacOsX
78

8-
tasks.register<Exec>("compileWebviewBridgeArm64") {
9+
tasks.register<Exec>("compileMacosBridgeArm64") {
910
onlyIf { isMac }
1011

11-
// inputs.file(swiftSource)
12+
inputs.files(swiftSource)
1213
outputs.file(targetLib)
1314

1415
doFirst {
@@ -17,9 +18,10 @@ tasks.register<Exec>("compileWebviewBridgeArm64") {
1718

1819
commandLine(
1920
"xcodebuild",
20-
"-project", swiftSource.absolutePath,
21-
"-target", "macosBridge",
21+
"-project", xcodeproj.absolutePath,
22+
"-scheme", "macosBridge",
2223
"-configuration", "Release",
24+
"BUILD_DIR=${xcodeproj.parentFile}/build",
2325
)
2426

2527
doLast {
@@ -33,9 +35,9 @@ tasks.register<Exec>("compileWebviewBridgeArm64") {
3335

3436
afterEvaluate {
3537
tasks.named("compileKotlin").configure {
36-
dependsOn("compileWebviewBridgeArm64")
38+
dependsOn("compileMacosBridgeArm64")
3739
}
3840
tasks.named("prepareAppResources").configure {
39-
dependsOn("compileWebviewBridgeArm64")
41+
dependsOn("compileMacosBridgeArm64")
4042
}
4143
}

desktopApp/build.gradle.kts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
1-
import java.util.Properties
1+
22
import org.jetbrains.compose.desktop.application.dsl.TargetFormat
3+
import java.util.Properties
34

45
plugins {
56
alias(libs.plugins.kotlin.jvm)
@@ -54,7 +55,7 @@ compose.desktop {
5455
val hasSigningProps = file.exists()
5556
packageBuildVersion = System.getenv("BUILD_NUMBER") ?: "21"
5657
bundleID = "dev.dimension.flare"
57-
minimumSystemVersion = "12.0"
58+
minimumSystemVersion = "14.0"
5859
appStore = hasSigningProps
5960

6061
jvmArgs(
@@ -135,9 +136,9 @@ val macExtraPlistKeys: String
135136

136137
extra["sqliteVersion"] = libs.versions.sqlite.get()
137138
extra["sqliteOsArch"] = "osx_arm64"
138-
extra["composeMediaPlayerVersion"] = libs.versions.composemediaplayer.get()
139139
extra["jnaVersion"] = libs.versions.jna.get()
140140
extra["nativeDestDir"] = "resources/macos-arm64"
141141

142142
apply(from = File(projectDir, "install-native-libs.gradle.kts"))
143143
apply(from = File(projectDir, "build-swift.gradle.kts"))
144+

desktopApp/install-native-libs.gradle.kts

Lines changed: 2 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -135,43 +135,6 @@ val sqliteTask =
135135
entryPathInJar = sqliteEntry,
136136
destFile = nativeDestDir.resolve(sqliteFileName),
137137
)
138-
139-
val cmpVersion = (findProperty("composeMediaPlayerVersion") as String?) ?: "0.8.1"
140-
val cmpJarName = "composemediaplayer-jvm-$cmpVersion.jar"
141-
val cmpJarUrl =
142-
"https://repo1.maven.org/maven2/io/github/kdroidfilter/composemediaplayer-jvm/$cmpVersion/$cmpJarName"
143-
144-
val cmpDirName =
145-
when {
146-
currentOs.isMacOsX && isArm -> "darwin-aarch64"
147-
currentOs.isMacOsX && isX64 -> "darwin-x86-64"
148-
currentOs.isLinux && isArm -> "linux-aarch64"
149-
currentOs.isLinux && isX64 -> "linux-x86-64"
150-
currentOs.isWindows && isX64 -> "win32-x86-64"
151-
currentOs.isWindows && isArm -> "win32-arm64"
152-
else -> throw GradleException("Unsupported OS or architecture: ${currentOs.name} $currentArch")
153-
}
154-
155-
val cmpLibName =
156-
when {
157-
currentOs.isMacOsX -> "libNativeVideoPlayer.dylib"
158-
currentOs.isLinux -> "libNativeVideoPlayer.so"
159-
currentOs.isWindows -> "NativeVideoPlayer.dll"
160-
else -> throw GradleException("Unsupported OS: ${currentOs.name}")
161-
}
162-
163-
val cmpEntry = "$cmpDirName/$cmpLibName"
164-
165-
val cmpTask =
166-
registerExtractFromJarTask(
167-
taskName = "installNativeVideoPlayer_$cmpVersion",
168-
jarUrl = cmpJarUrl,
169-
cacheSubDir = "composeMediaPlayer/$cmpVersion",
170-
jarFileName = cmpJarName,
171-
entryPathInJar = cmpEntry,
172-
destFile = nativeDestDir.resolve(cmpLibName),
173-
)
174-
175138
val jnaVersion = (findProperty("jnaVersion") as String?) ?: "5.17.0"
176139
val jnaJarName = "jna-$jnaVersion.jar"
177140
val jnaJarUrl = "https://repo1.maven.org/maven2/net/java/dev/jna/jna/$jnaVersion/$jnaJarName"
@@ -211,9 +174,9 @@ val jnaTask =
211174

212175
afterEvaluate {
213176
tasks.named("compileKotlin").configure {
214-
dependsOn(sqliteTask, cmpTask, jnaTask)
177+
dependsOn(sqliteTask, jnaTask)
215178
}
216179
tasks.named("prepareAppResources").configure {
217-
dependsOn(sqliteTask, cmpTask, jnaTask)
180+
dependsOn(sqliteTask, jnaTask)
218181
}
219182
}

desktopApp/src/main/kotlin/dev/dimension/flare/Main.kt

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import coil3.ImageLoader
1616
import coil3.compose.setSingletonImageLoaderFactory
1717
import coil3.request.crossfade
1818
import dev.dimension.flare.common.DeeplinkHandler
19+
import dev.dimension.flare.common.NativeWindowBridge
1920
import dev.dimension.flare.common.SandboxHelper
2021
import dev.dimension.flare.di.KoinHelper
2122
import dev.dimension.flare.di.composeUiModule
@@ -28,6 +29,7 @@ import dev.dimension.flare.ui.theme.ProvideThemeSettings
2829
import org.apache.commons.lang3.SystemUtils
2930
import org.jetbrains.compose.resources.painterResource
3031
import org.jetbrains.compose.resources.stringResource
32+
import org.koin.compose.koinInject
3133
import org.koin.core.context.startKoin
3234
import java.awt.Desktop
3335

@@ -49,12 +51,17 @@ fun main(args: Array<String>) {
4951
.build()
5052
}
5153
val extraWindowRoutes = remember { mutableStateMapOf<String, FloatingWindowState>() }
54+
val nativeWindowBridge = koinInject<NativeWindowBridge>()
5255

5356
fun openWindow(
5457
key: String,
5558
route: Route.WindowRoute,
5659
) {
57-
if (extraWindowRoutes.containsKey(key)) {
60+
if (route is Route.RawImage) {
61+
nativeWindowBridge.openImageImageViewer(route.rawImage)
62+
} else if (route is Route.StatusMedia) {
63+
nativeWindowBridge.openStatusImageViewer(route)
64+
} else if (extraWindowRoutes.containsKey(key)) {
5865
extraWindowRoutes[key]?.bringToFront?.invoke()
5966
} else {
6067
extraWindowRoutes.put(

0 commit comments

Comments
 (0)