From d9059ad132395d9969479918b428cfa78530471f Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 30 Jun 2026 11:21:33 -0400 Subject: [PATCH] fix: start video at deep-linked time via #t= media fragment (iOS) The real iOS issue: it won't load the media until the first tap and ignores JS currentTime until the element is seekable (populated very late), so every load/seek hack started playback from 0. A #t= media fragment on the src is honoured by the media loader during the tap-triggered load itself, so it starts at the right frame (and renders that frame instead of black). Bake the deep-linked playhead into the src fragment, cached per clip so live playhead changes don't reload the element; currentTime still drives in-session seeks. Co-Authored-By: Claude Opus 4.8 --- tl/src/tl/views.cljs | 53 +++++++++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 15 deletions(-) diff --git a/tl/src/tl/views.cljs b/tl/src/tl/views.cljs index 0028b27..a4eaaaf 100644 --- a/tl/src/tl/views.cljs +++ b/tl/src/tl/views.cljs @@ -216,21 +216,44 @@ (scroll-to-seg-track! segs local))))))) (defn video-monitor [] - [:video {:src @(rf/subscribe [::subs/clip-url]) :controls true :preload "auto" - :plays-inline true :webkit-playsinline "true" - ;; (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 - ;; element to the current playhead once its metadata is ready. - :on-loaded-metadata - (fn [_] - (when-not @play - (let [segs @(rf/subscribe [::subs/segments]) - fps @(rf/subscribe [::subs/fps]) - ph @(rf/subscribe [::subs/playhead])] - (seek-video! fps (scene/local->source segs ph))))) - :on-play (fn [_] (engage-play!)) - :on-pause (fn [_] (disengage!)) - :ref (fn [n] (when n (reset! video-el n)))}]) + ;; The src carries a #t= media fragment for the deep-linked playhead. That is + ;; the only thing that reliably starts iOS at the right time: iOS won't load + ;; the media until the first tap and ignores JS currentTime until the element + ;; is seekable (which happens very late), but it honours #t= during the media + ;; load itself — so the tap loads-and-starts at the right frame. (#t= also + ;; nudges iOS to render that frame instead of a black box.) currentTime still + ;; drives all in-session seeks once the element is loaded. + ;; + ;; Cache the src per clip so live playhead changes don't rewrite the fragment + ;; and reload the element — the fragment only matters at (re)load time. + (let [cache (atom nil)] ; {:clip url :src "url#t=secs"} + (fn [] + (let [clip @(rf/subscribe [::subs/clip-url]) + src (cond + (nil? clip) nil + (= clip (:clip @cache)) (:src @cache) + :else + (let [segs @(rf/subscribe [::subs/segments]) + fps @(rf/subscribe [::subs/fps]) + ph @(rf/subscribe [::subs/playhead]) + t (max 0.001 (/ (scene/local->source segs ph) fps)) + s (str clip "#t=" (.toFixed t 3))] + (reset! cache {:clip clip :src s}) + s))] + [:video {:src src :controls true :preload "auto" + :plays-inline true :webkit-playsinline "true" + ;; desktop / once-loaded: align to the playhead when metadata is + ;; ready (iOS already arrived there via the #t= fragment). + :on-loaded-metadata + (fn [_] + (when-not @play + (let [segs @(rf/subscribe [::subs/segments]) + fps @(rf/subscribe [::subs/fps]) + ph @(rf/subscribe [::subs/playhead])] + (seek-video! fps (scene/local->source segs ph))))) + :on-play (fn [_] (engage-play!)) + :on-pause (fn [_] (disengage!)) + :ref (fn [n] (when n (reset! video-el n)))}])))) (defn- clip-label "Track-local clip label for the content segment whose :mark is `mid`."