fix: video won't seek — playhead clock had no element handle
seek-to-frame! and the rAF clock both reach the <video> through the shared video-el atom, which was populated by an inline :ref closure — recreated every render and callable with nil by React, so video-el ended up nil. That made set! currentTime (jumps + scrubs) and the clock's currentTime read both silent no-ops: the red line moved (separate :playhead dispatch) but the picture never seeked or followed. Capture the element with a stable ref that ignores nil, and collapse the clock to a single source of truth: one always-on rAF copies currentTime into :playhead, and jump/scrub only seek (no optimistic :playhead write), so the line and the picture can't disagree. Replaces the old push!/tick/stop + on-play/on-pause/on-seeked bookkeeping. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
24558b1cae
commit
ea78a9ee08
1 changed files with 42 additions and 27 deletions
|
|
@ -39,13 +39,14 @@
|
|||
target (-> (- px (/ (.-clientWidth el) 2)) (max 0) (min max-sl))]
|
||||
(set! (.-scrollLeft el) target))))
|
||||
|
||||
;; Moving the playhead = seeking the video. The video is the single clock
|
||||
;; (video-monitor's rAF reads currentTime back into :playhead), so we never
|
||||
;; write :playhead directly — we just move the video and let it report back.
|
||||
(defn- seek-to-frame! [fps frames]
|
||||
(when-let [v @video-el]
|
||||
(set! (.-currentTime v) (secs frames fps))))
|
||||
|
||||
(defn- jump! [fps frame]
|
||||
(rf/dispatch [::events/set-playhead frame])
|
||||
(seek-to-frame! fps frame))
|
||||
(defn- jump! [fps frame] (seek-to-frame! fps frame))
|
||||
|
||||
(defn- jump-and-scroll!
|
||||
"Jump (seek video + move playhead) AND scroll the timeline to center the frame."
|
||||
|
|
@ -142,28 +143,42 @@
|
|||
(.addEventListener js/document "keydown" on-keydown)
|
||||
|
||||
(defn video-monitor
|
||||
"Native-controls <video> that acts as the playback clock: while it's playing,
|
||||
a requestAnimationFrame loop pushes currentTime (as frames) into :playhead.
|
||||
Also syncs on seek/pause so scrubbing (native controls or the timeline) moves
|
||||
the playhead. Sized by CSS (.video-pane video) to fit its pane."
|
||||
"Native-controls <video>, which is the single playback clock AND the single
|
||||
seek target. One always-on requestAnimationFrame copies its currentTime (as
|
||||
frames) into :playhead whenever it changes — covering playback, native-control
|
||||
scrubbing, jumps and timeline scrubs alike, with no per-event bookkeeping.
|
||||
|
||||
The element is captured into `video-el` (shared with seek-to-frame!) by a
|
||||
stable ref that ignores nil — an *inline* ref closure is re-created every
|
||||
render and React can call it with nil, which is what left video-el nil and
|
||||
made both the clock read and the seek write silently no-op."
|
||||
[fps]
|
||||
(let [raf (atom nil)
|
||||
push! (fn [] (when-let [v @video-el]
|
||||
(rf/dispatch [::events/set-playhead (* (.-currentTime v) fps)])))
|
||||
tick (fn tick [] (push!) (reset! raf (js/requestAnimationFrame tick)))
|
||||
stop (fn [] (when @raf (js/cancelAnimationFrame @raf) (reset! raf nil)))]
|
||||
(fn [_fps]
|
||||
[:video {:src media-src
|
||||
:controls true
|
||||
:preload "auto"
|
||||
;; Keep playback inline on iOS instead of jumping to fullscreen.
|
||||
:plays-inline true
|
||||
:webkit-playsinline "true"
|
||||
:ref (fn [node] (reset! video-el node))
|
||||
:on-play (fn [_] (reset! following? true)
|
||||
(rf/dispatch [::events/set-playing true]) (stop) (tick))
|
||||
:on-pause (fn [_] (rf/dispatch [::events/set-playing false]) (stop) (push!))
|
||||
:on-seeked (fn [_] (push!))}])))
|
||||
(let [raf (atom nil)
|
||||
seen (atom nil) ; last currentTime we pushed
|
||||
capture! (fn [n] (when n (reset! video-el n))) ; stable id; ignore React's nils
|
||||
tick (fn tick []
|
||||
(when-let [v @video-el]
|
||||
(let [t (.-currentTime v)]
|
||||
(when (not= t @seen) ; skip churn while paused/idle
|
||||
(reset! seen t)
|
||||
(rf/dispatch [::events/set-playhead (* t fps)]))))
|
||||
(reset! raf (js/requestAnimationFrame tick)))]
|
||||
(r/create-class
|
||||
{:display-name "video-monitor"
|
||||
:component-did-mount (fn [_] (tick))
|
||||
:component-will-unmount (fn [_] (some-> @raf js/cancelAnimationFrame) (reset! video-el nil))
|
||||
:reagent-render
|
||||
(fn [_fps]
|
||||
[:video {:src media-src
|
||||
:controls true
|
||||
:preload "auto"
|
||||
:ref capture!
|
||||
;; Keep playback inline on iOS instead of jumping to fullscreen.
|
||||
:plays-inline true
|
||||
:webkit-playsinline "true"
|
||||
:on-play (fn [_] (reset! following? true)
|
||||
(rf/dispatch [::events/set-playing true]))
|
||||
:on-pause (fn [_] (rf/dispatch [::events/set-playing false]))}])})))
|
||||
|
||||
;; --- commentary sidebar ---------------------------------------------------
|
||||
|
||||
|
|
@ -353,9 +368,9 @@
|
|||
[content zoom fps off end ev]
|
||||
(.preventDefault ev)
|
||||
(let [to (fn [clientX]
|
||||
(let [f (ev->frames content clientX zoom fps off end)]
|
||||
(rf/dispatch [::events/set-playhead f]) ; move the red line now
|
||||
(seek-to-frame! fps f))) ; and seek the video
|
||||
;; just seek; the video clock reports back into :playhead (rAF),
|
||||
;; so the red line and the picture can never disagree
|
||||
(seek-to-frame! fps (ev->frames content clientX zoom fps off end)))
|
||||
move (fn [e] (to (.-clientX e)))
|
||||
up (fn up [_]
|
||||
(.removeEventListener js/document "mousemove" move)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue