@bycrux/editor 0.11.2 → 1.0.1
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 +3 -1
- package/src/ControlsInfoModal.tsx +251 -61
- package/src/__tests__/ControlsInfoModal.test.tsx +43 -0
- package/src/__tests__/adapter.test.ts +59 -1
- package/src/__tests__/schema-assignability.test.ts +93 -0
- package/src/__tests__/schema.test.ts +15 -0
- package/src/__tests__/video-adapter-contract.test.ts +128 -5
- package/src/carousel/AddElementMenu.tsx +10 -4
- package/src/carousel/CarouselEditor.tsx +46 -8
- package/src/carousel/CarouselRenderModal.tsx +15 -10
- package/src/carousel/OverlayPicker.tsx +10 -3
- package/src/carousel/SlidePropertyPanel.tsx +43 -21
- package/src/components/FilmstripScrubber.tsx +277 -0
- package/src/components/__tests__/FilmstripScrubber.test.tsx +216 -0
- package/src/engine/__tests__/audio-clock.test.ts +655 -0
- package/src/engine/__tests__/audio-worklet-source.test.ts +316 -0
- package/src/engine/__tests__/batch-planner.test.ts +298 -0
- package/src/engine/__tests__/decode-worker-source.test.ts +249 -0
- package/src/engine/__tests__/demux-ranged.test.ts +495 -0
- package/src/engine/__tests__/demux-truncated.test.ts +136 -0
- package/src/engine/__tests__/demux.test.ts +305 -0
- package/src/engine/__tests__/eligibility.test.ts +146 -0
- package/src/engine/__tests__/engine-recovery.test.ts +263 -0
- package/src/engine/__tests__/engine.test.ts +326 -0
- package/src/engine/__tests__/frame-server-ranged.test.ts +261 -0
- package/src/engine/__tests__/frame-server.test.ts +326 -0
- package/src/engine/__tests__/media-loader-ranged.test.ts +289 -0
- package/src/engine/__tests__/media-loader.test.ts +100 -0
- package/src/engine/__tests__/scheduler-crop.test.ts +353 -0
- package/src/engine/__tests__/scheduler.test.ts +1310 -0
- package/src/engine/__tests__/scrub-resolve.test.ts +96 -0
- package/src/engine/__tests__/scrub-source.test.ts +195 -0
- package/src/engine/__tests__/source-host.test.ts +455 -0
- package/src/engine/__tests__/time-stretch.test.ts +182 -0
- package/src/engine/audio-clock.ts +1179 -0
- package/src/engine/audio-worklet-source.ts +160 -0
- package/src/engine/batch-planner.ts +308 -0
- package/src/engine/debug-hud.tsx +54 -0
- package/src/engine/decode-worker-source.ts +142 -0
- package/src/engine/demux.ts +900 -0
- package/src/engine/eligibility.ts +140 -0
- package/src/engine/frame-server.ts +644 -0
- package/src/engine/index.ts +950 -0
- package/src/engine/media-loader.ts +380 -0
- package/src/engine/mp4box.d.ts +214 -0
- package/src/engine/scheduler.ts +1324 -0
- package/src/engine/scrub-resolve.ts +66 -0
- package/src/engine/scrub-source.ts +496 -0
- package/src/engine/time-stretch.ts +224 -0
- package/src/index.ts +83 -1
- package/src/preview/OverlayPreview.tsx +2 -24
- package/src/schema.ts +146 -3
- package/src/state/__tests__/use-project-sync.test.tsx +56 -0
- package/src/state/use-project-sync.ts +17 -0
- package/src/test-setup.ts +32 -0
- package/src/text/FontPicker.tsx +85 -51
- package/src/text/TextFormattingToolbar.tsx +6 -1
- package/src/text/__tests__/FontPicker.options.test.ts +43 -0
- package/src/text/__tests__/TextFormattingToolbar.test.tsx +4 -4
- package/src/theme.ts +108 -3
- package/src/types.ts +601 -22
- package/src/ui/Loader.tsx +64 -0
- package/src/ui/NumberField.tsx +254 -0
- package/src/ui/Slider.tsx +128 -0
- package/src/ui/Tooltip.tsx +118 -0
- package/src/ui/__tests__/NumberField.test.tsx +298 -0
- package/src/ui/__tests__/Slider.test.tsx +150 -0
- package/src/ui/__tests__/Tooltip.test.tsx +105 -0
- package/src/ui/__tests__/usePersistentState.test.tsx +112 -0
- package/src/ui/badge.tsx +12 -4
- package/src/ui/index.ts +6 -0
- package/src/ui/input.tsx +1 -1
- package/src/ui/select.tsx +1 -1
- package/src/ui/switch.tsx +12 -2
- package/src/ui/textarea.tsx +1 -1
- package/src/ui/usePersistentState.ts +63 -0
- package/src/video/AudioPolishModal.tsx +983 -0
- package/src/video/CaptionListPanel.test.tsx +944 -0
- package/src/video/CaptionListPanel.tsx +1106 -0
- package/src/video/CaptionRegenModal.tsx +37 -9
- package/src/video/CaptionSpecimen.tsx +160 -0
- package/src/video/CaptionStyleGallery.tsx +466 -0
- package/src/video/CommandPalette.tsx +165 -0
- package/src/video/ImageToneMenu.tsx +137 -0
- package/src/video/OverlayInspector.tsx +977 -0
- package/src/video/RenderModal.tsx +1035 -62
- package/src/video/VersionCompare.tsx +258 -0
- package/src/video/VersionPanel.tsx +117 -42
- package/src/video/VideoEditor.tsx +2229 -243
- package/src/video/__tests__/AudioPolishModal.test.tsx +825 -0
- package/src/video/__tests__/CaptionListPanel.font.test.tsx +397 -0
- package/src/video/__tests__/CaptionListPanel.generate.test.tsx +153 -0
- package/src/video/__tests__/CaptionRegenModal.test.tsx +29 -0
- package/src/video/__tests__/CaptionSpecimen.test.tsx +213 -0
- package/src/video/__tests__/CaptionStyleGallery.test.tsx +362 -0
- package/src/video/__tests__/CommandPalette.test.tsx +119 -0
- package/src/video/__tests__/OverlayInspector.test.tsx +1367 -0
- package/src/video/__tests__/RenderModal.exportControls.test.tsx +319 -0
- package/src/video/__tests__/RenderModal.options.test.tsx +562 -0
- package/src/video/__tests__/RenderModal.progress.test.ts +65 -0
- package/src/video/__tests__/VersionCompare.test.tsx +182 -0
- package/src/video/__tests__/VersionPanel.test.tsx +279 -0
- package/src/video/__tests__/VideoEditor.audioPolish.test.tsx +241 -0
- package/src/video/__tests__/VideoEditor.captionDelete.test.tsx +147 -0
- package/src/video/__tests__/VideoEditor.captionGesture.test.tsx +170 -0
- package/src/video/__tests__/VideoEditor.captionSeam.test.tsx +134 -0
- package/src/video/__tests__/VideoEditor.clipKeyframes.test.tsx +59 -0
- package/src/video/__tests__/VideoEditor.context.test.tsx +44 -0
- package/src/video/__tests__/VideoEditor.editFocus.test.tsx +55 -0
- package/src/video/__tests__/VideoEditor.keymap.test.tsx +725 -0
- package/src/video/__tests__/VideoEditor.layout.test.tsx +338 -0
- package/src/video/__tests__/VideoEditor.propertiesPanel.test.tsx +765 -0
- package/src/video/__tests__/VideoEditor.rippleDeleteCaptions.test.tsx +159 -0
- package/src/video/__tests__/VideoEditor.sourcePreview.test.tsx +122 -0
- package/src/video/__tests__/VideoEditor.test.tsx +403 -50
- package/src/video/__tests__/audioMagnet.test.ts +158 -0
- package/src/video/__tests__/audioPolish.test.ts +1202 -0
- package/src/video/__tests__/captionActiveWord.test.ts +107 -0
- package/src/video/__tests__/captionLanes.test.ts +262 -0
- package/src/video/__tests__/captionPositioning.test.tsx +8 -3
- package/src/video/__tests__/captionWordFloor.test.ts +228 -0
- package/src/video/__tests__/clipboard-ops.test.ts +430 -0
- package/src/video/__tests__/cuts.insert.test.ts +244 -0
- package/src/video/__tests__/cuts.test.ts +1213 -34
- package/src/video/__tests__/export-limits.test.ts +195 -0
- package/src/video/__tests__/hover-scrub.test.ts +163 -0
- package/src/video/__tests__/keyframeOps.canKeyframeProp.test.ts +61 -0
- package/src/video/__tests__/keyframeOps.test.ts +643 -0
- package/src/video/__tests__/keymap.test.tsx +171 -0
- package/src/video/__tests__/render-progress.test.tsx +20 -4
- package/src/video/__tests__/shuttle.test.ts +210 -0
- package/src/video/__tests__/source-preview.test.ts +60 -0
- package/src/video/__tests__/timecode.test.ts +77 -0
- package/src/video/__tests__/use-report-context.test.tsx +101 -0
- package/src/video/audioMagnet.ts +72 -0
- package/src/video/audioPolish.ts +774 -0
- package/src/video/captionActiveWord.ts +74 -0
- package/src/video/captionLanes.ts +202 -0
- package/src/video/captionRepair.ts +9 -5
- package/src/video/captionStyleDefaults.ts +100 -0
- package/src/video/captionWordFloor.ts +83 -0
- package/src/video/clipboard-ops.ts +377 -0
- package/src/video/cuts.ts +713 -37
- package/src/video/export-limits.ts +102 -0
- package/src/video/hover-scrub.ts +102 -0
- package/src/video/imageTone.ts +58 -0
- package/src/video/imageToneExamples.ts +10 -0
- package/src/video/keyframeOps.ts +384 -0
- package/src/video/keymap.ts +146 -0
- package/src/video/panels/ClipPropertiesPanel.tsx +706 -0
- package/src/video/panels/LeftPanelTabs.tsx +187 -0
- package/src/video/panels/OverlayContentPanel.tsx +351 -0
- package/src/video/panels/TabNav.tsx +60 -0
- package/src/video/panels/__tests__/ClipPropertiesPanel.test.tsx +645 -0
- package/src/video/panels/__tests__/LeftPanelTabs.test.tsx +189 -0
- package/src/video/panels/__tests__/OverlayContentPanel.test.tsx +222 -0
- package/src/video/panels/__tests__/TabNav.test.tsx +71 -0
- package/src/video/preview/CaptionPreview.tsx +57 -69
- package/src/video/preview/EngineSurface.tsx +103 -0
- package/src/video/preview/OverlayItemsLayer.tsx +315 -103
- package/src/video/preview/PreviewPlayer.tsx +337 -56
- package/src/video/preview/SocialPreviewMenu.tsx +214 -0
- package/src/video/preview/SocialSafeZoneOverlay.tsx +478 -0
- package/src/video/preview/__tests__/CaptionPreview.fonts.test.tsx +97 -0
- package/src/video/preview/__tests__/EngineSurface.test.tsx +114 -0
- package/src/video/preview/__tests__/OverlayItemsLayer.edit.test.tsx +4 -2
- package/src/video/preview/__tests__/OverlayItemsLayer.keyframes.test.tsx +363 -0
- package/src/video/preview/__tests__/OverlayItemsLayer.selection.test.tsx +329 -0
- package/src/video/preview/__tests__/PreviewPlayer.engine.test.tsx +139 -0
- package/src/video/preview/__tests__/SocialPreviewMenu.test.tsx +121 -0
- package/src/video/preview/__tests__/SocialSafeZoneOverlay.test.tsx +165 -0
- package/src/video/preview/__tests__/captionDragState.test.ts +120 -1
- package/src/video/preview/__tests__/latencyCompensation.test.tsx +451 -0
- package/src/video/preview/__tests__/proxySupport.test.ts +75 -0
- package/src/video/preview/__tests__/transformStyle.test.ts +29 -1
- package/src/video/preview/__tests__/useDragOverlay.perAxis.test.ts +197 -0
- package/src/video/preview/__tests__/useEnginePlayback.test.tsx +530 -0
- package/src/video/preview/__tests__/useVideoPlayback.corpus.test.ts +313 -0
- package/src/video/preview/__tests__/useVideoPlayback.test.ts +38 -5
- package/src/video/preview/__tests__/useVideoPlayback.trackAudio.test.ts +278 -0
- package/src/video/preview/audio-context.ts +111 -0
- package/src/video/preview/captionDragState.ts +91 -1
- package/src/video/preview/proxySupport.ts +86 -0
- package/src/video/preview/transformStyle.ts +22 -12
- package/src/video/preview/useDragOverlay.ts +92 -18
- package/src/video/preview/useEnginePlayback.ts +625 -0
- package/src/video/preview/useVideoPlayback.ts +211 -167
- package/src/video/sdrCurves.ts +56 -0
- package/src/video/shuttle.ts +159 -0
- package/src/video/source-preview.ts +66 -0
- package/src/video/timecode.ts +59 -0
- package/src/video/timeline/EditableSegment.tsx +1 -1
- package/src/video/timeline/Scrubber.tsx +46 -174
- package/src/video/timeline/SpeedControl.tsx +95 -0
- package/src/video/timeline/Timeline.tsx +1061 -328
- package/src/video/timeline/TimelineContext.ts +17 -10
- package/src/video/timeline/TrackGutter.tsx +560 -0
- package/src/video/timeline/TrackSettingsPopover.tsx +228 -0
- package/src/video/timeline/VolumeControl.tsx +113 -0
- package/src/video/timeline/__tests__/Timeline.backgroundClick.test.tsx +110 -0
- package/src/video/timeline/__tests__/Timeline.crossfade.test.tsx +104 -0
- package/src/video/timeline/__tests__/Timeline.fadeCurveMenu.test.tsx +174 -0
- package/src/video/timeline/__tests__/Timeline.keyframeDelete.test.tsx +273 -0
- package/src/video/timeline/__tests__/Timeline.keyframeFollow.test.tsx +215 -0
- package/src/video/timeline/__tests__/Timeline.keyframeMenu.test.tsx +253 -0
- package/src/video/timeline/__tests__/Timeline.keymap.test.tsx +372 -0
- package/src/video/timeline/__tests__/Timeline.subcutRegen.test.tsx +351 -0
- package/src/video/timeline/__tests__/TrackGutter.test.tsx +372 -0
- package/src/video/timeline/__tests__/_canvasSelect.test.tsx +273 -0
- package/src/video/timeline/__tests__/_canvasSelect.ts +414 -0
- package/src/video/timeline/__tests__/dragdrop-math.test.ts +135 -0
- package/src/video/timeline/__tests__/effectiveItemAudio.test.ts +50 -0
- package/src/video/timeline/__tests__/enabledTrackItems.test.ts +164 -0
- package/src/video/timeline/__tests__/moveItemAcrossTracks.test.ts +363 -0
- package/src/video/timeline/__tests__/multiSelectOps.test.ts +447 -0
- package/src/video/timeline/__tests__/placement.test.ts +278 -0
- package/src/video/timeline/__tests__/resizeWindowedItem.test.ts +140 -0
- package/src/video/timeline/__tests__/timeline-model.test.ts +576 -0
- package/src/video/timeline/__tests__/visualItemLabel.test.ts +69 -0
- package/src/video/timeline/canvas/TimelineCanvas.tsx +1447 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.drop.test.tsx +439 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.edgeScroll.test.tsx +346 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.panefill.test.tsx +122 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.pendingDrops.test.tsx +316 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.pointer.test.tsx +606 -0
- package/src/video/timeline/canvas/__tests__/TimelineCanvas.test.tsx +407 -0
- package/src/video/timeline/canvas/__tests__/clip-bands.test.ts +77 -0
- package/src/video/timeline/canvas/__tests__/draw.test.ts +2198 -0
- package/src/video/timeline/canvas/__tests__/fade-curve.test.ts +187 -0
- package/src/video/timeline/canvas/__tests__/filmstrips.test.ts +561 -0
- package/src/video/timeline/canvas/__tests__/hit-test.test.ts +818 -0
- package/src/video/timeline/canvas/__tests__/pending-drop.test.ts +210 -0
- package/src/video/timeline/canvas/__tests__/pointer-machine.test.ts +3358 -0
- package/src/video/timeline/canvas/__tests__/snap.test.ts +257 -0
- package/src/video/timeline/canvas/__tests__/viewport.test.ts +399 -0
- package/src/video/timeline/canvas/__tests__/waveforms.test.ts +946 -0
- package/src/video/timeline/canvas/clip-bands.ts +56 -0
- package/src/video/timeline/canvas/draw.ts +2187 -0
- package/src/video/timeline/canvas/fade-curve.ts +111 -0
- package/src/video/timeline/canvas/filmstrips.ts +418 -0
- package/src/video/timeline/canvas/hit-test.ts +501 -0
- package/src/video/timeline/canvas/keyframe-strip.ts +73 -0
- package/src/video/timeline/canvas/pointer-machine.ts +1828 -0
- package/src/video/timeline/canvas/snap.ts +232 -0
- package/src/video/timeline/canvas/viewport.ts +457 -0
- package/src/video/timeline/canvas/waveforms.ts +664 -0
- package/src/video/timeline/makeCaptionEdit.ts +5 -1
- package/src/video/timeline/multiSelectOps.ts +214 -55
- package/src/video/timeline/placement.ts +282 -0
- package/src/video/timeline/timeline-model.ts +919 -0
- package/src/video/timeline/useItemDragDrop.ts +151 -178
- package/src/video/timeline/useTimelineZoom.ts +25 -60
- package/src/video/timeline/utils.ts +0 -12
- package/src/video/use-report-context.ts +75 -0
- package/src/video/preview/OverlayPropsModal.tsx +0 -292
- package/src/video/preview/__tests__/OverlayPropsModal.test.tsx +0 -32
- package/src/video/timeline/AudioTrackRow.tsx +0 -404
- package/src/video/timeline/AudioWaveformLayer.tsx +0 -117
- package/src/video/timeline/CaptionTrackRow.tsx +0 -235
- package/src/video/timeline/PlayheadLine.tsx +0 -18
- package/src/video/timeline/TranscriptModal.tsx +0 -70
- package/src/video/timeline/TranscriptPanel.tsx +0 -273
- package/src/video/timeline/VisualTrackRow.tsx +0 -300
- package/src/video/timeline/__tests__/CaptionTrackRow.test.tsx +0 -241
- package/src/video/timeline/__tests__/PlayheadLine.test.tsx +0 -60
- package/src/video/timeline/__tests__/TranscriptModal.test.tsx +0 -41
- package/src/video/timeline/__tests__/TranscriptPanel.test.tsx +0 -184
- package/src/video/timeline/__tests__/useItemDragDrop.test.ts +0 -72
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The scrubber's resolver mirrors `planTick`'s track-0 precedence, so its
|
|
3
|
+
* null branches are the ones worth pinning: gap, canvas project, missing
|
|
4
|
+
* proxy, higher-precedence src the engine can't decode. The happy path
|
|
5
|
+
* (proxied track-0 clip) is covered indirectly by `scheduler.test.ts` and
|
|
6
|
+
* directly by the T5 wiring tests.
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it } from 'vitest'
|
|
9
|
+
import type { EditorProject as Project, VisualItem } from '../../schema'
|
|
10
|
+
import { createScrubResolver } from '../scrub-resolve'
|
|
11
|
+
|
|
12
|
+
function clip(id: string, start: number, end: number, extra: Partial<VisualItem> = {}): VisualItem {
|
|
13
|
+
return {
|
|
14
|
+
id,
|
|
15
|
+
type: 'video',
|
|
16
|
+
src: `/media/${id}.mov`,
|
|
17
|
+
proxySrc: `/proxies/${id}_proxy.mp4`,
|
|
18
|
+
start,
|
|
19
|
+
end,
|
|
20
|
+
inPoint: 0,
|
|
21
|
+
outPoint: end - start,
|
|
22
|
+
...extra,
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function overlay(id: string, start: number, end: number): VisualItem {
|
|
27
|
+
return { id, type: 'overlay', src: `/overlays/${id}.jsx`, start, end }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function project(track0: VisualItem[], overlays: VisualItem[] = []): Project {
|
|
31
|
+
return {
|
|
32
|
+
id: 'p1',
|
|
33
|
+
status: 'draft',
|
|
34
|
+
settings: { resolution: [1080, 1920], fps: 30 },
|
|
35
|
+
tracks: overlays.length > 0 ? [track0, overlays] : [track0],
|
|
36
|
+
} as unknown as Project
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
describe('createScrubResolver', () => {
|
|
40
|
+
it('returns null when the getter yields no project', () => {
|
|
41
|
+
const resolve = createScrubResolver(() => null)
|
|
42
|
+
expect(resolve(0)).toBeNull()
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
it('returns null in a gap between track-0 clips', () => {
|
|
46
|
+
const p = project([clip('a', 0, 2), clip('b', 5, 7)])
|
|
47
|
+
const resolve = createScrubResolver(() => p)
|
|
48
|
+
expect(resolve(3)).toBeNull()
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
it('returns null on a canvas project (no track-0 video items)', () => {
|
|
52
|
+
const p = project([], [overlay('o', 0, 5)])
|
|
53
|
+
const resolve = createScrubResolver(() => p)
|
|
54
|
+
expect(resolve(1)).toBeNull()
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('returns null for a clip with no proxySrc yet', () => {
|
|
58
|
+
const p = project([clip('a', 0, 4, { proxySrc: undefined })])
|
|
59
|
+
const resolve = createScrubResolver(() => p)
|
|
60
|
+
expect(resolve(1)).toBeNull()
|
|
61
|
+
})
|
|
62
|
+
|
|
63
|
+
it('returns null when a higher-precedence preview src outranks the proxy', () => {
|
|
64
|
+
// `nobg_preview_src` beats `proxySrc` in the preview chain; the engine's
|
|
65
|
+
// demuxer is MP4-only, so `engineSrcFor` blocks it and the scrubber goes
|
|
66
|
+
// silent — same rule that keeps the whole project on <video> fallback.
|
|
67
|
+
const p = project([clip('a', 0, 4, { nobg_preview_src: '/proxies/a_nobg.webm' })])
|
|
68
|
+
const resolve = createScrubResolver(() => p)
|
|
69
|
+
expect(resolve(1)).toBeNull()
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('resolves to the proxy src and source-media time on the happy path', () => {
|
|
73
|
+
const c = clip('a', 2, 6, { inPoint: 10, outPoint: 14 })
|
|
74
|
+
const p = project([c])
|
|
75
|
+
const resolve = createScrubResolver(() => p)
|
|
76
|
+
// projectS=3 is 1s into the clip; mediaS = inPoint + 1 = 11.
|
|
77
|
+
expect(resolve(3)).toEqual({ src: c.proxySrc, mediaS: 11 })
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
it('picks the earliest-start clip on overlap (mirrors planTick tiebreak)', () => {
|
|
81
|
+
const early = clip('early', 0, 5)
|
|
82
|
+
const late = clip('late', 3, 8)
|
|
83
|
+
const p = project([early, late])
|
|
84
|
+
const resolve = createScrubResolver(() => p)
|
|
85
|
+
const out = resolve(4)
|
|
86
|
+
expect(out?.src).toBe(early.proxySrc)
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
it('re-reads the project on every call so edits are observed', () => {
|
|
90
|
+
let current: Project = project([clip('a', 0, 4)])
|
|
91
|
+
const resolve = createScrubResolver(() => current)
|
|
92
|
+
expect(resolve(1)).not.toBeNull()
|
|
93
|
+
current = project([])
|
|
94
|
+
expect(resolve(1)).toBeNull()
|
|
95
|
+
})
|
|
96
|
+
})
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* T8 pure-fn coverage for scrub-source.ts's grain machinery: Hann-window
|
|
3
|
+
* sample math, throttle+epsilon silence, release-ramp-to-zero, and the T7
|
|
4
|
+
* Bluetooth-latency guard. No WebCodecs mock — every test here is driven only
|
|
5
|
+
* as far as the `resolve()` gate (or a standalone exported helper), which is
|
|
6
|
+
* as far as these tests need to reach; the decode → grain-playback pipeline
|
|
7
|
+
* is out of scope per the plan.
|
|
8
|
+
*/
|
|
9
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
10
|
+
import { createHoverScrub } from '../../video/hover-scrub'
|
|
11
|
+
import type { AcquiredDemux } from '../index'
|
|
12
|
+
import {
|
|
13
|
+
MOVE_EPSILON_S,
|
|
14
|
+
RELEASE_FADE_S,
|
|
15
|
+
THROTTLE_MS,
|
|
16
|
+
createScrubSource,
|
|
17
|
+
hannWindow,
|
|
18
|
+
releaseGrain,
|
|
19
|
+
type ScrubTarget,
|
|
20
|
+
} from '../scrub-source'
|
|
21
|
+
|
|
22
|
+
// ── hannWindow — pure sample math ────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
describe('hannWindow', () => {
|
|
25
|
+
it('starts at 0', () => {
|
|
26
|
+
expect(hannWindow(8)[0]).toBe(0)
|
|
27
|
+
})
|
|
28
|
+
|
|
29
|
+
it('peaks at 1 at the midpoint', () => {
|
|
30
|
+
expect(hannWindow(8)[4]).toBeCloseTo(1, 10)
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
it('is symmetric around the midpoint (w[i] === w[n-i])', () => {
|
|
34
|
+
const w = hannWindow(16)
|
|
35
|
+
for (let i = 1; i < 16; i++) expect(w[i]).toBeCloseTo(w[16 - i], 10)
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
it('is the PERIODIC form: the last sample is not zero, it mirrors w[1]', () => {
|
|
39
|
+
// /n (not /(n-1)) means w[n-1] = 0.5*(1-cos(2π/n)), same as w[1] — small,
|
|
40
|
+
// but nonzero. A test asserting w[n-1]===0 would be asserting the wrong
|
|
41
|
+
// (symmetric, zero-ended) window shape.
|
|
42
|
+
const w = hannWindow(8)
|
|
43
|
+
expect(w[7]).toBeCloseTo(w[1], 10)
|
|
44
|
+
expect(w[7]).not.toBeCloseTo(0, 3)
|
|
45
|
+
})
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
// ── releaseGrain — the release-ramp math `stop()` applies to every live grain
|
|
49
|
+
|
|
50
|
+
describe('releaseGrain', () => {
|
|
51
|
+
it('ramps gain to zero over RELEASE_FADE_S and schedules stop at the ramp end', () => {
|
|
52
|
+
const gain = { gain: { value: 0.9, setValueAtTime: vi.fn(), linearRampToValueAtTime: vi.fn() } }
|
|
53
|
+
const src = { stop: vi.fn() }
|
|
54
|
+
releaseGrain(src as unknown as AudioBufferSourceNode, gain as unknown as GainNode, 10)
|
|
55
|
+
|
|
56
|
+
expect(gain.gain.setValueAtTime).toHaveBeenCalledWith(0.9, 10)
|
|
57
|
+
expect(gain.gain.linearRampToValueAtTime).toHaveBeenCalledWith(0, 10 + RELEASE_FADE_S)
|
|
58
|
+
expect(src.stop).toHaveBeenCalledWith(10 + RELEASE_FADE_S)
|
|
59
|
+
})
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
// ── createScrubSource — throttle, epsilon and the T7 Bluetooth guard ────────
|
|
63
|
+
|
|
64
|
+
interface FakeCtx {
|
|
65
|
+
state: 'running' | 'suspended' | 'closed'
|
|
66
|
+
outputLatency: number
|
|
67
|
+
baseLatency: number
|
|
68
|
+
currentTime: number
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function fakeCtx(over: Partial<FakeCtx> = {}): FakeCtx {
|
|
72
|
+
return { state: 'running', outputLatency: 0, baseLatency: 0, currentTime: 0, ...over }
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function installCtx(ctx: FakeCtx) {
|
|
76
|
+
;(window as unknown as { __montajSharedCtx?: FakeCtx }).__montajSharedCtx = ctx
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Never resolves — `fireGrain` awaits this and then hangs, so no code past it (the WebCodecs decode) ever runs. */
|
|
80
|
+
const hangingAcquireDemux: (src: string) => Promise<AcquiredDemux> = vi.fn(
|
|
81
|
+
() => new Promise<AcquiredDemux>(() => {}),
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
/** Never called — used where `resolve` is expected to be gated out before it would ever run. */
|
|
85
|
+
const notCalledAcquireDemux: (src: string) => Promise<AcquiredDemux> = vi.fn(async () => {
|
|
86
|
+
throw new Error('acquireDemux should not be called in this test')
|
|
87
|
+
})
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* A target `resolve` can return truthily. `onScrub` only updates its own
|
|
91
|
+
* throttle/epsilon state (`lastFireMs`/`lastFiredMediaS`) AFTER `resolve`
|
|
92
|
+
* returns non-null — a `resolve` that always returns `null` would never let
|
|
93
|
+
* that state advance, which would make every later call look "unthrottled"
|
|
94
|
+
* for the wrong reason. So the throttle/epsilon tests below resolve to a
|
|
95
|
+
* real target (and let `acquireDemux` hang forever) rather than short-
|
|
96
|
+
* circuiting on `null`.
|
|
97
|
+
*/
|
|
98
|
+
function fixedTarget(mediaS: number): ScrubTarget {
|
|
99
|
+
return { src: '/proxies/a_proxy.mp4', mediaS }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let nowSpy: ReturnType<typeof vi.spyOn> | undefined
|
|
103
|
+
|
|
104
|
+
/** One `performance.now()` reading per onScrub call, in order. */
|
|
105
|
+
function scriptNow(times: number[]) {
|
|
106
|
+
let i = 0
|
|
107
|
+
nowSpy = vi.spyOn(performance, 'now').mockImplementation(() => times[Math.min(i++, times.length - 1)])
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
afterEach(() => {
|
|
111
|
+
nowSpy?.mockRestore()
|
|
112
|
+
nowSpy = undefined
|
|
113
|
+
delete (window as unknown as { __montajSharedCtx?: FakeCtx }).__montajSharedCtx
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
describe('throttle + epsilon — stationary hover is silent', () => {
|
|
117
|
+
beforeEach(() => installCtx(fakeCtx()))
|
|
118
|
+
|
|
119
|
+
it('does not fire a second grain within THROTTLE_MS of the last, but does once past it', () => {
|
|
120
|
+
scriptNow([1000, 1000 + THROTTLE_MS / 2, 1000 + THROTTLE_MS + 200])
|
|
121
|
+
const resolve = vi.fn((t: number) => fixedTarget(t))
|
|
122
|
+
const source = createScrubSource({ acquireDemux: hangingAcquireDemux, resolve })
|
|
123
|
+
source.setEnabled(true)
|
|
124
|
+
const hover = createHoverScrub()
|
|
125
|
+
source.attach(hover)
|
|
126
|
+
|
|
127
|
+
hover.set(1.0) // t=1000ms: first move always fires
|
|
128
|
+
expect(resolve).toHaveBeenCalledTimes(1)
|
|
129
|
+
|
|
130
|
+
hover.set(1.05) // t=1000+16ms: inside THROTTLE_MS — silent
|
|
131
|
+
expect(resolve).toHaveBeenCalledTimes(1)
|
|
132
|
+
|
|
133
|
+
hover.set(1.1) // well past THROTTLE_MS and past MOVE_EPSILON_S — fires again
|
|
134
|
+
expect(resolve).toHaveBeenCalledTimes(2)
|
|
135
|
+
|
|
136
|
+
source.dispose()
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('does not fire when the move is smaller than MOVE_EPSILON_S, even after THROTTLE_MS has passed', () => {
|
|
140
|
+
scriptNow([1000, 1000 + THROTTLE_MS + 100])
|
|
141
|
+
const resolve = vi.fn((t: number) => fixedTarget(t))
|
|
142
|
+
const source = createScrubSource({ acquireDemux: hangingAcquireDemux, resolve })
|
|
143
|
+
source.setEnabled(true)
|
|
144
|
+
const hover = createHoverScrub()
|
|
145
|
+
source.attach(hover)
|
|
146
|
+
|
|
147
|
+
hover.set(1.0)
|
|
148
|
+
expect(resolve).toHaveBeenCalledTimes(1)
|
|
149
|
+
|
|
150
|
+
hover.set(1.0 + MOVE_EPSILON_S / 2) // past the throttle window, but the move itself is sub-epsilon
|
|
151
|
+
expect(resolve).toHaveBeenCalledTimes(1)
|
|
152
|
+
|
|
153
|
+
source.dispose()
|
|
154
|
+
})
|
|
155
|
+
|
|
156
|
+
it('goes silent (stop) on a null hover position without touching resolve', () => {
|
|
157
|
+
scriptNow([1000])
|
|
158
|
+
const resolve = vi.fn((t: number) => fixedTarget(t))
|
|
159
|
+
const source = createScrubSource({ acquireDemux: hangingAcquireDemux, resolve })
|
|
160
|
+
source.setEnabled(true)
|
|
161
|
+
const hover = createHoverScrub()
|
|
162
|
+
source.attach(hover)
|
|
163
|
+
|
|
164
|
+
hover.set(1.0)
|
|
165
|
+
expect(resolve).toHaveBeenCalledTimes(1)
|
|
166
|
+
|
|
167
|
+
hover.set(null)
|
|
168
|
+
expect(resolve).toHaveBeenCalledTimes(1)
|
|
169
|
+
|
|
170
|
+
source.dispose()
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
describe('T7 guard — Bluetooth/high-latency auto-disable', () => {
|
|
175
|
+
it('blocks every scrub above SCRUB_LATENCY_THRESHOLD_S and warns via onError exactly once', () => {
|
|
176
|
+
installCtx(fakeCtx({ outputLatency: 0.15 })) // > the 0.08s threshold
|
|
177
|
+
const resolve = vi.fn(() => null)
|
|
178
|
+
const onError = vi.fn()
|
|
179
|
+
const source = createScrubSource({ acquireDemux: notCalledAcquireDemux, resolve, onError })
|
|
180
|
+
|
|
181
|
+
source.setEnabled(true) // the guard also fires its one-time warning right on enable
|
|
182
|
+
expect(onError).toHaveBeenCalledTimes(1)
|
|
183
|
+
expect(onError.mock.calls[0][0]).toMatch(/latency too high/i)
|
|
184
|
+
|
|
185
|
+
const hover = createHoverScrub()
|
|
186
|
+
source.attach(hover)
|
|
187
|
+
hover.set(1.0)
|
|
188
|
+
hover.set(2.0)
|
|
189
|
+
|
|
190
|
+
expect(resolve).not.toHaveBeenCalled()
|
|
191
|
+
expect(onError).toHaveBeenCalledTimes(1) // still just the one warning, not one per attempt
|
|
192
|
+
|
|
193
|
+
source.dispose()
|
|
194
|
+
})
|
|
195
|
+
})
|