@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.
Files changed (153) hide show
  1. package/dist/actions/index.d.ts +3 -0
  2. package/dist/actions/index.js +1 -0
  3. package/dist/actions/splitClipAtPlayhead.d.ts +8 -0
  4. package/dist/actions/splitClipAtPlayhead.js +34 -0
  5. package/dist/actions/types.d.ts +8 -0
  6. package/dist/actions/types.js +1 -0
  7. package/dist/assets/importFiles.d.ts +29 -0
  8. package/dist/assets/importFiles.js +351 -0
  9. package/dist/assets/index.d.ts +10 -0
  10. package/dist/assets/index.js +13 -0
  11. package/dist/assets/store.d.ts +13 -0
  12. package/dist/assets/store.js +20 -0
  13. package/dist/assets/types.d.ts +23 -0
  14. package/dist/assets/types.js +1 -0
  15. package/dist/debug/trace.d.ts +20 -0
  16. package/dist/debug/trace.js +98 -0
  17. package/dist/editor/TimelineEngine.d.ts +65 -0
  18. package/dist/editor/TimelineEngine.js +413 -0
  19. package/dist/editor-context.d.ts +10 -0
  20. package/dist/editor-context.js +10 -0
  21. package/dist/elements/audio.d.ts +13 -0
  22. package/dist/elements/audio.js +4 -0
  23. package/dist/elements/base.d.ts +40 -0
  24. package/dist/elements/base.js +39 -0
  25. package/dist/elements/image.d.ts +13 -0
  26. package/dist/elements/image.js +4 -0
  27. package/dist/elements/text.d.ts +12 -0
  28. package/dist/elements/text.js +8 -0
  29. package/dist/elements/video.d.ts +13 -0
  30. package/dist/elements/video.js +4 -0
  31. package/dist/export/ExportWorker.d.ts +1 -0
  32. package/dist/export/ExportWorker.js +371 -0
  33. package/dist/export/exportVideo.d.ts +3 -0
  34. package/dist/export/exportVideo.js +118 -0
  35. package/dist/export/index.d.ts +2 -0
  36. package/dist/export/index.js +1 -0
  37. package/dist/export/lazyExport.d.ts +4 -0
  38. package/dist/export/lazyExport.js +4 -0
  39. package/dist/export/types.d.ts +37 -0
  40. package/dist/export/types.js +1 -0
  41. package/dist/index.d.ts +48 -0
  42. package/dist/index.js +28 -0
  43. package/dist/media/audio/AudioPlaybackController.d.ts +26 -0
  44. package/dist/media/audio/AudioPlaybackController.js +125 -0
  45. package/dist/media/video/DecoderBackedVideoFrameProvider.d.ts +51 -0
  46. package/dist/media/video/DecoderBackedVideoFrameProvider.js +125 -0
  47. package/dist/media/video/FrameCache.d.ts +28 -0
  48. package/dist/media/video/FrameCache.js +88 -0
  49. package/dist/media/video/StreamingFrameProducer.d.ts +57 -0
  50. package/dist/media/video/StreamingFrameProducer.js +356 -0
  51. package/dist/media/video/VideoDecoderManager.d.ts +59 -0
  52. package/dist/media/video/VideoDecoderManager.js +342 -0
  53. package/dist/media/video/VideoFrameProvider.d.ts +101 -0
  54. package/dist/media/video/VideoFrameProvider.js +257 -0
  55. package/dist/media/video/demuxer/MediabunnyDemuxer.d.ts +23 -0
  56. package/dist/media/video/demuxer/MediabunnyDemuxer.js +88 -0
  57. package/dist/media/video/demuxer/createMediabunnyBackend.d.ts +32 -0
  58. package/dist/media/video/demuxer/createMediabunnyBackend.js +156 -0
  59. package/dist/media/video/index.d.ts +8 -0
  60. package/dist/media/video/index.js +5 -0
  61. package/dist/playback/PlaybackEngine.d.ts +50 -0
  62. package/dist/playback/PlaybackEngine.js +188 -0
  63. package/dist/renderer/gpu/GpuRenderer.d.ts +38 -0
  64. package/dist/renderer/gpu/GpuRenderer.js +208 -0
  65. package/dist/renderer/gpu/RenderGraph.d.ts +10 -0
  66. package/dist/renderer/gpu/RenderGraph.js +80 -0
  67. package/dist/renderer/gpu/ShaderProgram.d.ts +14 -0
  68. package/dist/renderer/gpu/ShaderProgram.js +76 -0
  69. package/dist/renderer/gpu/TexturePool.d.ts +25 -0
  70. package/dist/renderer/gpu/TexturePool.js +93 -0
  71. package/dist/renderer/gpu/VideoTexture.d.ts +13 -0
  72. package/dist/renderer/gpu/VideoTexture.js +54 -0
  73. package/dist/renderer/gpu/WebGLContext.d.ts +28 -0
  74. package/dist/renderer/gpu/WebGLContext.js +102 -0
  75. package/dist/renderer/gpu/debug/DebugGpuRenderer.d.ts +27 -0
  76. package/dist/renderer/gpu/debug/DebugGpuRenderer.js +108 -0
  77. package/dist/renderer/gpu/debug/DebugOverlay.d.ts +17 -0
  78. package/dist/renderer/gpu/debug/DebugOverlay.js +83 -0
  79. package/dist/renderer/gpu/debug/GpuDebugCounters.d.ts +38 -0
  80. package/dist/renderer/gpu/debug/GpuDebugCounters.js +72 -0
  81. package/dist/renderer/gpu/debug/GpuDebugGlobal.d.ts +16 -0
  82. package/dist/renderer/gpu/debug/GpuDebugGlobal.js +14 -0
  83. package/dist/renderer/gpu/debug/GpuRendererDebugPanel.d.ts +31 -0
  84. package/dist/renderer/gpu/debug/GpuRendererDebugPanel.js +128 -0
  85. package/dist/renderer/gpu/debug/RecordingGl.d.ts +88 -0
  86. package/dist/renderer/gpu/debug/RecordingGl.js +214 -0
  87. package/dist/renderer/gpu/debug/playground.d.ts +20 -0
  88. package/dist/renderer/gpu/debug/playground.js +64 -0
  89. package/dist/renderer/gpu/debug/scenarios.d.ts +7 -0
  90. package/dist/renderer/gpu/debug/scenarios.js +145 -0
  91. package/dist/renderer/gpu/debug/types.d.ts +16 -0
  92. package/dist/renderer/gpu/debug/types.js +1 -0
  93. package/dist/renderer/gpu/layers/FrameProbeLayer.d.ts +16 -0
  94. package/dist/renderer/gpu/layers/FrameProbeLayer.js +127 -0
  95. package/dist/renderer/gpu/layers/ImageLayer.d.ts +23 -0
  96. package/dist/renderer/gpu/layers/ImageLayer.js +124 -0
  97. package/dist/renderer/gpu/layers/TestLayer.d.ts +16 -0
  98. package/dist/renderer/gpu/layers/TestLayer.js +109 -0
  99. package/dist/renderer/gpu/layers/TextLayer.d.ts +19 -0
  100. package/dist/renderer/gpu/layers/TextLayer.js +166 -0
  101. package/dist/renderer/gpu/layers/VideoLayer.d.ts +38 -0
  102. package/dist/renderer/gpu/layers/VideoLayer.js +194 -0
  103. package/dist/renderer/gpu/layers/drawRect.d.ts +13 -0
  104. package/dist/renderer/gpu/layers/drawRect.js +55 -0
  105. package/dist/renderer/gpu/layers/objectFit.d.ts +7 -0
  106. package/dist/renderer/gpu/layers/objectFit.js +26 -0
  107. package/dist/renderer/gpu/layers/textLayout.d.ts +47 -0
  108. package/dist/renderer/gpu/layers/textLayout.js +82 -0
  109. package/dist/renderer/gpu/layers/types.d.ts +18 -0
  110. package/dist/renderer/gpu/layers/types.js +1 -0
  111. package/dist/renderer/gpu/shaders/quad.frag.d.ts +1 -0
  112. package/dist/renderer/gpu/shaders/quad.frag.js +14 -0
  113. package/dist/renderer/gpu/shaders/quad.vert.d.ts +1 -0
  114. package/dist/renderer/gpu/shaders/quad.vert.js +28 -0
  115. package/dist/renderer/gpu/types.d.ts +21 -0
  116. package/dist/renderer/gpu/types.js +1 -0
  117. package/dist/renderer/gpu/viewport.d.ts +7 -0
  118. package/dist/renderer/gpu/viewport.js +33 -0
  119. package/dist/renderer/types.d.ts +7 -0
  120. package/dist/renderer/types.js +1 -0
  121. package/dist/resolver/resolveTimeline.d.ts +3 -0
  122. package/dist/resolver/resolveTimeline.js +249 -0
  123. package/dist/resolver/scene.d.ts +54 -0
  124. package/dist/resolver/scene.js +1 -0
  125. package/dist/stores/playback.store.d.ts +50 -0
  126. package/dist/stores/playback.store.js +42 -0
  127. package/dist/stores/selection.store.d.ts +12 -0
  128. package/dist/stores/selection.store.js +19 -0
  129. package/dist/stores/tracks.store.d.ts +19 -0
  130. package/dist/stores/tracks.store.js +27 -0
  131. package/dist/stores/transitions.store.d.ts +9 -0
  132. package/dist/stores/transitions.store.js +7 -0
  133. package/dist/track/track.d.ts +8 -0
  134. package/dist/track/track.js +19 -0
  135. package/dist/types/index.d.ts +117 -0
  136. package/dist/types/index.js +1 -0
  137. package/dist/utils/frames.d.ts +20 -0
  138. package/dist/utils/frames.js +40 -0
  139. package/dist/utils/id.d.ts +1 -0
  140. package/dist/utils/id.js +3 -0
  141. package/dist/utils/snap.d.ts +5 -0
  142. package/dist/utils/snap.js +79 -0
  143. package/dist/visitor/add.d.ts +3 -0
  144. package/dist/visitor/add.js +16 -0
  145. package/dist/visitor/clone.d.ts +3 -0
  146. package/dist/visitor/clone.js +25 -0
  147. package/dist/visitor/remove.d.ts +5 -0
  148. package/dist/visitor/remove.js +29 -0
  149. package/dist/visitor/split.d.ts +3 -0
  150. package/dist/visitor/split.js +31 -0
  151. package/dist/visitor/update.d.ts +4 -0
  152. package/dist/visitor/update.js +31 -0
  153. package/package.json +31 -0
@@ -0,0 +1,342 @@
1
+ import { MediabunnyDemuxer, } from './demuxer/MediabunnyDemuxer';
2
+ const DEFAULT_IDLE_TIMEOUT_MS = 5000;
3
+ const DEFAULT_FPS = 30;
4
+ const MAX_DECODE_QUEUE_DEPTH = 4;
5
+ function _vdmTrace(msg, data) {
6
+ if (typeof globalThis !== 'undefined' && globalThis.__VDM_DEBUG__) {
7
+ if (data) {
8
+ console.log(`[VDM-TRACE] ${msg}`, data);
9
+ }
10
+ else {
11
+ console.log(`[VDM-TRACE] ${msg}`);
12
+ }
13
+ }
14
+ }
15
+ const VALID_TRANSITIONS = {
16
+ Idle: ['Opening', 'Disposed'],
17
+ Opening: ['Ready', 'Errored', 'Disposed'],
18
+ Ready: ['Resetting', 'Draining', 'Disposed', 'Errored'],
19
+ Resetting: ['Ready', 'Errored', 'Disposed'],
20
+ Draining: ['Idle', 'Errored', 'Disposed'],
21
+ Disposed: [],
22
+ Errored: ['Idle', 'Disposed'],
23
+ };
24
+ export class VideoDecoderManager {
25
+ constructor(options = {}) {
26
+ this.onFrame = null;
27
+ this._state = 'Idle';
28
+ this._src = null;
29
+ this._demuxer = null;
30
+ this._decoder = null;
31
+ this._idleTimer = null;
32
+ this._idleCallback = null;
33
+ this._feedGeneration = 0;
34
+ this._feedActive = false;
35
+ this._feedPendingRange = null;
36
+ this._demuxerFactory = options.demuxerFactory;
37
+ this._idleTimeoutMs = options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
38
+ this._fps = options.fps ?? DEFAULT_FPS;
39
+ this._onStateChange = options.onStateChange ?? null;
40
+ this._onDroppedFrame = options.onDroppedFrame ?? null;
41
+ this._onError = options.onError ?? null;
42
+ this._decoderFactory = options.decoderFactory ?? ((output, error) => {
43
+ if (typeof VideoDecoder === 'undefined') {
44
+ throw new Error('VideoDecoderManager: VideoDecoder not available');
45
+ }
46
+ return new VideoDecoder({ output, error });
47
+ });
48
+ }
49
+ get fps() {
50
+ return this._fps;
51
+ }
52
+ get state() {
53
+ return this._state;
54
+ }
55
+ get src() {
56
+ return this._src;
57
+ }
58
+ setIdleCallback(cb) {
59
+ this._idleCallback = cb;
60
+ }
61
+ async open(src) {
62
+ this._assertTransition('Opening');
63
+ this._src = src;
64
+ try {
65
+ this._demuxer = new MediabunnyDemuxer(this._demuxerFactory);
66
+ await this._demuxer.open(src);
67
+ const config = {
68
+ ...this._demuxer.getConfig(),
69
+ optimizeForLatency: true,
70
+ };
71
+ const usPerFrame = 1000000 / this._fps;
72
+ this._decoder = this._decoderFactory((frame) => {
73
+ if (this._state === 'Disposed') {
74
+ frame.close();
75
+ return;
76
+ }
77
+ if (this.onFrame) {
78
+ const sourceFrameIdx = Math.round(frame.timestamp / usPerFrame);
79
+ _vdmTrace('decoder.output → onFrame', {
80
+ sourceFrameIdx,
81
+ timestampUs: frame.timestamp,
82
+ decoderQueueSize: this._decoder
83
+ ? this._decoder.decodeQueueSize
84
+ : undefined,
85
+ state: this._state,
86
+ });
87
+ this.onFrame(frame, sourceFrameIdx);
88
+ }
89
+ else {
90
+ frame.close();
91
+ }
92
+ }, (error) => {
93
+ _vdmTrace('decoder.error', { message: error.message });
94
+ this._handleError(error);
95
+ });
96
+ this._decoder.configure(config);
97
+ this._transition('Ready');
98
+ }
99
+ catch (error) {
100
+ this._handleError(error);
101
+ throw error;
102
+ }
103
+ }
104
+ async reopen(src) {
105
+ if (this._state === 'Errored') {
106
+ this._cleanupResources();
107
+ this._transition('Idle');
108
+ }
109
+ if (this._state !== 'Idle') {
110
+ throw new Error(`VideoDecoderManager: reopen() invalid from state ${this._state}`);
111
+ }
112
+ await this.open(src);
113
+ }
114
+ feed(timeRangeUs) {
115
+ if (this._state !== 'Ready') {
116
+ _vdmTrace('feed dropped — state not Ready', {
117
+ state: this._state,
118
+ timeRangeUs,
119
+ });
120
+ return;
121
+ }
122
+ if (this._feedActive) {
123
+ if (this._feedPendingRange === null) {
124
+ this._feedPendingRange = timeRangeUs;
125
+ }
126
+ else {
127
+ this._feedPendingRange = [
128
+ Math.min(this._feedPendingRange[0], timeRangeUs[0]),
129
+ Math.max(this._feedPendingRange[1], timeRangeUs[1]),
130
+ ];
131
+ }
132
+ _vdmTrace('feed coalesced into pending', {
133
+ incomingRange: timeRangeUs,
134
+ coalescedPending: this._feedPendingRange,
135
+ decoderQueueSize: this._decoder
136
+ ? this._decoder.decodeQueueSize
137
+ : undefined,
138
+ });
139
+ return;
140
+ }
141
+ this._feedActive = true;
142
+ const gen = this._feedGeneration;
143
+ _vdmTrace('feed → _feedAsync start', {
144
+ timeRangeUs,
145
+ gen,
146
+ decoderQueueSize: this._decoder
147
+ ? this._decoder.decodeQueueSize
148
+ : undefined,
149
+ });
150
+ void this._feedAsync(timeRangeUs, gen);
151
+ }
152
+ async reset(toKeyframeUs) {
153
+ if (this._state === 'Disposed' || this._state === 'Errored')
154
+ return;
155
+ if (this._state !== 'Ready') {
156
+ throw new Error(`VideoDecoderManager: reset() invalid from state ${this._state}`);
157
+ }
158
+ this._feedGeneration++;
159
+ this._feedActive = false;
160
+ this._feedPendingRange = null;
161
+ this._assertTransition('Resetting');
162
+ try {
163
+ await this._demuxer.seekToKeyframe(toKeyframeUs);
164
+ this._decoder.reset();
165
+ this._decoder.configure({
166
+ ...this._demuxer.getConfig(),
167
+ optimizeForLatency: true,
168
+ });
169
+ this._transition('Ready');
170
+ }
171
+ catch (error) {
172
+ this._handleError(error);
173
+ throw error;
174
+ }
175
+ }
176
+ async debugFlush() {
177
+ if (this._state !== 'Ready' || !this._decoder)
178
+ return;
179
+ _vdmTrace('debugFlush → decoder.flush()', {
180
+ decoderQueueSize: this._decoder.decodeQueueSize,
181
+ });
182
+ await this._decoder.flush();
183
+ _vdmTrace('debugFlush complete', {});
184
+ }
185
+ async drain() {
186
+ if (this._state === 'Disposed')
187
+ return;
188
+ this._assertTransition('Draining');
189
+ try {
190
+ await this._decoder.flush();
191
+ this._cleanupResources();
192
+ this._transition('Idle');
193
+ }
194
+ catch (error) {
195
+ this._handleError(error);
196
+ throw error;
197
+ }
198
+ }
199
+ markIdle() {
200
+ if (this._state === 'Disposed')
201
+ return;
202
+ this._clearIdleTimer();
203
+ this._idleTimer = setTimeout(() => {
204
+ this._idleCallback?.();
205
+ }, this._idleTimeoutMs);
206
+ }
207
+ markActive() {
208
+ if (this._state === 'Disposed')
209
+ return;
210
+ this._clearIdleTimer();
211
+ }
212
+ dispose() {
213
+ if (this._state === 'Disposed')
214
+ return;
215
+ this._feedGeneration++;
216
+ this._feedActive = false;
217
+ this._feedPendingRange = null;
218
+ this._clearIdleTimer();
219
+ this._cleanupResources();
220
+ this._src = null;
221
+ this._transition('Disposed');
222
+ }
223
+ async _feedAsync(timeRangeUs, gen) {
224
+ let packetCount = 0;
225
+ let firstTimestampUs = null;
226
+ let lastTimestampUs = null;
227
+ let stateNotReadyExit = false;
228
+ try {
229
+ for await (const chunk of this._demuxer.packets(timeRangeUs)) {
230
+ if (gen !== this._feedGeneration) {
231
+ _vdmTrace('_feedAsync stale-gen exit during iteration', {
232
+ gen,
233
+ currentGen: this._feedGeneration,
234
+ packetsConsumed: packetCount,
235
+ });
236
+ return;
237
+ }
238
+ if (this._state !== 'Ready') {
239
+ _vdmTrace('_feedAsync state-not-Ready exit during iteration', {
240
+ state: this._state,
241
+ packetsConsumed: packetCount,
242
+ });
243
+ stateNotReadyExit = true;
244
+ break;
245
+ }
246
+ if (firstTimestampUs === null)
247
+ firstTimestampUs = chunk.timestamp;
248
+ lastTimestampUs = chunk.timestamp;
249
+ const queueDepth = this._decoder.decodeQueueSize;
250
+ if (queueDepth !== undefined && queueDepth >= MAX_DECODE_QUEUE_DEPTH) {
251
+ await new Promise(resolve => setTimeout(resolve, 0));
252
+ if (gen !== this._feedGeneration)
253
+ return;
254
+ if (this._state !== 'Ready') {
255
+ stateNotReadyExit = true;
256
+ break;
257
+ }
258
+ }
259
+ this._decoder.decode(chunk);
260
+ packetCount++;
261
+ }
262
+ }
263
+ catch (error) {
264
+ if (gen === this._feedGeneration && this._state !== 'Disposed') {
265
+ this._feedActive = false;
266
+ this._feedPendingRange = null;
267
+ _vdmTrace('_feedAsync ERROR', {
268
+ gen,
269
+ packetCount,
270
+ error: error instanceof Error ? error.message : String(error),
271
+ });
272
+ const err = error instanceof Error ? error : new Error(String(error));
273
+ this._onDroppedFrame?.(0);
274
+ this._handleError(err);
275
+ }
276
+ return;
277
+ }
278
+ if (stateNotReadyExit) {
279
+ if (gen === this._feedGeneration) {
280
+ this._feedActive = false;
281
+ this._feedPendingRange = null;
282
+ }
283
+ return;
284
+ }
285
+ if (gen !== this._feedGeneration)
286
+ return;
287
+ const pending = this._feedPendingRange;
288
+ this._feedPendingRange = null;
289
+ this._feedActive = false;
290
+ _vdmTrace('_feedAsync completed', {
291
+ timeRangeUs,
292
+ packetCount,
293
+ firstTimestampUs,
294
+ lastTimestampUs,
295
+ hasPending: pending !== null,
296
+ pending,
297
+ decoderQueueSize: this._decoder
298
+ ? this._decoder.decodeQueueSize
299
+ : undefined,
300
+ });
301
+ if (pending !== null && this._state === 'Ready') {
302
+ this._feedActive = true;
303
+ _vdmTrace('_feedAsync chaining pending', { pending });
304
+ void this._feedAsync(pending, this._feedGeneration);
305
+ }
306
+ }
307
+ _assertTransition(target) {
308
+ if (this._state === 'Disposed') {
309
+ throw new Error(`VideoDecoderManager: invalid transition ${this._state} → ${target}`);
310
+ }
311
+ const allowed = VALID_TRANSITIONS[this._state];
312
+ if (!allowed.includes(target)) {
313
+ throw new Error(`VideoDecoderManager: invalid transition ${this._state} → ${target}`);
314
+ }
315
+ this._transition(target);
316
+ }
317
+ _transition(next) {
318
+ this._state = next;
319
+ this._onStateChange?.(next);
320
+ }
321
+ _handleError(error) {
322
+ const err = error instanceof Error ? error : new Error(String(error));
323
+ if (this._state !== 'Disposed' &&
324
+ this._state !== 'Idle' &&
325
+ this._state !== 'Errored') {
326
+ this._transition('Errored');
327
+ }
328
+ this._onError?.(err);
329
+ }
330
+ _cleanupResources() {
331
+ this._decoder?.close();
332
+ this._decoder = null;
333
+ this._demuxer?.dispose();
334
+ this._demuxer = null;
335
+ }
336
+ _clearIdleTimer() {
337
+ if (this._idleTimer !== null) {
338
+ clearTimeout(this._idleTimer);
339
+ this._idleTimer = null;
340
+ }
341
+ }
342
+ }
@@ -0,0 +1,101 @@
1
+ import type { DemuxerFactory } from './demuxer/MediabunnyDemuxer';
2
+ import type { VideoDecoderFactory } from './VideoDecoderManager';
3
+ export type ProvidedFrame = VideoFrame | ImageBitmap;
4
+ export interface VideoFrameProvider {
5
+ getCurrent(sourceFrame: number): ProvidedFrame | null;
6
+ setPlayhead(sourceFrame: number, opts?: {
7
+ lookaheadFrames?: number;
8
+ }): void;
9
+ markIdle(): void;
10
+ markActive(): void;
11
+ dispose(): void;
12
+ }
13
+ export interface MetricsHook {
14
+ onHit?: (sourceFrame: number) => void;
15
+ onMiss?: (sourceFrame: number) => void;
16
+ onDecodeLatency?: (sourceFrame: number, ms: number) => void;
17
+ }
18
+ export interface MockVideoFrameProviderOptions {
19
+ maxFrames?: number;
20
+ idleTimeoutMs?: number;
21
+ frameWidth?: number;
22
+ frameHeight?: number;
23
+ metrics?: MetricsHook;
24
+ cacheHooks?: import('./FrameCache').FrameCacheHooks;
25
+ lookaheadFrames?: number;
26
+ }
27
+ export interface SyntheticVideoFrameProviderOptions {
28
+ maxFrames?: number;
29
+ idleTimeoutMs?: number;
30
+ frameWidth?: number;
31
+ frameHeight?: number;
32
+ fps?: number;
33
+ metrics?: MetricsHook;
34
+ cacheHooks?: import('./FrameCache').FrameCacheHooks;
35
+ lookaheadFrames?: number;
36
+ }
37
+ type ProviderState = 'active' | 'idle' | 'disposed';
38
+ export declare class MockVideoFrameProvider implements VideoFrameProvider {
39
+ private readonly _cache;
40
+ private readonly _idleTimeoutMs;
41
+ private readonly _frameWidth;
42
+ private readonly _frameHeight;
43
+ private readonly _metrics;
44
+ private readonly _defaultLookahead;
45
+ private _state;
46
+ private readonly _pending;
47
+ private _idleTimer;
48
+ private _idleCallback;
49
+ constructor(options?: MockVideoFrameProviderOptions);
50
+ setIdleCallback(cb: (() => void) | null): void;
51
+ getCurrent(sourceFrame: number): VideoFrame | null;
52
+ setPlayhead(sourceFrame: number, opts?: {
53
+ lookaheadFrames?: number;
54
+ }): void;
55
+ markIdle(): void;
56
+ markActive(): void;
57
+ dispose(): void;
58
+ get state(): ProviderState;
59
+ get cacheSize(): number;
60
+ private _scheduleFrame;
61
+ private _clearIdleTimer;
62
+ private _createMockFrame;
63
+ }
64
+ export declare class SyntheticVideoFrameProvider implements VideoFrameProvider {
65
+ private readonly _cache;
66
+ private readonly _idleTimeoutMs;
67
+ private readonly _frameWidth;
68
+ private readonly _frameHeight;
69
+ private readonly _fps;
70
+ private readonly _metrics;
71
+ private readonly _canvas;
72
+ private readonly _ctx;
73
+ private readonly _defaultLookahead;
74
+ private _state;
75
+ private readonly _pending;
76
+ private _idleTimer;
77
+ private _idleCallback;
78
+ constructor(options?: SyntheticVideoFrameProviderOptions);
79
+ setIdleCallback(cb: (() => void) | null): void;
80
+ getCurrent(sourceFrame: number): VideoFrame | null;
81
+ setPlayhead(sourceFrame: number, opts?: {
82
+ lookaheadFrames?: number;
83
+ }): void;
84
+ markIdle(): void;
85
+ markActive(): void;
86
+ dispose(): void;
87
+ get state(): ProviderState;
88
+ get cacheSize(): number;
89
+ private _scheduleFrame;
90
+ private _clearIdleTimer;
91
+ private _createSyntheticFrame;
92
+ }
93
+ export interface VideoFrameProviderDeps {
94
+ demuxerFactory?: DemuxerFactory;
95
+ decoderFactory?: VideoDecoderFactory;
96
+ fps?: number;
97
+ lookaheadFrames?: number;
98
+ frameConverter?: (frame: VideoFrame) => Promise<ImageBitmap>;
99
+ }
100
+ export declare function createVideoFrameProvider(src: string, deps?: VideoFrameProviderDeps): VideoFrameProvider;
101
+ export {};
@@ -0,0 +1,257 @@
1
+ import { FrameCache } from './FrameCache';
2
+ import { GpuDebugCounters } from '../../renderer/gpu/debug/GpuDebugCounters';
3
+ import { StreamingFrameProducer } from './StreamingFrameProducer';
4
+ const DEFAULT_IDLE_TIMEOUT_MS = 5000;
5
+ const DEFAULT_FRAME_WIDTH = 640;
6
+ const DEFAULT_FRAME_HEIGHT = 360;
7
+ const DEFAULT_FPS = 30;
8
+ const DEFAULT_LOOKAHEAD_FRAMES = 8;
9
+ function canUseSyntheticFrames() {
10
+ return (typeof OffscreenCanvas !== 'undefined' &&
11
+ typeof VideoFrame !== 'undefined');
12
+ }
13
+ export class MockVideoFrameProvider {
14
+ constructor(options = {}) {
15
+ this._state = 'active';
16
+ this._pending = new Set();
17
+ this._idleTimer = null;
18
+ this._idleCallback = null;
19
+ this._cache = new FrameCache({
20
+ maxFrames: options.maxFrames,
21
+ hooks: options.cacheHooks,
22
+ });
23
+ this._idleTimeoutMs = options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
24
+ this._frameWidth = options.frameWidth ?? 320;
25
+ this._frameHeight = options.frameHeight ?? 240;
26
+ this._metrics = options.metrics ?? {};
27
+ this._defaultLookahead = options.lookaheadFrames ?? DEFAULT_LOOKAHEAD_FRAMES;
28
+ }
29
+ setIdleCallback(cb) {
30
+ this._idleCallback = cb;
31
+ }
32
+ getCurrent(sourceFrame) {
33
+ if (this._state === 'disposed')
34
+ return null;
35
+ this._cache.setPivot(sourceFrame);
36
+ const frame = this._cache.get(sourceFrame);
37
+ if (frame !== null) {
38
+ this._metrics.onHit?.(sourceFrame);
39
+ GpuDebugCounters.cacheHits++;
40
+ }
41
+ else {
42
+ this._metrics.onMiss?.(sourceFrame);
43
+ GpuDebugCounters.cacheMisses++;
44
+ }
45
+ GpuDebugCounters.cacheSize = this._cache.size;
46
+ return frame;
47
+ }
48
+ setPlayhead(sourceFrame, opts) {
49
+ if (this._state === 'disposed')
50
+ return;
51
+ this._cache.setPivot(sourceFrame);
52
+ const lookahead = opts?.lookaheadFrames ?? this._defaultLookahead;
53
+ for (let i = 0; i <= lookahead; i++) {
54
+ this._scheduleFrame(sourceFrame + i);
55
+ }
56
+ }
57
+ markIdle() {
58
+ if (this._state === 'disposed')
59
+ return;
60
+ this._state = 'idle';
61
+ this._clearIdleTimer();
62
+ this._idleTimer = setTimeout(() => {
63
+ this._idleCallback?.();
64
+ }, this._idleTimeoutMs);
65
+ }
66
+ markActive() {
67
+ if (this._state === 'disposed')
68
+ return;
69
+ this._clearIdleTimer();
70
+ this._state = 'active';
71
+ }
72
+ dispose() {
73
+ if (this._state === 'disposed')
74
+ return;
75
+ this._state = 'disposed';
76
+ this._clearIdleTimer();
77
+ this._pending.clear();
78
+ this._cache.dispose();
79
+ GpuDebugCounters.cacheSize = 0;
80
+ }
81
+ get state() {
82
+ return this._state;
83
+ }
84
+ get cacheSize() {
85
+ return this._cache.size;
86
+ }
87
+ _scheduleFrame(sourceFrame) {
88
+ if (this._pending.has(sourceFrame))
89
+ return;
90
+ if (this._cache.has(sourceFrame))
91
+ return;
92
+ this._pending.add(sourceFrame);
93
+ const startedAt = performance.now();
94
+ setTimeout(() => {
95
+ if (this._state === 'disposed') {
96
+ this._pending.delete(sourceFrame);
97
+ return;
98
+ }
99
+ const frame = this._createMockFrame();
100
+ this._cache.put(sourceFrame, frame);
101
+ this._pending.delete(sourceFrame);
102
+ GpuDebugCounters.cacheSize = this._cache.size;
103
+ this._metrics.onDecodeLatency?.(sourceFrame, performance.now() - startedAt);
104
+ GpuDebugCounters.recordDecodeLatency(performance.now() - startedAt);
105
+ }, 0);
106
+ }
107
+ _clearIdleTimer() {
108
+ if (this._idleTimer !== null) {
109
+ clearTimeout(this._idleTimer);
110
+ this._idleTimer = null;
111
+ }
112
+ }
113
+ _createMockFrame() {
114
+ return {
115
+ displayWidth: this._frameWidth,
116
+ displayHeight: this._frameHeight,
117
+ close: () => { },
118
+ };
119
+ }
120
+ }
121
+ export class SyntheticVideoFrameProvider {
122
+ constructor(options = {}) {
123
+ this._state = 'active';
124
+ this._pending = new Set();
125
+ this._idleTimer = null;
126
+ this._idleCallback = null;
127
+ this._cache = new FrameCache({
128
+ maxFrames: options.maxFrames,
129
+ hooks: options.cacheHooks,
130
+ });
131
+ this._idleTimeoutMs = options.idleTimeoutMs ?? DEFAULT_IDLE_TIMEOUT_MS;
132
+ this._frameWidth = options.frameWidth ?? DEFAULT_FRAME_WIDTH;
133
+ this._frameHeight = options.frameHeight ?? DEFAULT_FRAME_HEIGHT;
134
+ this._fps = options.fps ?? DEFAULT_FPS;
135
+ this._metrics = options.metrics ?? {};
136
+ this._defaultLookahead = options.lookaheadFrames ?? DEFAULT_LOOKAHEAD_FRAMES;
137
+ this._canvas = new OffscreenCanvas(this._frameWidth, this._frameHeight);
138
+ const ctx = this._canvas.getContext('2d');
139
+ if (!ctx) {
140
+ throw new Error('SyntheticVideoFrameProvider: 2D context unavailable');
141
+ }
142
+ this._ctx = ctx;
143
+ }
144
+ setIdleCallback(cb) {
145
+ this._idleCallback = cb;
146
+ }
147
+ getCurrent(sourceFrame) {
148
+ if (this._state === 'disposed')
149
+ return null;
150
+ this._cache.setPivot(sourceFrame);
151
+ const frame = this._cache.get(sourceFrame);
152
+ if (frame !== null) {
153
+ this._metrics.onHit?.(sourceFrame);
154
+ GpuDebugCounters.cacheHits++;
155
+ }
156
+ else {
157
+ this._metrics.onMiss?.(sourceFrame);
158
+ GpuDebugCounters.cacheMisses++;
159
+ }
160
+ GpuDebugCounters.cacheSize = this._cache.size;
161
+ return frame;
162
+ }
163
+ setPlayhead(sourceFrame, opts) {
164
+ if (this._state === 'disposed')
165
+ return;
166
+ this._cache.setPivot(sourceFrame);
167
+ const lookahead = opts?.lookaheadFrames ?? this._defaultLookahead;
168
+ for (let i = 0; i <= lookahead; i++) {
169
+ this._scheduleFrame(sourceFrame + i);
170
+ }
171
+ }
172
+ markIdle() {
173
+ if (this._state === 'disposed')
174
+ return;
175
+ this._state = 'idle';
176
+ this._clearIdleTimer();
177
+ this._idleTimer = setTimeout(() => {
178
+ this._idleCallback?.();
179
+ }, this._idleTimeoutMs);
180
+ }
181
+ markActive() {
182
+ if (this._state === 'disposed')
183
+ return;
184
+ this._clearIdleTimer();
185
+ this._state = 'active';
186
+ }
187
+ dispose() {
188
+ if (this._state === 'disposed')
189
+ return;
190
+ this._state = 'disposed';
191
+ this._clearIdleTimer();
192
+ this._pending.clear();
193
+ this._cache.dispose();
194
+ GpuDebugCounters.cacheSize = 0;
195
+ }
196
+ get state() {
197
+ return this._state;
198
+ }
199
+ get cacheSize() {
200
+ return this._cache.size;
201
+ }
202
+ _scheduleFrame(sourceFrame) {
203
+ if (this._pending.has(sourceFrame))
204
+ return;
205
+ if (this._cache.has(sourceFrame))
206
+ return;
207
+ this._pending.add(sourceFrame);
208
+ const startedAt = performance.now();
209
+ setTimeout(() => {
210
+ if (this._state === 'disposed') {
211
+ this._pending.delete(sourceFrame);
212
+ return;
213
+ }
214
+ const frame = this._createSyntheticFrame(sourceFrame);
215
+ this._cache.put(sourceFrame, frame);
216
+ this._pending.delete(sourceFrame);
217
+ GpuDebugCounters.cacheSize = this._cache.size;
218
+ const latency = performance.now() - startedAt;
219
+ this._metrics.onDecodeLatency?.(sourceFrame, latency);
220
+ GpuDebugCounters.recordDecodeLatency(latency);
221
+ }, 0);
222
+ }
223
+ _clearIdleTimer() {
224
+ if (this._idleTimer !== null) {
225
+ clearTimeout(this._idleTimer);
226
+ this._idleTimer = null;
227
+ }
228
+ }
229
+ _createSyntheticFrame(sourceFrame) {
230
+ const hue = (sourceFrame * 37) % 360;
231
+ this._ctx.fillStyle = `hsl(${hue}, 70%, 45%)`;
232
+ this._ctx.fillRect(0, 0, this._frameWidth, this._frameHeight);
233
+ this._ctx.fillStyle = '#ffffff';
234
+ this._ctx.font = 'bold 48px monospace';
235
+ this._ctx.textAlign = 'center';
236
+ this._ctx.textBaseline = 'middle';
237
+ this._ctx.fillText(String(sourceFrame), this._frameWidth / 2, this._frameHeight / 2);
238
+ const timestamp = Math.round((sourceFrame / this._fps) * 1000000);
239
+ return new VideoFrame(this._canvas, { timestamp });
240
+ }
241
+ }
242
+ export function createVideoFrameProvider(src, deps) {
243
+ if (deps?.demuxerFactory) {
244
+ return new StreamingFrameProducer({
245
+ src,
246
+ fps: deps.fps,
247
+ lookaheadFrames: deps.lookaheadFrames,
248
+ demuxerFactory: deps.demuxerFactory,
249
+ decoderFactory: deps.decoderFactory,
250
+ frameConverter: deps.frameConverter,
251
+ });
252
+ }
253
+ if (canUseSyntheticFrames()) {
254
+ return new SyntheticVideoFrameProvider();
255
+ }
256
+ return new MockVideoFrameProvider();
257
+ }