@elah/core 0.1.0
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/dist/actions/index.d.ts +3 -0
- package/dist/actions/index.js +1 -0
- package/dist/actions/splitClipAtPlayhead.d.ts +8 -0
- package/dist/actions/splitClipAtPlayhead.js +34 -0
- package/dist/actions/types.d.ts +8 -0
- package/dist/actions/types.js +1 -0
- package/dist/assets/importFiles.d.ts +29 -0
- package/dist/assets/importFiles.js +351 -0
- package/dist/assets/index.d.ts +10 -0
- package/dist/assets/index.js +13 -0
- package/dist/assets/store.d.ts +13 -0
- package/dist/assets/store.js +20 -0
- package/dist/assets/types.d.ts +23 -0
- package/dist/assets/types.js +1 -0
- package/dist/debug/trace.d.ts +20 -0
- package/dist/debug/trace.js +98 -0
- package/dist/editor/TimelineEngine.d.ts +65 -0
- package/dist/editor/TimelineEngine.js +413 -0
- package/dist/editor-context.d.ts +10 -0
- package/dist/editor-context.js +10 -0
- package/dist/elements/audio.d.ts +13 -0
- package/dist/elements/audio.js +4 -0
- package/dist/elements/base.d.ts +40 -0
- package/dist/elements/base.js +39 -0
- package/dist/elements/image.d.ts +13 -0
- package/dist/elements/image.js +4 -0
- package/dist/elements/text.d.ts +12 -0
- package/dist/elements/text.js +8 -0
- package/dist/elements/video.d.ts +13 -0
- package/dist/elements/video.js +4 -0
- package/dist/export/ExportWorker.d.ts +1 -0
- package/dist/export/ExportWorker.js +371 -0
- package/dist/export/exportVideo.d.ts +3 -0
- package/dist/export/exportVideo.js +118 -0
- package/dist/export/index.d.ts +2 -0
- package/dist/export/index.js +1 -0
- package/dist/export/lazyExport.d.ts +4 -0
- package/dist/export/lazyExport.js +4 -0
- package/dist/export/types.d.ts +37 -0
- package/dist/export/types.js +1 -0
- package/dist/index.d.ts +48 -0
- package/dist/index.js +28 -0
- package/dist/media/audio/AudioPlaybackController.d.ts +26 -0
- package/dist/media/audio/AudioPlaybackController.js +125 -0
- package/dist/media/video/DecoderBackedVideoFrameProvider.d.ts +51 -0
- package/dist/media/video/DecoderBackedVideoFrameProvider.js +125 -0
- package/dist/media/video/FrameCache.d.ts +28 -0
- package/dist/media/video/FrameCache.js +88 -0
- package/dist/media/video/StreamingFrameProducer.d.ts +57 -0
- package/dist/media/video/StreamingFrameProducer.js +356 -0
- package/dist/media/video/VideoDecoderManager.d.ts +59 -0
- package/dist/media/video/VideoDecoderManager.js +342 -0
- package/dist/media/video/VideoFrameProvider.d.ts +101 -0
- package/dist/media/video/VideoFrameProvider.js +257 -0
- package/dist/media/video/demuxer/MediabunnyDemuxer.d.ts +23 -0
- package/dist/media/video/demuxer/MediabunnyDemuxer.js +88 -0
- package/dist/media/video/demuxer/createMediabunnyBackend.d.ts +32 -0
- package/dist/media/video/demuxer/createMediabunnyBackend.js +156 -0
- package/dist/media/video/index.d.ts +8 -0
- package/dist/media/video/index.js +5 -0
- package/dist/playback/PlaybackEngine.d.ts +50 -0
- package/dist/playback/PlaybackEngine.js +188 -0
- package/dist/renderer/gpu/GpuRenderer.d.ts +38 -0
- package/dist/renderer/gpu/GpuRenderer.js +208 -0
- package/dist/renderer/gpu/RenderGraph.d.ts +10 -0
- package/dist/renderer/gpu/RenderGraph.js +80 -0
- package/dist/renderer/gpu/ShaderProgram.d.ts +14 -0
- package/dist/renderer/gpu/ShaderProgram.js +76 -0
- package/dist/renderer/gpu/TexturePool.d.ts +25 -0
- package/dist/renderer/gpu/TexturePool.js +93 -0
- package/dist/renderer/gpu/VideoTexture.d.ts +13 -0
- package/dist/renderer/gpu/VideoTexture.js +54 -0
- package/dist/renderer/gpu/WebGLContext.d.ts +28 -0
- package/dist/renderer/gpu/WebGLContext.js +102 -0
- package/dist/renderer/gpu/debug/DebugGpuRenderer.d.ts +27 -0
- package/dist/renderer/gpu/debug/DebugGpuRenderer.js +108 -0
- package/dist/renderer/gpu/debug/DebugOverlay.d.ts +17 -0
- package/dist/renderer/gpu/debug/DebugOverlay.js +83 -0
- package/dist/renderer/gpu/debug/GpuDebugCounters.d.ts +38 -0
- package/dist/renderer/gpu/debug/GpuDebugCounters.js +72 -0
- package/dist/renderer/gpu/debug/GpuDebugGlobal.d.ts +16 -0
- package/dist/renderer/gpu/debug/GpuDebugGlobal.js +14 -0
- package/dist/renderer/gpu/debug/GpuRendererDebugPanel.d.ts +31 -0
- package/dist/renderer/gpu/debug/GpuRendererDebugPanel.js +128 -0
- package/dist/renderer/gpu/debug/RecordingGl.d.ts +88 -0
- package/dist/renderer/gpu/debug/RecordingGl.js +214 -0
- package/dist/renderer/gpu/debug/playground.d.ts +20 -0
- package/dist/renderer/gpu/debug/playground.js +64 -0
- package/dist/renderer/gpu/debug/scenarios.d.ts +7 -0
- package/dist/renderer/gpu/debug/scenarios.js +145 -0
- package/dist/renderer/gpu/debug/types.d.ts +16 -0
- package/dist/renderer/gpu/debug/types.js +1 -0
- package/dist/renderer/gpu/layers/FrameProbeLayer.d.ts +16 -0
- package/dist/renderer/gpu/layers/FrameProbeLayer.js +127 -0
- package/dist/renderer/gpu/layers/ImageLayer.d.ts +23 -0
- package/dist/renderer/gpu/layers/ImageLayer.js +124 -0
- package/dist/renderer/gpu/layers/TestLayer.d.ts +16 -0
- package/dist/renderer/gpu/layers/TestLayer.js +109 -0
- package/dist/renderer/gpu/layers/TextLayer.d.ts +19 -0
- package/dist/renderer/gpu/layers/TextLayer.js +166 -0
- package/dist/renderer/gpu/layers/VideoLayer.d.ts +38 -0
- package/dist/renderer/gpu/layers/VideoLayer.js +194 -0
- package/dist/renderer/gpu/layers/drawRect.d.ts +13 -0
- package/dist/renderer/gpu/layers/drawRect.js +55 -0
- package/dist/renderer/gpu/layers/objectFit.d.ts +7 -0
- package/dist/renderer/gpu/layers/objectFit.js +26 -0
- package/dist/renderer/gpu/layers/textLayout.d.ts +47 -0
- package/dist/renderer/gpu/layers/textLayout.js +82 -0
- package/dist/renderer/gpu/layers/types.d.ts +18 -0
- package/dist/renderer/gpu/layers/types.js +1 -0
- package/dist/renderer/gpu/shaders/quad.frag.d.ts +1 -0
- package/dist/renderer/gpu/shaders/quad.frag.js +14 -0
- package/dist/renderer/gpu/shaders/quad.vert.d.ts +1 -0
- package/dist/renderer/gpu/shaders/quad.vert.js +28 -0
- package/dist/renderer/gpu/types.d.ts +21 -0
- package/dist/renderer/gpu/types.js +1 -0
- package/dist/renderer/gpu/viewport.d.ts +7 -0
- package/dist/renderer/gpu/viewport.js +33 -0
- package/dist/renderer/types.d.ts +7 -0
- package/dist/renderer/types.js +1 -0
- package/dist/resolver/resolveTimeline.d.ts +3 -0
- package/dist/resolver/resolveTimeline.js +249 -0
- package/dist/resolver/scene.d.ts +54 -0
- package/dist/resolver/scene.js +1 -0
- package/dist/stores/playback.store.d.ts +50 -0
- package/dist/stores/playback.store.js +42 -0
- package/dist/stores/selection.store.d.ts +12 -0
- package/dist/stores/selection.store.js +19 -0
- package/dist/stores/tracks.store.d.ts +19 -0
- package/dist/stores/tracks.store.js +27 -0
- package/dist/stores/transitions.store.d.ts +9 -0
- package/dist/stores/transitions.store.js +7 -0
- package/dist/track/track.d.ts +8 -0
- package/dist/track/track.js +19 -0
- package/dist/types/index.d.ts +117 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/frames.d.ts +20 -0
- package/dist/utils/frames.js +40 -0
- package/dist/utils/id.d.ts +1 -0
- package/dist/utils/id.js +3 -0
- package/dist/utils/snap.d.ts +5 -0
- package/dist/utils/snap.js +79 -0
- package/dist/visitor/add.d.ts +3 -0
- package/dist/visitor/add.js +16 -0
- package/dist/visitor/clone.d.ts +3 -0
- package/dist/visitor/clone.js +25 -0
- package/dist/visitor/remove.d.ts +5 -0
- package/dist/visitor/remove.js +29 -0
- package/dist/visitor/split.d.ts +3 -0
- package/dist/visitor/split.js +31 -0
- package/dist/visitor/update.d.ts +4 -0
- package/dist/visitor/update.js +31 -0
- package/package.json +31 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface DemuxerBackend {
|
|
2
|
+
open(src: string): Promise<void>;
|
|
3
|
+
getConfig(): VideoDecoderConfig;
|
|
4
|
+
packets(timeRange: [number, number]): AsyncIterable<EncodedVideoChunk>;
|
|
5
|
+
seekToKeyframe(time: number): Promise<void>;
|
|
6
|
+
dispose(): void;
|
|
7
|
+
}
|
|
8
|
+
export type DemuxerFactory = () => DemuxerBackend;
|
|
9
|
+
export declare class MediabunnyDemuxer {
|
|
10
|
+
private readonly _factory;
|
|
11
|
+
private _backend;
|
|
12
|
+
private _src;
|
|
13
|
+
private _disposed;
|
|
14
|
+
constructor(factory?: DemuxerFactory);
|
|
15
|
+
get src(): string | null;
|
|
16
|
+
get isOpen(): boolean;
|
|
17
|
+
open(src: string): Promise<void>;
|
|
18
|
+
getConfig(): VideoDecoderConfig;
|
|
19
|
+
packets(timeRange: [number, number]): AsyncIterable<EncodedVideoChunk>;
|
|
20
|
+
seekToKeyframe(time: number): Promise<void>;
|
|
21
|
+
dispose(): void;
|
|
22
|
+
private _assertOpen;
|
|
23
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
export class MediabunnyDemuxer {
|
|
2
|
+
constructor(factory) {
|
|
3
|
+
this._backend = null;
|
|
4
|
+
this._src = null;
|
|
5
|
+
this._disposed = false;
|
|
6
|
+
this._factory = factory ?? null;
|
|
7
|
+
}
|
|
8
|
+
get src() {
|
|
9
|
+
return this._src;
|
|
10
|
+
}
|
|
11
|
+
get isOpen() {
|
|
12
|
+
return this._src !== null && !this._disposed;
|
|
13
|
+
}
|
|
14
|
+
async open(src) {
|
|
15
|
+
if (this._disposed) {
|
|
16
|
+
throw new Error('MediabunnyDemuxer: disposed');
|
|
17
|
+
}
|
|
18
|
+
if (this._src !== null) {
|
|
19
|
+
throw new Error('MediabunnyDemuxer: already open');
|
|
20
|
+
}
|
|
21
|
+
const backend = this._factory
|
|
22
|
+
? this._factory()
|
|
23
|
+
: await createDefaultBackend();
|
|
24
|
+
await backend.open(src);
|
|
25
|
+
this._backend = backend;
|
|
26
|
+
this._src = src;
|
|
27
|
+
}
|
|
28
|
+
getConfig() {
|
|
29
|
+
this._assertOpen();
|
|
30
|
+
return this._backend.getConfig();
|
|
31
|
+
}
|
|
32
|
+
packets(timeRange) {
|
|
33
|
+
this._assertOpen();
|
|
34
|
+
return this._backend.packets(timeRange);
|
|
35
|
+
}
|
|
36
|
+
async seekToKeyframe(time) {
|
|
37
|
+
this._assertOpen();
|
|
38
|
+
await this._backend.seekToKeyframe(time);
|
|
39
|
+
}
|
|
40
|
+
dispose() {
|
|
41
|
+
if (this._disposed)
|
|
42
|
+
return;
|
|
43
|
+
this._disposed = true;
|
|
44
|
+
this._backend?.dispose();
|
|
45
|
+
this._backend = null;
|
|
46
|
+
this._src = null;
|
|
47
|
+
}
|
|
48
|
+
_assertOpen() {
|
|
49
|
+
if (this._disposed) {
|
|
50
|
+
throw new Error('MediabunnyDemuxer: disposed');
|
|
51
|
+
}
|
|
52
|
+
if (!this._backend || this._src === null) {
|
|
53
|
+
throw new Error('MediabunnyDemuxer: not open');
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
async function createDefaultBackend() {
|
|
58
|
+
let mediabunny;
|
|
59
|
+
try {
|
|
60
|
+
const moduleName = 'mediabunny';
|
|
61
|
+
mediabunny = await import(moduleName);
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
throw new Error([
|
|
65
|
+
'MediabunnyDemuxer: mediabunny could not be loaded automatically.',
|
|
66
|
+
'',
|
|
67
|
+
'To enable real video decode, inject a DemuxerFactory into GpuRenderer:',
|
|
68
|
+
'',
|
|
69
|
+
' // In your app (e.g. apps/playground/src/createPlaygroundDemuxerFactory.ts):',
|
|
70
|
+
' import * as mediabunny from "mediabunny"',
|
|
71
|
+
' import { createMediabunnyBackend } from "@elah/editor"',
|
|
72
|
+
'',
|
|
73
|
+
' const renderer = new GpuRenderer({',
|
|
74
|
+
' demuxerFactory: () => createMediabunnyBackend(mediabunny),',
|
|
75
|
+
' })',
|
|
76
|
+
'',
|
|
77
|
+
'Without this, the renderer falls back to SyntheticVideoFrameProvider (dev mode).',
|
|
78
|
+
'See: apps/playground/src/createPlaygroundDemuxerFactory.ts',
|
|
79
|
+
].join('\n'));
|
|
80
|
+
}
|
|
81
|
+
const { createMediabunnyBackend, isMediabunnyCompatible } = await import('./createMediabunnyBackend');
|
|
82
|
+
if (!isMediabunnyCompatible(mediabunny)) {
|
|
83
|
+
throw new Error('MediabunnyDemuxer: mediabunny module does not expose the expected API ' +
|
|
84
|
+
'(needs Input, BlobSource, EncodedPacketSink, ALL_FORMATS). ' +
|
|
85
|
+
'See createMediabunnyBackend.ts.');
|
|
86
|
+
}
|
|
87
|
+
return createMediabunnyBackend(mediabunny);
|
|
88
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { DemuxerBackend } from './MediabunnyDemuxer';
|
|
2
|
+
interface MbEncodedPacket {
|
|
3
|
+
readonly timestamp: number;
|
|
4
|
+
toEncodedVideoChunk(): EncodedVideoChunk;
|
|
5
|
+
}
|
|
6
|
+
interface MbVideoTrack {
|
|
7
|
+
getDecoderConfig(): Promise<VideoDecoderConfig | null>;
|
|
8
|
+
}
|
|
9
|
+
interface MbInput {
|
|
10
|
+
getPrimaryVideoTrack(): Promise<MbVideoTrack | null>;
|
|
11
|
+
[key: string]: unknown;
|
|
12
|
+
}
|
|
13
|
+
interface MbEncodedPacketSink {
|
|
14
|
+
getKeyPacket(seconds: number): Promise<MbEncodedPacket | null>;
|
|
15
|
+
getNextPacket(prev: MbEncodedPacket): Promise<MbEncodedPacket | null>;
|
|
16
|
+
}
|
|
17
|
+
export interface MediabunnyModule {
|
|
18
|
+
Input: new (opts: {
|
|
19
|
+
formats: unknown[];
|
|
20
|
+
source: unknown;
|
|
21
|
+
}) => MbInput;
|
|
22
|
+
BlobSource: new (blob: Blob) => unknown;
|
|
23
|
+
EncodedPacketSink: new (track: MbVideoTrack) => MbEncodedPacketSink;
|
|
24
|
+
ALL_FORMATS: unknown[];
|
|
25
|
+
[key: string]: unknown;
|
|
26
|
+
}
|
|
27
|
+
export interface CreateMediabunnyBackendOpts {
|
|
28
|
+
blobResolver?: (src: string) => Promise<Blob>;
|
|
29
|
+
}
|
|
30
|
+
export declare function isMediabunnyCompatible(module: unknown): module is MediabunnyModule;
|
|
31
|
+
export declare function createMediabunnyBackend(mb: MediabunnyModule, opts?: CreateMediabunnyBackendOpts): DemuxerBackend;
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
function _demuxTrace(msg, data) {
|
|
2
|
+
if (typeof globalThis !== 'undefined' &&
|
|
3
|
+
globalThis.__DEMUX_DEBUG__) {
|
|
4
|
+
if (data) {
|
|
5
|
+
console.log(`[DEMUX-TRACE] ${msg}`, data);
|
|
6
|
+
}
|
|
7
|
+
else {
|
|
8
|
+
console.log(`[DEMUX-TRACE] ${msg}`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function isMediabunnyCompatible(module) {
|
|
13
|
+
if (!module || typeof module !== 'object')
|
|
14
|
+
return false;
|
|
15
|
+
const m = module;
|
|
16
|
+
return (typeof m['Input'] === 'function' &&
|
|
17
|
+
typeof m['BlobSource'] === 'function' &&
|
|
18
|
+
typeof m['EncodedPacketSink'] === 'function' &&
|
|
19
|
+
Array.isArray(m['ALL_FORMATS']));
|
|
20
|
+
}
|
|
21
|
+
export function createMediabunnyBackend(mb, opts = {}) {
|
|
22
|
+
_assertMediabunnyApi(mb);
|
|
23
|
+
const resolve = opts.blobResolver ?? defaultBlobResolver;
|
|
24
|
+
let _sink = null;
|
|
25
|
+
let _config = null;
|
|
26
|
+
let _seekPacket = null;
|
|
27
|
+
let _nextPacket = null;
|
|
28
|
+
let _lastEndSec = null;
|
|
29
|
+
let _disposed = false;
|
|
30
|
+
return {
|
|
31
|
+
async open(src) {
|
|
32
|
+
if (_disposed)
|
|
33
|
+
throw new Error('createMediabunnyBackend: already disposed');
|
|
34
|
+
const blob = await resolve(src);
|
|
35
|
+
const input = new mb.Input({
|
|
36
|
+
formats: mb.ALL_FORMATS,
|
|
37
|
+
source: new mb.BlobSource(blob),
|
|
38
|
+
});
|
|
39
|
+
const track = await input.getPrimaryVideoTrack();
|
|
40
|
+
if (!track) {
|
|
41
|
+
throw new Error(`createMediabunnyBackend: no video track found in "${src}". ` +
|
|
42
|
+
'Ensure the file is a supported video format (MP4, WebM, MKV, MOV).');
|
|
43
|
+
}
|
|
44
|
+
const config = await track.getDecoderConfig();
|
|
45
|
+
if (!config) {
|
|
46
|
+
throw new Error(`createMediabunnyBackend: video track codec is not supported by this browser in "${src}". ` +
|
|
47
|
+
'Try a file encoded with H.264 (AVC) or VP8/VP9.');
|
|
48
|
+
}
|
|
49
|
+
_config = config;
|
|
50
|
+
_sink = new mb.EncodedPacketSink(track);
|
|
51
|
+
},
|
|
52
|
+
getConfig() {
|
|
53
|
+
_assertOpen(_sink, _config);
|
|
54
|
+
return _config;
|
|
55
|
+
},
|
|
56
|
+
async *packets([startUs, endUs]) {
|
|
57
|
+
_assertOpen(_sink, _config);
|
|
58
|
+
const startSec = startUs / 1e6;
|
|
59
|
+
const endSec = endUs / 1e6;
|
|
60
|
+
let pkt;
|
|
61
|
+
let yieldFirstUnconditionally = false;
|
|
62
|
+
let branch;
|
|
63
|
+
if (_seekPacket !== null) {
|
|
64
|
+
pkt = _seekPacket;
|
|
65
|
+
_seekPacket = null;
|
|
66
|
+
_nextPacket = null;
|
|
67
|
+
branch = 'seek';
|
|
68
|
+
}
|
|
69
|
+
else if (_nextPacket !== null &&
|
|
70
|
+
_lastEndSec !== null &&
|
|
71
|
+
Math.abs(startSec - _lastEndSec) < 1e-9) {
|
|
72
|
+
pkt = _nextPacket;
|
|
73
|
+
_nextPacket = null;
|
|
74
|
+
yieldFirstUnconditionally = true;
|
|
75
|
+
branch = 'contiguous';
|
|
76
|
+
}
|
|
77
|
+
else {
|
|
78
|
+
pkt = await _sink.getKeyPacket(startSec);
|
|
79
|
+
_nextPacket = null;
|
|
80
|
+
branch = 'keyframe';
|
|
81
|
+
}
|
|
82
|
+
_demuxTrace('packets() start', {
|
|
83
|
+
branch,
|
|
84
|
+
startSec,
|
|
85
|
+
endSec,
|
|
86
|
+
lastEndSec: _lastEndSec,
|
|
87
|
+
contiguityGap: _lastEndSec !== null ? startSec - _lastEndSec : null,
|
|
88
|
+
firstPktSec: pkt ? pkt.timestamp : null,
|
|
89
|
+
});
|
|
90
|
+
const yielded = [];
|
|
91
|
+
while (pkt !== null && (yieldFirstUnconditionally || pkt.timestamp < endSec)) {
|
|
92
|
+
if (_disposed)
|
|
93
|
+
break;
|
|
94
|
+
yielded.push(pkt.timestamp);
|
|
95
|
+
yield pkt.toEncodedVideoChunk();
|
|
96
|
+
yieldFirstUnconditionally = false;
|
|
97
|
+
pkt = await _sink.getNextPacket(pkt);
|
|
98
|
+
}
|
|
99
|
+
_demuxTrace('packets() done', {
|
|
100
|
+
branch,
|
|
101
|
+
yieldedCount: yielded.length,
|
|
102
|
+
yieldedSec: yielded,
|
|
103
|
+
nextPktSec: pkt ? pkt.timestamp : null,
|
|
104
|
+
});
|
|
105
|
+
_nextPacket = pkt;
|
|
106
|
+
_lastEndSec = endSec;
|
|
107
|
+
},
|
|
108
|
+
async seekToKeyframe(timeUs) {
|
|
109
|
+
_assertOpen(_sink, _config);
|
|
110
|
+
const timeSec = timeUs / 1e6;
|
|
111
|
+
_seekPacket = await _sink.getKeyPacket(timeSec);
|
|
112
|
+
_nextPacket = null;
|
|
113
|
+
_lastEndSec = null;
|
|
114
|
+
},
|
|
115
|
+
dispose() {
|
|
116
|
+
_disposed = true;
|
|
117
|
+
_sink = null;
|
|
118
|
+
_config = null;
|
|
119
|
+
_seekPacket = null;
|
|
120
|
+
_nextPacket = null;
|
|
121
|
+
_lastEndSec = null;
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const defaultBlobResolver = (src) => fetch(src).then((r) => {
|
|
126
|
+
if (!r.ok) {
|
|
127
|
+
throw new Error(`createMediabunnyBackend: failed to fetch "${src}" (${r.status} ${r.statusText})`);
|
|
128
|
+
}
|
|
129
|
+
return r.blob();
|
|
130
|
+
});
|
|
131
|
+
function _assertOpen(sink, config) {
|
|
132
|
+
if (!sink || !config) {
|
|
133
|
+
throw new Error('createMediabunnyBackend: backend not open — call open(src) first');
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function _assertMediabunnyApi(m) {
|
|
137
|
+
if (!isMediabunnyCompatible(m)) {
|
|
138
|
+
throw new Error([
|
|
139
|
+
'createMediabunnyBackend: the mediabunny module does not expose the expected API.',
|
|
140
|
+
'',
|
|
141
|
+
'Required exports: Input, BlobSource, EncodedPacketSink, ALL_FORMATS.',
|
|
142
|
+
'',
|
|
143
|
+
'Possible causes:',
|
|
144
|
+
' 1. mediabunny is not installed. Run: npm install mediabunny',
|
|
145
|
+
' 2. Incompatible version. This adapter targets mediabunny v1.x.',
|
|
146
|
+
' 3. Wrong import. Use: import * as mediabunny from "mediabunny"',
|
|
147
|
+
'',
|
|
148
|
+
'Usage:',
|
|
149
|
+
' import * as mediabunny from "mediabunny"',
|
|
150
|
+
' import { createMediabunnyBackend } from "@elah/editor"',
|
|
151
|
+
' new GpuRenderer({ demuxerFactory: () => createMediabunnyBackend(mediabunny) })',
|
|
152
|
+
'',
|
|
153
|
+
'See: apps/playground/src/createPlaygroundDemuxerFactory.ts for a full example.',
|
|
154
|
+
].join('\n'));
|
|
155
|
+
}
|
|
156
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type { VideoFrameProvider, VideoFrameProviderDeps } from './VideoFrameProvider';
|
|
2
|
+
export { createVideoFrameProvider, MockVideoFrameProvider, SyntheticVideoFrameProvider } from './VideoFrameProvider';
|
|
3
|
+
export { StreamingFrameProducer } from './StreamingFrameProducer';
|
|
4
|
+
export type { StreamingFrameProducerOptions } from './StreamingFrameProducer';
|
|
5
|
+
export { DecoderBackedVideoFrameProvider } from './DecoderBackedVideoFrameProvider';
|
|
6
|
+
export { FrameCache } from './FrameCache';
|
|
7
|
+
export type { DemuxerFactory, DemuxerBackend } from './demuxer/MediabunnyDemuxer';
|
|
8
|
+
export { createMediabunnyBackend, isMediabunnyCompatible } from './demuxer/createMediabunnyBackend';
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { createVideoFrameProvider, MockVideoFrameProvider, SyntheticVideoFrameProvider } from './VideoFrameProvider';
|
|
2
|
+
export { StreamingFrameProducer } from './StreamingFrameProducer';
|
|
3
|
+
export { DecoderBackedVideoFrameProvider } from './DecoderBackedVideoFrameProvider';
|
|
4
|
+
export { FrameCache } from './FrameCache';
|
|
5
|
+
export { createMediabunnyBackend, isMediabunnyCompatible } from './demuxer/createMediabunnyBackend';
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface PlaybackSnapshot {
|
|
2
|
+
currentFrame: number;
|
|
3
|
+
isPlaying: boolean;
|
|
4
|
+
playbackRate: number;
|
|
5
|
+
loop: boolean;
|
|
6
|
+
epoch: number;
|
|
7
|
+
}
|
|
8
|
+
export interface PlaybackEngineConfig {
|
|
9
|
+
fps: number;
|
|
10
|
+
getTotalFrames: () => number;
|
|
11
|
+
now?: () => number;
|
|
12
|
+
}
|
|
13
|
+
type PlaybackListener = (snapshot: PlaybackSnapshot) => void;
|
|
14
|
+
export declare class PlaybackEngine {
|
|
15
|
+
private anchorFrame;
|
|
16
|
+
private anchorTime;
|
|
17
|
+
private _playing;
|
|
18
|
+
private _rate;
|
|
19
|
+
private _loop;
|
|
20
|
+
private _epoch;
|
|
21
|
+
private _lastNotifiedFrame;
|
|
22
|
+
private _lastTimeupdateAt;
|
|
23
|
+
private readonly fps;
|
|
24
|
+
private readonly getTotalFrames;
|
|
25
|
+
private readonly now;
|
|
26
|
+
private rafId;
|
|
27
|
+
private listeners;
|
|
28
|
+
private timeupdateListeners;
|
|
29
|
+
private readonly onVisibility;
|
|
30
|
+
constructor(config: PlaybackEngineConfig);
|
|
31
|
+
getFrameAt(t?: number): number;
|
|
32
|
+
private integratedFrameAt;
|
|
33
|
+
get currentFrame(): number;
|
|
34
|
+
get currentTime(): number;
|
|
35
|
+
get isPlaying(): boolean;
|
|
36
|
+
get playbackRate(): number;
|
|
37
|
+
get loop(): boolean;
|
|
38
|
+
play(): void;
|
|
39
|
+
pause(): void;
|
|
40
|
+
seek(frame: number): void;
|
|
41
|
+
setPlaybackRate(rate: number): void;
|
|
42
|
+
setLoop(loop: boolean): void;
|
|
43
|
+
subscribe(listener: PlaybackListener): () => void;
|
|
44
|
+
subscribeTimeupdate(listener: PlaybackListener): () => void;
|
|
45
|
+
destroy(): void;
|
|
46
|
+
private startRAF;
|
|
47
|
+
private notify;
|
|
48
|
+
private emit;
|
|
49
|
+
}
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { trace } from '../debug/trace';
|
|
2
|
+
export class PlaybackEngine {
|
|
3
|
+
constructor(config) {
|
|
4
|
+
this.anchorFrame = 0;
|
|
5
|
+
this.anchorTime = 0;
|
|
6
|
+
this._playing = false;
|
|
7
|
+
this._rate = 1;
|
|
8
|
+
this._loop = false;
|
|
9
|
+
this._epoch = 0;
|
|
10
|
+
this._lastNotifiedFrame = 0;
|
|
11
|
+
this._lastTimeupdateAt = -Infinity;
|
|
12
|
+
this.rafId = null;
|
|
13
|
+
this.listeners = new Set();
|
|
14
|
+
this.timeupdateListeners = new Set();
|
|
15
|
+
this.onVisibility = () => {
|
|
16
|
+
if (typeof document === 'undefined' || !this._playing)
|
|
17
|
+
return;
|
|
18
|
+
const t = this.now();
|
|
19
|
+
if (document.hidden) {
|
|
20
|
+
this.anchorFrame = this.integratedFrameAt(t);
|
|
21
|
+
this.anchorTime = t;
|
|
22
|
+
}
|
|
23
|
+
else {
|
|
24
|
+
this.anchorTime = t;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
this.fps = config.fps;
|
|
28
|
+
this.getTotalFrames = config.getTotalFrames;
|
|
29
|
+
this.now =
|
|
30
|
+
config.now ??
|
|
31
|
+
(() => {
|
|
32
|
+
return performance.now() / 1000;
|
|
33
|
+
});
|
|
34
|
+
if (typeof document !== 'undefined') {
|
|
35
|
+
document.addEventListener('visibilitychange', this.onVisibility);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
getFrameAt(t = this.now()) {
|
|
39
|
+
if (!this._playing)
|
|
40
|
+
return this.anchorFrame;
|
|
41
|
+
if (typeof document !== 'undefined' && document.hidden)
|
|
42
|
+
return this.anchorFrame;
|
|
43
|
+
return this.integratedFrameAt(t);
|
|
44
|
+
}
|
|
45
|
+
integratedFrameAt(t) {
|
|
46
|
+
return this.anchorFrame + (t - this.anchorTime) * this.fps * this._rate;
|
|
47
|
+
}
|
|
48
|
+
get currentFrame() {
|
|
49
|
+
return Math.floor(this.getFrameAt());
|
|
50
|
+
}
|
|
51
|
+
get currentTime() {
|
|
52
|
+
return this.currentFrame / this.fps;
|
|
53
|
+
}
|
|
54
|
+
get isPlaying() {
|
|
55
|
+
return this._playing;
|
|
56
|
+
}
|
|
57
|
+
get playbackRate() {
|
|
58
|
+
return this._rate;
|
|
59
|
+
}
|
|
60
|
+
get loop() {
|
|
61
|
+
return this._loop;
|
|
62
|
+
}
|
|
63
|
+
play() {
|
|
64
|
+
if (this._playing)
|
|
65
|
+
return;
|
|
66
|
+
this.anchorTime = this.now();
|
|
67
|
+
this._playing = true;
|
|
68
|
+
this._epoch++;
|
|
69
|
+
this.notify();
|
|
70
|
+
this.startRAF();
|
|
71
|
+
}
|
|
72
|
+
pause() {
|
|
73
|
+
if (!this._playing)
|
|
74
|
+
return;
|
|
75
|
+
this.anchorFrame = this.getFrameAt();
|
|
76
|
+
this.anchorTime = this.now();
|
|
77
|
+
this._playing = false;
|
|
78
|
+
this._epoch++;
|
|
79
|
+
this.notify();
|
|
80
|
+
}
|
|
81
|
+
seek(frame) {
|
|
82
|
+
const next = Math.max(0, Math.floor(frame));
|
|
83
|
+
trace('SET_PLAYHEAD', { target: next, fromAnchor: this.anchorFrame, playing: this._playing });
|
|
84
|
+
this.anchorFrame = next;
|
|
85
|
+
this.anchorTime = this.now();
|
|
86
|
+
this._epoch++;
|
|
87
|
+
this.notify();
|
|
88
|
+
}
|
|
89
|
+
setPlaybackRate(rate) {
|
|
90
|
+
if (rate === this._rate)
|
|
91
|
+
return;
|
|
92
|
+
this.anchorFrame = this.getFrameAt();
|
|
93
|
+
this.anchorTime = this.now();
|
|
94
|
+
this._rate = rate;
|
|
95
|
+
this._epoch++;
|
|
96
|
+
this.notify();
|
|
97
|
+
}
|
|
98
|
+
setLoop(loop) {
|
|
99
|
+
if (loop === this._loop)
|
|
100
|
+
return;
|
|
101
|
+
this._loop = loop;
|
|
102
|
+
this._epoch++;
|
|
103
|
+
this.notify();
|
|
104
|
+
}
|
|
105
|
+
subscribe(listener) {
|
|
106
|
+
this.listeners.add(listener);
|
|
107
|
+
return () => this.listeners.delete(listener);
|
|
108
|
+
}
|
|
109
|
+
subscribeTimeupdate(listener) {
|
|
110
|
+
this.timeupdateListeners.add(listener);
|
|
111
|
+
return () => this.timeupdateListeners.delete(listener);
|
|
112
|
+
}
|
|
113
|
+
destroy() {
|
|
114
|
+
if (this.rafId !== null) {
|
|
115
|
+
cancelAnimationFrame(this.rafId);
|
|
116
|
+
this.rafId = null;
|
|
117
|
+
}
|
|
118
|
+
if (typeof document !== 'undefined') {
|
|
119
|
+
document.removeEventListener('visibilitychange', this.onVisibility);
|
|
120
|
+
}
|
|
121
|
+
this.listeners.clear();
|
|
122
|
+
this.timeupdateListeners.clear();
|
|
123
|
+
}
|
|
124
|
+
startRAF() {
|
|
125
|
+
if (this.rafId !== null)
|
|
126
|
+
return;
|
|
127
|
+
const tick = () => {
|
|
128
|
+
if (!this._playing) {
|
|
129
|
+
this.rafId = null;
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
this.rafId = requestAnimationFrame(tick);
|
|
133
|
+
const f = this.getFrameAt();
|
|
134
|
+
const intF = Math.floor(f);
|
|
135
|
+
const totalF = Math.max(this.getTotalFrames(), this.fps * 10);
|
|
136
|
+
if (intF >= totalF) {
|
|
137
|
+
if (this._loop) {
|
|
138
|
+
this.anchorFrame = 0;
|
|
139
|
+
this.anchorTime = this.now();
|
|
140
|
+
this._lastNotifiedFrame = 0;
|
|
141
|
+
this._epoch++;
|
|
142
|
+
this.notify();
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
this.anchorFrame = totalF - 1;
|
|
146
|
+
this.anchorTime = this.now();
|
|
147
|
+
this._playing = false;
|
|
148
|
+
this.rafId = null;
|
|
149
|
+
this._lastNotifiedFrame = totalF - 1;
|
|
150
|
+
this._epoch++;
|
|
151
|
+
this.notify();
|
|
152
|
+
}
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (intF !== this._lastNotifiedFrame) {
|
|
156
|
+
this._lastNotifiedFrame = intF;
|
|
157
|
+
this.notify();
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
this._lastNotifiedFrame = Math.floor(this.getFrameAt());
|
|
161
|
+
this.rafId = requestAnimationFrame(tick);
|
|
162
|
+
}
|
|
163
|
+
notify() {
|
|
164
|
+
trace('PLAYHEAD', { frame: this.currentFrame, epoch: this._epoch, playing: this._playing });
|
|
165
|
+
const snapshot = {
|
|
166
|
+
currentFrame: this.currentFrame,
|
|
167
|
+
isPlaying: this._playing,
|
|
168
|
+
playbackRate: this._rate,
|
|
169
|
+
loop: this._loop,
|
|
170
|
+
epoch: this._epoch,
|
|
171
|
+
};
|
|
172
|
+
this.emit(this.listeners, snapshot);
|
|
173
|
+
const t = this.now();
|
|
174
|
+
if (t - this._lastTimeupdateAt >= 0.1) {
|
|
175
|
+
this._lastTimeupdateAt = t;
|
|
176
|
+
this.emit(this.timeupdateListeners, snapshot);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
emit(set, snapshot) {
|
|
180
|
+
set.forEach((fn) => {
|
|
181
|
+
try {
|
|
182
|
+
fn(snapshot);
|
|
183
|
+
}
|
|
184
|
+
catch {
|
|
185
|
+
}
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { Scene } from '../../resolver/scene';
|
|
2
|
+
import type { Renderer } from '../types';
|
|
3
|
+
import { VideoLayer } from './layers/VideoLayer';
|
|
4
|
+
import type { RendererOptions } from './types';
|
|
5
|
+
export declare class GpuRenderer implements Renderer {
|
|
6
|
+
private readonly _options;
|
|
7
|
+
private _glCtx;
|
|
8
|
+
private _renderGraph;
|
|
9
|
+
private _texturePool;
|
|
10
|
+
private _videoLayer;
|
|
11
|
+
private _textLayer;
|
|
12
|
+
private _imageLayer;
|
|
13
|
+
private _debugPanel;
|
|
14
|
+
private _container;
|
|
15
|
+
private _mounted;
|
|
16
|
+
private _debugEnabled;
|
|
17
|
+
private _lastScene;
|
|
18
|
+
private _viewport;
|
|
19
|
+
private _lastRenderTime;
|
|
20
|
+
private _fps;
|
|
21
|
+
private _lastRenderDurationMs;
|
|
22
|
+
private _noOpTicks;
|
|
23
|
+
constructor(options?: RendererOptions);
|
|
24
|
+
mount(container: HTMLElement): void;
|
|
25
|
+
resize(cssWidth: number, cssHeight: number, dpr?: number): void;
|
|
26
|
+
render(scene: Scene): void;
|
|
27
|
+
getCanvas(): HTMLCanvasElement | null;
|
|
28
|
+
setDebug(enabled: boolean): void;
|
|
29
|
+
dispose(): void;
|
|
30
|
+
get videoLayer(): VideoLayer | null;
|
|
31
|
+
get fps(): number;
|
|
32
|
+
get lastRenderDurationMs(): number;
|
|
33
|
+
private _buildLayerContext;
|
|
34
|
+
private _attachDebugPanel;
|
|
35
|
+
private _buildDebugSnapshot;
|
|
36
|
+
private _handleContextLost;
|
|
37
|
+
private _handleContextRestored;
|
|
38
|
+
}
|