-
Notifications
You must be signed in to change notification settings - Fork 1
Use Choreographer-based timing in MotionVideoPlayerCompose #63
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,7 @@ package com.tejpratapsingh.motionlib.ui.custom.video | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import android.graphics.Bitmap | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import android.media.MediaPlayer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import android.view.Choreographer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import androidx.compose.foundation.Image | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import androidx.compose.foundation.background | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import androidx.compose.foundation.layout.Arrangement | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -40,7 +41,6 @@ import com.tejpratapsingh.motionlib.core.MotionAudio | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.tejpratapsingh.motionlib.core.MotionConfig | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.tejpratapsingh.motionlib.core.motion.MotionVideoProducer | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import com.tejpratapsingh.motionlib.core.provideCurrentConfig | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import kotlinx.coroutines.delay | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import java.util.Locale | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| @Composable | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
@@ -64,31 +64,47 @@ fun MotionVideoPlayerCompose( | |||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| previewBitmap = motionVideoProducer.motionComposerView.getViewBitmap() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Playback loop | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| LaunchedEffect(isPlaying) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (isPlaying) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val frameDurationMs = (1000.0 / motionConfig.fps).toLong() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var lastFrameTime = System.currentTimeMillis() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| while (isPlaying) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val currentTime = System.currentTimeMillis() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val elapsed = currentTime - lastFrameTime | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (elapsed >= frameDurationMs) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val framesToAdvance = (elapsed / frameDurationMs).toInt().coerceAtLeast(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val nextFrame = currentFrame + framesToAdvance | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (nextFrame <= totalFrames) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentFrame = nextFrame | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Loop | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentFrame = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| activePlayers.values.forEach { it.stop(); it.release() } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| activePlayers.clear() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Playback loop driven by display vsync; advances by video fps time budget. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| DisposableEffect(isPlaying, motionConfig.fps, totalFrames) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isPlaying) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onDispose { } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val choreographer = Choreographer.getInstance() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val frameDurationNanos = (1_000_000_000L / motionConfig.fps).coerceAtLeast(1L) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛑 Crash Risk: Integer division by
Suggested change
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard against zero/negative
🛡️ Proposed fix- val frameDurationNanos = (1_000_000_000L / motionConfig.fps).coerceAtLeast(1L)
+ val safeFps = motionConfig.fps.coerceAtLeast(1)
+ val frameDurationNanos = (1_000_000_000L / safeFps).coerceAtLeast(1L)🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var lastFrameNanos = 0L | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| var accumulatedNanos = 0L | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val callback = object : Choreographer.FrameCallback { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| override fun doFrame(frameTimeNanos: Long) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (!isPlaying) return | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛑 Crash Risk: Reading |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (lastFrameNanos != 0L) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| accumulatedNanos += (frameTimeNanos - lastFrameNanos).coerceAtLeast(0L) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastFrameNanos = frameTimeNanos | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (accumulatedNanos >= frameDurationNanos) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val framesToAdvance = (accumulatedNanos / frameDurationNanos).toInt().coerceAtLeast(1) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| accumulatedNanos %= frameDurationNanos | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| val nextFrame = currentFrame + framesToAdvance | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (nextFrame <= totalFrames) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛑 Logic Error: The condition
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentFrame = nextFrame | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛑 Crash Risk: Writing to |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } else { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| currentFrame = 0 | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| activePlayers.values.forEach { it.stop(); it.release() } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| activePlayers.clear() | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lastFrameTime = currentTime | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| choreographer.postFrameCallback(this) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+86
to
+100
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛑 Logic Error: The callback continues to post itself via
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| delay(10) // Smooth check | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| choreographer.postFrameCallback(callback) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onDispose { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| choreographer.removeFrameCallback(callback) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+68
to
110
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Using Additionally, I've added a safety check for LaunchedEffect(isPlaying, motionConfig.fps, totalFrames) {
if (!isPlaying) return@LaunchedEffect
val frameDurationNanos = (1_000_000_000L / motionConfig.fps.coerceAtLeast(1)).toLong()
var lastFrameNanos = 0L
var accumulatedNanos = 0L
while (true) {
withFrameNanos { frameTimeNanos ->
if (lastFrameNanos != 0L) {
accumulatedNanos += (frameTimeNanos - lastFrameNanos).coerceAtLeast(0L)
}
lastFrameNanos = frameTimeNanos
if (accumulatedNanos >= frameDurationNanos) {
val framesToAdvance = (accumulatedNanos / frameDurationNanos).toInt()
accumulatedNanos %= frameDurationNanos
val nextFrame = currentFrame + framesToAdvance
if (nextFrame <= totalFrames) {
currentFrame = nextFrame
} else {
currentFrame = 0
activePlayers.values.forEach { it.stop(); it.release() }
activePlayers.clear()
}
}
}
}
} |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
Choreographerimport is no longer necessary if you use the more idiomaticwithFrameNanosapproach for Compose timing.