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))]
|
target (-> (- px (/ (.-clientWidth el) 2)) (max 0) (min max-sl))]
|
||||||
(set! (.-scrollLeft el) target))))
|
(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]
|
(defn- seek-to-frame! [fps frames]
|
||||||
(when-let [v @video-el]
|
(when-let [v @video-el]
|
||||||
(set! (.-currentTime v) (secs frames fps))))
|
(set! (.-currentTime v) (secs frames fps))))
|
||||||
|
|
||||||
(defn- jump! [fps frame]
|
(defn- jump! [fps frame] (seek-to-frame! fps frame))
|
||||||
(rf/dispatch [::events/set-playhead frame])
|
|
||||||
(seek-to-frame! fps frame))
|
|
||||||
|
|
||||||
(defn- jump-and-scroll!
|
(defn- jump-and-scroll!
|
||||||
"Jump (seek video + move playhead) AND scroll the timeline to center the frame."
|
"Jump (seek video + move playhead) AND scroll the timeline to center the frame."
|
||||||
|
|
@ -142,28 +143,42 @@
|
||||||
(.addEventListener js/document "keydown" on-keydown)
|
(.addEventListener js/document "keydown" on-keydown)
|
||||||
|
|
||||||
(defn video-monitor
|
(defn video-monitor
|
||||||
"Native-controls <video> that acts as the playback clock: while it's playing,
|
"Native-controls <video>, which is the single playback clock AND the single
|
||||||
a requestAnimationFrame loop pushes currentTime (as frames) into :playhead.
|
seek target. One always-on requestAnimationFrame copies its currentTime (as
|
||||||
Also syncs on seek/pause so scrubbing (native controls or the timeline) moves
|
frames) into :playhead whenever it changes — covering playback, native-control
|
||||||
the playhead. Sized by CSS (.video-pane video) to fit its pane."
|
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]
|
[fps]
|
||||||
(let [raf (atom nil)
|
(let [raf (atom nil)
|
||||||
push! (fn [] (when-let [v @video-el]
|
seen (atom nil) ; last currentTime we pushed
|
||||||
(rf/dispatch [::events/set-playhead (* (.-currentTime v) fps)])))
|
capture! (fn [n] (when n (reset! video-el n))) ; stable id; ignore React's nils
|
||||||
tick (fn tick [] (push!) (reset! raf (js/requestAnimationFrame tick)))
|
tick (fn tick []
|
||||||
stop (fn [] (when @raf (js/cancelAnimationFrame @raf) (reset! raf nil)))]
|
(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]
|
(fn [_fps]
|
||||||
[:video {:src media-src
|
[:video {:src media-src
|
||||||
:controls true
|
:controls true
|
||||||
:preload "auto"
|
:preload "auto"
|
||||||
|
:ref capture!
|
||||||
;; Keep playback inline on iOS instead of jumping to fullscreen.
|
;; Keep playback inline on iOS instead of jumping to fullscreen.
|
||||||
:plays-inline true
|
:plays-inline true
|
||||||
:webkit-playsinline "true"
|
:webkit-playsinline "true"
|
||||||
:ref (fn [node] (reset! video-el node))
|
|
||||||
:on-play (fn [_] (reset! following? true)
|
:on-play (fn [_] (reset! following? true)
|
||||||
(rf/dispatch [::events/set-playing true]) (stop) (tick))
|
(rf/dispatch [::events/set-playing true]))
|
||||||
:on-pause (fn [_] (rf/dispatch [::events/set-playing false]) (stop) (push!))
|
:on-pause (fn [_] (rf/dispatch [::events/set-playing false]))}])})))
|
||||||
:on-seeked (fn [_] (push!))}])))
|
|
||||||
|
|
||||||
;; --- commentary sidebar ---------------------------------------------------
|
;; --- commentary sidebar ---------------------------------------------------
|
||||||
|
|
||||||
|
|
@ -353,9 +368,9 @@
|
||||||
[content zoom fps off end ev]
|
[content zoom fps off end ev]
|
||||||
(.preventDefault ev)
|
(.preventDefault ev)
|
||||||
(let [to (fn [clientX]
|
(let [to (fn [clientX]
|
||||||
(let [f (ev->frames content clientX zoom fps off end)]
|
;; just seek; the video clock reports back into :playhead (rAF),
|
||||||
(rf/dispatch [::events/set-playhead f]) ; move the red line now
|
;; so the red line and the picture can never disagree
|
||||||
(seek-to-frame! fps f))) ; and seek the video
|
(seek-to-frame! fps (ev->frames content clientX zoom fps off end)))
|
||||||
move (fn [e] (to (.-clientX e)))
|
move (fn [e] (to (.-clientX e)))
|
||||||
up (fn up [_]
|
up (fn up [_]
|
||||||
(.removeEventListener js/document "mousemove" move)
|
(.removeEventListener js/document "mousemove" move)
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue