Drive playhead from the video element; reseek on stack moves

- the <video>'s own play/pause now engage/disengage the segment-aware tick
  (:on-play/:on-pause), so the native controls drive the playhead and follow the
  multicam cuts, not just the toolbar button (which now just toggles the video)
- entering/leaving a context (expand/collapse/pop-to) pauses and seeks the video
  to that context's remembered playhead ([:view :playheads ctx], 0 = its start),
  so the playhead tracks the timeline stack instead of ticking a stale context

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-06-29 02:42:40 -04:00
parent 8ee2cdf7b9
commit 5861113aeb
2 changed files with 51 additions and 28 deletions

View file

@ -71,14 +71,22 @@
(rf/reg-event-db ::set-zoom (fn [db [_ z]] (assoc-in db [:view :zoom] z))) (rf/reg-event-db ::set-zoom (fn [db [_ z]] (assoc-in db [:view :zoom] z)))
(rf/reg-event-db ::set-row-h (fn [db [_ h]] (assoc-in db [:view :row-h] h))) (rf/reg-event-db ::set-row-h (fn [db [_ h]] (assoc-in db [:view :row-h] h)))
(rf/reg-event-db ::expand (fn [db [_ gid]] (update-in db [:view :stack] conj gid))) ;; moving around the timeline stack: pause and seek the video to the context
(rf/reg-event-db ::collapse (fn [db _] (update-in db [:view :stack] ;; we land in, at its remembered playhead (0 = that context's start).
(fn [s] (if (> (count s) 1) (pop s) s))))) (defn- enter-ctx
(rf/reg-event-db ::pop-to [db stack-fn]
(fn [db [_ gid]] (let [db (update-in db [:view :stack] stack-fn)
(update-in db [:view :stack] ctx (peek (get-in db [:view :stack]))
(fn [s] (let [i (first (keep-indexed #(when (= gid %2) %1) s))] segs (scene/content-segments (:scene db) ctx)
(if i (subvec s 0 (inc i)) s)))))) sf (scene/local->source segs (scene/playhead (:view db) ctx))]
{:db db :player/pause true :player/seek (when sf (/ sf (:fps db)))}))
(rf/reg-event-fx ::expand (fn [{:keys [db]} [_ gid]] (enter-ctx db #(conj % gid))))
(rf/reg-event-fx ::collapse (fn [{:keys [db]} _] (enter-ctx db #(if (> (count %) 1) (pop %) %))))
(rf/reg-event-fx ::pop-to
(fn [{:keys [db]} [_ gid]]
(enter-ctx db (fn [s] (let [i (first (keep-indexed #(when (= gid %2) %1) s))]
(if i (subvec s 0 (inc i)) s))))))
;; --- authoring ------------------------------------------------------------ ;; --- authoring ------------------------------------------------------------
;; The draft is just a normal annotation group, flagged :draft (:new while ;; The draft is just a normal annotation group, flagged :draft (:new while

View file

@ -23,15 +23,19 @@
(set! (.-currentTime @video-el) (/ source-frame fps)))) (set! (.-currentTime @video-el) (/ source-frame fps))))
(defn- seg-at (defn- seg-at
"Index of the segment whose local range contains `local` (0 if none)." "Index of the segment whose LOCAL range contains `local` (0 if none)."
[segs local] [segs local]
(or (first (keep-indexed (fn [i {[c d] :local}] (when (<= c local d) i)) segs)) 0)) (or (first (keep-indexed (fn [i {[c d] :local}] (when (<= c local d) i)) segs)) 0))
(defn- stop-play! [] (defn- seg-at-src
(when @raf (js/cancelAnimationFrame @raf) (reset! raf nil)) "Index of the segment whose SOURCE range contains source frame `sf` (0 if none)."
(reset! play nil) [segs sf]
(when-let [v @video-el] (.pause v)) (or (first (keep-indexed (fn [i {[a b] :src}] (when (<= a sf b) i)) segs)) 0))
(rf/dispatch [::events/set-playing false]))
;; The <video> element is the source of truth for play/pause. Its :on-play /
;; :on-pause (below) engage/disengage this segment-aware tick, so the native
;; controls and the toolbar button behave identically. `play` holds the frozen
;; context being driven; context changes pause + reseek (see ::expand etc).
(defn- play-tick [] (defn- play-tick []
(when-let [{:keys [ctx segs fps idx]} @play] (when-let [{:keys [ctx segs fps idx]} @play]
@ -44,25 +48,34 @@
(when (> (js/Math.abs (- sf ns)) 1.5) ; only seek at a real discontinuity (when (> (js/Math.abs (- sf ns)) 1.5) ; only seek at a real discontinuity
(seek-video! fps ns)) (seek-video! fps ns))
(rf/dispatch [::events/set-playhead ctx nl])) (rf/dispatch [::events/set-playhead ctx nl]))
(stop-play!)) (.pause v)) ; end → pause event disengages
(rf/dispatch [::events/set-playhead ctx (+ ls (- sf ss))]))) (rf/dispatch [::events/set-playhead ctx (+ ls (- sf ss))])))
(when @play (reset! raf (js/requestAnimationFrame play-tick))))) (when @play (reset! raf (js/requestAnimationFrame play-tick)))))
(defn- start-play! [] (defn- engage-play!
(let [ctx @(rf/subscribe [::subs/context]) "Drive the playhead from the (already-playing) video, in the current context,
segs @(rf/subscribe [::subs/segments]) from wherever currentTime is. Idempotent."
[]
(let [segs @(rf/subscribe [::subs/segments])
ctx @(rf/subscribe [::subs/context])
fps @(rf/subscribe [::subs/fps]) fps @(rf/subscribe [::subs/fps])
ph @(rf/subscribe [::subs/playhead])] v @video-el]
(when (and @video-el (seq segs)) (when (and v (seq segs) (not @play))
(let [idx (seg-at segs ph) (reset! play {:ctx ctx :segs segs :fps fps :idx (seg-at-src segs (* (.-currentTime v) fps))})
{[ss _] :src [ls _] :local} (nth segs idx)] (rf/dispatch [::events/set-playing true])
(reset! play {:ctx ctx :segs segs :fps fps :idx idx}) (reset! raf (js/requestAnimationFrame play-tick)))))
(seek-video! fps (+ ss (- ph ls))) ; start from the playhead's source
(.play @video-el)
(rf/dispatch [::events/set-playing true])
(reset! raf (js/requestAnimationFrame play-tick))))))
(defn- toggle-play! [] (if @play (stop-play!) (start-play!))) (defn- disengage! []
(when @raf (js/cancelAnimationFrame @raf) (reset! raf nil))
(reset! play nil)
(rf/dispatch [::events/set-playing false]))
(defn- toggle-play! []
(when-let [v @video-el] (if (.-paused v) (.play v) (.pause v))))
;; effects the stack events use to keep the player on the current context
(rf/reg-fx :player/pause (fn [_] (when-let [v @video-el] (.pause v))))
(rf/reg-fx :player/seek (fn [secs] (when (and @video-el secs) (set! (.-currentTime @video-el) secs))))
(defn- goto! [local] (defn- goto! [local]
"Move the playhead to local frame `local` and seek the video to match." "Move the playhead to local frame `local` and seek the video to match."
@ -76,6 +89,8 @@
(defn video-monitor [] (defn video-monitor []
[:video {:src @(rf/subscribe [::subs/clip-url]) :controls true :preload "auto" [:video {:src @(rf/subscribe [::subs/clip-url]) :controls true :preload "auto"
:plays-inline true :webkit-playsinline "true" :plays-inline true :webkit-playsinline "true"
:on-play (fn [_] (engage-play!))
:on-pause (fn [_] (disengage!))
:ref (fn [n] (when n (reset! video-el n)))}]) :ref (fn [n] (when n (reset! video-el n)))}])
(defn- clip-label (defn- clip-label