perf(video): reduce jitter and per-recomposition work in note video pipeline
Tightens the hot path that runs whenever a video appears inside a note in the feed (RichText -> ZoomableContentView -> VideoView). - VideoView: resolve aspect ratio once per (uri, dim), and prime MediaAspectRatioCache from the imeta dim tag so repeat appearances (PiP, dialog, list re-enter) don't have to wait for ExoPlayer's onVideoSizeChanged before reserving layout space. - VideoView: key the manual "tap to show" toggle on videoUri so a recycled feed slot doesn't inherit stale state from the previous video. - VideoViewInner: hoist proxyPortForVideo() into a remember(videoUri) — the result was being recomputed every recomposition only to be dropped by GetMediaItem's URI-keyed remember. - RenderVideoPlayer: stop holding container size in compose state. The size is only read in onDoubleTap, so a non-state IntArray holder removes a recomposition of the whole player tree on every layout pass. Also memoize isLiveStreaming() so the .m3u8 substring scan doesn't run on every recomposition. - RenderTopButtons: same isLiveStreaming() memoization. - GetVideoController: switch the remaining non-lambda Log.d call to the lambda overload so the message string isn't formatted when the log level is filtered out.
This commit is contained in:
+1
-1
@@ -52,7 +52,7 @@ fun GetVideoController(
|
||||
if (BackgroundMedia.isPlaying()) {
|
||||
// There is a video playing, start this one on mute.
|
||||
state.controller.volume = 0f
|
||||
Log.d("PlaybackService", "OnEach Muted due to BackgroundMedia.isPlaying")
|
||||
Log.d("PlaybackService") { "OnEach Muted due to BackgroundMedia.isPlaying" }
|
||||
} else {
|
||||
// There is no other video playing. Use the default mute state to
|
||||
// decide if sound is on or not.
|
||||
|
||||
+7
-5
@@ -34,7 +34,6 @@ import androidx.compose.ui.geometry.Size
|
||||
import androidx.compose.ui.input.pointer.pointerInput
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.layout.onSizeChanged
|
||||
import androidx.compose.ui.unit.IntSize
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import androidx.media3.ui.compose.ContentFrame
|
||||
@@ -90,19 +89,22 @@ fun RenderVideoPlayer(
|
||||
hasBlurhash: Boolean = false,
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val containerSize = remember { mutableStateOf(IntSize.Zero) }
|
||||
val isLive = isLiveStreaming(mediaItem.src.videoUri)
|
||||
// Hold the container size in a non-state holder so layout passes don't trigger an
|
||||
// unnecessary recomposition of the whole player tree just to update a value that is only
|
||||
// ever read inside the onDoubleTap callback below.
|
||||
val containerWidth = remember { intArrayOf(0) }
|
||||
val isLive = remember(mediaItem.src.videoUri) { isLiveStreaming(mediaItem.src.videoUri) }
|
||||
|
||||
Box(
|
||||
modifier =
|
||||
borderModifier
|
||||
.onSizeChanged { containerSize.value = it }
|
||||
.onSizeChanged { containerWidth[0] = it.width }
|
||||
.pointerInput(isLive, controllerState) {
|
||||
detectTapGestures(
|
||||
onTap = { controllerVisible.value = !controllerVisible.value },
|
||||
onDoubleTap = { offset ->
|
||||
if (!isLive) {
|
||||
val isLeftSide = offset.x < containerSize.value.width / 2
|
||||
val isLeftSide = offset.x < containerWidth[0] / 2
|
||||
if (isLeftSide) {
|
||||
controllerState.controller.seekBackward()
|
||||
} else {
|
||||
|
||||
+17
-5
@@ -105,15 +105,29 @@ fun VideoView(
|
||||
thumbhash: String? = null,
|
||||
) {
|
||||
val initialAutoStart = if (alwaysShowVideo) true else accountViewModel.settings.startVideoPlayback()
|
||||
val automaticallyStartPlayback = remember { mutableStateOf(initialAutoStart) }
|
||||
// Reset the manual-show toggle when the video URI changes so a recycled feed slot
|
||||
// doesn't inherit "tapped to show" state from a prior video.
|
||||
val automaticallyStartPlayback = remember(videoUri) { mutableStateOf(initialAutoStart) }
|
||||
|
||||
// Once the video is being shown, only honor the user's autoplay preference when it was auto-loaded.
|
||||
// If the user manually tapped the download button, they want it to play.
|
||||
val autoplay = alwaysShowVideo || (initialAutoStart && accountViewModel.settings.autoPlayVideos()) || (!initialAutoStart && automaticallyStartPlayback.value)
|
||||
|
||||
if (blurhash == null && thumbhash == null) {
|
||||
val ratio = dimensions?.aspectRatio() ?: MediaAspectRatioCache.get(videoUri)
|
||||
// Resolve the aspect ratio once per composition. Prime the URL-keyed cache from the imeta
|
||||
// dim tag so the next time this video appears (PiP, dialog, list re-enter) the cache hits
|
||||
// without waiting for ExoPlayer's onVideoSizeChanged.
|
||||
val ratio =
|
||||
remember(videoUri, dimensions) {
|
||||
val fromDim = dimensions?.takeIf { it.hasSize() }
|
||||
if (fromDim != null) {
|
||||
MediaAspectRatioCache.add(videoUri, fromDim.width, fromDim.height)
|
||||
fromDim.aspectRatio()
|
||||
} else {
|
||||
MediaAspectRatioCache.get(videoUri)
|
||||
}
|
||||
}
|
||||
|
||||
if (blurhash == null && thumbhash == null) {
|
||||
val modifier =
|
||||
if (ratio != null && automaticallyStartPlayback.value) {
|
||||
Modifier.aspectRatio(ratio)
|
||||
@@ -149,8 +163,6 @@ fun VideoView(
|
||||
}
|
||||
}
|
||||
} else {
|
||||
val ratio = dimensions?.aspectRatio() ?: MediaAspectRatioCache.get(videoUri)
|
||||
|
||||
val modifier =
|
||||
if (ratio != null) {
|
||||
Modifier.aspectRatio(ratio)
|
||||
|
||||
+6
-1
@@ -59,6 +59,11 @@ fun VideoViewInner(
|
||||
// keeps a copy of the value to avoid recompositions here when the DEFAULT value changes
|
||||
val muted = remember(videoUri) { DEFAULT_MUTED_SETTING.value }
|
||||
|
||||
// The proxy port is decided once per video URI; recomputing on every recomposition does
|
||||
// pointless work (the result is anyway dropped because GetMediaItem.remember is keyed on the
|
||||
// URI alone, so the cached MediaItemData is locked in on the first frame).
|
||||
val proxyPort = remember(videoUri) { accountViewModel.httpClientBuilder.proxyPortForVideo(videoUri) }
|
||||
|
||||
GetMediaItem(
|
||||
videoUri = videoUri,
|
||||
title = title,
|
||||
@@ -67,7 +72,7 @@ fun VideoViewInner(
|
||||
callbackUri = nostrUriCallback,
|
||||
mimeType = mimeType,
|
||||
aspectRatio = aspectRatio,
|
||||
proxyPort = accountViewModel.httpClientBuilder.proxyPortForVideo(videoUri),
|
||||
proxyPort = proxyPort,
|
||||
keepPlaying = true,
|
||||
waveformData = waveform,
|
||||
isLiveStream = isLiveStream,
|
||||
|
||||
+1
-1
@@ -105,7 +105,7 @@ fun RenderTopButtons(
|
||||
accountViewModel: AccountViewModel,
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val isLive = isLiveStreaming(mediaData.videoUri)
|
||||
val isLive = remember(mediaData.videoUri) { isLiveStreaming(mediaData.videoUri) }
|
||||
val pipSupported =
|
||||
remember {
|
||||
context.packageManager.hasSystemFeature(PackageManager.FEATURE_PICTURE_IN_PICTURE)
|
||||
|
||||
Reference in New Issue
Block a user