fix: thumbnail parser overflow, autostop-killed jobs, unified playhead
Thumbnails failed immediately on any clip past ~2 min. The one-pass select
expression joined frames with a flat a+b+c+... chain, which overflows ffmpeg's
recursive expression parser past ~120 terms ("Error while parsing expression").
Join the same terms as a balanced tree (depth O(log N)) instead — identical
selection, still one decode pass, exact per-tile frames preserved.
- generate_thumbnails management command: runs synchronously and streams each
phase to stdout, so jobs are watchable without tailing app logs
- surface ffmpeg's stderr instead of swallowing it in CalledProcessError
- fly: min_machines_running = 1. Background thumbnail jobs outlive the HTTP
request that triggers them, so scale-to-zero let the proxy autostop the VM
mid-job and leave projects stuck "running"
- timeline: replace the two split playhead bars (pinned lanes vs scrolling
tracks) with one full-height overlay, positioned in JS to track horizontal
scroll and sit above the translucent header
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
20ae2b4316
commit
28aafc047f
5 changed files with 135 additions and 17 deletions
|
|
@ -364,8 +364,11 @@ html.dark .timeline-head {
|
|||
1px 1px 0 var(--paper), -1px -1px 0 var(--paper);
|
||||
}
|
||||
|
||||
.playhead { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--ink);
|
||||
z-index: 10; pointer-events: none; }
|
||||
/* one full-height playhead spanning both the pinned lanes and the scrolling
|
||||
tracks; positioned in JS (see position-playhead!) so it tracks horizontal
|
||||
scroll while sitting above the translucent header. */
|
||||
.timeline-playhead { position: absolute; top: 0; bottom: 0; width: 2px; background: var(--ink);
|
||||
z-index: 14; pointer-events: none; }
|
||||
.playhead-handle { position: absolute; top: 0; left: -5px; width: 12px; height: 10px;
|
||||
background: var(--ink); border-radius: 0; }
|
||||
|
||||
|
|
@ -379,7 +382,6 @@ html.dark .timeline-head {
|
|||
.ann-bar-label { position: absolute; top: 1px; font-family: var(--chicago); font-size: 9px;
|
||||
line-height: 16px; color: var(--ink); white-space: nowrap;
|
||||
pointer-events: none; }
|
||||
.lane-playhead { bottom: 0; }
|
||||
|
||||
/* --- mobile: stack the top region vertically (video over annotation) ---- */
|
||||
@media (max-width: 720px) {
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@
|
|||
(defonce video-el (atom nil))
|
||||
(defonce raf (atom nil))
|
||||
(defonce scroll-el (atom nil))
|
||||
(defonce playhead-el (atom nil)) ; the single full-height playhead overlay
|
||||
(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
|
||||
(declare commit-link!)
|
||||
|
|
@ -71,6 +72,23 @@
|
|||
(let [x (px playhead fps zoom)]
|
||||
(set! (.-scrollLeft el) (max 0 (- x (/ (.-clientWidth el) 2)))))))
|
||||
|
||||
;; The playhead is a single full-height overlay on `.timeline` (spanning the
|
||||
;; pinned annotation lanes AND the scrolling tracks as one line) rather than a
|
||||
;; bar per region. It lives outside the horizontal scroller, so we track the
|
||||
;; scroller's scrollLeft/offsetLeft by hand and hide it once it slips under the
|
||||
;; track-name gutter or past the right edge.
|
||||
(defn- position-playhead! [fps zoom playhead]
|
||||
(when-let [ph @playhead-el]
|
||||
(if-let [sc @scroll-el]
|
||||
(let [x (- (px playhead fps zoom) (.-scrollLeft sc))
|
||||
gutter (.-offsetLeft sc)
|
||||
view (.-clientWidth sc)]
|
||||
(if (and (>= x 0) (<= x view))
|
||||
(do (set! (.. ph -style -left) (str (+ gutter x) "px"))
|
||||
(set! (.. ph -style -display) "block"))
|
||||
(set! (.. ph -style -display) "none")))
|
||||
(set! (.. ph -style -display) "none"))))
|
||||
|
||||
(defn- seg-at
|
||||
"Index of the segment whose LOCAL range contains `local` (0 if none)."
|
||||
[segs local]
|
||||
|
|
@ -271,7 +289,10 @@
|
|||
tracks-h (* row-h (count tracks))
|
||||
track-y (into {} (map-indexed (fn [i t] [(:id t) i]) tracks))]
|
||||
(when playing? (r/after-render #(follow! fps zoom playhead)))
|
||||
(r/after-render #(position-playhead! fps zoom playhead))
|
||||
[:div.timeline {:class (when-not @tracks-open? "tracks-collapsed")}
|
||||
[:div.playhead.timeline-playhead {:ref (fn [n] (reset! playhead-el n))}
|
||||
[:div.playhead-handle]]
|
||||
[:div.timeline-head
|
||||
[:div.gutter-lanes {:style {:height lane-h}}
|
||||
[:button.track-toggle {:title (if @tracks-open? "Collapse track names" "Show track names")
|
||||
|
|
@ -280,8 +301,6 @@
|
|||
[:div.lanes-hscroll {:ref (fn [n] (reset! lanes-scroll n))}
|
||||
[:div.ann-lanes {:style {:height lane-h :width width}
|
||||
:on-mouse-down #(scrub! @content fps zoom %)}
|
||||
[:div.playhead.lane-playhead {:style {:left (px playhead fps zoom)}}
|
||||
[:div.playhead-handle]]
|
||||
;; annotation bars — solid for saved, dashed for the in-progress draft
|
||||
(for [[i a] (map-indexed vector anns)
|
||||
[j [lo hi]] (map-indexed vector (:bars a))]
|
||||
|
|
@ -310,13 +329,13 @@
|
|||
[:div.gutter-label {:style {:height row-h :line-height (str (dec row-h) "px")}}
|
||||
(:name t)])]
|
||||
[:div.hscroll {:ref (fn [n] (reset! scroll-el n))
|
||||
:on-scroll #(when @lanes-scroll
|
||||
(set! (.-scrollLeft @lanes-scroll)
|
||||
(.. % -target -scrollLeft)))}
|
||||
:on-scroll #(let [sl (.. % -target -scrollLeft)]
|
||||
(when @lanes-scroll
|
||||
(set! (.-scrollLeft @lanes-scroll) sl))
|
||||
(position-playhead! fps zoom playhead))}
|
||||
[:div.content.track-content {:ref (fn [n] (reset! content n))
|
||||
:on-mouse-down #(scrub! @content fps zoom %)
|
||||
:style {:width width :height tracks-h}}
|
||||
[:div.playhead {:style {:left (px playhead fps zoom)}}]
|
||||
(for [[i t] (map-indexed vector tracks)]
|
||||
^{:key (str "lane-" (:id t))}
|
||||
[:div.track-lane {:style {:top (* i row-h) :width width :height row-h}}])
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue