11package dev.dimension.flare.ui.component.platform
22
3- import androidx.collection.lruCache
43import androidx.compose.foundation.ExperimentalFoundationApi
5- import androidx.compose.foundation.combinedClickable
6- import androidx.compose.foundation.layout.Box
74import 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
115import 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
186import androidx.compose.ui.Modifier
19- import androidx.compose.ui.draw.clipToBounds
20- import androidx.compose.ui.geometry.Size
217import 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}
0 commit comments