Skip to content

Commit

Permalink
improve video playback ux
Browse files Browse the repository at this point in the history
  • Loading branch information
Tlaster committed May 15, 2024
1 parent 8ac5582 commit 9635eb1
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 114 deletions.
2 changes: 2 additions & 0 deletions app/src/main/java/dev/dimension/flare/di/AndroidModule.kt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import dev.dimension.flare.common.PlayerPoll
import dev.dimension.flare.data.repository.ComposeNotifyUseCase
import dev.dimension.flare.data.repository.SettingsRepository
import dev.dimension.flare.ui.component.CacheDataSourceFactory
import dev.dimension.flare.ui.component.VideoPlayerPool
import dev.dimension.flare.ui.component.status.DefaultStatusEvent
import dev.dimension.flare.ui.component.status.StatusEvent
import dev.dimension.flare.ui.component.status.bluesky.BlueskyStatusEvent
Expand Down Expand Up @@ -42,4 +43,5 @@ val androidModule =
),
)
}
singleOf(::VideoPlayerPool)
}
125 changes: 95 additions & 30 deletions app/src/main/java/dev/dimension/flare/ui/component/VideoPlayer.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import android.content.Context
import android.util.Xml
import android.view.ViewGroup
import androidx.annotation.OptIn
import androidx.collection.lruCache
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.aspectRatio
Expand All @@ -15,13 +16,13 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.viewinterop.AndroidView
import androidx.media3.common.C
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.common.util.UnstableApi
Expand All @@ -42,9 +43,16 @@ import androidx.media3.ui.AspectRatioFrameLayout.RESIZE_MODE_ZOOM
import androidx.media3.ui.PlayerView
import dev.dimension.flare.BuildConfig
import dev.dimension.flare.R
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.koin.compose.koinInject
import org.xmlpull.v1.XmlPullParser
import java.io.File
import kotlin.concurrent.timer
import kotlin.time.Duration.Companion.minutes

@OptIn(UnstableApi::class)
@Composable
Expand All @@ -60,7 +68,7 @@ fun VideoPlayer(
contentScale: ContentScale = ContentScale.Crop,
onClick: (() -> Unit)? = null,
onLongClick: (() -> Unit)? = null,
factory: ProgressiveMediaSource.Factory = koinInject(),
playerPool: VideoPlayerPool = koinInject(),
remainingTimeContent: @Composable (BoxScope.(Long) -> Unit)? = null,
loadingPlaceholder: @Composable BoxScope.() -> Unit = {
if (previewUri != null) {
Expand Down Expand Up @@ -98,7 +106,8 @@ fun VideoPlayer(
}
},
) {
var isLoaded by remember { mutableStateOf(false) }
val scope = rememberCoroutineScope()
var isLoaded by remember { mutableStateOf(true) }
var remainingTime by remember { mutableLongStateOf(0L) }
Box(modifier = modifier) {
AndroidView(
Expand All @@ -108,13 +117,9 @@ fun VideoPlayer(
.matchParentSize(),
factory = { context ->
val exoPlayer =
ExoPlayer.Builder(context)
.build()
playerPool.get(uri)
.apply {
setMediaSource(factory.createMediaSource(MediaItem.fromUri(uri)))
prepare()
playWhenReady = true
repeatMode = Player.REPEAT_MODE_ALL
play()
volume = if (muted) 0f else 1f
}
val parser = context.resources.getXml(R.xml.video_view)
Expand All @@ -127,26 +132,15 @@ fun VideoPlayer(
controllerShowTimeoutMs = -1
useController = showControls
player = exoPlayer
exoPlayer.addListener(
object : Player.Listener {
fun calculateRemainingTime() {
if (exoPlayer.duration != C.TIME_UNSET) {
remainingTime = exoPlayer.duration - exoPlayer.currentPosition
}
postDelayed(::calculateRemainingTime, 500)
}

override fun onIsLoadingChanged(isLoading: Boolean) {
isLoaded = !isLoading || exoPlayer.duration > 0
scope.launch {
while (true) {
isLoaded = !exoPlayer.isLoading || exoPlayer.duration > 0
if (remainingTimeContent != null) {
remainingTime = exoPlayer.duration - exoPlayer.currentPosition
}

override fun onIsPlayingChanged(isPlaying: Boolean) {
if (isPlaying && remainingTimeContent != null) {
postDelayed(::calculateRemainingTime, 500)
}
}
},
)
delay(500)
}
}
layoutParams =
ViewGroup.LayoutParams(
ViewGroup.LayoutParams.MATCH_PARENT,
Expand All @@ -171,8 +165,9 @@ fun VideoPlayer(
}
}
},
onRelease = {
it.player?.release()
onRelease = { playerView ->
playerView.player = null
playerPool.release(uri)
},
)
if (!isLoaded) {
Expand Down Expand Up @@ -239,3 +234,73 @@ object VideoCache {
return cache as SimpleCache
}
}

@UnstableApi
class VideoPlayerPool(
private val context: Context,
private val factory: ProgressiveMediaSource.Factory,
private val scope: CoroutineScope,
) {
private val positionPool = mutableMapOf<String, Long>()
private val lockCount = linkedMapOf<String, Long>()
private val pool =
lruCache<String, ExoPlayer>(
maxSize = 10,
create = { uri ->
ExoPlayer.Builder(context)
.build()
.apply {
setMediaSource(factory.createMediaSource(MediaItem.fromUri(uri)))
prepare()
playWhenReady = true
repeatMode = Player.REPEAT_MODE_ALL
}
},
onEntryRemoved = { evicted, key, oldValue, newValue ->
if (evicted) {
positionPool.put(key, oldValue.currentPosition)
oldValue.release()
} else if (newValue != null) {
val position = positionPool.get(key)
if (position != null) {
newValue.seekTo(position)
positionPool.remove(key)
}
}
},
)

private val clearTimer =
timer(period = 1.minutes.inWholeMilliseconds) {
pool.snapshot().forEach { (uri, _) ->
if (lockCount.getOrElse(uri) { 0 } == 0L) {
pool.remove(uri)?.let {
scope.launch {
withContext(Dispatchers.Main) {
it.release()
}
}
}
}
}
}

fun get(uri: String): ExoPlayer {
lock(uri)
return pool.get(uri)!!
}

fun lock(uri: String) {
lockCount.put(uri, lockCount.getOrElse(uri) { 0 } + 1)
}

fun release(uri: String): Boolean {
lockCount.put(uri, lockCount.getOrElse(uri) { 0 } - 1)
val count = lockCount.getOrElse(uri) { 0 }
if (count == 0L) {
pool.get(uri)?.pause()
}

return count == 0L
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ import dev.dimension.flare.ui.model.UiMedia
import dev.dimension.flare.ui.model.UiPoll
import dev.dimension.flare.ui.model.UiStatus
import dev.dimension.flare.ui.model.UiUser
import dev.dimension.flare.ui.model.localizedShortTime
import dev.dimension.flare.ui.model.localizedFullTime
import dev.dimension.flare.ui.model.medias
import dev.dimension.flare.ui.model.onError
import dev.dimension.flare.ui.model.onLoading
Expand Down Expand Up @@ -720,7 +720,7 @@ private fun StatusPollComponent(
}
}
Text(
text = poll.expiresAt.localizedShortTime,
text = poll.expiresAt.localizedFullTime,
)
}
}
Expand Down
72 changes: 10 additions & 62 deletions app/src/main/java/dev/dimension/flare/ui/model/UiStatusExtraExt.kt
Original file line number Diff line number Diff line change
@@ -1,11 +1,9 @@
package dev.dimension.flare.ui.model

import android.text.format.DateUtils
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.res.stringResource
import dev.dimension.flare.R
import kotlinx.datetime.Clock
import kotlinx.datetime.Instant
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toJavaLocalDateTime
Expand All @@ -14,71 +12,21 @@ import java.time.format.DateTimeFormatter

val UiStatus.localizedShortTime: String
@Composable
get() = extra.createdAt.localizedShortTime
get() =
when (val type = extra.localizedShortTimeType) {
is LocalizedShortTime.MonthDay ->
DateTimeFormatter.ofPattern(stringResource(id = R.string.date_format_month_day))
.format(type.localDateTime)
is LocalizedShortTime.String -> type.value
is LocalizedShortTime.YearMonthDay ->
DateTimeFormatter.ofPattern(stringResource(id = R.string.date_format_year_month_day))
.format(type.localDateTime)
}

val UiStatus.localizedFullTime: String
@Composable
get() = extra.createdAt.localizedFullTime

val Instant.localizedShortTime: String
@Composable
get() {
val formatYearMonthDay = stringResource(id = R.string.date_format_year_month_day)
val formatMonthDay = stringResource(id = R.string.date_format_month_day)
return remember(this) {
val compareTo = Clock.System.now()
val timeZone = TimeZone.currentSystemDefault()
val time = toLocalDateTime(timeZone)
val diff = compareTo - this
when {
// dd MMM yy
compareTo.toLocalDateTime(timeZone).year != time.year -> {
DateTimeFormatter.ofPattern(formatYearMonthDay).format(time.toJavaLocalDateTime())
}
// dd MMM
diff.inWholeDays >= 7 -> {
DateTimeFormatter.ofPattern(formatMonthDay).format(time.toJavaLocalDateTime())
}
// xx day(s)
diff.inWholeDays >= 1 -> {
DateUtils.getRelativeTimeSpanString(
toEpochMilliseconds(),
System.currentTimeMillis(),
DateUtils.DAY_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE,
).toString()
}
// xx hr(s)
diff.inWholeHours >= 1 -> {
DateUtils.getRelativeTimeSpanString(
toEpochMilliseconds(),
System.currentTimeMillis(),
DateUtils.HOUR_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE,
).toString()
}
// xx sec(s)
diff.inWholeMinutes < 1 -> {
DateUtils.getRelativeTimeSpanString(
toEpochMilliseconds(),
System.currentTimeMillis(),
DateUtils.SECOND_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE,
).toString()
}
// xx min(s)
else -> {
DateUtils.getRelativeTimeSpanString(
toEpochMilliseconds(),
System.currentTimeMillis(),
DateUtils.MINUTE_IN_MILLIS,
DateUtils.FORMAT_ABBREV_RELATIVE,
).toString()
}
}
}
}

val Instant.localizedFullTime: String
@Composable
get() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,6 @@ import androidx.compose.material3.ModalDrawerSheet
import androidx.compose.material3.ModalNavigationDrawer
import androidx.compose.material3.NavigationDrawerItem
import androidx.compose.material3.Text
import androidx.compose.material3.adaptive.ExperimentalMaterial3AdaptiveApi
import androidx.compose.material3.adaptive.currentWindowAdaptiveInfo
import androidx.compose.material3.adaptive.navigationsuite.ExperimentalMaterial3AdaptiveNavigationSuiteApi
import androidx.compose.material3.adaptive.navigationsuite.NavigationSuiteScaffoldDefaults
Expand Down Expand Up @@ -126,7 +125,6 @@ data class RootNavController(

@OptIn(
ExperimentalMaterial3AdaptiveNavigationSuiteApi::class,
ExperimentalMaterial3AdaptiveApi::class,
ExperimentalSharedTransitionApi::class,
)
@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,35 +98,36 @@ import org.koin.compose.koinInject
@OptIn(ExperimentalSharedTransitionApi::class)
@Composable
@Destination<RootGraph>(
style = FullScreenDialogStyle::class,
// style = FullScreenDialogStyle::class,
deepLinks = [
DeepLink(
uriPattern = "flare://$FULL_ROUTE_PLACEHOLDER",
),
],
)
internal fun StatusMediaRoute(
internal fun AnimatedVisibilityScope.StatusMediaRoute(
statusKey: MicroBlogKey,
index: Int,
preview: String?,
navigator: DestinationsNavigator,
accountType: AccountType,
) {
SetDialogDestinationToEdgeToEdge()
AnimatedVisibility(true) {
SharedTransitionScope {
StatusMediaScreen(
statusKey = statusKey,
accountType = accountType,
index = index,
onDismiss = navigator::navigateUp,
preview = preview,
toStatus = {
navigator.navigate(StatusRouteDestination(statusKey, accountType))
},
)
}
}
sharedTransitionScope: SharedTransitionScope,
) = with(sharedTransitionScope) {
// SetDialogDestinationToEdgeToEdge()
// AnimatedVisibility(true) {
// SharedTransitionScope {
StatusMediaScreen(
statusKey = statusKey,
accountType = accountType,
index = index,
onDismiss = navigator::navigateUp,
preview = preview,
toStatus = {
navigator.navigate(StatusRouteDestination(statusKey, accountType))
},
)
// }
// }
}

context(AnimatedVisibilityScope, SharedTransitionScope)
Expand Down
Loading

0 comments on commit 9635eb1

Please sign in to comment.