@bendyline/squisq-video-react 2.5.28 → 2.5.30
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/{captureVideoCodecs-HVCZBBNT.js → captureVideoCodecs-3QXX66ZI.js} +1 -1
- package/dist/chunk-AARDBHRJ.js +129 -0
- package/dist/{chunk-EJ3DVPFF.js → chunk-MOIVICAD.js} +1292 -0
- package/dist/{chunk-3IMR25SX.js → chunk-MVEZDB4G.js} +1 -1
- package/dist/{chunk-XPWYKETM.js → chunk-QKTQL3NK.js} +1 -1
- package/dist/{chunk-JKRF65OD.js → chunk-QZSP5DGO.js} +1 -1
- package/dist/{chunk-J6MJYH2I.js → chunk-SGLTT5CL.js} +62 -106
- package/dist/chunk-VKGKDTWK.js +292 -0
- package/dist/{chunk-VE3OUBRG.js → chunk-XDJ2FPQQ.js} +1 -1
- package/dist/{chunk-TRCTDIWS.js → chunk-XPPQWQEO.js} +189 -3
- package/dist/{chunk-ZEA6XJL7.js → chunk-YO7JVV3K.js} +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/components/index.js +4 -4
- package/dist/cover-image/index.js +2 -2
- package/dist/dashboard-image/index.js +1 -1
- package/dist/encoder/index.js +1 -1
- package/dist/hooks/index.js +2 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -6
- package/dist/{lossyEncoder-27NI5K4Y.js → lossyEncoder-U4V3AER7.js} +1 -1
- package/dist/media-edit/index.d.ts +2 -2
- package/dist/media-edit/index.js +1 -1
- package/dist/mediaBakeEngine-IHLCD2GH.js +12 -0
- package/dist/{mediaEditEngine-3WUTBNJN.js → mediaEditEngine-22T4WV4X.js} +5 -2
- package/dist/{mediaEditRenderManager-W_o6RRhT.d.ts → mediaEditRenderManager-DhqIn8yo.d.ts} +97 -5
- package/dist/workers/mediaEdit.worker.js +15 -2
- package/package.json +4 -4
|
@@ -1,24 +1,16 @@
|
|
|
1
1
|
import {
|
|
2
|
-
|
|
2
|
+
openMediaAudio,
|
|
3
|
+
probeMediaDuration
|
|
4
|
+
} from "./chunk-AARDBHRJ.js";
|
|
5
|
+
import {
|
|
3
6
|
AudioSample,
|
|
4
|
-
AudioSampleSink,
|
|
5
7
|
AudioSampleSource,
|
|
6
|
-
BlobSource,
|
|
7
8
|
BufferTarget,
|
|
8
|
-
FLAC,
|
|
9
|
-
Input,
|
|
10
|
-
MATROSKA,
|
|
11
|
-
MP3,
|
|
12
|
-
MP4,
|
|
13
|
-
OGG,
|
|
14
9
|
Output,
|
|
15
|
-
QTFF,
|
|
16
|
-
WAVE,
|
|
17
|
-
WEBM,
|
|
18
10
|
WavOutputFormat,
|
|
19
11
|
WebMOutputFormat,
|
|
20
12
|
canEncodeAudio
|
|
21
|
-
} from "./chunk-
|
|
13
|
+
} from "./chunk-MOIVICAD.js";
|
|
22
14
|
|
|
23
15
|
// src/mediaEdit/mediaEditEngine.ts
|
|
24
16
|
import {
|
|
@@ -75,102 +67,34 @@ async function createAudioRenderSink(channels, sampleRate) {
|
|
|
75
67
|
};
|
|
76
68
|
}
|
|
77
69
|
|
|
78
|
-
// src/mediaEdit/
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
async function* open() {
|
|
100
|
-
const sink = new AudioSampleSink(track);
|
|
101
|
-
const resamplers = Array.from(
|
|
102
|
-
{ length: channels },
|
|
103
|
-
() => createResampler(sourceRate, MEDIA_FX_SAMPLE_RATE)
|
|
104
|
-
);
|
|
105
|
-
let placed = 0;
|
|
106
|
-
let emitted = 0;
|
|
107
|
-
const feed = (planes) => {
|
|
108
|
-
const out = resamplers.map((r, c) => r.process(planes[c]));
|
|
109
|
-
const room2 = frames - emitted;
|
|
110
|
-
const take = Math.min(out[0].length, room2);
|
|
111
|
-
if (take <= 0) return null;
|
|
112
|
-
emitted += take;
|
|
113
|
-
return out.map((ch) => ch.length === take ? ch : ch.subarray(0, take));
|
|
114
|
-
};
|
|
115
|
-
for await (const sample of sink.samples(startSec, endSec)) {
|
|
116
|
-
try {
|
|
117
|
-
const at = Math.round((sample.timestamp - startSec) * sourceRate);
|
|
118
|
-
let planes = Array.from({ length: channels }, (_, c) => {
|
|
119
|
-
const plane = new Float32Array(sample.numberOfFrames);
|
|
120
|
-
sample.copyTo(plane, {
|
|
121
|
-
planeIndex: Math.min(c, sample.numberOfChannels - 1),
|
|
122
|
-
format: "f32-planar"
|
|
123
|
-
});
|
|
124
|
-
return plane;
|
|
125
|
-
});
|
|
126
|
-
if (at > placed) {
|
|
127
|
-
const gap = Math.min(at - placed, sourceFrames - placed);
|
|
128
|
-
if (gap > 0) {
|
|
129
|
-
const out2 = feed(Array.from({ length: channels }, () => new Float32Array(gap)));
|
|
130
|
-
placed += gap;
|
|
131
|
-
if (out2) yield out2;
|
|
132
|
-
}
|
|
70
|
+
// src/mediaEdit/waveformPeaks.ts
|
|
71
|
+
var WAVEFORM_PEAKS_PER_SEC = 100;
|
|
72
|
+
function createPeakAccumulator(sampleRate, peaksPerSec = WAVEFORM_PEAKS_PER_SEC, expectedFrames = 0) {
|
|
73
|
+
const framesPerPeak = sampleRate / peaksPerSec;
|
|
74
|
+
let peaks = new Float32Array(Math.max(16, Math.ceil(expectedFrames / framesPerPeak)));
|
|
75
|
+
let seen = 0;
|
|
76
|
+
return {
|
|
77
|
+
push(block) {
|
|
78
|
+
const length = block[0]?.length ?? 0;
|
|
79
|
+
if (length === 0) return;
|
|
80
|
+
const last = Math.floor((seen + length - 1) / framesPerPeak);
|
|
81
|
+
if (last >= peaks.length) {
|
|
82
|
+
const grown = new Float32Array(Math.max(last + 1, peaks.length * 2));
|
|
83
|
+
grown.set(peaks);
|
|
84
|
+
peaks = grown;
|
|
85
|
+
}
|
|
86
|
+
for (const channel of block) {
|
|
87
|
+
for (let i = 0; i < length; i++) {
|
|
88
|
+
const magnitude = Math.abs(channel[i]);
|
|
89
|
+
const slot = Math.floor((seen + i) / framesPerPeak);
|
|
90
|
+
if (magnitude > peaks[slot]) peaks[slot] = magnitude;
|
|
133
91
|
}
|
|
134
|
-
const overlap = placed - at;
|
|
135
|
-
if (overlap > 0) planes = planes.map((p) => p.subarray(Math.min(overlap, p.length)));
|
|
136
|
-
const room2 = sourceFrames - placed;
|
|
137
|
-
if (room2 <= 0) break;
|
|
138
|
-
if (planes[0].length > room2) planes = planes.map((p) => p.subarray(0, room2));
|
|
139
|
-
if (planes[0].length === 0) continue;
|
|
140
|
-
placed += planes[0].length;
|
|
141
|
-
const out = feed(planes);
|
|
142
|
-
if (out) yield out;
|
|
143
|
-
} finally {
|
|
144
|
-
sample.close();
|
|
145
92
|
}
|
|
93
|
+
seen += length;
|
|
94
|
+
},
|
|
95
|
+
finish() {
|
|
96
|
+
return peaks.slice(0, Math.ceil(seen / framesPerPeak));
|
|
146
97
|
}
|
|
147
|
-
if (placed < sourceFrames) {
|
|
148
|
-
const out = feed(
|
|
149
|
-
Array.from({ length: channels }, () => new Float32Array(sourceFrames - placed))
|
|
150
|
-
);
|
|
151
|
-
if (out) yield out;
|
|
152
|
-
}
|
|
153
|
-
const tail = resamplers.map((r) => r.flush());
|
|
154
|
-
const room = frames - emitted;
|
|
155
|
-
if (room > 0) {
|
|
156
|
-
const padded = tail.map((ch) => {
|
|
157
|
-
const full = new Float32Array(room);
|
|
158
|
-
full.set(ch.subarray(0, room));
|
|
159
|
-
return full;
|
|
160
|
-
});
|
|
161
|
-
emitted += room;
|
|
162
|
-
yield padded;
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
return {
|
|
166
|
-
sampleRate: MEDIA_FX_SAMPLE_RATE,
|
|
167
|
-
channels,
|
|
168
|
-
frames,
|
|
169
|
-
durationSec,
|
|
170
|
-
trackDurationSec,
|
|
171
|
-
sourceSampleRate: sourceRate,
|
|
172
|
-
open,
|
|
173
|
-
dispose: () => input.dispose()
|
|
174
98
|
};
|
|
175
99
|
}
|
|
176
100
|
|
|
@@ -291,6 +215,37 @@ async function previewMediaEditAudio(request, options = {}) {
|
|
|
291
215
|
source.dispose();
|
|
292
216
|
}
|
|
293
217
|
}
|
|
218
|
+
async function analyzeMediaEditWaveform(request, options = {}) {
|
|
219
|
+
const source = await openMediaAudio(request.source);
|
|
220
|
+
if (!source) {
|
|
221
|
+
return {
|
|
222
|
+
peaks: new Float32Array(0),
|
|
223
|
+
peaksPerSec: WAVEFORM_PEAKS_PER_SEC,
|
|
224
|
+
durationSec: await probeMediaDuration(request.source)
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
try {
|
|
228
|
+
const accumulator = createPeakAccumulator(
|
|
229
|
+
source.sampleRate,
|
|
230
|
+
WAVEFORM_PEAKS_PER_SEC,
|
|
231
|
+
source.frames
|
|
232
|
+
);
|
|
233
|
+
let seen = 0;
|
|
234
|
+
for await (const block of source.open()) {
|
|
235
|
+
options.signal?.throwIfAborted();
|
|
236
|
+
accumulator.push(block);
|
|
237
|
+
seen += block[0].length;
|
|
238
|
+
options.onProgress?.(Math.min(1, seen / Math.max(1, source.frames)));
|
|
239
|
+
}
|
|
240
|
+
return {
|
|
241
|
+
peaks: accumulator.finish(),
|
|
242
|
+
peaksPerSec: WAVEFORM_PEAKS_PER_SEC,
|
|
243
|
+
durationSec: await source.mediaDuration()
|
|
244
|
+
};
|
|
245
|
+
} finally {
|
|
246
|
+
source.dispose();
|
|
247
|
+
}
|
|
248
|
+
}
|
|
294
249
|
async function analyzeMediaEditPauses(request, options = {}) {
|
|
295
250
|
const source = await openMediaAudio(request.source);
|
|
296
251
|
if (!source) throw new Error("This media has no audio track to analyze.");
|
|
@@ -317,5 +272,6 @@ export {
|
|
|
317
272
|
renderMediaEditAudio,
|
|
318
273
|
analyzeMediaEditAudio,
|
|
319
274
|
previewMediaEditAudio,
|
|
275
|
+
analyzeMediaEditWaveform,
|
|
320
276
|
analyzeMediaEditPauses
|
|
321
277
|
};
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import {
|
|
2
|
+
MEDIA_INPUT_FORMATS,
|
|
3
|
+
openMediaAudio
|
|
4
|
+
} from "./chunk-AARDBHRJ.js";
|
|
5
|
+
import {
|
|
6
|
+
AudioSample,
|
|
7
|
+
AudioSampleSource,
|
|
8
|
+
BlobSource,
|
|
9
|
+
BufferTarget,
|
|
10
|
+
Input,
|
|
11
|
+
Mp4OutputFormat,
|
|
12
|
+
Output,
|
|
13
|
+
QUALITY_HIGH,
|
|
14
|
+
VideoSampleSink,
|
|
15
|
+
VideoSampleSource,
|
|
16
|
+
WavOutputFormat,
|
|
17
|
+
WebMOutputFormat,
|
|
18
|
+
canEncodeAudio,
|
|
19
|
+
canEncodeVideo,
|
|
20
|
+
getFirstEncodableVideoCodec
|
|
21
|
+
} from "./chunk-MOIVICAD.js";
|
|
22
|
+
|
|
23
|
+
// src/mediaEdit/mediaBakeEngine.ts
|
|
24
|
+
import {
|
|
25
|
+
MEDIA_BAKE_ENGINE,
|
|
26
|
+
createMediaBakeAudioStage,
|
|
27
|
+
createMediaTimeMap,
|
|
28
|
+
mediaBakeFrameRanges
|
|
29
|
+
} from "@bendyline/squisq/mediaEdit";
|
|
30
|
+
var SEEK_PAST_CUT_SEC = 6;
|
|
31
|
+
var FALLBACK_FRAME_SEC = 1 / 30;
|
|
32
|
+
var AUDIO_BITRATE_PER_CHANNEL = 96e3;
|
|
33
|
+
function audioBitrate(channels) {
|
|
34
|
+
return Math.min(192e3, AUDIO_BITRATE_PER_CHANNEL * channels);
|
|
35
|
+
}
|
|
36
|
+
function timeMapFor(request, sourceDuration) {
|
|
37
|
+
const map = createMediaTimeMap({
|
|
38
|
+
clipStart: request.clipStart,
|
|
39
|
+
clipEnd: request.clipEnd,
|
|
40
|
+
cuts: request.cuts,
|
|
41
|
+
sourceDuration
|
|
42
|
+
});
|
|
43
|
+
if (!(map.playedDuration > 0) || !Number.isFinite(map.playedDuration)) {
|
|
44
|
+
throw new Error("Nothing is left of this clip after its trim and cuts.");
|
|
45
|
+
}
|
|
46
|
+
return map;
|
|
47
|
+
}
|
|
48
|
+
function audioStageFor(request, map, sampleRate) {
|
|
49
|
+
return createMediaBakeAudioStage({
|
|
50
|
+
ranges: mediaBakeFrameRanges(map.keptRanges(), map.windowStart, sampleRate),
|
|
51
|
+
sampleRate,
|
|
52
|
+
gainDb: request.gain,
|
|
53
|
+
fadeInSec: request.fadeIn,
|
|
54
|
+
fadeOutSec: request.fadeOut
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
async function pumpAudio(audio, stage, source, options, onFrames) {
|
|
58
|
+
let written = 0;
|
|
59
|
+
for await (const block of audio.open()) {
|
|
60
|
+
options.signal?.throwIfAborted();
|
|
61
|
+
const kept = stage.process(block);
|
|
62
|
+
if (!kept) continue;
|
|
63
|
+
const frames = kept[0].length;
|
|
64
|
+
const data = new Float32Array(frames * kept.length);
|
|
65
|
+
kept.forEach((channel, c) => data.set(channel, c * frames));
|
|
66
|
+
const sample = new AudioSample({
|
|
67
|
+
data,
|
|
68
|
+
format: "f32-planar",
|
|
69
|
+
numberOfChannels: kept.length,
|
|
70
|
+
sampleRate: audio.sampleRate,
|
|
71
|
+
timestamp: written / audio.sampleRate
|
|
72
|
+
});
|
|
73
|
+
try {
|
|
74
|
+
await source.add(sample);
|
|
75
|
+
} finally {
|
|
76
|
+
sample.close();
|
|
77
|
+
}
|
|
78
|
+
written += frames;
|
|
79
|
+
onFrames?.(written);
|
|
80
|
+
}
|
|
81
|
+
return written;
|
|
82
|
+
}
|
|
83
|
+
async function pickAudioTarget(channels, sampleRate) {
|
|
84
|
+
const probe = { numberOfChannels: channels, sampleRate, bitrate: audioBitrate(channels) };
|
|
85
|
+
if (await canEncodeAudio("aac", probe)) {
|
|
86
|
+
return {
|
|
87
|
+
format: new Mp4OutputFormat(),
|
|
88
|
+
codec: "aac",
|
|
89
|
+
mimeType: "audio/mp4",
|
|
90
|
+
extension: "m4a"
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (await canEncodeAudio("opus", probe)) {
|
|
94
|
+
return {
|
|
95
|
+
format: new WebMOutputFormat(),
|
|
96
|
+
codec: "opus",
|
|
97
|
+
mimeType: "audio/webm",
|
|
98
|
+
extension: "webm"
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
return {
|
|
102
|
+
format: new WavOutputFormat(),
|
|
103
|
+
codec: "pcm-s16",
|
|
104
|
+
mimeType: "audio/wav",
|
|
105
|
+
extension: "wav"
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
async function bakeAudioClip(request, options) {
|
|
109
|
+
const audio = await openMediaAudio(request.audio ?? request.source, {
|
|
110
|
+
startSec: request.clipStart ?? 0,
|
|
111
|
+
endSec: request.clipEnd
|
|
112
|
+
});
|
|
113
|
+
if (!audio) throw new Error("This clip has no audio track to bake.");
|
|
114
|
+
try {
|
|
115
|
+
const map = timeMapFor(request, audio.trackDurationSec);
|
|
116
|
+
const stage = audioStageFor(request, map, audio.sampleRate);
|
|
117
|
+
const target = await pickAudioTarget(audio.channels, audio.sampleRate);
|
|
118
|
+
const buffer = new BufferTarget();
|
|
119
|
+
const output = new Output({ format: target.format, target: buffer });
|
|
120
|
+
const source = new AudioSampleSource(
|
|
121
|
+
target.codec === "pcm-s16" ? { codec: "pcm-s16" } : { codec: target.codec, bitrate: audioBitrate(audio.channels) }
|
|
122
|
+
);
|
|
123
|
+
output.addAudioTrack(source);
|
|
124
|
+
await output.start();
|
|
125
|
+
try {
|
|
126
|
+
const written = await pumpAudio(
|
|
127
|
+
audio,
|
|
128
|
+
stage,
|
|
129
|
+
source,
|
|
130
|
+
options,
|
|
131
|
+
(frames) => options.onProgress?.(Math.min(1, frames / Math.max(1, stage.totalFrames)))
|
|
132
|
+
);
|
|
133
|
+
source.close();
|
|
134
|
+
await output.finalize();
|
|
135
|
+
if (!buffer.buffer) throw new Error("The bake produced no output.");
|
|
136
|
+
options.onProgress?.(1);
|
|
137
|
+
return {
|
|
138
|
+
bytes: buffer.buffer,
|
|
139
|
+
mimeType: target.mimeType,
|
|
140
|
+
extension: target.extension,
|
|
141
|
+
durationSec: written / audio.sampleRate,
|
|
142
|
+
engine: MEDIA_BAKE_ENGINE
|
|
143
|
+
};
|
|
144
|
+
} catch (err) {
|
|
145
|
+
await output.cancel().catch(() => {
|
|
146
|
+
});
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
} finally {
|
|
150
|
+
audio.dispose();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async function pickVideoTarget(width, height, audio) {
|
|
154
|
+
const audioProbe = audio && {
|
|
155
|
+
numberOfChannels: audio.channels,
|
|
156
|
+
sampleRate: audio.sampleRate,
|
|
157
|
+
bitrate: audioBitrate(audio.channels)
|
|
158
|
+
};
|
|
159
|
+
const size = { width, height };
|
|
160
|
+
if (await canEncodeVideo("avc", size) && (!audioProbe || await canEncodeAudio("aac", audioProbe))) {
|
|
161
|
+
return {
|
|
162
|
+
format: new Mp4OutputFormat(),
|
|
163
|
+
video: "avc",
|
|
164
|
+
audio: "aac",
|
|
165
|
+
mimeType: "video/mp4",
|
|
166
|
+
extension: "mp4"
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
const webm = await getFirstEncodableVideoCodec(["vp9", "vp8"], size);
|
|
170
|
+
if (webm && (!audioProbe || await canEncodeAudio("opus", audioProbe))) {
|
|
171
|
+
return {
|
|
172
|
+
format: new WebMOutputFormat(),
|
|
173
|
+
video: webm,
|
|
174
|
+
audio: "opus",
|
|
175
|
+
mimeType: "video/webm",
|
|
176
|
+
extension: "webm"
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
throw new Error("This browser cannot encode video, so the clip cannot be baked.");
|
|
180
|
+
}
|
|
181
|
+
function bakeCropRectangle(crop, displayWidth, displayHeight) {
|
|
182
|
+
const even = (value) => Math.max(2, Math.floor(value / 2) * 2);
|
|
183
|
+
const left = crop ? Math.round(crop.x * displayWidth) : 0;
|
|
184
|
+
const top = crop ? Math.round(crop.y * displayHeight) : 0;
|
|
185
|
+
const width = even(
|
|
186
|
+
Math.min(displayWidth - left, crop ? Math.round(crop.w * displayWidth) : displayWidth)
|
|
187
|
+
);
|
|
188
|
+
const height = even(
|
|
189
|
+
Math.min(displayHeight - top, crop ? Math.round(crop.h * displayHeight) : displayHeight)
|
|
190
|
+
);
|
|
191
|
+
if (left === 0 && top === 0 && width === displayWidth && height === displayHeight) return null;
|
|
192
|
+
return { left, top, width, height };
|
|
193
|
+
}
|
|
194
|
+
function bakeDecodeRuns(kept) {
|
|
195
|
+
const runs = [];
|
|
196
|
+
for (const range of kept) {
|
|
197
|
+
const last = runs[runs.length - 1];
|
|
198
|
+
if (last && range.start - last.end < SEEK_PAST_CUT_SEC) last.end = range.end;
|
|
199
|
+
else runs.push({ ...range });
|
|
200
|
+
}
|
|
201
|
+
return runs;
|
|
202
|
+
}
|
|
203
|
+
async function bakeVideoClip(request, options) {
|
|
204
|
+
const input = new Input({ source: new BlobSource(request.source), formats: MEDIA_INPUT_FORMATS });
|
|
205
|
+
let audio = null;
|
|
206
|
+
try {
|
|
207
|
+
const track = await input.getPrimaryVideoTrack();
|
|
208
|
+
if (!track) throw new Error("This clip has no video track to bake.");
|
|
209
|
+
if (!await track.canDecode()) throw new Error("This browser cannot decode this video.");
|
|
210
|
+
audio = await openMediaAudio(request.audio ?? request.source, {
|
|
211
|
+
startSec: request.clipStart ?? 0,
|
|
212
|
+
endSec: request.clipEnd
|
|
213
|
+
});
|
|
214
|
+
const sourceDuration = Math.max(await track.computeDuration(), audio?.trackDurationSec ?? 0);
|
|
215
|
+
const map = timeMapFor(request, sourceDuration);
|
|
216
|
+
const crop = bakeCropRectangle(request.crop, track.displayWidth, track.displayHeight);
|
|
217
|
+
const width = crop?.width ?? track.displayWidth;
|
|
218
|
+
const height = crop?.height ?? track.displayHeight;
|
|
219
|
+
const target = await pickVideoTarget(width, height, audio);
|
|
220
|
+
const buffer = new BufferTarget();
|
|
221
|
+
const output = new Output({ format: target.format, target: buffer });
|
|
222
|
+
const videoSource = new VideoSampleSource({
|
|
223
|
+
codec: target.video,
|
|
224
|
+
quality: QUALITY_HIGH,
|
|
225
|
+
// A screen recording can change size mid-take; keep the first frame's box.
|
|
226
|
+
sizeChangeBehavior: "contain",
|
|
227
|
+
...crop ? { transform: { crop } } : {}
|
|
228
|
+
});
|
|
229
|
+
output.addVideoTrack(videoSource);
|
|
230
|
+
const audioSource = audio ? new AudioSampleSource({ codec: target.audio, bitrate: audioBitrate(audio.channels) }) : null;
|
|
231
|
+
if (audioSource) output.addAudioTrack(audioSource);
|
|
232
|
+
await output.start();
|
|
233
|
+
const pumpVideo = async () => {
|
|
234
|
+
const sink = new VideoSampleSink(track);
|
|
235
|
+
let lastEnd = 0;
|
|
236
|
+
for (const run of bakeDecodeRuns(map.keptRanges())) {
|
|
237
|
+
for await (const sample of sink.samples(run.start, run.end)) {
|
|
238
|
+
try {
|
|
239
|
+
options.signal?.throwIfAborted();
|
|
240
|
+
const length = sample.duration > 0 ? sample.duration : FALLBACK_FRAME_SEC;
|
|
241
|
+
const start = Math.max(lastEnd, map.snapSourceToPlayed(sample.timestamp));
|
|
242
|
+
const end = map.snapSourceToPlayed(sample.timestamp + length);
|
|
243
|
+
if (end - start < 1e-4) continue;
|
|
244
|
+
sample.setTimestamp(start);
|
|
245
|
+
sample.setDuration(end - start);
|
|
246
|
+
await videoSource.add(sample);
|
|
247
|
+
lastEnd = end;
|
|
248
|
+
options.onProgress?.(Math.min(1, end / map.playedDuration));
|
|
249
|
+
} finally {
|
|
250
|
+
sample.close();
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
videoSource.close();
|
|
255
|
+
};
|
|
256
|
+
const pumpSound = async () => {
|
|
257
|
+
if (!audio || !audioSource) return;
|
|
258
|
+
await pumpAudio(audio, audioStageFor(request, map, audio.sampleRate), audioSource, options);
|
|
259
|
+
audioSource.close();
|
|
260
|
+
};
|
|
261
|
+
try {
|
|
262
|
+
await Promise.all([pumpVideo(), pumpSound()]);
|
|
263
|
+
await output.finalize();
|
|
264
|
+
} catch (err) {
|
|
265
|
+
await output.cancel().catch(() => {
|
|
266
|
+
});
|
|
267
|
+
throw err;
|
|
268
|
+
}
|
|
269
|
+
if (!buffer.buffer) throw new Error("The bake produced no output.");
|
|
270
|
+
options.onProgress?.(1);
|
|
271
|
+
return {
|
|
272
|
+
bytes: buffer.buffer,
|
|
273
|
+
mimeType: target.mimeType,
|
|
274
|
+
extension: target.extension,
|
|
275
|
+
durationSec: map.playedDuration,
|
|
276
|
+
engine: MEDIA_BAKE_ENGINE
|
|
277
|
+
};
|
|
278
|
+
} finally {
|
|
279
|
+
audio?.dispose();
|
|
280
|
+
input.dispose();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
async function bakeMediaClip(request, options = {}) {
|
|
284
|
+
options.signal?.throwIfAborted();
|
|
285
|
+
return request.kind === "video" ? bakeVideoClip(request, options) : bakeAudioClip(request, options);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
export {
|
|
289
|
+
bakeCropRectangle,
|
|
290
|
+
bakeDecodeRuns,
|
|
291
|
+
bakeMediaClip
|
|
292
|
+
};
|
|
@@ -92,7 +92,7 @@ function createWavStreamWriter(sampleRate, channels, options = {}) {
|
|
|
92
92
|
// src/audioFile/audioFileEncoder.ts
|
|
93
93
|
var lossyModule = null;
|
|
94
94
|
function loadLossyModule() {
|
|
95
|
-
lossyModule ?? (lossyModule = import("./lossyEncoder-
|
|
95
|
+
lossyModule ?? (lossyModule = import("./lossyEncoder-U4V3AER7.js"));
|
|
96
96
|
return lossyModule;
|
|
97
97
|
}
|
|
98
98
|
function validateOptions(options) {
|