@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,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SP4 T3 — the decode worker's own two requirements, tested against the
|
|
3
|
+
* shipped string.
|
|
4
|
+
*
|
|
5
|
+
* The worker ships as an inlined source string (plan decision 6), so it can
|
|
6
|
+
* neither be imported as a module nor type-checked. It can, however, be
|
|
7
|
+
* *executed*: the string is plain JS whose only free identifiers are `self`,
|
|
8
|
+
* `VideoDecoder` and `EncodedVideoChunk`, so `new Function('self',
|
|
9
|
+
* 'VideoDecoder', 'EncodedVideoChunk', decodeWorkerSource)` runs it with all
|
|
10
|
+
* three shadowed by fakes. That is worth doing rather than testing a
|
|
11
|
+
* main-side re-implementation, because both requirements here are about what
|
|
12
|
+
* the *worker* does with frames it has been handed: closing them, and owing a
|
|
13
|
+
* `done` no matter what.
|
|
14
|
+
*
|
|
15
|
+
* The fakes model the two WebCodecs behaviors the requirements depend on:
|
|
16
|
+
* - `EncodedVideoChunk.timestamp` is a `long long`, so a float sample time
|
|
17
|
+
* is TRUNCATED at construction (SP1 §7.2's root cause);
|
|
18
|
+
* - a decoder emits nothing until `flush()` settles, and emission lands on a
|
|
19
|
+
* later microtask — which is exactly the window in which a newer request
|
|
20
|
+
* supersedes an older one.
|
|
21
|
+
*/
|
|
22
|
+
import { describe, it, expect } from 'vitest'
|
|
23
|
+
import { decodeWorkerSource } from '../decode-worker-source'
|
|
24
|
+
import { PRE_ROLL_EPSILON_US, shouldEmitFrame } from '../batch-planner'
|
|
25
|
+
import type { DecodeCmd, WorkerSample } from '../frame-server'
|
|
26
|
+
|
|
27
|
+
interface FakeFrame {
|
|
28
|
+
timestamp: number
|
|
29
|
+
closed: boolean
|
|
30
|
+
close: () => void
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
type Posted =
|
|
34
|
+
| { t: 'frame'; frame: FakeFrame; reqId: number }
|
|
35
|
+
| { t: 'done'; reqId: number; decoded: number; dropped: number }
|
|
36
|
+
| { t: 'error'; message: string; reqId?: number }
|
|
37
|
+
|
|
38
|
+
interface Harness {
|
|
39
|
+
send: (cmd: DecodeCmd) => void
|
|
40
|
+
posted: Posted[]
|
|
41
|
+
/** Every frame the fake decoder ever emitted, in emission order. */
|
|
42
|
+
frames: FakeFrame[]
|
|
43
|
+
/** Chunk timestamps in the order they were handed to `decode()`. */
|
|
44
|
+
fed: number[]
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Let every pending microtask (and therefore every `flush()`) settle. */
|
|
48
|
+
function settle(): Promise<void> {
|
|
49
|
+
return new Promise((resolve) => setTimeout(resolve, 0))
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function harness(opts: { decodeThrows?: boolean } = {}): Harness {
|
|
53
|
+
const posted: Posted[] = []
|
|
54
|
+
const frames: FakeFrame[] = []
|
|
55
|
+
const fed: number[] = []
|
|
56
|
+
let emit: (frame: FakeFrame) => void = () => {}
|
|
57
|
+
let queued: { timestamp: number }[] = []
|
|
58
|
+
|
|
59
|
+
class FakeEncodedVideoChunk {
|
|
60
|
+
type: string
|
|
61
|
+
timestamp: number
|
|
62
|
+
duration: number
|
|
63
|
+
data: Uint8Array
|
|
64
|
+
constructor(init: { type: string; timestamp: number; duration: number; data: Uint8Array }) {
|
|
65
|
+
this.type = init.type
|
|
66
|
+
// WebIDL `long long`: the float sample time loses its fraction here.
|
|
67
|
+
this.timestamp = Math.trunc(init.timestamp)
|
|
68
|
+
this.duration = Math.trunc(init.duration)
|
|
69
|
+
this.data = init.data
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class FakeVideoDecoder {
|
|
74
|
+
constructor(init: { output: (frame: FakeFrame) => void; error: (e: Error) => void }) {
|
|
75
|
+
emit = init.output
|
|
76
|
+
}
|
|
77
|
+
configure(): void {}
|
|
78
|
+
decode(chunk: { timestamp: number }): void {
|
|
79
|
+
if (opts.decodeThrows) throw new Error('boom')
|
|
80
|
+
fed.push(chunk.timestamp)
|
|
81
|
+
queued.push(chunk)
|
|
82
|
+
}
|
|
83
|
+
flush(): Promise<void> {
|
|
84
|
+
return Promise.resolve().then(() => {
|
|
85
|
+
const batch = queued
|
|
86
|
+
queued = []
|
|
87
|
+
// Real decoders emit in PRESENTATION order regardless of feed order.
|
|
88
|
+
batch.sort((a, b) => a.timestamp - b.timestamp)
|
|
89
|
+
for (const chunk of batch) {
|
|
90
|
+
const frame: FakeFrame = {
|
|
91
|
+
timestamp: chunk.timestamp,
|
|
92
|
+
closed: false,
|
|
93
|
+
close() {
|
|
94
|
+
this.closed = true
|
|
95
|
+
},
|
|
96
|
+
}
|
|
97
|
+
frames.push(frame)
|
|
98
|
+
emit(frame)
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const scope = {
|
|
105
|
+
onmessage: null as ((ev: { data: DecodeCmd }) => void) | null,
|
|
106
|
+
postMessage(msg: Posted): void {
|
|
107
|
+
posted.push(msg)
|
|
108
|
+
},
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
const factory = new Function('self', 'VideoDecoder', 'EncodedVideoChunk', decodeWorkerSource)
|
|
112
|
+
factory(scope, FakeVideoDecoder, FakeEncodedVideoChunk)
|
|
113
|
+
|
|
114
|
+
return {
|
|
115
|
+
posted,
|
|
116
|
+
frames,
|
|
117
|
+
fed,
|
|
118
|
+
send: (cmd) => scope.onmessage?.({ data: cmd }),
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function init(preRollEpsilonUs = PRE_ROLL_EPSILON_US): DecodeCmd {
|
|
123
|
+
return { t: 'init', codec: 'av01.0.05M.08', hardwareAcceleration: 'no-preference', preRollEpsilonUs }
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Samples at a real 29.97fps cadence, so the timestamps are the awkward floats §7.2 is about. */
|
|
127
|
+
function sample(index: number, isKey = index === 0): WorkerSample {
|
|
128
|
+
return { tsUs: index * 33333.333, durUs: 33333.333, isKey, data: new Uint8Array([index]) }
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function dones(posted: Posted[], reqId: number): Posted[] {
|
|
132
|
+
return posted.filter((m) => m.t === 'done' && m.reqId === reqId)
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function framesFor(posted: Posted[], reqId: number): number[] {
|
|
136
|
+
return posted.flatMap((m) => (m.t === 'frame' && m.reqId === reqId ? [m.frame.timestamp] : []))
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
describe('decode worker source', () => {
|
|
140
|
+
it('preroll-epsilon', async () => {
|
|
141
|
+
// Requested: the frame at cts 33333.333 (sample 1). Decoding starts at the
|
|
142
|
+
// keyframe, so sample 0 is genuine pre-roll and must be closed.
|
|
143
|
+
const target = 33333.333
|
|
144
|
+
const w = harness()
|
|
145
|
+
w.send(init())
|
|
146
|
+
w.send({ t: 'decodeRange', samples: [sample(0), sample(1), sample(2)], targetTsUs: target, reqId: 0 })
|
|
147
|
+
await settle()
|
|
148
|
+
|
|
149
|
+
// The chunk fed at 33333.333 comes back stamped 33333 — an INTEGER, one
|
|
150
|
+
// notch BELOW the target it was asked for.
|
|
151
|
+
expect(w.frames.map((f) => f.timestamp)).toEqual([0, 33333, 66666])
|
|
152
|
+
// With the 1µs epsilon it survives, along with everything after it.
|
|
153
|
+
expect(framesFor(w.posted, 0)).toEqual([33333, 66666])
|
|
154
|
+
// Pre-roll is closed, never transferred, and counted as dropped.
|
|
155
|
+
expect(w.frames[0].closed).toBe(true)
|
|
156
|
+
expect(w.frames.filter((f) => !f.closed).map((f) => f.timestamp)).toEqual([33333, 66666])
|
|
157
|
+
expect(dones(w.posted, 0)).toEqual([{ t: 'done', reqId: 0, decoded: 2, dropped: 1 }])
|
|
158
|
+
|
|
159
|
+
// The same predicate, main-side, so both sides of the string boundary
|
|
160
|
+
// agree on the boundary condition.
|
|
161
|
+
expect(shouldEmitFrame(33333, target)).toBe(true)
|
|
162
|
+
expect(shouldEmitFrame(16666, target)).toBe(false)
|
|
163
|
+
|
|
164
|
+
// And the regression this guards: without the epsilon (the state SP1
|
|
165
|
+
// measured), the requested frame is itself rejected as pre-roll — ~2 of 3
|
|
166
|
+
// seeks silently painted nothing, and those seeks were quietly excluded
|
|
167
|
+
// from the latency distribution rather than failing loudly (§7.2).
|
|
168
|
+
const broken = harness()
|
|
169
|
+
broken.send(init(0))
|
|
170
|
+
broken.send({ t: 'decodeRange', samples: [sample(0), sample(1), sample(2)], targetTsUs: target, reqId: 0 })
|
|
171
|
+
await settle()
|
|
172
|
+
expect(framesFor(broken.posted, 0)).toEqual([66666])
|
|
173
|
+
expect(broken.frames[1].closed).toBe(true)
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
it('stale frames closed, done always posted', async () => {
|
|
177
|
+
const w = harness()
|
|
178
|
+
w.send(init())
|
|
179
|
+
|
|
180
|
+
// req 0 is fed and awaiting flush; its frames have not been emitted yet.
|
|
181
|
+
w.send({ t: 'decodeRange', samples: [sample(0), sample(1)], targetTsUs: 0, reqId: 0 })
|
|
182
|
+
// Two more arrive before any of that lands: 1 is superseded while still
|
|
183
|
+
// queued, 2 is the one that should actually run.
|
|
184
|
+
w.send({ t: 'decodeRange', samples: [sample(2), sample(3)], targetTsUs: 66666.666, reqId: 1 })
|
|
185
|
+
w.send({ t: 'decodeRange', samples: [sample(4), sample(5)], targetTsUs: 133333.332, reqId: 2 })
|
|
186
|
+
await settle()
|
|
187
|
+
|
|
188
|
+
// req 0: superseded mid-flush. Its frames were produced, so they must be
|
|
189
|
+
// CLOSED — never transferred, never leaked.
|
|
190
|
+
expect(framesFor(w.posted, 0)).toEqual([])
|
|
191
|
+
expect(w.frames.slice(0, 2).every((f) => f.closed)).toBe(true)
|
|
192
|
+
// req 1: superseded before reaching the decoder, so nothing was decoded
|
|
193
|
+
// for it at all.
|
|
194
|
+
expect(framesFor(w.posted, 1)).toEqual([])
|
|
195
|
+
expect(w.fed).toEqual([0, 33333, 133333, 166666])
|
|
196
|
+
// req 2: the live one, served normally.
|
|
197
|
+
expect(framesFor(w.posted, 2)).toEqual([133333, 166666])
|
|
198
|
+
|
|
199
|
+
// The total `done` contract: EVERY request gets exactly one, whatever
|
|
200
|
+
// happened to it. SP1 §7.4 is what the alternative costs — an orphaned
|
|
201
|
+
// pending entry and a seek promise that never settles.
|
|
202
|
+
expect(dones(w.posted, 0)).toHaveLength(1)
|
|
203
|
+
expect(dones(w.posted, 1)).toEqual([{ t: 'done', reqId: 1, decoded: 0, dropped: 0 }])
|
|
204
|
+
expect(dones(w.posted, 2)).toHaveLength(1)
|
|
205
|
+
// No `reset()` was needed to achieve any of that — supersession is purely
|
|
206
|
+
// a bookkeeping question, and `reset()` would leave the decoder
|
|
207
|
+
// unconfigured so the next `decode()` throws InvalidStateError.
|
|
208
|
+
expect(decodeWorkerSource).not.toContain('reset()')
|
|
209
|
+
})
|
|
210
|
+
|
|
211
|
+
it('still posts done when the decoder throws', async () => {
|
|
212
|
+
const w = harness({ decodeThrows: true })
|
|
213
|
+
w.send(init())
|
|
214
|
+
w.send({ t: 'decodeRange', samples: [sample(0)], targetTsUs: 0, reqId: 0 })
|
|
215
|
+
await settle()
|
|
216
|
+
|
|
217
|
+
expect(w.posted.filter((m) => m.t === 'error')).toHaveLength(1)
|
|
218
|
+
expect(dones(w.posted, 0)).toEqual([{ t: 'done', reqId: 0, decoded: 0, dropped: 0 }])
|
|
219
|
+
})
|
|
220
|
+
|
|
221
|
+
it('still posts done when decodeRange arrives before init', async () => {
|
|
222
|
+
const w = harness()
|
|
223
|
+
w.send({ t: 'decodeRange', samples: [sample(0)], targetTsUs: 0, reqId: 7 })
|
|
224
|
+
await settle()
|
|
225
|
+
|
|
226
|
+
expect(w.posted.filter((m) => m.t === 'error')).toHaveLength(1)
|
|
227
|
+
expect(dones(w.posted, 7)).toHaveLength(1)
|
|
228
|
+
})
|
|
229
|
+
|
|
230
|
+
it('feeds chunks to the decoder in the exact order handed over, never sorted', async () => {
|
|
231
|
+
// A reordered GOP as the planner slices it: decode order I0 P3 B1 B2, cts
|
|
232
|
+
// zig-zagging. The worker must not "helpfully" sort — that is SP1 §7.1,
|
|
233
|
+
// which killed the decoder outright on the first non-intra frame.
|
|
234
|
+
const gop: WorkerSample[] = [
|
|
235
|
+
{ tsUs: 0, durUs: 1000, isKey: true, data: new Uint8Array([0]) },
|
|
236
|
+
{ tsUs: 3000, durUs: 1000, isKey: false, data: new Uint8Array([1]) },
|
|
237
|
+
{ tsUs: 1000, durUs: 1000, isKey: false, data: new Uint8Array([2]) },
|
|
238
|
+
{ tsUs: 2000, durUs: 1000, isKey: false, data: new Uint8Array([3]) },
|
|
239
|
+
]
|
|
240
|
+
const w = harness()
|
|
241
|
+
w.send(init())
|
|
242
|
+
w.send({ t: 'decodeRange', samples: gop, targetTsUs: 0, reqId: 0 })
|
|
243
|
+
await settle()
|
|
244
|
+
|
|
245
|
+
expect(w.fed).toEqual([0, 3000, 1000, 2000])
|
|
246
|
+
// …while the frames come back in presentation order, as a real decoder emits them.
|
|
247
|
+
expect(framesFor(w.posted, 0)).toEqual([0, 1000, 2000, 3000])
|
|
248
|
+
})
|
|
249
|
+
})
|