@bycrux/editor 0.6.3 → 0.6.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bycrux/editor",
3
- "version": "0.6.3",
3
+ "version": "0.6.4",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -577,7 +577,7 @@ function ReviewSurface<P extends Project>({
577
577
  right below, one column — not a separate assets column. */}
578
578
  {(adapter.listVersionHistory || slots?.runHistory ||
579
579
  (assetsPlacement === 'sidebar' && slots?.assetsPanel)) && (
580
- <div className={`${assetsPlacement === 'sidebar' ? 'w-64' : 'w-48'} shrink-0 border-l border-[var(--editor-border)] bg-[var(--editor-surface)] flex flex-col overflow-hidden`}>
580
+ <div className={`${assetsPlacement === 'sidebar' ? 'w-56' : 'w-48'} shrink-0 border-l border-[var(--editor-border)] bg-[var(--editor-surface)] flex flex-col overflow-hidden`}>
581
581
  {adapter.listVersionHistory && (
582
582
  <VersionPanel versions={versions} restoring={restoring} onRestore={handleRestoreVersion} />
583
583
  )}
@@ -63,7 +63,13 @@ export default function CaptionPreview({ track, currentTime, fps, compileOverlay
63
63
  : null
64
64
 
65
65
  return (
66
- <div ref={wrapRef} className="absolute inset-0 pointer-events-none overflow-hidden">
66
+ // zIndex 45 keeps captions above the active <video> (z 1) and overlay items
67
+ // (z `trackIdx + 12`, ≈12–20) — mirroring the final render, where the caption
68
+ // track composites on top — while staying below the editing affordances
69
+ // (selection handles z 50, play button z 100). Without an explicit z-index the
70
+ // root sits at `auto`, so the opaque active video paints over it and captions
71
+ // never appear in the preview.
72
+ <div ref={wrapRef} className="absolute inset-0 pointer-events-none overflow-hidden" style={{ zIndex: 45 }}>
67
73
  <OverlayErrorBoundary label={`caption: ${track.style}`} resetKey={track.style}>
68
74
  {element && scale !== null && (
69
75
  <div style={{
@@ -214,6 +214,18 @@ export function useVideoPlayback(
214
214
  }
215
215
  }
216
216
 
217
+ // Start playback on a slot during an automated clip-boundary switch (context
218
+ // already running — no gesture needed). If the slot isn't buffered yet (a slow
219
+ // non-faststart HEVC tail-moov fetch can still be in flight), play() may reject
220
+ // with AbortError; retry on `canplay` so the switch never dead-stops at the cut.
221
+ function playSoon(video: HTMLVideoElement) {
222
+ const p = video.play()
223
+ if (p) p.catch(() => {
224
+ const onCanPlay = () => { video.removeEventListener('canplay', onCanPlay); video.play().catch(() => {}) }
225
+ video.addEventListener('canplay', onCanPlay)
226
+ })
227
+ }
228
+
217
229
  function ensureVideoGain(slot: 0 | 1): GainNode | null {
218
230
  if (videoGainRef.current[slot]) return videoGainRef.current[slot]
219
231
  const video = slot === 0 ? video0Ref.current : video1Ref.current
@@ -569,7 +581,7 @@ export function useVideoPlayback(
569
581
  if (preloadSrcRef.current !== src) { nv.src = src; nv.currentTime = effectiveInPoint(nc) }
570
582
  const gain = ensureVideoGain(ns)
571
583
  if (gain) gain.gain.value = nc.muted ? 0 : (nc.volume ?? 1)
572
- nv.play().catch(() => {})
584
+ playSoon(nv)
573
585
  }
574
586
  void (activeSlotRef.current === 0 ? video0Ref.current : video1Ref.current)?.pause()
575
587
  activeSlotRef.current = ns
@@ -659,21 +671,26 @@ export function useVideoPlayback(
659
671
  const clipInPoint = effectiveInPoint(clip)
660
672
  const outPoint = effectiveOutPoint(clip) ?? clip.end - clip.start + clipInPoint
661
673
 
662
- // Preload next clip into inactive slot ~1s before end
663
- const timeLeft = outPoint - video.currentTime
664
- if (timeLeft < 1.0) {
665
- const nextIdx = activeIdxRef.current + 1
666
- if (nextIdx < clips.length && clips[nextIdx].src) {
667
- const inactiveVideo = slot === 0 ? video1Ref.current : video0Ref.current
668
- const nextSrc = fileUrlRef.current(playbackSrcFor(clips[nextIdx]))
669
- if (inactiveVideo && preloadSrcRef.current !== nextSrc) {
670
- preloadSrcRef.current = nextSrc
671
- inactiveVideo.src = nextSrc
672
- inactiveVideo.currentTime = effectiveInPoint(clips[nextIdx])
673
- const inactiveSlot = (1 - slot) as 0 | 1
674
- const nextGain = ensureVideoGain(inactiveSlot)
675
- if (nextGain) nextGain.gain.value = clips[nextIdx].muted ? 0 : (clips[nextIdx].volume ?? 1)
676
- }
674
+ // Preload the next clip into the inactive slot as early as possible. The
675
+ // source files are large 4K 10-bit HEVC with the moov atom at the END of the
676
+ // file (not web-faststart), so the browser needs a slow tail range-fetch to
677
+ // index and seek before it can decode. The old "~1s before end" lead was far
678
+ // too short: at a cross-source cut the next slot wasn't ready and play()
679
+ // stalled, freezing playback at the boundary. (Same-source cuts hid the bug —
680
+ // the moov was already cached from the active slot.) Give the load the whole
681
+ // current clip as runway instead; the preloadSrcRef guard keeps it idempotent
682
+ // and a scrub clears it.
683
+ const nextIdx = activeIdxRef.current + 1
684
+ if (nextIdx < clips.length && clips[nextIdx].src) {
685
+ const inactiveVideo = slot === 0 ? video1Ref.current : video0Ref.current
686
+ const nextSrc = fileUrlRef.current(playbackSrcFor(clips[nextIdx]))
687
+ if (inactiveVideo && preloadSrcRef.current !== nextSrc) {
688
+ preloadSrcRef.current = nextSrc
689
+ inactiveVideo.src = nextSrc
690
+ inactiveVideo.currentTime = effectiveInPoint(clips[nextIdx])
691
+ const inactiveSlot = (1 - slot) as 0 | 1
692
+ const nextGain = ensureVideoGain(inactiveSlot)
693
+ if (nextGain) nextGain.gain.value = clips[nextIdx].muted ? 0 : (clips[nextIdx].volume ?? 1)
677
694
  }
678
695
  }
679
696
 
@@ -723,7 +740,7 @@ export function useVideoPlayback(
723
740
  }
724
741
  const nextGain = ensureVideoGain(nextSlot)
725
742
  if (nextGain) nextGain.gain.value = next.muted ? 0 : (next.volume ?? 1)
726
- nextVideo.play().catch(() => {})
743
+ playSoon(nextVideo)
727
744
  }
728
745
 
729
746
  activeSlotRef.current = nextSlot