@editframe/assets 0.16.8-beta.0 → 0.18.3-beta.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.
@@ -1,572 +1,443 @@
1
- import * as MP4Box from "mp4box";
2
- import debug from "debug";
3
- import { memoize } from "./memoize.js";
4
1
  import { MP4File } from "./MP4File.js";
5
- var __defProp = Object.defineProperty;
6
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
7
- var __decorateClass = (decorators, target, key, kind) => {
8
- var result = __getOwnPropDesc(target, key);
9
- for (var i = decorators.length - 1, decorator; i >= 0; i--)
10
- if (decorator = decorators[i])
11
- result = decorator(target, key, result) || result;
12
- if (result) __defProp(target, key, result);
13
- return result;
14
- };
2
+ import { memoize } from "./memoize.js";
3
+ import { FrameBuffer } from "./FrameBuffer.js";
4
+ import { SeekStrategy } from "./SeekStrategy.js";
5
+ import { MP4SampleAnalyzer } from "./MP4SampleAnalyzer.js";
6
+ import { DecoderManager } from "./DecoderManager.js";
7
+ import debug from "debug";
8
+ import _decorate from "@oxc-project/runtime/helpers/decorate";
15
9
  const log = debug("ef:av");
16
10
  const BUFFER_SIZE = 30;
17
- class AssetNotAvailableLocally extends Error {
18
- }
19
- class FileAsset {
20
- constructor(localName, file) {
21
- this.localName = localName;
22
- this.file = file;
23
- }
24
- async arrayBuffer() {
25
- return this.file.arrayBuffer();
26
- }
27
- get byteSize() {
28
- return this.file.size;
29
- }
30
- get fileExtension() {
31
- return this.file.name.split(".").pop();
32
- }
33
- slice(start, end) {
34
- return this.file.slice(start, end);
35
- }
36
- }
37
- class ISOFileAsset extends FileAsset {
38
- constructor(localName, file, mp4boxFile) {
39
- super(localName, file);
40
- this.localName = localName;
41
- this.file = file;
42
- this.mp4boxFile = mp4boxFile;
43
- }
44
- get fileInfo() {
45
- return this.mp4boxFile.getInfo();
46
- }
47
- get containerFormat() {
48
- return "mp4";
49
- }
50
- }
51
- __decorateClass([
52
- memoize
53
- ], ISOFileAsset.prototype, "fileInfo");
54
- const _VideoAsset = class _VideoAsset2 extends ISOFileAsset {
55
- constructor(localName, mp4boxFile, file) {
56
- super(localName, file, mp4boxFile);
57
- this.decodedFrames = [];
58
- this.requestedSampleNumber = 0;
59
- this.outCursor = 0;
60
- this.sampleCursor = 0;
61
- this.eventListeners = {};
62
- this.latestSeekCts = 0;
63
- this.videoDecoder = new VideoDecoder({
64
- error: (e) => {
65
- console.error("Video Decoder Error", e);
66
- throw e;
67
- },
68
- // eslint-disable-next-line @typescript-eslint/no-misused-promises
69
- output: async (decodedFrame) => {
70
- const clone = decodedFrame.clone();
71
- log("🖼️ frame cts=", decodedFrame.timestamp);
72
- this.decodedFrames.push(clone);
73
- this.pruneBuffer();
74
- decodedFrame.close();
75
- this.outCursor = this.samples.findIndex(
76
- (sample) => sample.cts === decodedFrame.timestamp
77
- );
78
- log(`cursor=${this.sampleCursor} outCursor=${this.outCursor}`);
79
- this.emit("frame", clone);
80
- }
81
- });
82
- this.configureDecoder();
83
- }
84
- static async createFromReadableStream(id, stream, file) {
85
- let fileStart = 0;
86
- const inputFile = new MP4File();
87
- const reader = stream.getReader();
88
- const processChunk = ({
89
- done,
90
- value
91
- }) => {
92
- if (done) {
93
- return;
94
- }
95
- if (!value) {
96
- return;
97
- }
98
- const mp4buffer = value.buffer;
99
- mp4buffer.fileStart = fileStart;
100
- const isLast = file.size === fileStart + value.byteLength;
101
- inputFile.appendBuffer(mp4buffer, isLast);
102
- fileStart += value.byteLength;
103
- return reader.read().then(processChunk);
104
- };
105
- await reader.read().then(processChunk);
106
- return new _VideoAsset2(id, inputFile, file);
107
- }
108
- /**
109
- * **Only use this function in tests to reset a VideoAsset to its initial state.**
110
- *
111
- * @deprecated
112
- */
113
- async TEST_ONLY_RESET() {
114
- if (this.videoDecoder.state !== "closed") {
115
- await this.videoDecoder.flush();
116
- }
117
- this.configureDecoder();
118
- this.requestedSampleNumber = 0;
119
- this.outCursor = 0;
120
- this.sampleCursor = 0;
121
- for (const frame of this.decodedFrames) {
122
- frame.close();
123
- }
124
- this.decodedFrames = [];
125
- this.lastDecodedSample = void 0;
126
- this.lastSoughtFrame?.close();
127
- this.lastSoughtFrame = void 0;
128
- }
129
- addEventListener(type, callback) {
130
- this.eventListeners[type] ||= /* @__PURE__ */ new Set();
131
- this.eventListeners[type]?.add(callback);
132
- }
133
- removeEventListener(type, callback) {
134
- this.eventListeners[type]?.delete(callback);
135
- }
136
- emit(type, ...args) {
137
- for (const listener of this.eventListeners[type] ?? []) {
138
- listener(...args);
139
- }
140
- }
141
- get videoCodec() {
142
- if (!this.defaultVideoTrack) {
143
- throw new Error("No default video track found");
144
- }
145
- return this.defaultVideoTrack?.codec;
146
- }
147
- get fragmentInfo() {
148
- const fragments = [];
149
- const [first, ...samples] = this.samples;
150
- if (!first) {
151
- return fragments;
152
- }
153
- let currentFragment = {
154
- offset: first.offset,
155
- size: first.size,
156
- start_ms: first.cts,
157
- duration_ms: 0
158
- };
159
- for (const sample of samples) {
160
- if (sample.is_sync) {
161
- if (currentFragment) {
162
- currentFragment.duration_ms = sample.cts - currentFragment.start_ms;
163
- fragments.push(currentFragment);
164
- }
165
- currentFragment = {
166
- offset: sample.offset,
167
- size: sample.size,
168
- start_ms: sample.cts,
169
- duration_ms: 0
170
- };
171
- } else {
172
- currentFragment.size += sample.size;
173
- }
174
- }
175
- return fragments;
176
- }
177
- pruneBuffer() {
178
- if (this.decodedFrames.length > BUFFER_SIZE) {
179
- this.decodedFrames.shift()?.close();
180
- }
181
- }
182
- get editsOffset() {
183
- if (!this.defaultVideoTrack?.edits) {
184
- return 0;
185
- }
186
- return this.defaultVideoTrack.edits.reduce((acc, edit) => {
187
- return acc + edit.media_time;
188
- }, 0);
189
- }
190
- async waitUntilVideoQueueDrained() {
191
- if (this.videoDecoder.decodeQueueSize === 0) {
192
- return;
193
- }
194
- await new Promise((resolve) => {
195
- this.videoDecoder.addEventListener(
196
- "dequeue",
197
- () => {
198
- resolve();
199
- },
200
- { once: true }
201
- );
202
- });
203
- await this.waitUntilVideoQueueDrained();
204
- }
205
- get canDecodeNextSample() {
206
- return this.sampleCursor < this.samples.length;
207
- }
208
- async decodeNextSample() {
209
- if (!this.canDecodeNextSample) {
210
- throw new Error("No more samples to decode");
211
- }
212
- await this.decodeSlice(this.sampleCursor, this.sampleCursor);
213
- this.sampleCursor++;
214
- }
215
- async decodeSlice(start, end) {
216
- const samples = this.samples.slice(start, end + 1);
217
- const firstSample = samples[0];
218
- const lastSample = samples[samples.length - 1];
219
- if (!firstSample || !lastSample) {
220
- throw new Error("Samples not found");
221
- }
222
- const sliceStart = firstSample.offset;
223
- const sliceEnd = lastSample.offset + lastSample.size;
224
- const buffer = await this.file.slice(sliceStart, sliceEnd).arrayBuffer();
225
- const firstSampleOffset = firstSample.offset;
226
- for (let i = start; i <= end; i++) {
227
- await this.waitUntilVideoQueueDrained();
228
- const sample = this.getSample(i);
229
- log("Decoding sample #", i, `cts=${sample.cts}`);
230
- const sampleStart = sample.offset - firstSampleOffset;
231
- const sampleEnd = sample.offset + sample.size - firstSampleOffset;
232
- const chunk = new EncodedVideoChunk({
233
- data: buffer.slice(sampleStart, sampleEnd),
234
- timestamp: sample.cts,
235
- duration: sample.duration,
236
- type: sample.is_sync ? "key" : "delta"
237
- });
238
- if (this.videoDecoder.state === "closed") {
239
- console.info("Decoder closed, skipping decode");
240
- continue;
241
- }
242
- this.videoDecoder.decode(chunk);
243
- const nextSample = this.defaultVideoTrak?.samples?.[i + 1];
244
- if (nextSample === void 0) {
245
- log("ENDFLUSH");
246
- await this.videoDecoder.flush();
247
- }
248
- }
249
- }
250
- get decoderConfiguration() {
251
- if (!this.defaultVideoTrack) {
252
- throw new Error("No default video track found");
253
- }
254
- let description = new Uint8Array();
255
- const trak = this.mp4boxFile.getTrackById(this.defaultVideoTrack.id);
256
- for (const entry of trak.mdia.minf.stbl.stsd.entries) {
257
- if (entry.avcC ?? entry.hvcC) {
258
- const stream = new MP4Box.DataStream(
259
- void 0,
260
- 0,
261
- MP4Box.DataStream.BIG_ENDIAN
262
- );
263
- if (entry.avcC) {
264
- entry.avcC.write(stream);
265
- } else {
266
- entry.hvcC.write(stream);
267
- }
268
- description = new Uint8Array(stream.buffer, 8);
269
- break;
270
- }
271
- }
272
- return {
273
- codec: this.defaultVideoTrack.codec,
274
- codedWidth: this.defaultVideoTrack.track_width,
275
- codedHeight: this.defaultVideoTrack.track_height,
276
- optimizeForLatency: true,
277
- description
278
- };
279
- }
280
- /**
281
- * Configures the video decoder with the appropriate codec, dimensions, and hardware acceleration settings.
282
- * If the decoder is already configured, it will be reset before being reconfigured.
283
- */
284
- configureDecoder() {
285
- if (this.videoDecoder.state === "configured") {
286
- this.videoDecoder.reset();
287
- }
288
- log("Attempting to configure decoder", this.decoderConfiguration);
289
- this.videoDecoder.configure(this.decoderConfiguration);
290
- }
291
- // Default to -1 to throw error if called without an index
292
- getSample(index = -1) {
293
- const sample = this.samples?.[index];
294
- if (!sample) {
295
- throw new Error(`Sample not found at index ${index}`);
296
- }
297
- return sample;
298
- }
299
- get timescale() {
300
- if (!this.defaultVideoTrack) {
301
- throw new Error("No default video track found");
302
- }
303
- return this.defaultVideoTrack.timescale;
304
- }
305
- get samples() {
306
- if (!this.defaultVideoTrak.samples) {
307
- throw new Error("No video samples found");
308
- }
309
- return this.defaultVideoTrak.samples;
310
- }
311
- get displayOrderedSamples() {
312
- return Array.from(this.samples).sort((a, b) => {
313
- return a.cts - b.cts;
314
- });
315
- }
316
- getSampleClosetToTime(seconds) {
317
- const targetTime = Math.round(seconds * this.timescale + this.editsOffset);
318
- const sampleIndex = this.displayOrderedSamples.findIndex(
319
- (sample) => sample.cts >= targetTime
320
- );
321
- if (sampleIndex === -1) {
322
- return this.displayOrderedSamples[this.displayOrderedSamples.length - 1];
323
- }
324
- return this.displayOrderedSamples[sampleIndex];
325
- }
326
- seekingWillEmitNewFrame(seconds) {
327
- if (!this.lastSoughtFrame) {
328
- return true;
329
- }
330
- if (this.seekingWillGoBackwards(seconds)) {
331
- return true;
332
- }
333
- const nextCts = this.getSampleClosetToTime(seconds).cts;
334
- return nextCts > this.lastSoughtFrame.timestamp;
335
- }
336
- seekingWillSkipPictureGroup(seconds) {
337
- let start = this.sampleCursor;
338
- const end = this.getSampleClosetToTime(seconds).number;
339
- let syncFrameCrossings = 0;
340
- while (start <= end) {
341
- const sample = this.getSample(start);
342
- if (sample.is_sync) {
343
- if (syncFrameCrossings > 1) {
344
- return true;
345
- }
346
- syncFrameCrossings++;
347
- }
348
- start++;
349
- }
350
- return false;
351
- }
352
- seekingWillGoBackwards(seconds) {
353
- const targetSample = this.getSampleClosetToTime(seconds);
354
- const targetIndex = this.displayOrderedSamples.indexOf(targetSample);
355
- const targetInCache = this.decodedFrames.find(
356
- (frame) => frame.timestamp === targetSample.cts
357
- );
358
- const atEnd = this.sampleCursor === this.samples.length - 1;
359
- log(
360
- "this.outCursor <= targetSample.number",
361
- this.outCursor <= targetSample.number
362
- );
363
- log("this.outCursor <= targetIndex", this.outCursor <= targetIndex);
364
- if (atEnd) {
365
- return false;
366
- }
367
- if (targetInCache) {
368
- return false;
369
- }
370
- log({
371
- sampleCursor: this.sampleCursor,
372
- outCursor: this.outCursor,
373
- target: targetSample.number,
374
- targetIndex,
375
- inCache: !!targetInCache,
376
- atEnd
377
- });
378
- return this.outCursor > targetIndex;
379
- }
380
- async seekToTime(seconds) {
381
- const sample = this.getSampleClosetToTime(seconds);
382
- const cts = sample.cts;
383
- this.latestSeekCts = cts;
384
- const alreadyDecodedFrame = this.decodedFrames.find(
385
- (f) => f.timestamp === cts
386
- );
387
- if (alreadyDecodedFrame) {
388
- return alreadyDecodedFrame;
389
- }
390
- if (this.seekingWillSkipPictureGroup(seconds)) {
391
- await this.videoDecoder.flush();
392
- let syncSampleNumber = sample.number;
393
- while (!this.getSample(syncSampleNumber).is_sync) {
394
- syncSampleNumber--;
395
- if (syncSampleNumber < 0) {
396
- throw new Error("No sync sample found when traversing backwards");
397
- }
398
- }
399
- this.sampleCursor = syncSampleNumber;
400
- }
401
- if (this.seekingWillGoBackwards(seconds)) {
402
- log("BACKWARDS FLUSH");
403
- await this.videoDecoder.flush();
404
- for (const frame2 of this.decodedFrames) {
405
- frame2.close();
406
- }
407
- this.decodedFrames = [];
408
- let syncSampleNumber = sample.number;
409
- while (!this.getSample(syncSampleNumber).is_sync) {
410
- syncSampleNumber--;
411
- if (syncSampleNumber < 0) {
412
- throw new Error("No sync sample found when traversing backwards");
413
- }
414
- }
415
- this.sampleCursor = syncSampleNumber;
416
- }
417
- let frame;
418
- const maybeFrame = (_frame) => {
419
- if (frame) {
420
- return;
421
- }
422
- log("Maybe frame", _frame.timestamp, cts);
423
- if (_frame.timestamp === cts) {
424
- this.removeEventListener("frame", maybeFrame);
425
- frame = _frame;
426
- }
427
- };
428
- this.addEventListener("frame", maybeFrame);
429
- while (frame === void 0 && this.canDecodeNextSample) {
430
- await this.decodeNextSample();
431
- }
432
- this.removeEventListener("frame", maybeFrame);
433
- if (frame) {
434
- if (this.lastSoughtFrame && !this.decodedFrames.includes(this.lastSoughtFrame)) {
435
- try {
436
- this.lastSoughtFrame.close();
437
- } catch (error) {
438
- }
439
- }
440
- this.lastSoughtFrame = frame;
441
- }
442
- return frame;
443
- }
444
- get defaultVideoTrack() {
445
- return this.fileInfo.videoTracks[0];
446
- }
447
- get defaultVideoTrak() {
448
- return this.mp4boxFile.getTrackById(this.defaultVideoTrack?.id ?? -1);
449
- }
450
- get duration() {
451
- return this.fileInfo.duration / this.fileInfo.timescale;
452
- }
11
+ var AssetNotAvailableLocally = class extends Error {};
12
+ var FileAsset = class {
13
+ constructor(localName, file) {
14
+ this.localName = localName;
15
+ this.file = file;
16
+ }
17
+ async arrayBuffer() {
18
+ return this.file.arrayBuffer();
19
+ }
20
+ get byteSize() {
21
+ return this.file.size;
22
+ }
23
+ get fileExtension() {
24
+ return this.file.name.split(".").pop();
25
+ }
26
+ slice(start, end) {
27
+ return this.file.slice(start, end);
28
+ }
29
+ };
30
+ var ISOFileAsset = class extends FileAsset {
31
+ constructor(localName, file, mp4boxFile) {
32
+ super(localName, file);
33
+ this.localName = localName;
34
+ this.file = file;
35
+ this.mp4boxFile = mp4boxFile;
36
+ }
37
+ get fileInfo() {
38
+ return this.mp4boxFile.getInfo();
39
+ }
40
+ get containerFormat() {
41
+ return "mp4";
42
+ }
453
43
  };
454
- __decorateClass([
455
- memoize
456
- ], _VideoAsset.prototype, "editsOffset");
457
- __decorateClass([
458
- memoize
459
- ], _VideoAsset.prototype, "timescale");
460
- __decorateClass([
461
- memoize
462
- ], _VideoAsset.prototype, "samples");
463
- __decorateClass([
464
- memoize
465
- ], _VideoAsset.prototype, "displayOrderedSamples");
466
- __decorateClass([
467
- memoize
468
- ], _VideoAsset.prototype, "defaultVideoTrack");
469
- __decorateClass([
470
- memoize
471
- ], _VideoAsset.prototype, "defaultVideoTrak");
472
- __decorateClass([
473
- memoize
474
- ], _VideoAsset.prototype, "duration");
475
- let VideoAsset = _VideoAsset;
476
- const _AudioAsset = class _AudioAsset2 extends ISOFileAsset {
477
- static async createFromReadableStream(id, stream, file) {
478
- let fileStart = 0;
479
- const inputFile = new MP4File();
480
- const reader = stream.getReader();
481
- const processChunk = ({
482
- done,
483
- value
484
- }) => {
485
- if (done) {
486
- return;
487
- }
488
- if (!value) {
489
- return;
490
- }
491
- const mp4buffer = value.buffer;
492
- mp4buffer.fileStart = fileStart;
493
- fileStart += value.byteLength;
494
- inputFile.appendBuffer(mp4buffer);
495
- return reader.read().then(processChunk);
496
- };
497
- await reader.read().then(processChunk);
498
- return new _AudioAsset2(id, file, inputFile);
499
- }
500
- get defaultAudioTrack() {
501
- return this.fileInfo.audioTracks[0];
502
- }
503
- get defaultAudioTrak() {
504
- return this.mp4boxFile.getTrackById(this.defaultAudioTrack?.id ?? -1);
505
- }
506
- get audioCodec() {
507
- if (!this.defaultAudioTrack) {
508
- throw new Error("No default audio track found");
509
- }
510
- return this.defaultAudioTrack.codec;
511
- }
512
- get samplerate() {
513
- if (!this.defaultAudioTrack) {
514
- throw new Error("No default audio track found");
515
- }
516
- return this.defaultAudioTrack.audio.sample_rate;
517
- }
518
- get channelCount() {
519
- if (!this.defaultAudioTrack) {
520
- throw new Error("No default audio track found");
521
- }
522
- return this.defaultAudioTrack.audio.channel_count;
523
- }
44
+ _decorate([memoize], ISOFileAsset.prototype, "fileInfo", null);
45
+ var VideoAsset = class VideoAsset extends ISOFileAsset {
46
+ static async createFromReadableStream(id, stream, file, options = {}) {
47
+ let fileStart = 0;
48
+ const inputFile = new MP4File();
49
+ const reader = stream.getReader();
50
+ const processChunk = ({ done, value }) => {
51
+ if (done) return;
52
+ if (!value) return;
53
+ const mp4buffer = value.buffer;
54
+ mp4buffer.fileStart = fileStart;
55
+ const isLast = file.size === fileStart + value.byteLength;
56
+ inputFile.appendBuffer(mp4buffer, isLast);
57
+ fileStart += value.byteLength;
58
+ return reader.read().then(processChunk);
59
+ };
60
+ await reader.read().then(processChunk);
61
+ const asset = new VideoAsset(id, inputFile, file);
62
+ asset.startTimeOffsetMs = options.startTimeOffsetMs;
63
+ return asset;
64
+ }
65
+ /**
66
+ * Creates a VideoAsset from a complete MP4 file (like JIT transcoded segments).
67
+ *
68
+ * This is used for JIT transcoded segments which are complete MP4 files that always
69
+ * start on keyframes, unlike fragmented MP4s used in the asset pipeline.
70
+ */
71
+ static async createFromCompleteMP4(id, file, options = {}) {
72
+ const fileBuffer = await file.arrayBuffer();
73
+ const inputFile = new MP4File();
74
+ const mp4buffer = fileBuffer;
75
+ mp4buffer.fileStart = 0;
76
+ inputFile.appendBuffer(mp4buffer, true);
77
+ inputFile.flush();
78
+ await inputFile.readyPromise;
79
+ const asset = new VideoAsset(id, inputFile, file);
80
+ asset.isJitSegment = true;
81
+ asset.startTimeOffsetMs = options.startTimeOffsetMs;
82
+ return asset;
83
+ }
84
+ /**
85
+ * **Only use this function in tests to reset a VideoAsset to its initial state.**
86
+ *
87
+ * @deprecated
88
+ */
89
+ async TEST_ONLY_RESET() {
90
+ if (this.decoderManager.state !== "closed") await this.decoderManager.flush();
91
+ this.decoderManager.configureDecoder();
92
+ this.requestedSampleNumber = 0;
93
+ this.outCursor = 0;
94
+ this.sampleCursor = 0;
95
+ this.frameBuffer.clear();
96
+ this.lastDecodedSample = void 0;
97
+ this.lastSoughtFrame?.close();
98
+ this.lastSoughtFrame = void 0;
99
+ }
100
+ addEventListener(type, callback) {
101
+ this.eventListeners[type] ||= /* @__PURE__ */ new Set();
102
+ this.eventListeners[type]?.add(callback);
103
+ }
104
+ removeEventListener(type, callback) {
105
+ this.eventListeners[type]?.delete(callback);
106
+ }
107
+ emit(type, ...args) {
108
+ for (const listener of this.eventListeners[type] ?? []) listener(...args);
109
+ }
110
+ constructor(localName, mp4boxFile, file) {
111
+ super(localName, file, mp4boxFile);
112
+ this.requestedSampleNumber = 0;
113
+ this.outCursor = 0;
114
+ this.sampleCursor = 0;
115
+ this.latestSeekCts = 0;
116
+ this.isJitSegment = false;
117
+ this.isBeingReplaced = false;
118
+ this.eventListeners = {};
119
+ this.frameBuffer = new FrameBuffer(BUFFER_SIZE);
120
+ this.sampleAnalyzer = new MP4SampleAnalyzer(mp4boxFile, this.defaultVideoTrack);
121
+ this.seekStrategy = new SeekStrategy();
122
+ this.decoderManager = new DecoderManager(mp4boxFile, this.defaultVideoTrack, (decodedFrame) => {
123
+ const clone = decodedFrame.clone();
124
+ this.frameBuffer.add(clone);
125
+ decodedFrame.close();
126
+ this.outCursor = this.samples.findIndex((sample) => sample.cts === decodedFrame.timestamp);
127
+ this.emit("frame", clone);
128
+ }, (e) => {
129
+ console.error("Video Decoder Error", e);
130
+ throw e;
131
+ });
132
+ this.decoderManager.configureDecoder();
133
+ }
134
+ get videoDecoder() {
135
+ return this.decoderManager.videoDecoder;
136
+ }
137
+ get decodedFrames() {
138
+ return this.frameBuffer.frames;
139
+ }
140
+ set decodedFrames(frames) {
141
+ this.frameBuffer.clear();
142
+ frames.forEach((frame) => this.frameBuffer.add(frame));
143
+ }
144
+ get videoCodec() {
145
+ if (!this.defaultVideoTrack) throw new Error("No default video track found");
146
+ return this.defaultVideoTrack?.codec;
147
+ }
148
+ get fragmentInfo() {
149
+ const fragments = [];
150
+ const [first, ...samples] = this.samples;
151
+ if (!first) return fragments;
152
+ let currentFragment = {
153
+ offset: first.offset,
154
+ size: first.size,
155
+ start_ms: first.cts,
156
+ duration_ms: 0
157
+ };
158
+ for (const sample of samples) if (sample.is_sync) {
159
+ if (currentFragment) {
160
+ currentFragment.duration_ms = sample.cts - currentFragment.start_ms;
161
+ fragments.push(currentFragment);
162
+ }
163
+ currentFragment = {
164
+ offset: sample.offset,
165
+ size: sample.size,
166
+ start_ms: sample.cts,
167
+ duration_ms: 0
168
+ };
169
+ } else currentFragment.size += sample.size;
170
+ return fragments;
171
+ }
172
+ pruneBuffer() {}
173
+ get editsOffset() {
174
+ return this.sampleAnalyzer.editsOffset;
175
+ }
176
+ async waitUntilVideoQueueDrained() {
177
+ return this.decoderManager.waitUntilVideoQueueDrained();
178
+ }
179
+ get canDecodeNextSample() {
180
+ return this.sampleCursor < this.samples.length;
181
+ }
182
+ async decodeNextSample() {
183
+ if (!this.canDecodeNextSample) throw new Error("No more samples to decode");
184
+ await this.decodeSlice(this.sampleCursor, this.sampleCursor);
185
+ this.sampleCursor++;
186
+ }
187
+ async decodeSlice(start, end) {
188
+ const samples = this.samples.slice(start, end + 1);
189
+ const firstSample = samples[0];
190
+ const lastSample = samples[samples.length - 1];
191
+ if (!firstSample || !lastSample) throw new Error("Samples not found");
192
+ const sliceStart = firstSample.offset;
193
+ const sliceEnd = lastSample.offset + lastSample.size;
194
+ const buffer = await this.file.slice(sliceStart, sliceEnd).arrayBuffer();
195
+ const firstSampleOffset = firstSample.offset;
196
+ for (let i = start; i <= end; i++) {
197
+ await this.waitUntilVideoQueueDrained();
198
+ const sample = this.getSample(i);
199
+ log("Decoding sample #", i, `cts=${sample.cts}`);
200
+ const sampleStart = sample.offset - firstSampleOffset;
201
+ const sampleEnd = sample.offset + sample.size - firstSampleOffset;
202
+ const chunk = new EncodedVideoChunk({
203
+ data: buffer.slice(sampleStart, sampleEnd),
204
+ timestamp: sample.cts,
205
+ duration: sample.duration,
206
+ type: sample.is_sync ? "key" : "delta"
207
+ });
208
+ this.decoderManager.decode(chunk);
209
+ const nextSample = this.defaultVideoTrak?.samples?.[i + 1];
210
+ if (nextSample === void 0) {
211
+ log("ENDFLUSH");
212
+ await this.decoderManager.flush();
213
+ }
214
+ }
215
+ }
216
+ get decoderConfiguration() {
217
+ return this.decoderManager.decoderConfiguration;
218
+ }
219
+ configureDecoder() {
220
+ this.decoderManager.configureDecoder();
221
+ }
222
+ getSample(index = -1) {
223
+ return this.sampleAnalyzer.getSample(index);
224
+ }
225
+ get timescale() {
226
+ return this.sampleAnalyzer.timescale;
227
+ }
228
+ get samples() {
229
+ return this.sampleAnalyzer.samples;
230
+ }
231
+ get displayOrderedSamples() {
232
+ return this.sampleAnalyzer.displayOrderedSamples;
233
+ }
234
+ getSampleClosetToTime(seconds) {
235
+ return this.sampleAnalyzer.getSampleClosetToTime(seconds);
236
+ }
237
+ seekingWillEmitNewFrame(seconds) {
238
+ if (!this.lastSoughtFrame) return true;
239
+ if (this.seekingWillGoBackwards(seconds)) return true;
240
+ const nextCts = this.getSampleClosetToTime(seconds).cts;
241
+ return nextCts > this.lastSoughtFrame.timestamp;
242
+ }
243
+ seekingWillSkipPictureGroup(seconds) {
244
+ const targetSample = this.getSampleClosetToTime(seconds);
245
+ const state = {
246
+ sampleCursor: this.sampleCursor,
247
+ outCursor: this.outCursor,
248
+ frameBuffer: this.frameBuffer
249
+ };
250
+ return this.seekStrategy.seekingWillSkipPictureGroup(state, targetSample, this.samples);
251
+ }
252
+ seekingWillGoBackwards(seconds) {
253
+ const targetSample = this.getSampleClosetToTime(seconds);
254
+ const state = {
255
+ sampleCursor: this.sampleCursor,
256
+ outCursor: this.outCursor,
257
+ frameBuffer: this.frameBuffer
258
+ };
259
+ return this.seekStrategy.seekingWillGoBackwards(state, targetSample, this.displayOrderedSamples);
260
+ }
261
+ /**
262
+ * Optimized flush decision for JIT segments that always start on keyframes.
263
+ * JIT segments have better keyframe distribution and shorter duration,
264
+ * so we can be less aggressive about flushing.
265
+ */
266
+ shouldFlushForJitSegment(seconds, _shouldFlushPictureGroup, shouldFlushBackwards) {
267
+ if (shouldFlushBackwards) {
268
+ const targetSample = this.getSampleClosetToTime(seconds);
269
+ const targetInCache = this.frameBuffer.findByTimestamp(targetSample.cts);
270
+ if (!targetInCache) {
271
+ const currentPosition = this.outCursor;
272
+ const targetPosition = this.samples.findIndex((s) => s.cts === targetSample.cts);
273
+ const jumpDistance = currentPosition - targetPosition;
274
+ if (jumpDistance > 10) return true;
275
+ return false;
276
+ }
277
+ }
278
+ return false;
279
+ }
280
+ /**
281
+ * Finds the optimal sample cursor position for segments with keyframes.
282
+ * Uses optimal keyframe selection for both single and multi-keyframe segments.
283
+ */
284
+ findOptimalSampleCursorForJitSeek(targetSample) {
285
+ let syncSampleNumber = targetSample.number;
286
+ while (!this.getSample(syncSampleNumber).is_sync) {
287
+ syncSampleNumber--;
288
+ if (syncSampleNumber < 0) throw new Error("No sync sample found when traversing backwards");
289
+ }
290
+ return syncSampleNumber;
291
+ }
292
+ /**
293
+ * Marks this VideoAsset as being replaced, which will abort any ongoing seek operations
294
+ */
295
+ markAsBeingReplaced() {
296
+ this.isBeingReplaced = true;
297
+ if (this.activeSeekAbortController) this.activeSeekAbortController.abort("VideoAsset being replaced");
298
+ }
299
+ async seekToTime(seconds) {
300
+ seconds += (this.startTimeOffsetMs ?? 0) / 1e3;
301
+ const correctedSeconds = seconds;
302
+ this.activeSeekAbortController = new AbortController();
303
+ const abortSignal = this.activeSeekAbortController.signal;
304
+ if (this.isBeingReplaced) throw new Error("VideoAsset seek aborted - VideoAsset being replaced");
305
+ const sample = this.getSampleClosetToTime(correctedSeconds);
306
+ const cts = sample.cts;
307
+ this.latestSeekCts = cts;
308
+ const alreadyDecodedFrame = this.frameBuffer.findByTimestamp(cts);
309
+ if (alreadyDecodedFrame) return alreadyDecodedFrame;
310
+ const shouldFlushPictureGroup = this.seekingWillSkipPictureGroup(seconds);
311
+ const shouldFlushBackwards = this.seekingWillGoBackwards(seconds);
312
+ const shouldFlush = this.isJitSegment ? this.shouldFlushForJitSegment(seconds, shouldFlushPictureGroup, shouldFlushBackwards) : shouldFlushPictureGroup || shouldFlushBackwards;
313
+ if (shouldFlush) {
314
+ if (this.isBeingReplaced) throw new Error("VideoAsset seek aborted - VideoAsset being replaced");
315
+ if (this.decoderManager.state === "closed") throw new Error("VideoAsset decoder closed - recreation in progress");
316
+ try {
317
+ await this.decoderManager.flush();
318
+ } catch (error) {
319
+ if (error instanceof Error && error.name === "InvalidStateError" && error.message.includes("closed codec")) throw new Error("VideoAsset decoder closed during flush - recreation in progress");
320
+ throw error;
321
+ }
322
+ this.sampleCursor = this.findOptimalSampleCursorForJitSeek(sample);
323
+ }
324
+ let frame;
325
+ const maybeFrame = (_frame) => {
326
+ if (frame) return;
327
+ log("Maybe frame", _frame.timestamp, cts);
328
+ if (_frame.timestamp === cts) {
329
+ this.removeEventListener("frame", maybeFrame);
330
+ frame = _frame;
331
+ }
332
+ };
333
+ this.addEventListener("frame", maybeFrame);
334
+ while (frame === void 0 && this.canDecodeNextSample) {
335
+ if (abortSignal.aborted) {
336
+ this.removeEventListener("frame", maybeFrame);
337
+ throw new Error("VideoAsset seek aborted - VideoAsset being replaced");
338
+ }
339
+ if (this.isBeingReplaced) {
340
+ this.removeEventListener("frame", maybeFrame);
341
+ throw new Error("VideoAsset seek aborted - VideoAsset being replaced");
342
+ }
343
+ if (this.decoderManager.state === "closed") {
344
+ this.removeEventListener("frame", maybeFrame);
345
+ throw new Error("VideoAsset decoder closed during seek - recreation in progress");
346
+ }
347
+ try {
348
+ await this.decodeNextSample();
349
+ } catch (error) {
350
+ if (error instanceof Error && error.name === "InvalidStateError" && error.message.includes("closed codec")) {
351
+ console.log("🎬 VideoAsset: Decoder was closed during decode - VideoAsset being replaced");
352
+ this.removeEventListener("frame", maybeFrame);
353
+ throw new Error("VideoAsset decoder closed during decode - recreation in progress");
354
+ }
355
+ throw error;
356
+ }
357
+ }
358
+ this.removeEventListener("frame", maybeFrame);
359
+ this.activeSeekAbortController = void 0;
360
+ if (frame) {
361
+ if (this.lastSoughtFrame && !this.frameBuffer.frames.includes(this.lastSoughtFrame)) try {
362
+ this.lastSoughtFrame.close();
363
+ } catch (error) {}
364
+ this.lastSoughtFrame = frame;
365
+ }
366
+ return frame;
367
+ }
368
+ get defaultVideoTrack() {
369
+ return this.fileInfo.videoTracks?.[0];
370
+ }
371
+ get defaultVideoTrak() {
372
+ return this.mp4boxFile.getTrackById(this.defaultVideoTrack?.id ?? -1);
373
+ }
374
+ get duration() {
375
+ return this.fileInfo.duration / this.fileInfo.timescale;
376
+ }
524
377
  };
525
- __decorateClass([
526
- memoize
527
- ], _AudioAsset.prototype, "defaultAudioTrack");
528
- __decorateClass([
529
- memoize
530
- ], _AudioAsset.prototype, "defaultAudioTrak");
531
- __decorateClass([
532
- memoize
533
- ], _AudioAsset.prototype, "audioCodec");
534
- __decorateClass([
535
- memoize
536
- ], _AudioAsset.prototype, "samplerate");
537
- __decorateClass([
538
- memoize
539
- ], _AudioAsset.prototype, "channelCount");
540
- let AudioAsset = _AudioAsset;
541
- const _ImageAsset = class _ImageAsset2 extends FileAsset {
542
- static async createFromReadableStream(id, file) {
543
- if (file.size === 0) {
544
- throw new AssetNotAvailableLocally();
545
- }
546
- return new _ImageAsset2(id, file);
547
- }
548
- get objectUrl() {
549
- return URL.createObjectURL(this.file);
550
- }
551
- get format() {
552
- return this.fileExtension;
553
- }
554
- get type() {
555
- return `image/${this.format}`;
556
- }
378
+ _decorate([memoize], VideoAsset.prototype, "editsOffset", null);
379
+ _decorate([memoize], VideoAsset.prototype, "timescale", null);
380
+ _decorate([memoize], VideoAsset.prototype, "samples", null);
381
+ _decorate([memoize], VideoAsset.prototype, "displayOrderedSamples", null);
382
+ _decorate([memoize], VideoAsset.prototype, "defaultVideoTrack", null);
383
+ _decorate([memoize], VideoAsset.prototype, "defaultVideoTrak", null);
384
+ _decorate([memoize], VideoAsset.prototype, "duration", null);
385
+ var AudioAsset = class AudioAsset extends ISOFileAsset {
386
+ static async createFromReadableStream(id, stream, file) {
387
+ let fileStart = 0;
388
+ const inputFile = new MP4File();
389
+ const reader = stream.getReader();
390
+ const processChunk = ({ done, value }) => {
391
+ if (done) return;
392
+ if (!value) return;
393
+ const mp4buffer = value.buffer;
394
+ mp4buffer.fileStart = fileStart;
395
+ fileStart += value.byteLength;
396
+ inputFile.appendBuffer(mp4buffer);
397
+ return reader.read().then(processChunk);
398
+ };
399
+ await reader.read().then(processChunk);
400
+ return new AudioAsset(id, file, inputFile);
401
+ }
402
+ get defaultAudioTrack() {
403
+ return this.fileInfo.audioTracks[0];
404
+ }
405
+ get defaultAudioTrak() {
406
+ return this.mp4boxFile.getTrackById(this.defaultAudioTrack?.id ?? -1);
407
+ }
408
+ get audioCodec() {
409
+ if (!this.defaultAudioTrack) throw new Error("No default audio track found");
410
+ return this.defaultAudioTrack.codec;
411
+ }
412
+ get samplerate() {
413
+ if (!this.defaultAudioTrack) throw new Error("No default audio track found");
414
+ return this.defaultAudioTrack.audio.sample_rate;
415
+ }
416
+ get channelCount() {
417
+ if (!this.defaultAudioTrack) throw new Error("No default audio track found");
418
+ return this.defaultAudioTrack.audio.channel_count;
419
+ }
557
420
  };
558
- __decorateClass([
559
- memoize
560
- ], _ImageAsset.prototype, "objectUrl");
561
- __decorateClass([
562
- memoize
563
- ], _ImageAsset.prototype, "format");
564
- let ImageAsset = _ImageAsset;
565
- export {
566
- AssetNotAvailableLocally,
567
- AudioAsset,
568
- FileAsset,
569
- ISOFileAsset,
570
- ImageAsset,
571
- VideoAsset
421
+ _decorate([memoize], AudioAsset.prototype, "defaultAudioTrack", null);
422
+ _decorate([memoize], AudioAsset.prototype, "defaultAudioTrak", null);
423
+ _decorate([memoize], AudioAsset.prototype, "audioCodec", null);
424
+ _decorate([memoize], AudioAsset.prototype, "samplerate", null);
425
+ _decorate([memoize], AudioAsset.prototype, "channelCount", null);
426
+ var ImageAsset = class ImageAsset extends FileAsset {
427
+ static async createFromReadableStream(id, file) {
428
+ if (file.size === 0) throw new AssetNotAvailableLocally();
429
+ return new ImageAsset(id, file);
430
+ }
431
+ get objectUrl() {
432
+ return URL.createObjectURL(this.file);
433
+ }
434
+ get format() {
435
+ return this.fileExtension;
436
+ }
437
+ get type() {
438
+ return `image/${this.format}`;
439
+ }
572
440
  };
441
+ _decorate([memoize], ImageAsset.prototype, "objectUrl", null);
442
+ _decorate([memoize], ImageAsset.prototype, "format", null);
443
+ export { AssetNotAvailableLocally, AudioAsset, FileAsset, ISOFileAsset, ImageAsset, VideoAsset };