@editframe/assets 0.17.6-beta.0 → 0.18.7-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.
- package/dist/Probe.d.ts +441 -29
- package/dist/Probe.js +156 -21
- package/dist/VideoRenderOptions.d.ts +27 -5
- package/dist/VideoRenderOptions.js +1 -1
- package/dist/generateTrackFragmentIndexMediabunny.d.ts +3 -0
- package/dist/generateTrackFragmentIndexMediabunny.js +343 -0
- package/dist/generateTrackMediabunny.d.ts +8 -0
- package/dist/generateTrackMediabunny.js +69 -0
- package/dist/idempotentTask.js +81 -48
- package/dist/index.d.ts +2 -2
- package/dist/index.js +2 -2
- package/dist/tasks/cacheRemoteAsset.d.ts +0 -1
- package/dist/tasks/findOrCreateCaptions.js +1 -1
- package/dist/tasks/generateTrack.d.ts +1 -2
- package/dist/tasks/generateTrack.js +5 -32
- package/dist/tasks/generateTrackFragmentIndex.js +22 -69
- package/dist/truncateDecimal.d.ts +1 -0
- package/dist/truncateDecimal.js +5 -0
- package/package.json +2 -14
- package/src/tasks/generateTrack.test.ts +90 -0
- package/src/tasks/generateTrack.ts +7 -48
- package/src/tasks/generateTrackFragmentIndex.test.ts +115 -0
- package/src/tasks/generateTrackFragmentIndex.ts +46 -85
- package/types.json +1 -1
- package/dist/DecoderManager.d.ts +0 -62
- package/dist/DecoderManager.js +0 -114
- package/dist/EncodedAsset.d.ts +0 -143
- package/dist/EncodedAsset.js +0 -443
- package/dist/FrameBuffer.d.ts +0 -62
- package/dist/FrameBuffer.js +0 -89
- package/dist/MP4File.d.ts +0 -37
- package/dist/MP4File.js +0 -209
- package/dist/MP4SampleAnalyzer.d.ts +0 -59
- package/dist/MP4SampleAnalyzer.js +0 -119
- package/dist/SeekStrategy.d.ts +0 -82
- package/dist/SeekStrategy.js +0 -101
- package/dist/memoize.js +0 -11
- package/dist/mp4FileWritable.d.ts +0 -3
- package/dist/mp4FileWritable.js +0 -19
package/dist/MP4File.js
DELETED
|
@@ -1,209 +0,0 @@
|
|
|
1
|
-
import * as MP4Box from "mp4box";
|
|
2
|
-
import debug from "debug";
|
|
3
|
-
const log = debug("ef:av:mp4file");
|
|
4
|
-
var MP4File = class extends MP4Box.ISOFile {
|
|
5
|
-
constructor(options = {}) {
|
|
6
|
-
super();
|
|
7
|
-
this.waitingForSamples = [];
|
|
8
|
-
this._hasSeenLastSamples = false;
|
|
9
|
-
this._arrayBufferFileStart = 0;
|
|
10
|
-
this.readyTimeoutMs = options.readyTimeoutMs ?? 100;
|
|
11
|
-
this.sampleWaitTimeoutMs = options.sampleWaitTimeoutMs ?? 100;
|
|
12
|
-
this.readyPromise = new Promise((resolve, reject) => {
|
|
13
|
-
this.onReady = () => {
|
|
14
|
-
if (this.timeoutId) {
|
|
15
|
-
clearTimeout(this.timeoutId);
|
|
16
|
-
this.timeoutId = void 0;
|
|
17
|
-
}
|
|
18
|
-
resolve();
|
|
19
|
-
};
|
|
20
|
-
this.onError = (error) => {
|
|
21
|
-
if (this.timeoutId) {
|
|
22
|
-
clearTimeout(this.timeoutId);
|
|
23
|
-
this.timeoutId = void 0;
|
|
24
|
-
}
|
|
25
|
-
reject(error);
|
|
26
|
-
};
|
|
27
|
-
this.timeoutId = setTimeout(() => {
|
|
28
|
-
this.timeoutId = void 0;
|
|
29
|
-
reject(/* @__PURE__ */ new Error("MP4File ready timeout - file may be invalid or incomplete"));
|
|
30
|
-
}, this.readyTimeoutMs);
|
|
31
|
-
});
|
|
32
|
-
this.readyPromise.catch(() => {});
|
|
33
|
-
}
|
|
34
|
-
setSegmentOptions(id, user, options) {
|
|
35
|
-
const trak = this.getTrackById(id);
|
|
36
|
-
if (trak) {
|
|
37
|
-
trak.nextSample = 0;
|
|
38
|
-
this.fragmentedTracks.push({
|
|
39
|
-
id,
|
|
40
|
-
user,
|
|
41
|
-
trak,
|
|
42
|
-
segmentStream: null,
|
|
43
|
-
nb_samples: "nbSamples" in options && options.nbSamples || 1e3,
|
|
44
|
-
rapAlignement: ("rapAlignement" in options && options.rapAlignement) ?? true
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
/**
|
|
49
|
-
* Fragments all tracks in a file into separate array buffers.
|
|
50
|
-
*/
|
|
51
|
-
async fragmentAllTracks() {
|
|
52
|
-
const trackBuffers = {};
|
|
53
|
-
try {
|
|
54
|
-
for await (const segment of this.fragmentIterator()) (trackBuffers[segment.track] ??= []).push(segment.data);
|
|
55
|
-
} catch (error) {
|
|
56
|
-
console.warn("fragmentAllTracks failed:", error);
|
|
57
|
-
}
|
|
58
|
-
return trackBuffers;
|
|
59
|
-
}
|
|
60
|
-
async *fragmentIterator() {
|
|
61
|
-
try {
|
|
62
|
-
await this.readyPromise;
|
|
63
|
-
} catch (error) {
|
|
64
|
-
console.warn("MP4File not ready:", error);
|
|
65
|
-
return;
|
|
66
|
-
}
|
|
67
|
-
const trackInfo = {};
|
|
68
|
-
const videoTracks = this.getInfo().videoTracks;
|
|
69
|
-
const audioTracks = this.getInfo().audioTracks;
|
|
70
|
-
if (videoTracks.length === 0 && audioTracks.length === 0) {
|
|
71
|
-
console.warn("No video or audio tracks found in MP4 file");
|
|
72
|
-
return;
|
|
73
|
-
}
|
|
74
|
-
for (const videoTrack of videoTracks) {
|
|
75
|
-
trackInfo[videoTrack.id] = { index: 0 };
|
|
76
|
-
this.setSegmentOptions(videoTrack.id, null, { rapAlignement: true });
|
|
77
|
-
}
|
|
78
|
-
for (const audioTrack of audioTracks) {
|
|
79
|
-
trackInfo[audioTrack.id] = { index: 0 };
|
|
80
|
-
const sampleRate = audioTrack.audio.sample_rate;
|
|
81
|
-
const probablePacketSize = 1024;
|
|
82
|
-
const probableFourSecondsOfSamples = Math.ceil(sampleRate / probablePacketSize * 4);
|
|
83
|
-
this.setSegmentOptions(audioTrack.id, null, { nbSamples: probableFourSecondsOfSamples });
|
|
84
|
-
}
|
|
85
|
-
if (this.fragmentedTracks.length === 0) {
|
|
86
|
-
console.warn("No fragmented tracks set up");
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
const initSegments = this.initializeSegmentation();
|
|
90
|
-
for (const initSegment of initSegments) yield {
|
|
91
|
-
track: initSegment.id,
|
|
92
|
-
segment: "init",
|
|
93
|
-
data: initSegment.buffer
|
|
94
|
-
};
|
|
95
|
-
const fragmentStartSamples = {};
|
|
96
|
-
let finishedReading = false;
|
|
97
|
-
do {
|
|
98
|
-
/**
|
|
99
|
-
* For each track marked for fragmentation, check if the next sample is
|
|
100
|
-
* there (i.e. if the sample information is known (i.e. moof has arrived)
|
|
101
|
-
* and if it has been downloaded) and create a fragment with it
|
|
102
|
-
*/
|
|
103
|
-
for (const fragTrak of this.fragmentedTracks) {
|
|
104
|
-
const trak = fragTrak.trak;
|
|
105
|
-
if (trak.nextSample === void 0) throw new Error("trak.nextSample is undefined");
|
|
106
|
-
if (trak.samples === void 0) throw new Error("trak.samples is undefined");
|
|
107
|
-
log("trak.nextSample", fragTrak.id, trak.nextSample);
|
|
108
|
-
log("trak.samples.length", fragTrak.id, trak.samples.length);
|
|
109
|
-
while (trak.nextSample < trak.samples.length) {
|
|
110
|
-
let result = void 0;
|
|
111
|
-
const fragTrakNextSample = trak.samples[trak.nextSample];
|
|
112
|
-
if (fragTrakNextSample) fragmentStartSamples[fragTrak.id] ||= fragTrakNextSample;
|
|
113
|
-
try {
|
|
114
|
-
result = this.createFragment(fragTrak.id, trak.nextSample, fragTrak.segmentStream);
|
|
115
|
-
} catch (error) {
|
|
116
|
-
console.error("Failed to createFragment", error);
|
|
117
|
-
}
|
|
118
|
-
if (result) {
|
|
119
|
-
fragTrak.segmentStream = result;
|
|
120
|
-
trak.nextSample++;
|
|
121
|
-
} else {
|
|
122
|
-
finishedReading = await this.waitForMoreSamples();
|
|
123
|
-
break;
|
|
124
|
-
}
|
|
125
|
-
const nextSample = trak.samples[trak.nextSample];
|
|
126
|
-
const emitSegment = fragTrak.rapAlignement === true && nextSample?.is_sync || !fragTrak.rapAlignement && trak.nextSample % fragTrak.nb_samples === 0 || trak.nextSample >= trak.samples.length;
|
|
127
|
-
if (emitSegment) {
|
|
128
|
-
const trackInfoForFrag = trackInfo[fragTrak.id];
|
|
129
|
-
if (!trackInfoForFrag) throw new Error("trackInfoForFrag is undefined");
|
|
130
|
-
const startSample = fragmentStartSamples[fragTrak.id];
|
|
131
|
-
const endSample = trak.samples[trak.nextSample - 1];
|
|
132
|
-
if (!startSample || !endSample) throw new Error("startSample or endSample is undefined");
|
|
133
|
-
log(`Yielding fragment #${trackInfoForFrag.index} for track=${fragTrak.id}`, `startTime=${startSample.cts}`, `endTime=${endSample.cts + endSample.duration}`);
|
|
134
|
-
yield {
|
|
135
|
-
track: fragTrak.id,
|
|
136
|
-
segment: trackInfoForFrag.index,
|
|
137
|
-
data: fragTrak.segmentStream.buffer,
|
|
138
|
-
cts: startSample.cts,
|
|
139
|
-
dts: startSample.dts,
|
|
140
|
-
duration: endSample.cts - startSample.cts + endSample.duration
|
|
141
|
-
};
|
|
142
|
-
trackInfoForFrag.index += 1;
|
|
143
|
-
fragTrak.segmentStream = null;
|
|
144
|
-
delete fragmentStartSamples[fragTrak.id];
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
finishedReading = await this.waitForMoreSamples();
|
|
149
|
-
} while (!finishedReading);
|
|
150
|
-
for (const fragTrak of this.fragmentedTracks) {
|
|
151
|
-
const trak = fragTrak.trak;
|
|
152
|
-
if (trak.nextSample === void 0) throw new Error("trak.nextSample is undefined");
|
|
153
|
-
if (trak.samples === void 0) throw new Error("trak.samples is undefined");
|
|
154
|
-
while (trak.nextSample < trak.samples.length) {
|
|
155
|
-
let result = void 0;
|
|
156
|
-
try {
|
|
157
|
-
result = this.createFragment(fragTrak.id, trak.nextSample, fragTrak.segmentStream);
|
|
158
|
-
} catch (error) {
|
|
159
|
-
console.error("Failed to createFragment", error);
|
|
160
|
-
}
|
|
161
|
-
if (result) {
|
|
162
|
-
fragTrak.segmentStream = result;
|
|
163
|
-
trak.nextSample++;
|
|
164
|
-
} else {
|
|
165
|
-
finishedReading = await this.waitForMoreSamples();
|
|
166
|
-
break;
|
|
167
|
-
}
|
|
168
|
-
const nextSample = trak.samples[trak.nextSample];
|
|
169
|
-
const emitSegment = fragTrak.rapAlignement === true && nextSample?.is_sync || !fragTrak.rapAlignement && trak.nextSample % fragTrak.nb_samples === 0 || trak.nextSample >= trak.samples.length;
|
|
170
|
-
if (emitSegment) {
|
|
171
|
-
const trackInfoForFrag = trackInfo[fragTrak.id];
|
|
172
|
-
if (!trackInfoForFrag) throw new Error("trackInfoForFrag is undefined");
|
|
173
|
-
const startSample = fragmentStartSamples[fragTrak.id];
|
|
174
|
-
const endSample = trak.samples[trak.nextSample - 1];
|
|
175
|
-
if (!startSample || !endSample) throw new Error("startSample or endSample is undefined");
|
|
176
|
-
log(`Yielding fragment #${trackInfoForFrag.index} for track=${fragTrak.id}`, `startTime=${startSample.cts}`, `endTime=${endSample.cts + endSample.duration}`);
|
|
177
|
-
yield {
|
|
178
|
-
track: fragTrak.id,
|
|
179
|
-
segment: trackInfoForFrag.index,
|
|
180
|
-
data: fragTrak.segmentStream.buffer,
|
|
181
|
-
cts: startSample.cts,
|
|
182
|
-
dts: startSample.dts,
|
|
183
|
-
duration: endSample.cts - startSample.cts + endSample.duration
|
|
184
|
-
};
|
|
185
|
-
trackInfoForFrag.index += 1;
|
|
186
|
-
fragTrak.segmentStream = null;
|
|
187
|
-
delete fragmentStartSamples[fragTrak.id];
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
waitForMoreSamples() {
|
|
193
|
-
if (this._hasSeenLastSamples) return Promise.resolve(true);
|
|
194
|
-
return new Promise((resolve) => {
|
|
195
|
-
this.waitingForSamples.push(resolve);
|
|
196
|
-
setTimeout(() => {
|
|
197
|
-
const index = this.waitingForSamples.indexOf(resolve);
|
|
198
|
-
if (index !== -1) this.waitingForSamples.splice(index, 1);
|
|
199
|
-
resolve(true);
|
|
200
|
-
}, this.sampleWaitTimeoutMs);
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
processSamples(last) {
|
|
204
|
-
this._hasSeenLastSamples = last;
|
|
205
|
-
for (const observer of this.waitingForSamples) observer(last);
|
|
206
|
-
this.waitingForSamples = [];
|
|
207
|
-
}
|
|
208
|
-
};
|
|
209
|
-
export { MP4File };
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
import type * as MP4Box from "mp4box";
|
|
2
|
-
export interface KeyframePosition {
|
|
3
|
-
sampleNumber: number;
|
|
4
|
-
cts: number;
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Handles MP4 sample analysis and time-to-sample conversion logic.
|
|
8
|
-
* Provides efficient access to video samples with caching and time-based lookup.
|
|
9
|
-
*/
|
|
10
|
-
export declare class MP4SampleAnalyzer {
|
|
11
|
-
private readonly mp4boxFile;
|
|
12
|
-
private readonly defaultVideoTrack;
|
|
13
|
-
constructor(mp4boxFile: MP4Box.ISOFile, defaultVideoTrack: any);
|
|
14
|
-
/**
|
|
15
|
-
* Gets a sample by index with error handling
|
|
16
|
-
*/
|
|
17
|
-
getSample(index: number): MP4Box.Sample;
|
|
18
|
-
/**
|
|
19
|
-
* Gets the timescale for the default video track
|
|
20
|
-
*/
|
|
21
|
-
get timescale(): number;
|
|
22
|
-
/**
|
|
23
|
-
* Gets all video samples for the default track
|
|
24
|
-
*/
|
|
25
|
-
get samples(): MP4Box.Sample[];
|
|
26
|
-
/**
|
|
27
|
-
* Gets samples sorted by composition timestamp (CTS) for display order
|
|
28
|
-
*/
|
|
29
|
-
get displayOrderedSamples(): MP4Box.Sample[];
|
|
30
|
-
/**
|
|
31
|
-
* Finds the sample closest to the given time in seconds
|
|
32
|
-
*/
|
|
33
|
-
getSampleClosetToTime(seconds: number): MP4Box.Sample;
|
|
34
|
-
/**
|
|
35
|
-
* Calculates the edit offset from track edits
|
|
36
|
-
*/
|
|
37
|
-
get editsOffset(): number;
|
|
38
|
-
/**
|
|
39
|
-
* Gets the default video track structure from MP4Box
|
|
40
|
-
*/
|
|
41
|
-
get defaultVideoTrak(): any;
|
|
42
|
-
/**
|
|
43
|
-
* Determines if this segment has multiple keyframes (typical for 30s scrub segments)
|
|
44
|
-
*/
|
|
45
|
-
hasMultipleKeyframes(): boolean;
|
|
46
|
-
/**
|
|
47
|
-
* Gets positions of all keyframes within the segment
|
|
48
|
-
*/
|
|
49
|
-
getKeyframePositions(): KeyframePosition[];
|
|
50
|
-
/**
|
|
51
|
-
* Calculates the average interval between keyframes in timestamp units
|
|
52
|
-
*/
|
|
53
|
-
getKeyframeInterval(): number;
|
|
54
|
-
/**
|
|
55
|
-
* Finds the optimal keyframe to start decoding from for a given target sample.
|
|
56
|
-
* Returns the keyframe closest to but not after the target.
|
|
57
|
-
*/
|
|
58
|
-
findOptimalKeyframeForSeek(targetSample: MP4Box.Sample): MP4Box.Sample;
|
|
59
|
-
}
|
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import { memoize } from "./memoize.js";
|
|
2
|
-
import _decorate from "@oxc-project/runtime/helpers/decorate";
|
|
3
|
-
/**
|
|
4
|
-
* Handles MP4 sample analysis and time-to-sample conversion logic.
|
|
5
|
-
* Provides efficient access to video samples with caching and time-based lookup.
|
|
6
|
-
*/
|
|
7
|
-
var MP4SampleAnalyzer = class {
|
|
8
|
-
constructor(mp4boxFile, defaultVideoTrack) {
|
|
9
|
-
this.mp4boxFile = mp4boxFile;
|
|
10
|
-
this.defaultVideoTrack = defaultVideoTrack;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Gets a sample by index with error handling
|
|
14
|
-
*/
|
|
15
|
-
getSample(index) {
|
|
16
|
-
const sample = this.samples?.[index];
|
|
17
|
-
if (!sample) throw new Error(`Sample not found at index ${index}`);
|
|
18
|
-
return sample;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Gets the timescale for the default video track
|
|
22
|
-
*/
|
|
23
|
-
get timescale() {
|
|
24
|
-
if (!this.defaultVideoTrack) throw new Error("No default video track found");
|
|
25
|
-
return this.defaultVideoTrack.timescale;
|
|
26
|
-
}
|
|
27
|
-
/**
|
|
28
|
-
* Gets all video samples for the default track
|
|
29
|
-
*/
|
|
30
|
-
get samples() {
|
|
31
|
-
if (!this.defaultVideoTrak.samples) throw new Error("No video samples found");
|
|
32
|
-
return this.defaultVideoTrak.samples;
|
|
33
|
-
}
|
|
34
|
-
/**
|
|
35
|
-
* Gets samples sorted by composition timestamp (CTS) for display order
|
|
36
|
-
*/
|
|
37
|
-
get displayOrderedSamples() {
|
|
38
|
-
return Array.from(this.samples).sort((a, b) => {
|
|
39
|
-
return a.cts - b.cts;
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Finds the sample closest to the given time in seconds
|
|
44
|
-
*/
|
|
45
|
-
getSampleClosetToTime(seconds) {
|
|
46
|
-
const targetTime = Math.round(seconds * this.timescale + this.editsOffset);
|
|
47
|
-
const sampleIndex = this.displayOrderedSamples.findIndex((sample) => sample.cts >= targetTime);
|
|
48
|
-
if (sampleIndex === -1) return this.displayOrderedSamples[this.displayOrderedSamples.length - 1];
|
|
49
|
-
return this.displayOrderedSamples[sampleIndex];
|
|
50
|
-
}
|
|
51
|
-
/**
|
|
52
|
-
* Calculates the edit offset from track edits
|
|
53
|
-
*/
|
|
54
|
-
get editsOffset() {
|
|
55
|
-
if (!this.defaultVideoTrack?.edits) return 0;
|
|
56
|
-
return this.defaultVideoTrack.edits.reduce((acc, edit) => {
|
|
57
|
-
return acc + edit.media_time;
|
|
58
|
-
}, 0);
|
|
59
|
-
}
|
|
60
|
-
/**
|
|
61
|
-
* Gets the default video track structure from MP4Box
|
|
62
|
-
*/
|
|
63
|
-
get defaultVideoTrak() {
|
|
64
|
-
return this.mp4boxFile.getTrackById(this.defaultVideoTrack?.id ?? -1);
|
|
65
|
-
}
|
|
66
|
-
/**
|
|
67
|
-
* Determines if this segment has multiple keyframes (typical for 30s scrub segments)
|
|
68
|
-
*/
|
|
69
|
-
hasMultipleKeyframes() {
|
|
70
|
-
const keyframes = this.samples.filter((sample) => sample.is_sync);
|
|
71
|
-
return keyframes.length > 1;
|
|
72
|
-
}
|
|
73
|
-
/**
|
|
74
|
-
* Gets positions of all keyframes within the segment
|
|
75
|
-
*/
|
|
76
|
-
getKeyframePositions() {
|
|
77
|
-
return this.samples.filter((sample) => sample.is_sync).map((sample) => ({
|
|
78
|
-
sampleNumber: sample.number,
|
|
79
|
-
cts: sample.cts
|
|
80
|
-
}));
|
|
81
|
-
}
|
|
82
|
-
/**
|
|
83
|
-
* Calculates the average interval between keyframes in timestamp units
|
|
84
|
-
*/
|
|
85
|
-
getKeyframeInterval() {
|
|
86
|
-
const keyframes = this.getKeyframePositions();
|
|
87
|
-
if (keyframes.length <= 1) return 0;
|
|
88
|
-
let totalInterval = 0;
|
|
89
|
-
for (let i = 1; i < keyframes.length; i++) {
|
|
90
|
-
const currentKeyframe = keyframes[i];
|
|
91
|
-
const previousKeyframe = keyframes[i - 1];
|
|
92
|
-
if (currentKeyframe && previousKeyframe) totalInterval += currentKeyframe.cts - previousKeyframe.cts;
|
|
93
|
-
}
|
|
94
|
-
return totalInterval / (keyframes.length - 1);
|
|
95
|
-
}
|
|
96
|
-
/**
|
|
97
|
-
* Finds the optimal keyframe to start decoding from for a given target sample.
|
|
98
|
-
* Returns the keyframe closest to but not after the target.
|
|
99
|
-
*/
|
|
100
|
-
findOptimalKeyframeForSeek(targetSample) {
|
|
101
|
-
const keyframes = this.getKeyframePositions();
|
|
102
|
-
if (keyframes.length === 0) throw new Error("No keyframes found for optimal seek");
|
|
103
|
-
if (targetSample.is_sync) return targetSample;
|
|
104
|
-
let optimalKeyframe = keyframes[0];
|
|
105
|
-
if (!optimalKeyframe) throw new Error("No keyframes available");
|
|
106
|
-
for (const keyframe of keyframes) if (keyframe.cts <= targetSample.cts) optimalKeyframe = keyframe;
|
|
107
|
-
else break;
|
|
108
|
-
const sample = this.samples[optimalKeyframe.sampleNumber];
|
|
109
|
-
if (!sample) throw new Error(`Sample not found at keyframe position ${optimalKeyframe.sampleNumber}`);
|
|
110
|
-
return sample;
|
|
111
|
-
}
|
|
112
|
-
};
|
|
113
|
-
_decorate([memoize], MP4SampleAnalyzer.prototype, "timescale", null);
|
|
114
|
-
_decorate([memoize], MP4SampleAnalyzer.prototype, "samples", null);
|
|
115
|
-
_decorate([memoize], MP4SampleAnalyzer.prototype, "displayOrderedSamples", null);
|
|
116
|
-
_decorate([memoize], MP4SampleAnalyzer.prototype, "editsOffset", null);
|
|
117
|
-
_decorate([memoize], MP4SampleAnalyzer.prototype, "defaultVideoTrak", null);
|
|
118
|
-
_decorate([memoize], MP4SampleAnalyzer.prototype, "getKeyframePositions", null);
|
|
119
|
-
export { MP4SampleAnalyzer };
|
package/dist/SeekStrategy.d.ts
DELETED
|
@@ -1,82 +0,0 @@
|
|
|
1
|
-
import type * as MP4Box from "mp4box";
|
|
2
|
-
/**
|
|
3
|
-
* Interface for the minimal FrameBuffer API needed by SeekStrategy
|
|
4
|
-
*/
|
|
5
|
-
interface FrameBufferLike {
|
|
6
|
-
findByTimestamp(timestamp: number): VideoFrame | undefined;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* State information needed for seeking decisions
|
|
10
|
-
*/
|
|
11
|
-
export interface SeekState {
|
|
12
|
-
/** Current position in the samples array for decoding */
|
|
13
|
-
sampleCursor: number;
|
|
14
|
-
/** Position of the last successfully decoded frame output */
|
|
15
|
-
outCursor: number;
|
|
16
|
-
/** Optional frame buffer for checking cached frames */
|
|
17
|
-
frameBuffer?: FrameBufferLike;
|
|
18
|
-
}
|
|
19
|
-
/**
|
|
20
|
-
* SeekStrategy encapsulates the critical seeking decision logic extracted from VideoAsset.
|
|
21
|
-
*
|
|
22
|
-
* **CRITICAL**: This class preserves the exact seeking behavior and decoder flush conditions
|
|
23
|
-
* from the original VideoAsset implementation to maintain "warm" decoder state performance.
|
|
24
|
-
*
|
|
25
|
-
* The logic here directly controls when the decoder must be flushed, which is the most
|
|
26
|
-
* performance-critical aspect of frame-accurate seeking.
|
|
27
|
-
*/
|
|
28
|
-
export declare class SeekStrategy {
|
|
29
|
-
/**
|
|
30
|
-
* Determines if seeking will skip picture groups (GOPs), requiring a decoder flush.
|
|
31
|
-
*
|
|
32
|
-
* This is an EXACT extraction of the original `seekingWillSkipPictureGroup` logic.
|
|
33
|
-
*
|
|
34
|
-
* @param state Current seek state with cursor positions
|
|
35
|
-
* @param targetSample The sample we want to seek to
|
|
36
|
-
* @param allSamples All samples in display order
|
|
37
|
-
* @returns true if seeking will cross more than one sync frame (GOP boundary)
|
|
38
|
-
*/
|
|
39
|
-
seekingWillSkipPictureGroup(state: SeekState, targetSample: MP4Box.Sample, allSamples: MP4Box.Sample[]): boolean;
|
|
40
|
-
/**
|
|
41
|
-
* Determines if seeking will go backwards in time, requiring a decoder flush.
|
|
42
|
-
*
|
|
43
|
-
* This is an EXACT extraction of the original `seekingWillGoBackwards` logic.
|
|
44
|
-
*
|
|
45
|
-
* @param state Current seek state with cursor positions and frame buffer
|
|
46
|
-
* @param targetSample The sample we want to seek to
|
|
47
|
-
* @param displayOrderedSamples Samples sorted by composition timestamp
|
|
48
|
-
* @returns true if seeking backwards and target frame is not cached
|
|
49
|
-
*/
|
|
50
|
-
seekingWillGoBackwards(state: SeekState, targetSample: MP4Box.Sample, displayOrderedSamples: MP4Box.Sample[]): boolean;
|
|
51
|
-
/**
|
|
52
|
-
* Finds the sync sample at or before the target sample number.
|
|
53
|
-
*
|
|
54
|
-
* This is an EXACT extraction of the sync sample finding logic from the original
|
|
55
|
-
* `seekToTime` method. Used when decoder flush is required to find optimal restart point.
|
|
56
|
-
*
|
|
57
|
-
* @param targetSample The sample we want to seek to
|
|
58
|
-
* @param allSamples All samples in the video
|
|
59
|
-
* @returns The sample number of the sync frame to start decoding from
|
|
60
|
-
* @throws Error if no sync sample found when traversing backwards
|
|
61
|
-
*/
|
|
62
|
-
findSyncSampleBefore(targetSample: MP4Box.Sample, allSamples: MP4Box.Sample[]): number;
|
|
63
|
-
/**
|
|
64
|
-
* The master decision function that determines if the decoder should be flushed.
|
|
65
|
-
*
|
|
66
|
-
* This consolidates the EXACT flush decision logic from the original VideoAsset.seekToTime().
|
|
67
|
-
* The decoder is flushed ONLY when:
|
|
68
|
-
* 1. Seeking will skip picture groups (crosses multiple GOP boundaries), OR
|
|
69
|
-
* 2. Seeking backwards and target frame is not in cache
|
|
70
|
-
*
|
|
71
|
-
* **CRITICAL**: This preserves the sophisticated flush minimization that keeps
|
|
72
|
-
* the decoder "warm" for optimal performance.
|
|
73
|
-
*
|
|
74
|
-
* @param state Current seek state
|
|
75
|
-
* @param targetSample The sample we want to seek to
|
|
76
|
-
* @param allSamples All samples in the video
|
|
77
|
-
* @param displayOrderedSamples Samples sorted by composition timestamp
|
|
78
|
-
* @returns true if decoder should be flushed before seeking
|
|
79
|
-
*/
|
|
80
|
-
shouldFlushDecoder(state: SeekState, targetSample: MP4Box.Sample, allSamples: MP4Box.Sample[], displayOrderedSamples?: MP4Box.Sample[]): boolean;
|
|
81
|
-
}
|
|
82
|
-
export {};
|
package/dist/SeekStrategy.js
DELETED
|
@@ -1,101 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* SeekStrategy encapsulates the critical seeking decision logic extracted from VideoAsset.
|
|
3
|
-
*
|
|
4
|
-
* **CRITICAL**: This class preserves the exact seeking behavior and decoder flush conditions
|
|
5
|
-
* from the original VideoAsset implementation to maintain "warm" decoder state performance.
|
|
6
|
-
*
|
|
7
|
-
* The logic here directly controls when the decoder must be flushed, which is the most
|
|
8
|
-
* performance-critical aspect of frame-accurate seeking.
|
|
9
|
-
*/
|
|
10
|
-
var SeekStrategy = class {
|
|
11
|
-
/**
|
|
12
|
-
* Determines if seeking will skip picture groups (GOPs), requiring a decoder flush.
|
|
13
|
-
*
|
|
14
|
-
* This is an EXACT extraction of the original `seekingWillSkipPictureGroup` logic.
|
|
15
|
-
*
|
|
16
|
-
* @param state Current seek state with cursor positions
|
|
17
|
-
* @param targetSample The sample we want to seek to
|
|
18
|
-
* @param allSamples All samples in display order
|
|
19
|
-
* @returns true if seeking will cross more than one sync frame (GOP boundary)
|
|
20
|
-
*/
|
|
21
|
-
seekingWillSkipPictureGroup(state, targetSample, allSamples) {
|
|
22
|
-
let start = state.sampleCursor;
|
|
23
|
-
const end = targetSample.number;
|
|
24
|
-
let syncFrameCrossings = 0;
|
|
25
|
-
while (start <= end) {
|
|
26
|
-
const sample = allSamples[start];
|
|
27
|
-
if (!sample) break;
|
|
28
|
-
if (sample.is_sync) {
|
|
29
|
-
if (syncFrameCrossings > 1) return true;
|
|
30
|
-
syncFrameCrossings++;
|
|
31
|
-
}
|
|
32
|
-
start++;
|
|
33
|
-
}
|
|
34
|
-
return false;
|
|
35
|
-
}
|
|
36
|
-
/**
|
|
37
|
-
* Determines if seeking will go backwards in time, requiring a decoder flush.
|
|
38
|
-
*
|
|
39
|
-
* This is an EXACT extraction of the original `seekingWillGoBackwards` logic.
|
|
40
|
-
*
|
|
41
|
-
* @param state Current seek state with cursor positions and frame buffer
|
|
42
|
-
* @param targetSample The sample we want to seek to
|
|
43
|
-
* @param displayOrderedSamples Samples sorted by composition timestamp
|
|
44
|
-
* @returns true if seeking backwards and target frame is not cached
|
|
45
|
-
*/
|
|
46
|
-
seekingWillGoBackwards(state, targetSample, displayOrderedSamples) {
|
|
47
|
-
const targetIndex = displayOrderedSamples.indexOf(targetSample);
|
|
48
|
-
const targetInCache = state.frameBuffer?.findByTimestamp(targetSample.cts);
|
|
49
|
-
const atEnd = state.sampleCursor === displayOrderedSamples.length - 1;
|
|
50
|
-
if (atEnd) return false;
|
|
51
|
-
if (targetInCache) return false;
|
|
52
|
-
return state.outCursor > targetIndex;
|
|
53
|
-
}
|
|
54
|
-
/**
|
|
55
|
-
* Finds the sync sample at or before the target sample number.
|
|
56
|
-
*
|
|
57
|
-
* This is an EXACT extraction of the sync sample finding logic from the original
|
|
58
|
-
* `seekToTime` method. Used when decoder flush is required to find optimal restart point.
|
|
59
|
-
*
|
|
60
|
-
* @param targetSample The sample we want to seek to
|
|
61
|
-
* @param allSamples All samples in the video
|
|
62
|
-
* @returns The sample number of the sync frame to start decoding from
|
|
63
|
-
* @throws Error if no sync sample found when traversing backwards
|
|
64
|
-
*/
|
|
65
|
-
findSyncSampleBefore(targetSample, allSamples) {
|
|
66
|
-
let syncSampleNumber = targetSample.number;
|
|
67
|
-
while (syncSampleNumber >= 0) {
|
|
68
|
-
const sample = allSamples[syncSampleNumber];
|
|
69
|
-
if (!sample) break;
|
|
70
|
-
if (sample.is_sync) return syncSampleNumber;
|
|
71
|
-
syncSampleNumber--;
|
|
72
|
-
}
|
|
73
|
-
throw new Error("No sync sample found when traversing backwards");
|
|
74
|
-
}
|
|
75
|
-
/**
|
|
76
|
-
* The master decision function that determines if the decoder should be flushed.
|
|
77
|
-
*
|
|
78
|
-
* This consolidates the EXACT flush decision logic from the original VideoAsset.seekToTime().
|
|
79
|
-
* The decoder is flushed ONLY when:
|
|
80
|
-
* 1. Seeking will skip picture groups (crosses multiple GOP boundaries), OR
|
|
81
|
-
* 2. Seeking backwards and target frame is not in cache
|
|
82
|
-
*
|
|
83
|
-
* **CRITICAL**: This preserves the sophisticated flush minimization that keeps
|
|
84
|
-
* the decoder "warm" for optimal performance.
|
|
85
|
-
*
|
|
86
|
-
* @param state Current seek state
|
|
87
|
-
* @param targetSample The sample we want to seek to
|
|
88
|
-
* @param allSamples All samples in the video
|
|
89
|
-
* @param displayOrderedSamples Samples sorted by composition timestamp
|
|
90
|
-
* @returns true if decoder should be flushed before seeking
|
|
91
|
-
*/
|
|
92
|
-
shouldFlushDecoder(state, targetSample, allSamples, displayOrderedSamples) {
|
|
93
|
-
const targetInCache = state.frameBuffer?.findByTimestamp(targetSample.cts);
|
|
94
|
-
if (targetInCache) return false;
|
|
95
|
-
const orderedSamples = displayOrderedSamples || allSamples;
|
|
96
|
-
if (this.seekingWillSkipPictureGroup(state, targetSample, allSamples)) return true;
|
|
97
|
-
if (this.seekingWillGoBackwards(state, targetSample, orderedSamples)) return true;
|
|
98
|
-
return false;
|
|
99
|
-
}
|
|
100
|
-
};
|
|
101
|
-
export { SeekStrategy };
|
package/dist/memoize.js
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
/** method decorator to memoize the value of a getter */
|
|
2
|
-
const memoize = (_target, _propertyKey, descriptor) => {
|
|
3
|
-
const get = descriptor.get;
|
|
4
|
-
if (!get) return;
|
|
5
|
-
const memoized = /* @__PURE__ */ new WeakMap();
|
|
6
|
-
descriptor.get = function() {
|
|
7
|
-
if (!memoized.has(this)) memoized.set(this, get.call(this));
|
|
8
|
-
return memoized.get(this);
|
|
9
|
-
};
|
|
10
|
-
};
|
|
11
|
-
export { memoize };
|
package/dist/mp4FileWritable.js
DELETED
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
import { Writable } from "node:stream";
|
|
2
|
-
const mp4FileWritable = (mp4File) => {
|
|
3
|
-
let arrayBufferStart = 0;
|
|
4
|
-
return new Writable({
|
|
5
|
-
write: (chunk, _encoding, callback) => {
|
|
6
|
-
const mp4BoxBuffer = chunk.buffer;
|
|
7
|
-
mp4BoxBuffer.fileStart = arrayBufferStart;
|
|
8
|
-
arrayBufferStart += chunk.length;
|
|
9
|
-
mp4File.appendBuffer(mp4BoxBuffer, false);
|
|
10
|
-
callback();
|
|
11
|
-
},
|
|
12
|
-
final: (callback) => {
|
|
13
|
-
mp4File.flush();
|
|
14
|
-
mp4File.processSamples(true);
|
|
15
|
-
callback();
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
};
|
|
19
|
-
export { mp4FileWritable };
|