@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,380 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SP4 T2 — byte loader for the playback engine's demuxer.
|
|
3
|
+
*
|
|
4
|
+
* The spike's `media-loader.ts` could hardcode its transport (an Electron
|
|
5
|
+
* `window.montajSpike.readFile` bridge, else `fetch('/media/' + rel)`)
|
|
6
|
+
* because it owned its whole world. `@bycrux/editor` is a library and owns
|
|
7
|
+
* none of it: a host path like `/Users/…/orig_proxy_hable1.mp4` means
|
|
8
|
+
* nothing to a browser, and each host maps it differently (montaj's ui →
|
|
9
|
+
* `/api/files?path=…`; Hub → a presigned R2 URL). The editor already has
|
|
10
|
+
* exactly one primitive for that — `EditorAdapter.fileUrl(path)` — and the
|
|
11
|
+
* legacy player already routes proxy sources through it unconditionally
|
|
12
|
+
* (`useVideoPlayback.ts`: `el.src = fileUrlRef.current(playbackSrcFor(clip))`).
|
|
13
|
+
* So `loadBytes` takes the same function by injection and applies it the same
|
|
14
|
+
* way; T6 threads `adapter.fileUrl` straight through, no adapter change and
|
|
15
|
+
* no new host contract.
|
|
16
|
+
*
|
|
17
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
18
|
+
* RANGED LOADING — why this module returns a byte SOURCE, not bytes
|
|
19
|
+
* ─────────────────────────────────────────────────────────────────────────
|
|
20
|
+
* T2 shipped `loadBytes`: one `fetch(url).then(r => r.arrayBuffer())` for the
|
|
21
|
+
* whole proxy before a single frame could be decoded. That was correct for the
|
|
22
|
+
* proxies SP3 sized (tens of MB) and it is the engine's clearest scaling limit
|
|
23
|
+
* once they are not — time-to-first-frame grew with FILE SIZE, because nothing
|
|
24
|
+
* decodes until the last byte lands, and every open clip held its full proxy
|
|
25
|
+
* resident. A 2:19 source encoded all-intra is ~400 MB, so "Preparing preview…"
|
|
26
|
+
* sat on screen for as long as that download took, and a silence-trimmed B-roll
|
|
27
|
+
* timeline — fifty clips off one proxy — paid it while needing only a small
|
|
28
|
+
* byte-window each.
|
|
29
|
+
*
|
|
30
|
+
* All-intra is NOT the thing to fix (it is what makes a scrub decode exactly
|
|
31
|
+
* one frame; a GOP was measured ~50% slower per seek and rejected). The fix is
|
|
32
|
+
* to stop reading the file end-to-end: `openByteSource` returns a handle that
|
|
33
|
+
* knows the file's LENGTH and can serve arbitrary byte ranges, so `demux.ts`
|
|
34
|
+
* fetches `moov` (a couple hundred KB, sized by DURATION not bitrate), builds
|
|
35
|
+
* the sample index from it, and then pulls only the `mdat` spans the playhead
|
|
36
|
+
* actually touches. Time-to-first-frame becomes a function of the moov plus one
|
|
37
|
+
* frame's bytes — independent of how big the proxy is.
|
|
38
|
+
*
|
|
39
|
+
* Two properties this deliberately keeps:
|
|
40
|
+
*
|
|
41
|
+
* - **A whole-file fallback.** A host whose `fileUrl` target ignores `Range`
|
|
42
|
+
* answers the probe with `200` and the entire body; that is not an error,
|
|
43
|
+
* it is the old behavior, so the source reports `ranged: false` and serves
|
|
44
|
+
* every read out of the buffer it already has. montaj's `/api/files` serves
|
|
45
|
+
* 206/416 via Starlette's `FileResponse` and Hub's presigned R2 URLs do it
|
|
46
|
+
* natively, so this is the rare path, not the common one.
|
|
47
|
+
* - **Block-aligned caching.** Reads are rounded out to {@link BLOCK_BYTES}
|
|
48
|
+
* and cached, because the audio and video tracks of one proxy are
|
|
49
|
+
* INTERLEAVED: the audio packets for a second of media are scattered through
|
|
50
|
+
* the same megabytes as that second's video frames. Without alignment the
|
|
51
|
+
* audio clock would re-fetch, byte for byte, spans the frame server had just
|
|
52
|
+
* pulled. With it they share, and the cache is what bounds resident memory
|
|
53
|
+
* ({@link MAX_CACHED_BYTES}) instead of the file size doing it.
|
|
54
|
+
*/
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* A host path → fetchable URL mapper. Structurally identical to
|
|
58
|
+
* `EditorAdapter['fileUrl']`, declared locally so the engine layer does not
|
|
59
|
+
* take a dependency on the whole adapter surface just to read bytes.
|
|
60
|
+
*/
|
|
61
|
+
export type FileUrlResolver = (path: string) => string
|
|
62
|
+
|
|
63
|
+
export interface LoadBytesOptions {
|
|
64
|
+
/**
|
|
65
|
+
* Cancels an in-flight load. T5 terminates a per-source decode session on
|
|
66
|
+
* every clip-boundary swap; a load still running for the source it just
|
|
67
|
+
* left should not keep occupying a connection (or resolve into a session
|
|
68
|
+
* that no longer exists).
|
|
69
|
+
*/
|
|
70
|
+
signal?: AbortSignal
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Fetch a media source's complete bytes.
|
|
75
|
+
*
|
|
76
|
+
* No longer the demuxer's path — {@link openByteSource} is (see the module
|
|
77
|
+
* doc). Kept as the package's one-call "give me this file" primitive, and as
|
|
78
|
+
* the thing the whole-file fallback is defined against.
|
|
79
|
+
*
|
|
80
|
+
* `src` is a host path exactly as it appears on a project item (`proxySrc`,
|
|
81
|
+
* `normalizedSrc`, `src`) — it is passed to `fileUrl` unconditionally,
|
|
82
|
+
* matching `useVideoPlayback.ts`'s handling. Hosts whose `fileUrl` is
|
|
83
|
+
* identity for already-absolute URLs get that behavior for free; the engine
|
|
84
|
+
* does not second-guess the mapping.
|
|
85
|
+
*
|
|
86
|
+
* Throws (never returns a partial buffer) on a non-2xx response, so the
|
|
87
|
+
* caller's failure path is a single `catch` — T5 routes a failed proxy load
|
|
88
|
+
* to the same "Preparing preview…" clip state as a proxy that has not
|
|
89
|
+
* arrived yet, rather than reverting the project to the legacy player.
|
|
90
|
+
*/
|
|
91
|
+
export async function loadBytes(
|
|
92
|
+
src: string,
|
|
93
|
+
fileUrl: FileUrlResolver,
|
|
94
|
+
options: LoadBytesOptions = {},
|
|
95
|
+
): Promise<ArrayBuffer> {
|
|
96
|
+
if (!src) throw new Error('loadBytes: empty src')
|
|
97
|
+
|
|
98
|
+
const url = fileUrl(src)
|
|
99
|
+
const res = await fetch(url, { signal: options.signal })
|
|
100
|
+
if (!res.ok) {
|
|
101
|
+
throw new Error(`loadBytes: ${src} → ${res.status}${res.statusText ? ` ${res.statusText}` : ''}`)
|
|
102
|
+
}
|
|
103
|
+
return res.arrayBuffer()
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Ranged byte source ──────────────────────────────────────────────────────
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* How much of the file the opening probe asks for.
|
|
110
|
+
*
|
|
111
|
+
* Doubles as the range-support test (a host that honours it answers `206` and
|
|
112
|
+
* a `Content-Range` carrying the total length; one that does not answers `200`
|
|
113
|
+
* and the whole file) and as the first read, so a `+faststart` proxy — which
|
|
114
|
+
* ffmpeg writes with `ftyp` and `moov` at the FRONT, see `lib/proxy.py` — is
|
|
115
|
+
* usually parseable after exactly one round trip. Deliberately equal to
|
|
116
|
+
* {@link BLOCK_BYTES} so the probe's bytes land as a normal cache block and the
|
|
117
|
+
* demuxer's first `read` is free rather than a second request for the same
|
|
118
|
+
* megabyte.
|
|
119
|
+
*/
|
|
120
|
+
export const PROBE_BYTES = 1024 * 1024
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Read granularity. Every range request is rounded out to a multiple of this,
|
|
124
|
+
* so two tracks reading the same region of an interleaved file hit one cached
|
|
125
|
+
* block instead of issuing two overlapping fetches (see the module doc).
|
|
126
|
+
*/
|
|
127
|
+
export const BLOCK_BYTES = 1024 * 1024
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Ceiling on cached file bytes per source. The point of ranged loading is that
|
|
131
|
+
* memory stops tracking file size, so this has to be a hard cap and not a hint:
|
|
132
|
+
* once it is exceeded the least-recently-used spans are dropped, and a source
|
|
133
|
+
* that is scrubbed end to end costs this much, not its own size.
|
|
134
|
+
*/
|
|
135
|
+
export const MAX_CACHED_BYTES = 8 * 1024 * 1024
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* A random-access handle on one media file.
|
|
139
|
+
*
|
|
140
|
+
* `size` is known before any media data is read — that is the whole point, it
|
|
141
|
+
* is what lets `demux.ts` walk top-level boxes and skip `mdat` instead of
|
|
142
|
+
* downloading it.
|
|
143
|
+
*/
|
|
144
|
+
export interface MediaByteSource {
|
|
145
|
+
/** The `src` this reads, for error messages. */
|
|
146
|
+
readonly src: string
|
|
147
|
+
/** Total length of the file in bytes. */
|
|
148
|
+
readonly size: number
|
|
149
|
+
/**
|
|
150
|
+
* `true` when the host answered the opening probe with `206 Partial Content`
|
|
151
|
+
* — i.e. reads really are ranged. `false` means the host ignored `Range` and
|
|
152
|
+
* handed over the whole file, which is the T2 behavior preserved as a
|
|
153
|
+
* fallback; {@link whole} is then non-null and no further network I/O happens.
|
|
154
|
+
*/
|
|
155
|
+
readonly ranged: boolean
|
|
156
|
+
/** The complete file, on the non-ranged fallback path only; `null` when ranged. */
|
|
157
|
+
readonly whole: ArrayBuffer | null
|
|
158
|
+
/**
|
|
159
|
+
* Bytes for `[start, end)`. Clamped to the file. The returned array may be a
|
|
160
|
+
* view into a cached block, so callers that keep the bytes must copy.
|
|
161
|
+
*/
|
|
162
|
+
read(start: number, end: number): Promise<Uint8Array>
|
|
163
|
+
/** Drop every cached byte and abort reads still in flight. Idempotent. */
|
|
164
|
+
close(): void
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** One cached, block-aligned span of the file. */
|
|
168
|
+
interface CachedSpan {
|
|
169
|
+
start: number
|
|
170
|
+
end: number
|
|
171
|
+
bytes: Uint8Array
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function alignDown(n: number): number {
|
|
175
|
+
return Math.floor(n / BLOCK_BYTES) * BLOCK_BYTES
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function alignUp(n: number): number {
|
|
179
|
+
return Math.ceil(n / BLOCK_BYTES) * BLOCK_BYTES
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Total file length out of a `Content-Range: bytes 0-1048575/402653184` header.
|
|
184
|
+
*
|
|
185
|
+
* Returns `null` for a header that is missing, unparseable, or reports the
|
|
186
|
+
* total as `*` (length unknown). The caller treats every one of those the same
|
|
187
|
+
* way — as "this host is not usable for ranged reads" — and falls back to the
|
|
188
|
+
* whole-file load rather than guessing a length it would then walk boxes
|
|
189
|
+
* against.
|
|
190
|
+
*/
|
|
191
|
+
export function totalFromContentRange(header: string | null | undefined): number | null {
|
|
192
|
+
if (!header) return null
|
|
193
|
+
const m = /^\s*bytes\s+(\d+)\s*-\s*(\d+)\s*\/\s*(\d+)\s*$/i.exec(header)
|
|
194
|
+
if (!m) return null
|
|
195
|
+
const total = Number(m[3])
|
|
196
|
+
return Number.isFinite(total) && total > 0 ? total : null
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
class RangedByteSource implements MediaByteSource {
|
|
200
|
+
readonly whole: ArrayBuffer | null = null
|
|
201
|
+
/** LRU, oldest first — Map insertion order, re-inserted on hit. */
|
|
202
|
+
private readonly spans = new Map<string, CachedSpan>()
|
|
203
|
+
private readonly inflight = new Map<string, Promise<CachedSpan>>()
|
|
204
|
+
private cachedBytes = 0
|
|
205
|
+
private closed = false
|
|
206
|
+
private readonly controller = new AbortController()
|
|
207
|
+
|
|
208
|
+
constructor(
|
|
209
|
+
readonly src: string,
|
|
210
|
+
readonly size: number,
|
|
211
|
+
private readonly url: string,
|
|
212
|
+
head: Uint8Array,
|
|
213
|
+
headStart: number,
|
|
214
|
+
) {
|
|
215
|
+
if (head.length > 0) {
|
|
216
|
+
this.store({ start: headStart, end: headStart + head.length, bytes: head })
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
readonly ranged = true
|
|
221
|
+
|
|
222
|
+
async read(start: number, end: number): Promise<Uint8Array> {
|
|
223
|
+
const lo = Math.max(0, Math.min(start, this.size))
|
|
224
|
+
const hi = Math.max(lo, Math.min(end, this.size))
|
|
225
|
+
if (hi === lo) return new Uint8Array(0)
|
|
226
|
+
if (this.closed) throw new Error(`read: ${this.src} — byte source closed`)
|
|
227
|
+
|
|
228
|
+
const hit = this.find(lo, hi)
|
|
229
|
+
if (hit) return hit.bytes.subarray(lo - hit.start, hi - hit.start)
|
|
230
|
+
|
|
231
|
+
const spanStart = alignDown(lo)
|
|
232
|
+
const spanEnd = Math.min(alignUp(hi), this.size)
|
|
233
|
+
const key = `${spanStart}:${spanEnd}`
|
|
234
|
+
let pending = this.inflight.get(key)
|
|
235
|
+
if (!pending) {
|
|
236
|
+
pending = this.fetchSpan(spanStart, spanEnd).finally(() => {
|
|
237
|
+
this.inflight.delete(key)
|
|
238
|
+
})
|
|
239
|
+
this.inflight.set(key, pending)
|
|
240
|
+
}
|
|
241
|
+
const span = await pending
|
|
242
|
+
return span.bytes.subarray(lo - span.start, hi - span.start)
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
close(): void {
|
|
246
|
+
if (this.closed) return
|
|
247
|
+
this.closed = true
|
|
248
|
+
this.spans.clear()
|
|
249
|
+
this.cachedBytes = 0
|
|
250
|
+
this.controller.abort()
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
private find(start: number, end: number): CachedSpan | null {
|
|
254
|
+
for (const [key, span] of this.spans) {
|
|
255
|
+
if (span.start > start || span.end < end) continue
|
|
256
|
+
// Touch: re-insert so Map iteration order stays oldest-first.
|
|
257
|
+
this.spans.delete(key)
|
|
258
|
+
this.spans.set(key, span)
|
|
259
|
+
return span
|
|
260
|
+
}
|
|
261
|
+
return null
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private async fetchSpan(start: number, end: number): Promise<CachedSpan> {
|
|
265
|
+
const res = await fetch(this.url, {
|
|
266
|
+
headers: { Range: `bytes=${start}-${end - 1}` },
|
|
267
|
+
signal: this.controller.signal,
|
|
268
|
+
})
|
|
269
|
+
if (!res.ok) {
|
|
270
|
+
throw new Error(
|
|
271
|
+
`read: ${this.src} [${start}-${end - 1}] → ${res.status}${res.statusText ? ` ${res.statusText}` : ''}`,
|
|
272
|
+
)
|
|
273
|
+
}
|
|
274
|
+
const bytes = new Uint8Array(await res.arrayBuffer())
|
|
275
|
+
// A host that answers `200` to a ranged read handed back the WHOLE file
|
|
276
|
+
// (some proxies drop the header on a retry). Honour what actually arrived
|
|
277
|
+
// rather than the offsets we asked for, so the bytes are still indexed
|
|
278
|
+
// correctly; the span simply covers more of the file than requested.
|
|
279
|
+
const span: CachedSpan =
|
|
280
|
+
res.status === 206
|
|
281
|
+
? { start, end: start + bytes.length, bytes }
|
|
282
|
+
: { start: 0, end: bytes.length, bytes }
|
|
283
|
+
this.store(span)
|
|
284
|
+
return span
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
private store(span: CachedSpan): void {
|
|
288
|
+
if (this.closed || span.bytes.length === 0) return
|
|
289
|
+
const key = `${span.start}:${span.end}`
|
|
290
|
+
const existing = this.spans.get(key)
|
|
291
|
+
if (existing) {
|
|
292
|
+
this.spans.delete(key)
|
|
293
|
+
this.cachedBytes -= existing.bytes.length
|
|
294
|
+
}
|
|
295
|
+
this.spans.set(key, span)
|
|
296
|
+
this.cachedBytes += span.bytes.length
|
|
297
|
+
// Evict oldest-first, but never the span just stored — a read is about to
|
|
298
|
+
// slice out of it.
|
|
299
|
+
while (this.cachedBytes > MAX_CACHED_BYTES && this.spans.size > 1) {
|
|
300
|
+
const oldest = this.spans.keys().next()
|
|
301
|
+
if (oldest.done) break
|
|
302
|
+
const victim = this.spans.get(oldest.value)
|
|
303
|
+
this.spans.delete(oldest.value)
|
|
304
|
+
if (victim) this.cachedBytes -= victim.bytes.length
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/** The whole-file fallback: a byte source over bytes that are already resident. */
|
|
310
|
+
class WholeFileByteSource implements MediaByteSource {
|
|
311
|
+
readonly ranged = false
|
|
312
|
+
readonly size: number
|
|
313
|
+
private view: Uint8Array | null
|
|
314
|
+
|
|
315
|
+
constructor(
|
|
316
|
+
readonly src: string,
|
|
317
|
+
readonly whole: ArrayBuffer,
|
|
318
|
+
) {
|
|
319
|
+
this.size = whole.byteLength
|
|
320
|
+
this.view = new Uint8Array(whole)
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
async read(start: number, end: number): Promise<Uint8Array> {
|
|
324
|
+
if (!this.view) throw new Error(`read: ${this.src} — byte source closed`)
|
|
325
|
+
const lo = Math.max(0, Math.min(start, this.size))
|
|
326
|
+
const hi = Math.max(lo, Math.min(end, this.size))
|
|
327
|
+
return this.view.subarray(lo, hi)
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
close(): void {
|
|
331
|
+
this.view = null
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Open a media source for random access.
|
|
337
|
+
*
|
|
338
|
+
* One request goes out: a `Range` read of the first {@link PROBE_BYTES}. It
|
|
339
|
+
* answers three questions at once — does this host do ranged reads, how long is
|
|
340
|
+
* the file, and what is in its first megabyte (which for a `+faststart` proxy is
|
|
341
|
+
* `ftyp` + `moov`, i.e. everything the demuxer needs to build a sample index).
|
|
342
|
+
*
|
|
343
|
+
* A `200` answer means the host ignored `Range` and sent the whole file; that
|
|
344
|
+
* body becomes a {@link WholeFileByteSource} and the caller demuxes it exactly
|
|
345
|
+
* as it did before ranged loading existed. Same for a `206` whose
|
|
346
|
+
* `Content-Range` does not carry a usable total length: without a length there
|
|
347
|
+
* is no box walk to do, so falling back is more honest than guessing.
|
|
348
|
+
*
|
|
349
|
+
* Throws on a non-2xx response, matching {@link loadBytes} so callers keep a
|
|
350
|
+
* single `catch`.
|
|
351
|
+
*/
|
|
352
|
+
export async function openByteSource(
|
|
353
|
+
src: string,
|
|
354
|
+
fileUrl: FileUrlResolver,
|
|
355
|
+
options: LoadBytesOptions = {},
|
|
356
|
+
): Promise<MediaByteSource> {
|
|
357
|
+
if (!src) throw new Error('openByteSource: empty src')
|
|
358
|
+
|
|
359
|
+
const url = fileUrl(src)
|
|
360
|
+
const res = await fetch(url, {
|
|
361
|
+
headers: { Range: `bytes=0-${PROBE_BYTES - 1}` },
|
|
362
|
+
signal: options.signal,
|
|
363
|
+
})
|
|
364
|
+
if (!res.ok) {
|
|
365
|
+
throw new Error(
|
|
366
|
+
`openByteSource: ${src} → ${res.status}${res.statusText ? ` ${res.statusText}` : ''}`,
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const body = await res.arrayBuffer()
|
|
371
|
+
if (res.status !== 206) return new WholeFileByteSource(src, body)
|
|
372
|
+
|
|
373
|
+
const total = totalFromContentRange(res.headers?.get?.('Content-Range'))
|
|
374
|
+
if (total === null) return new WholeFileByteSource(src, body)
|
|
375
|
+
// A 206 that already covers the file is the whole file — skip the range
|
|
376
|
+
// machinery for a proxy small enough to arrive in the probe.
|
|
377
|
+
if (body.byteLength >= total) return new WholeFileByteSource(src, body)
|
|
378
|
+
|
|
379
|
+
return new RangedByteSource(src, total, url, new Uint8Array(body), 0)
|
|
380
|
+
}
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SP4 T2 — hand-written type declarations for `mp4box` (the 0.5.x line).
|
|
3
|
+
*
|
|
4
|
+
* mp4box.js ships no types of its own: its `package.json` has no `types`
|
|
5
|
+
* field, `main` points at a plain-JS UMD bundle (`dist/mp4box.all.js`), and
|
|
6
|
+
* there is no `@types/mp4box` for the 0.5 line. Without this file every
|
|
7
|
+
* `import * as MP4Box from 'mp4box'` is a TS7016 error under `strict`.
|
|
8
|
+
*
|
|
9
|
+
* The spike (`spikes/playback-engine/src/mp4box.d.ts`) got away with the
|
|
10
|
+
* one-liner `declare module 'mp4box';`, which types the whole module as
|
|
11
|
+
* `any`. That is fine for a throwaway harness and wrong for a published
|
|
12
|
+
* package: `@bycrux/editor` ships `src/` and hosts typecheck it, so an `any`
|
|
13
|
+
* module would silently erase every mistake in `demux.ts` — including the
|
|
14
|
+
* exact class of mistake (reading `cts` where `dts` was meant) that SP1 bug
|
|
15
|
+
* §7.1 was. These declarations therefore cover *only what `demux.ts` calls*,
|
|
16
|
+
* typed honestly, and stop there. Everything mp4box can do beyond muxing our
|
|
17
|
+
* proxies is deliberately absent rather than stubbed.
|
|
18
|
+
*
|
|
19
|
+
* This file lives under `src/` on purpose: `package.json`'s `files` list
|
|
20
|
+
* publishes `src`, and `demux.ts` pulls it into any host's program with an
|
|
21
|
+
* explicit `/// <reference path>` (a bare ambient `.d.ts` sitting in a
|
|
22
|
+
* package directory is NOT auto-included by a consumer's `tsc`, only by this
|
|
23
|
+
* package's own `include: ["src"]`).
|
|
24
|
+
*
|
|
25
|
+
* Field names are mp4box's own (snake_case, `is_sync`, `cts`/`dts`) — kept
|
|
26
|
+
* verbatim so this file reads as a description of the library rather than a
|
|
27
|
+
* second naming convention to keep in sync.
|
|
28
|
+
*/
|
|
29
|
+
declare module 'mp4box' {
|
|
30
|
+
/**
|
|
31
|
+
* mp4box's byte reader/writer. Only used here to re-serialize a codec
|
|
32
|
+
* configuration box (avcC/hvcC/vpcC/av1C) back into the raw bytes
|
|
33
|
+
* `VideoDecoderConfig.description` wants.
|
|
34
|
+
*
|
|
35
|
+
* Note the inverted-looking constants: mp4box models endianness as a
|
|
36
|
+
* boolean where `LITTLE_ENDIAN === true` and `BIG_ENDIAN === false`. They
|
|
37
|
+
* are typed as `boolean` rather than a literal union precisely so nobody
|
|
38
|
+
* is tempted to write `true`/`false` at a call site instead of naming the
|
|
39
|
+
* constant.
|
|
40
|
+
*/
|
|
41
|
+
export class DataStream {
|
|
42
|
+
static BIG_ENDIAN: boolean
|
|
43
|
+
static LITTLE_ENDIAN: boolean
|
|
44
|
+
constructor(arrayBuffer?: ArrayBuffer, byteOffset?: number, endianness?: boolean)
|
|
45
|
+
/** The bytes written so far, trimmed to `position`. */
|
|
46
|
+
buffer: ArrayBuffer
|
|
47
|
+
position: number
|
|
48
|
+
/**
|
|
49
|
+
* Declared for `demux.test.ts`'s stand-in box, which writes a real
|
|
50
|
+
* 8-byte box header + payload so the header-strip in
|
|
51
|
+
* `descriptionForEntries` is asserted against mp4box's actual
|
|
52
|
+
* `DataStream` rather than a hand-rolled imitation of it.
|
|
53
|
+
*/
|
|
54
|
+
writeUint8Array(arr: Uint8Array): void
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A parsed ISOBMFF box. `write` is the only member `demux.ts` touches: it
|
|
59
|
+
* re-emits the box (8-byte header + payload) into a `DataStream`.
|
|
60
|
+
*/
|
|
61
|
+
export interface MP4BoxBox {
|
|
62
|
+
write(stream: DataStream): void
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* One node of the MPEG-4 descriptor tree mp4box builds while parsing an
|
|
67
|
+
* `esds` box. Unlike the boxes above this is NOT round-trippable with
|
|
68
|
+
* `write()` — it is a nested descriptor tree, which is why extracting an
|
|
69
|
+
* AudioSpecificConfig needs its own walk (see `demux.ts`).
|
|
70
|
+
*/
|
|
71
|
+
export interface MP4BoxDescriptor {
|
|
72
|
+
tag: number
|
|
73
|
+
descs?: MP4BoxDescriptor[]
|
|
74
|
+
/** Present on DecoderSpecificInfo (tag 0x05): the raw AudioSpecificConfig bytes. */
|
|
75
|
+
data?: ArrayLike<number>
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* One entry of a track's sample-description box (`stsd`). Which codec-config
|
|
80
|
+
* child is present depends on the track's codec; all four are optional and
|
|
81
|
+
* `demux.ts` takes the first one it finds.
|
|
82
|
+
*/
|
|
83
|
+
export interface MP4BoxSampleEntry {
|
|
84
|
+
avcC?: MP4BoxBox
|
|
85
|
+
hvcC?: MP4BoxBox
|
|
86
|
+
vpcC?: MP4BoxBox
|
|
87
|
+
av1C?: MP4BoxBox
|
|
88
|
+
esds?: { esd?: MP4BoxDescriptor }
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* The slice of a `trak` box `demux.ts` walks: the `stsd` entries, and the
|
|
93
|
+
* sample list mp4box builds from the sample tables.
|
|
94
|
+
*
|
|
95
|
+
* `samples` is populated by `buildSampleLists()` the moment `moov` finishes
|
|
96
|
+
* parsing — BEFORE any `mdat` byte has been seen — which is what makes ranged
|
|
97
|
+
* loading possible: every sample's `offset`/`size`/`cts`/`dts`/`is_sync` is
|
|
98
|
+
* already known, and only `data` is missing. `demux.ts`'s ranged path reads
|
|
99
|
+
* this array directly instead of going through `setExtractionOptions` +
|
|
100
|
+
* `onSamples`, because the extraction API's whole job is to hand back sample
|
|
101
|
+
* DATA, which is precisely the thing we are trying not to download.
|
|
102
|
+
*/
|
|
103
|
+
export interface MP4BoxTrak {
|
|
104
|
+
mdia: { minf: { stbl: { stsd: { entries: MP4BoxSampleEntry[] } } } }
|
|
105
|
+
samples: MP4Sample[]
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* A track as reported by `onReady`'s info object.
|
|
110
|
+
*
|
|
111
|
+
* `duration` is in this track's own `timescale` (mdhd), not the movie
|
|
112
|
+
* timescale — `movie_duration`/`movie_timescale` carry that pair separately.
|
|
113
|
+
*
|
|
114
|
+
* `track_width`/`track_height` come from tkhd and describe the *display*
|
|
115
|
+
* size after the track's rotation matrix; `video.width`/`video.height` come
|
|
116
|
+
* from the visual sample entry and are the CODED size the decoder needs.
|
|
117
|
+
* On portrait phone footage these differ.
|
|
118
|
+
*/
|
|
119
|
+
export interface MP4TrackInfo {
|
|
120
|
+
id: number
|
|
121
|
+
codec: string
|
|
122
|
+
timescale: number
|
|
123
|
+
duration: number
|
|
124
|
+
nb_samples: number
|
|
125
|
+
track_width: number
|
|
126
|
+
track_height: number
|
|
127
|
+
/** Present on video tracks. */
|
|
128
|
+
video?: { width: number; height: number }
|
|
129
|
+
/** Present on audio tracks. */
|
|
130
|
+
audio?: { sample_rate: number; channel_count: number; sample_size: number }
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** The info object handed to `onReady` once the moov box has been parsed. */
|
|
134
|
+
export interface MP4Info {
|
|
135
|
+
duration: number
|
|
136
|
+
timescale: number
|
|
137
|
+
isFragmented: boolean
|
|
138
|
+
tracks: MP4TrackInfo[]
|
|
139
|
+
videoTracks: MP4TrackInfo[]
|
|
140
|
+
audioTracks: MP4TrackInfo[]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* One extracted sample, as delivered to `onSamples` — **in decode order**,
|
|
145
|
+
* exactly as the container stores it. `cts` is presentation time and `dts`
|
|
146
|
+
* decode time, both in the track's timescale; they differ whenever the
|
|
147
|
+
* track carries B-frames.
|
|
148
|
+
*/
|
|
149
|
+
export interface MP4Sample {
|
|
150
|
+
number: number
|
|
151
|
+
track_id: number
|
|
152
|
+
is_sync: boolean
|
|
153
|
+
timescale: number
|
|
154
|
+
dts: number
|
|
155
|
+
cts: number
|
|
156
|
+
duration: number
|
|
157
|
+
size: number
|
|
158
|
+
/**
|
|
159
|
+
* Byte offset of this sample's data in the FILE, resolved from
|
|
160
|
+
* `stco`/`co64` + `stsc` while the sample list is built. Known from the
|
|
161
|
+
* moov alone, which is what the ranged loader ranges on.
|
|
162
|
+
*/
|
|
163
|
+
offset: number
|
|
164
|
+
/**
|
|
165
|
+
* The sample's encoded bytes. Typed as always-present because it is, on
|
|
166
|
+
* every sample delivered through `onSamples` — mp4box only emits a sample
|
|
167
|
+
* once it has read the data. A sample taken straight off
|
|
168
|
+
* `MP4BoxTrak.samples` is a different animal: mp4box has not filled this in
|
|
169
|
+
* yet, which is exactly why the ranged path reads `offset`/`size` and
|
|
170
|
+
* fetches the bytes itself rather than touching this field.
|
|
171
|
+
*/
|
|
172
|
+
data: Uint8Array
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* An `ArrayBuffer` tagged with its offset in the source file. mp4box reads
|
|
177
|
+
* `fileStart` off every buffer handed to `appendBuffer` to know where the
|
|
178
|
+
* chunk belongs; with a whole-file load it is always 0.
|
|
179
|
+
*/
|
|
180
|
+
export interface MP4ArrayBuffer extends ArrayBuffer {
|
|
181
|
+
fileStart: number
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/** The parser handle returned by `createFile()`. */
|
|
185
|
+
export interface MP4File {
|
|
186
|
+
/** Fires (synchronously, inside `appendBuffer`) once `moov` is parsed. */
|
|
187
|
+
onReady?: (info: MP4Info) => void
|
|
188
|
+
/** Fires with a human-readable message; mp4box does not throw. */
|
|
189
|
+
onError?: (e: string) => void
|
|
190
|
+
/** Fires with a batch of samples in DECODE order for one track. */
|
|
191
|
+
onSamples?: (trackId: number, user: unknown, samples: MP4Sample[]) => void
|
|
192
|
+
/** Returns the next expected `fileStart`. */
|
|
193
|
+
appendBuffer(data: MP4ArrayBuffer): number
|
|
194
|
+
/** Begin emitting samples for tracks registered via `setExtractionOptions`. */
|
|
195
|
+
start(): void
|
|
196
|
+
stop(): void
|
|
197
|
+
/** Synchronously drains any samples not yet emitted. */
|
|
198
|
+
flush(): void
|
|
199
|
+
setExtractionOptions(
|
|
200
|
+
trackId: number,
|
|
201
|
+
user?: unknown,
|
|
202
|
+
options?: { nbSamples?: number; rapAlignement?: boolean },
|
|
203
|
+
): void
|
|
204
|
+
getTrackById(trackId: number): MP4BoxTrak | undefined
|
|
205
|
+
releaseUsedSamples(trackId: number, sampleNumber: number): void
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* @param keepMdatData when false, mdat bytes are dropped after parsing.
|
|
210
|
+
* `demux.ts` leaves it at the default (true) because it extracts every
|
|
211
|
+
* sample's `data` up front.
|
|
212
|
+
*/
|
|
213
|
+
export function createFile(keepMdatData?: boolean): MP4File
|
|
214
|
+
}
|