From 5e946bbe3aa7fe9e8b38b716cef55109bb36e763 Mon Sep 17 00:00:00 2001 From: baiqing Date: Wed, 24 Jun 2026 14:08:45 +0800 Subject: [PATCH] fix(preview): hold the played frame on pause until the exact stop-frame composite lands MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After timeline playback stops (user pause OR end-of-timeline) the preview sometimes jumped to an EARLIER frame than the real pause position, then snapped forward. Root cause is the two-surface switch: on pause the composite became visible immediately, but useTimelineFrame held the PRE-PLAYBACK composite (it was disabled during playback and intentionally never clears its dataUrl), and the true stop frame was only requested ~140ms later (the scrub debounce timer was reset on every rAF activeFrame write) plus an async ffmpeg/wgpu/PNG round-trip — so the stale frame showed first, then the correct one. Intermittent because it is a timing race; sometimes the held frame was already close enough. Upstream (VideoEngine.swift / PreviewView.swift) has ONE surface: a single AVPlayerLayer parked on the player's current CMTime, so pause() just freezes the current frame — the displayed frame always equals the player position, never an earlier one. The WebView can't run that, so faithfulness means the play→pause SWITCH between our two surfaces must be frame-consistent. Fix (minimal, no self-invented opacity/debounce hacks): - Split the debounce by intent: targetFrame (immediate, rounded/clamped stop frame) vs scrubFrame (debounced 140ms, paused scrubbing only). On the play→pause settle, request targetFrame at once — no 140ms delay. - useTimelineFrame now returns { dataUrl, readyFrame }; readyFrame is the frame the current dataUrl was composited for, so Preview can gate the surface swap on "composite == exact stop frame". - Hold the already-paused