Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -597,6 +597,51 @@ class CompositionPreviewViewModel(application: Application) : AndroidViewModel(a
compositionPlayer.play()
}

/**
* Reproduces a CompositionPlayer stuck-seek bug:
* - Requires the composition to end in a GapMediaItem and FrameConsumer to be enabled.
* - Step 1: seekTo(duration-1) — lands the playhead in the trailing gap. If the gap emits
* no frame, Media3's onFrameProcessed never clears waitingForFrameAfterSeek.
* - Step 2: seekTo(0) — silently DEFERRED behind the latched flag.
* - Step 3: play() — no visible playback (position never returns to 0).
* Watch `adb logcat -s SeekDbg RepDbg` while pressing the button.
*/
fun reproduceStuckSeekBug() {
viewModelScope.launch {
val player = compositionPlayer
val duration = player.duration
Log.d("RepDbg", "start: duration=$duration currentPos=${player.currentPosition}")
if (duration <= 0) {
Log.d("RepDbg", "abort: duration<=0. Add media + a trailing Gap before pressing.")
return@launch
}

// Step 1: land the playhead at duration-1. Position moves 0 → duration-1, a frame
// is emitted, onFrameProcessed clears the flag.
Log.d("RepDbg", "step1: seekTo(${duration - 1}) — expect PROPAGATING + onFrameProcessed clears flag")
player.seekTo(duration - 1)
kotlinx.coroutines.delay(1000)
Log.d("RepDbg", "step1 done: currentPos=${player.currentPosition}")

// Step 2: redundant seek to the *same* position (mirrors Squirrel's "post-scrub refresh
// seek to <confirmed current pos>"). No-op seek → no frame delivered → onFrameProcessed
// never fires → waitingForFrameAfterSeek stays latched.
Log.d("RepDbg", "step2: seekTo(${duration - 1}) again (no-op) — expect PROPAGATING but NO onFrameProcessed follows")
player.seekTo(duration - 1)
kotlinx.coroutines.delay(1000)
Log.d("RepDbg", "step2 done: currentPos=${player.currentPosition}")

// Step 3: any follow-up seek is now silently DEFERRED behind the latched flag.
Log.d("RepDbg", "step3: seekTo(0) — expect DEFERRED (flag latched)")
player.seekTo(0)
kotlinx.coroutines.delay(200)
Log.d("RepDbg", "step3 done: currentPos=${player.currentPosition} (should still be at end)")

Log.d("RepDbg", "step4: play() — expect no visible playback")
player.play()
}
}

fun exportComposition() {
// Cancel and clean up files from any ongoing export.
cancelExport()
Expand Down Expand Up @@ -790,9 +835,15 @@ class CompositionPreviewViewModel(application: Application) : AndroidViewModel(a

val sequenceBuilder = EditedMediaItemSequence.Builder(trackTypes)

Log.d(
"RepDbg",
"buildSequence[$sequenceIndex] items=${sequenceItems.size} " +
"types=${sequenceItems.joinToString(",") { it.javaClass.simpleName }}",
)
for (item in sequenceItems) {
when (item) {
is Gap -> {
Log.d("RepDbg", " addGap durationUs=${item.durationUs}")
sequenceBuilder.addGap(item.durationUs)
}
is Media -> {
Expand Down Expand Up @@ -826,6 +877,7 @@ class CompositionPreviewViewModel(application: Application) : AndroidViewModel(a
// Setting duration explicitly is only required for preview with CompositionPlayer,
// and is not needed for export with Transformer.
.setDurationUs(item.durationUs)
Log.d("RepDbg", " addMedia durationUs=${item.durationUs} uri=${item.uri}")
sequenceBuilder.addItem(itemBuilder.build())
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,12 @@ internal fun CompositionPreviewPane(
Button(onClick = { viewModel.play() }, enabled = uiState.isCompositionSet) {
Text(text = stringResource(R.string.play))
}
Button(
onClick = { viewModel.reproduceStuckSeekBug() },
enabled = uiState.isCompositionSet,
) {
Text(text = "Reproduce stuck-seek bug")
}
Button(onClick = onOpenExportOptions) {
Text(text = stringResource(R.string.export_settings))
}
Expand Down
4 changes: 3 additions & 1 deletion libraries/effect_ndk/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@
// See the License for the specific language governing permissions and
// limitations under the License.

plugins { id("media3.android-library") }
plugins {
id("media3.android-library")
}

android {
namespace = "androidx.media3.effect.ndk"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,10 @@ public void setScrubbingModeEnabled(boolean scrubbingModeEnabled) {
: this.playWhenReadyChangeReason);

// Bypass debouncing for the final seek when scrubbing ends.
Log.d(
"SeekDbg",
"setScrubbingModeEnabled(false) CLEARING flag; pendingSeekPositionMs="
+ pendingSeekPositionMs);
waitingForFrameAfterSeek = false;
if (pendingSeekPositionMs != C.TIME_UNSET) {
long pendingSeekPosition = pendingSeekPositionMs;
Expand Down Expand Up @@ -1191,18 +1195,65 @@ private void handleSeekInternal(long positionMs) {
"positionMs=%d",
positionMs);

Log.d(
"SeekDbg",
"handleSeekInternal enter positionMs="
+ positionMs
+ " frameProcessor="
+ (frameProcessor != null)
+ " waitingForFrameAfterSeek="
+ waitingForFrameAfterSeek
+ " pendingSeekPositionMs="
+ pendingSeekPositionMs
+ " aggregatedContentPos="
+ getContentPositionMs());
if (frameProcessor != null) {
if (waitingForFrameAfterSeek) {
pendingSeekPositionMs = positionMs;
Log.d(
"SeekDbg",
"handleSeekInternal DEFERRED (flag latched) -> pendingSeekPositionMs=" + positionMs);
return;
}
waitingForFrameAfterSeek = true;
}
Log.d(
"SeekDbg", "handleSeekInternal PROPAGATING to inner players positionMs=" + positionMs);
CompositionPlayerInternal compositionPlayerInternal =
checkNotNull(this.compositionPlayerInternal);
compositionPlayerInternal.startSeek(positionMs);
for (int i = 0; i < playerHolders.size(); i++) {
androidx.media3.exoplayer.ExoPlayer innerPlayer = playerHolders.get(i).player;
Log.d(
"SeekDbg",
" inner["
+ i
+ "] PRE-seek state="
+ innerPlayer.getPlaybackState()
+ " contentPos="
+ innerPlayer.getContentPosition()
+ " duration="
+ innerPlayer.getDuration()
+ " playWhenReady="
+ innerPlayer.getPlayWhenReady()
+ " mediaItemCount="
+ innerPlayer.getMediaItemCount()
+ " currentMediaItemIndex="
+ innerPlayer.getCurrentMediaItemIndex());
playerHolders.get(i).player.seekTo(positionMs);
Log.d(
"SeekDbg",
" inner["
+ i
+ "] POST-seek state="
+ innerPlayer.getPlaybackState()
+ " contentPos="
+ innerPlayer.getContentPosition()
+ " currentMediaItemIndex="
+ innerPlayer.getCurrentMediaItemIndex()
+ " (requested "
+ positionMs
+ ")");
// Flush the HardwareBufferFrameReader and FrameAggregator after the player seeks to ensure
// frames from before the seek do not race with the flush calls.
if (frameProcessor != null) {
Expand All @@ -1224,6 +1275,12 @@ private void handleSeekInternal(long positionMs) {
}
}
compositionPlayerInternal.endSeek();
Log.d(
"SeekDbg",
"handleSeekInternal DONE requestedMs="
+ positionMs
+ " aggregatedContentPos="
+ getContentPositionMs());
}

@Override
Expand Down Expand Up @@ -1566,6 +1623,16 @@ private void prepareCompositionPlayerInternal() {
}

private void setCompositionInternal(Composition composition, long startPositionMs) {
Log.d(
"SeekDbg",
"setCompositionInternal startPositionMs="
+ startPositionMs
+ " CLEARING waitingForFrameAfterSeek (was "
+ waitingForFrameAfterSeek
+ ") pendingSeekPositionMs=(was "
+ pendingSeekPositionMs
+ ") priorAggregatedPos="
+ getContentPositionMs());
waitingForFrameAfterSeek = false;
pendingSeekPositionMs = C.TIME_UNSET;
for (int i = 0; i < playerHolders.size(); i++) {
Expand Down Expand Up @@ -2545,11 +2612,22 @@ public void onVideoSizeChanged(VideoSize videoSize) {
public void onFrameProcessed() {
applicationHandler.post(
() -> {
Log.d(
"SeekDbg",
"onFrameProcessed waitingForFrameAfterSeek="
+ waitingForFrameAfterSeek
+ " pendingSeekPositionMs="
+ pendingSeekPositionMs
+ " aggregatedContentPos="
+ getContentPositionMs());
if (waitingForFrameAfterSeek) {
waitingForFrameAfterSeek = false;
if (pendingSeekPositionMs != C.TIME_UNSET) {
long pendingSeekPosition = pendingSeekPositionMs;
pendingSeekPositionMs = C.TIME_UNSET;
Log.d(
"SeekDbg",
"onFrameProcessed FLUSHING pending seek -> " + pendingSeekPosition);
handleSeekInternal(pendingSeekPosition);
}
}
Expand Down