fix: defer video seek until seekable, flush on loadedmetadata (iOS)

The previous fix still failed on iOS: at the first tap the element isn't
seekable (readyState 0), so the seek in engage-play! was dropped, and the
existing on-loadedmetadata seek is guarded by (when-not @play) — already true by
then — so it was skipped too, playing from 0. Now seek-video! stashes the target
when the element isn't seekable yet, and loadedmetadata/canplay flush it
(regardless of play state).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-06-30 11:02:54 -04:00
parent 5c58af9036
commit 7585dd4e7f

View file

@ -15,6 +15,7 @@
(defonce body-scroll-el (atom nil)) ; the vertical (tracks) scroll container (defonce body-scroll-el (atom nil)) ; the vertical (tracks) scroll container
(defonce playhead-el (atom nil)) ; the single full-height playhead overlay (defonce playhead-el (atom nil)) ; the single full-height playhead overlay
(defonce played-track (atom nil)) ; last track auto-scrolled into view during playback (defonce played-track (atom nil)) ; last track auto-scrolled into view during playback
(defonce pending-seek (atom nil)) ; seconds to seek to once the element is seekable (iOS)
(defonce play (atom nil)) ; {:ctx :segs :fps :idx} while playing, else nil (defonce play (atom nil)) ; {:ctx :segs :fps :idx} while playing, else nil
(defonce active-insert! (atom nil)) ; the live content-editor's (insert! link) fn (defonce active-insert! (atom nil)) ; the live content-editor's (insert! link) fn
(declare commit-link!) (declare commit-link!)
@ -67,7 +68,22 @@
(defn- seek-video! [fps source-frame] (defn- seek-video! [fps source-frame]
(when (and @video-el source-frame) (when (and @video-el source-frame)
(set! (.-currentTime @video-el) (/ source-frame fps)))) (let [v @video-el secs (/ source-frame fps)]
;; iOS only honours currentTime once the element is seekable (readyState
;; >= HAVE_METADATA). Before that — a deep link, or the very first tap that
;; triggers loading — stash the target and apply it on loadedmetadata.
(if (>= (.-readyState v) 1)
(do (reset! pending-seek nil) (set! (.-currentTime v) secs))
(reset! pending-seek secs)))))
(defn- flush-pending-seek!
"Apply a seek that was deferred while the element wasn't seekable. Returns true
if it did one."
[]
(when-let [secs @pending-seek]
(reset! pending-seek nil)
(when-let [v @video-el] (set! (.-currentTime v) secs))
true))
(defn- follow! (defn- follow!
([fps zoom playhead] (follow! fps zoom playhead false)) ([fps zoom playhead] (follow! fps zoom playhead false))
@ -219,13 +235,18 @@
;; (Re)loading a clip resets the element to currentTime 0, so a restored ;; (Re)loading a clip resets the element to currentTime 0, so a restored
;; / deep-linked playhead would show in the UI but play from 0. Seek the ;; / deep-linked playhead would show in the UI but play from 0. Seek the
;; element to the current playhead once its metadata is ready. ;; element to the current playhead once its metadata is ready.
;; metadata is now loaded ⇒ the element is finally seekable. Apply any
;; seek we deferred (the iOS deep-link / first-tap case); otherwise, if
;; we're not driving playback, align the element to the playhead.
:on-loaded-metadata :on-loaded-metadata
(fn [_] (fn [_]
(when-not @play (when-not (flush-pending-seek!)
(let [segs @(rf/subscribe [::subs/segments]) (when-not @play
fps @(rf/subscribe [::subs/fps]) (let [segs @(rf/subscribe [::subs/segments])
ph @(rf/subscribe [::subs/playhead])] fps @(rf/subscribe [::subs/fps])
(seek-video! fps (scene/local->source segs ph))))) ph @(rf/subscribe [::subs/playhead])]
(seek-video! fps (scene/local->source segs ph))))))
:on-can-play (fn [_] (flush-pending-seek!)) ; belt-and-suspenders for iOS
:on-play (fn [_] (engage-play!)) :on-play (fn [_] (engage-play!))
:on-pause (fn [_] (disengage!)) :on-pause (fn [_] (disengage!))
:ref (fn [n] (when n (reset! video-el n)))}]) :ref (fn [n] (when n (reset! video-el n)))}])