@remotion/timeline-utils 4.0.460 → 4.0.462
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/audio-waveform/audio-waveform-worker-types.d.ts +28 -0
- package/dist/audio-waveform/audio-waveform-worker-types.js +2 -0
- package/dist/audio-waveform/constants.d.ts +1 -0
- package/dist/audio-waveform/constants.js +4 -0
- package/dist/audio-waveform/draw-peaks.d.ts +7 -0
- package/dist/audio-waveform/draw-peaks.js +68 -0
- package/dist/audio-waveform/load-waveform-peaks.d.ts +13 -0
- package/dist/audio-waveform/load-waveform-peaks.js +76 -0
- package/dist/audio-waveform/make-audio-waveform-worker.d.ts +1 -0
- package/dist/audio-waveform/make-audio-waveform-worker.js +10 -0
- package/dist/audio-waveform/parse-color.d.ts +1 -0
- package/dist/audio-waveform/parse-color.js +17 -0
- package/dist/audio-waveform/slice-waveform-peaks.d.ts +7 -0
- package/dist/audio-waveform/slice-waveform-peaks.js +15 -0
- package/dist/audio-waveform/waveform-peak-processor.d.ts +23 -0
- package/dist/audio-waveform/waveform-peak-processor.js +77 -0
- package/dist/audio-waveform-worker.d.ts +1 -0
- package/dist/audio-waveform-worker.js +114 -0
- package/dist/esm/audio-waveform-worker.mjs +374 -0
- package/dist/esm/index.mjs +272 -7
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -1
- package/package.json +19 -5
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TimelineLoopDisplay } from '../loop-display';
|
|
2
|
+
export type AudioWaveformWorkerInitMessage = {
|
|
3
|
+
readonly type: 'init';
|
|
4
|
+
readonly canvas: OffscreenCanvas;
|
|
5
|
+
};
|
|
6
|
+
export type AudioWaveformWorkerRenderMessage = {
|
|
7
|
+
readonly type: 'render';
|
|
8
|
+
readonly requestId: number;
|
|
9
|
+
readonly src: string;
|
|
10
|
+
readonly width: number;
|
|
11
|
+
readonly height: number;
|
|
12
|
+
readonly volume: number;
|
|
13
|
+
readonly startFrom: number;
|
|
14
|
+
readonly durationInFrames: number;
|
|
15
|
+
readonly fps: number;
|
|
16
|
+
readonly playbackRate: number;
|
|
17
|
+
readonly loopDisplay: TimelineLoopDisplay | undefined;
|
|
18
|
+
};
|
|
19
|
+
export type AudioWaveformWorkerDisposeMessage = {
|
|
20
|
+
readonly type: 'dispose';
|
|
21
|
+
};
|
|
22
|
+
export type AudioWaveformWorkerIncomingMessage = AudioWaveformWorkerInitMessage | AudioWaveformWorkerRenderMessage | AudioWaveformWorkerDisposeMessage;
|
|
23
|
+
export type AudioWaveformWorkerErrorMessage = {
|
|
24
|
+
readonly type: 'error';
|
|
25
|
+
readonly requestId: number;
|
|
26
|
+
readonly message: string;
|
|
27
|
+
};
|
|
28
|
+
export type AudioWaveformWorkerOutgoingMessage = AudioWaveformWorkerErrorMessage;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const TARGET_SAMPLE_RATE = 100;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const drawBars: ({ canvas, color, peaks, volume, width, }: {
|
|
2
|
+
readonly canvas: HTMLCanvasElement | OffscreenCanvas;
|
|
3
|
+
readonly peaks: Float32Array<ArrayBufferLike>;
|
|
4
|
+
readonly color: string;
|
|
5
|
+
readonly volume: number;
|
|
6
|
+
readonly width: number;
|
|
7
|
+
}) => void;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.drawBars = void 0;
|
|
4
|
+
const parse_color_1 = require("./parse-color");
|
|
5
|
+
const CLIPPING_COLOR = '#FF7F50';
|
|
6
|
+
const drawBars = ({ canvas, color, peaks, volume, width, }) => {
|
|
7
|
+
const ctx = canvas.getContext('2d');
|
|
8
|
+
if (!ctx) {
|
|
9
|
+
throw new Error('Failed to get canvas context');
|
|
10
|
+
}
|
|
11
|
+
const { height } = canvas;
|
|
12
|
+
const w = canvas.width;
|
|
13
|
+
// Skip drawing when the target canvas has not been laid out yet.
|
|
14
|
+
// `createImageData(0, h)` / `(w, 0)` throws a DOMException, which
|
|
15
|
+
// surfaces in Studio's console for compositions with many audio
|
|
16
|
+
// sequences — some segments are 0 px wide at certain zoom levels.
|
|
17
|
+
if (w === 0 || height === 0) {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
ctx.clearRect(0, 0, w, height);
|
|
21
|
+
if (volume === 0)
|
|
22
|
+
return;
|
|
23
|
+
const [r, g, b, a] = (0, parse_color_1.parseColor)(color);
|
|
24
|
+
const [cr, cg, cb, ca] = (0, parse_color_1.parseColor)(CLIPPING_COLOR);
|
|
25
|
+
const imageData = ctx.createImageData(w, height);
|
|
26
|
+
const { data } = imageData;
|
|
27
|
+
const numBars = width;
|
|
28
|
+
for (let barIndex = 0; barIndex < numBars; barIndex++) {
|
|
29
|
+
const x = barIndex;
|
|
30
|
+
if (x >= w)
|
|
31
|
+
break;
|
|
32
|
+
const peakIndex = Math.floor((barIndex / numBars) * peaks.length);
|
|
33
|
+
const peak = peaks[peakIndex] || 0;
|
|
34
|
+
const scaledPeak = peak * volume;
|
|
35
|
+
const halfBar = Math.max(0, Math.min(height / 2, (scaledPeak * height) / 2));
|
|
36
|
+
if (halfBar === 0)
|
|
37
|
+
continue;
|
|
38
|
+
const mid = height / 2;
|
|
39
|
+
const barY = Math.round(mid - halfBar);
|
|
40
|
+
const barEnd = Math.round(mid + halfBar);
|
|
41
|
+
const isClipping = scaledPeak > 1;
|
|
42
|
+
const clipTopEnd = isClipping ? Math.min(barY + 2, barEnd) : barY;
|
|
43
|
+
const clipBotStart = isClipping ? Math.max(barEnd - 2, barY) : barEnd;
|
|
44
|
+
for (let y = barY; y < clipTopEnd; y++) {
|
|
45
|
+
const idx = (y * w + x) * 4;
|
|
46
|
+
data[idx] = cr;
|
|
47
|
+
data[idx + 1] = cg;
|
|
48
|
+
data[idx + 2] = cb;
|
|
49
|
+
data[idx + 3] = ca;
|
|
50
|
+
}
|
|
51
|
+
for (let y = clipTopEnd; y < clipBotStart; y++) {
|
|
52
|
+
const idx = (y * w + x) * 4;
|
|
53
|
+
data[idx] = r;
|
|
54
|
+
data[idx + 1] = g;
|
|
55
|
+
data[idx + 2] = b;
|
|
56
|
+
data[idx + 3] = a;
|
|
57
|
+
}
|
|
58
|
+
for (let y = clipBotStart; y < barEnd; y++) {
|
|
59
|
+
const idx = (y * w + x) * 4;
|
|
60
|
+
data[idx] = cr;
|
|
61
|
+
data[idx + 1] = cg;
|
|
62
|
+
data[idx + 2] = cb;
|
|
63
|
+
data[idx + 3] = ca;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
ctx.putImageData(imageData, 0, 0);
|
|
67
|
+
};
|
|
68
|
+
exports.drawBars = drawBars;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { TARGET_SAMPLE_RATE } from './constants';
|
|
2
|
+
export { TARGET_SAMPLE_RATE };
|
|
3
|
+
type Progress = {
|
|
4
|
+
readonly peaks: Float32Array;
|
|
5
|
+
readonly completedPeaks: number;
|
|
6
|
+
readonly totalPeaks: number;
|
|
7
|
+
readonly final: boolean;
|
|
8
|
+
};
|
|
9
|
+
type LoadWaveformPeaksOptions = {
|
|
10
|
+
readonly onProgress?: (progress: Progress) => void;
|
|
11
|
+
readonly progressIntervalInMs?: number;
|
|
12
|
+
};
|
|
13
|
+
export declare function loadWaveformPeaks(url: string, signal: AbortSignal, options?: LoadWaveformPeaksOptions): Promise<Float32Array>;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.TARGET_SAMPLE_RATE = void 0;
|
|
4
|
+
exports.loadWaveformPeaks = loadWaveformPeaks;
|
|
5
|
+
const mediabunny_1 = require("mediabunny");
|
|
6
|
+
const constants_1 = require("./constants");
|
|
7
|
+
Object.defineProperty(exports, "TARGET_SAMPLE_RATE", { enumerable: true, get: function () { return constants_1.TARGET_SAMPLE_RATE; } });
|
|
8
|
+
const waveform_peak_processor_1 = require("./waveform-peak-processor");
|
|
9
|
+
const DEFAULT_PROGRESS_INTERVAL_IN_MS = 50;
|
|
10
|
+
const peaksCache = new Map();
|
|
11
|
+
async function loadWaveformPeaks(url, signal, options) {
|
|
12
|
+
var _a, _b;
|
|
13
|
+
const cached = peaksCache.get(url);
|
|
14
|
+
if (cached) {
|
|
15
|
+
(0, waveform_peak_processor_1.emitWaveformProgress)({
|
|
16
|
+
peaks: cached,
|
|
17
|
+
completedPeaks: cached.length,
|
|
18
|
+
totalPeaks: cached.length,
|
|
19
|
+
final: true,
|
|
20
|
+
onProgress: options === null || options === void 0 ? void 0 : options.onProgress,
|
|
21
|
+
});
|
|
22
|
+
return cached;
|
|
23
|
+
}
|
|
24
|
+
const input = new mediabunny_1.Input({
|
|
25
|
+
formats: mediabunny_1.ALL_FORMATS,
|
|
26
|
+
source: new mediabunny_1.UrlSource(url),
|
|
27
|
+
});
|
|
28
|
+
try {
|
|
29
|
+
const audioTrack = await input.getPrimaryAudioTrack();
|
|
30
|
+
if (!audioTrack) {
|
|
31
|
+
return new Float32Array(0);
|
|
32
|
+
}
|
|
33
|
+
if (await audioTrack.isLive()) {
|
|
34
|
+
throw new Error('Live streams are not currently supported by Remotion. Sorry! Source: ' +
|
|
35
|
+
url);
|
|
36
|
+
}
|
|
37
|
+
if (await audioTrack.isRelativeToUnixEpoch()) {
|
|
38
|
+
throw new Error('Streams with UNIX timestamps are not currently supported by Remotion. Sorry! Source: ' +
|
|
39
|
+
url);
|
|
40
|
+
}
|
|
41
|
+
const sampleRate = await audioTrack.getSampleRate();
|
|
42
|
+
const durationInSeconds = (_a = (await audioTrack.getDurationFromMetadata({ skipLiveWait: true }))) !== null && _a !== void 0 ? _a : (await audioTrack.computeDuration({ skipLiveWait: true }));
|
|
43
|
+
const totalPeaks = Math.ceil(durationInSeconds * constants_1.TARGET_SAMPLE_RATE);
|
|
44
|
+
const samplesPerPeak = Math.max(1, Math.floor(sampleRate / constants_1.TARGET_SAMPLE_RATE));
|
|
45
|
+
const sink = new mediabunny_1.AudioSampleSink(audioTrack);
|
|
46
|
+
const processor = (0, waveform_peak_processor_1.createWaveformPeakProcessor)({
|
|
47
|
+
totalPeaks,
|
|
48
|
+
samplesPerPeak,
|
|
49
|
+
onProgress: options === null || options === void 0 ? void 0 : options.onProgress,
|
|
50
|
+
progressIntervalInMs: (_b = options === null || options === void 0 ? void 0 : options.progressIntervalInMs) !== null && _b !== void 0 ? _b : DEFAULT_PROGRESS_INTERVAL_IN_MS,
|
|
51
|
+
now: () => Date.now(),
|
|
52
|
+
});
|
|
53
|
+
for await (const sample of sink.samples()) {
|
|
54
|
+
if (signal.aborted) {
|
|
55
|
+
sample.close();
|
|
56
|
+
return new Float32Array(0);
|
|
57
|
+
}
|
|
58
|
+
const bytesNeeded = sample.allocationSize({
|
|
59
|
+
format: 'f32',
|
|
60
|
+
planeIndex: 0,
|
|
61
|
+
});
|
|
62
|
+
const floats = new Float32Array(bytesNeeded / 4);
|
|
63
|
+
sample.copyTo(floats, { format: 'f32', planeIndex: 0 });
|
|
64
|
+
const channels = Math.max(1, sample.numberOfChannels);
|
|
65
|
+
sample.close();
|
|
66
|
+
processor.processSampleChunk(floats, channels);
|
|
67
|
+
}
|
|
68
|
+
processor.finalize();
|
|
69
|
+
const { peaks } = processor;
|
|
70
|
+
peaksCache.set(url, peaks);
|
|
71
|
+
return peaks;
|
|
72
|
+
}
|
|
73
|
+
finally {
|
|
74
|
+
input.dispose();
|
|
75
|
+
}
|
|
76
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const makeAudioWaveformWorker: () => Worker;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.makeAudioWaveformWorker = void 0;
|
|
4
|
+
const makeAudioWaveformWorker = () => {
|
|
5
|
+
// @ts-expect-error `import.meta.url` is required for bundling the worker entry.
|
|
6
|
+
return new Worker(new URL('./audio-waveform-worker.mjs', import.meta.url), {
|
|
7
|
+
type: 'module',
|
|
8
|
+
});
|
|
9
|
+
};
|
|
10
|
+
exports.makeAudioWaveformWorker = makeAudioWaveformWorker;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const parseColor: (color: string) => [number, number, number, number];
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.parseColor = void 0;
|
|
4
|
+
const colorCache = new Map();
|
|
5
|
+
const parseColor = (color) => {
|
|
6
|
+
const cached = colorCache.get(color);
|
|
7
|
+
if (cached)
|
|
8
|
+
return cached;
|
|
9
|
+
const ctx = new OffscreenCanvas(1, 1).getContext('2d');
|
|
10
|
+
ctx.fillStyle = color;
|
|
11
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
12
|
+
const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
|
|
13
|
+
const result = [r, g, b, a];
|
|
14
|
+
colorCache.set(color, result);
|
|
15
|
+
return result;
|
|
16
|
+
};
|
|
17
|
+
exports.parseColor = parseColor;
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const sliceWaveformPeaks: ({ durationInFrames, fps, peaks, playbackRate, startFrom, }: {
|
|
2
|
+
readonly peaks: Float32Array<ArrayBufferLike>;
|
|
3
|
+
readonly startFrom: number;
|
|
4
|
+
readonly durationInFrames: number;
|
|
5
|
+
readonly fps: number;
|
|
6
|
+
readonly playbackRate: number;
|
|
7
|
+
}) => Float32Array<ArrayBufferLike>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.sliceWaveformPeaks = void 0;
|
|
4
|
+
const constants_1 = require("./constants");
|
|
5
|
+
const sliceWaveformPeaks = ({ durationInFrames, fps, peaks, playbackRate, startFrom, }) => {
|
|
6
|
+
if (peaks.length === 0) {
|
|
7
|
+
return peaks;
|
|
8
|
+
}
|
|
9
|
+
const startTimeInSeconds = startFrom / fps;
|
|
10
|
+
const durationInSeconds = (durationInFrames / fps) * playbackRate;
|
|
11
|
+
const startPeakIndex = Math.floor(startTimeInSeconds * constants_1.TARGET_SAMPLE_RATE);
|
|
12
|
+
const endPeakIndex = Math.ceil((startTimeInSeconds + durationInSeconds) * constants_1.TARGET_SAMPLE_RATE);
|
|
13
|
+
return peaks.subarray(Math.max(0, startPeakIndex), Math.min(peaks.length, endPeakIndex));
|
|
14
|
+
};
|
|
15
|
+
exports.sliceWaveformPeaks = sliceWaveformPeaks;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
type Progress = {
|
|
2
|
+
readonly peaks: Float32Array;
|
|
3
|
+
readonly completedPeaks: number;
|
|
4
|
+
readonly totalPeaks: number;
|
|
5
|
+
readonly final: boolean;
|
|
6
|
+
};
|
|
7
|
+
type WaveformPeakProcessorOptions = {
|
|
8
|
+
readonly totalPeaks: number;
|
|
9
|
+
readonly samplesPerPeak: number;
|
|
10
|
+
readonly onProgress?: (progress: Progress) => void;
|
|
11
|
+
readonly progressIntervalInMs: number;
|
|
12
|
+
readonly now: () => number;
|
|
13
|
+
};
|
|
14
|
+
type WaveformPeakProcessor = {
|
|
15
|
+
readonly peaks: Float32Array;
|
|
16
|
+
processSampleChunk: (floats: Float32Array, channels: number) => void;
|
|
17
|
+
finalize: () => void;
|
|
18
|
+
};
|
|
19
|
+
export declare const emitWaveformProgress: ({ completedPeaks, final, onProgress, peaks, totalPeaks, }: Progress & {
|
|
20
|
+
readonly onProgress?: ((progress: Progress) => void) | undefined;
|
|
21
|
+
}) => void;
|
|
22
|
+
export declare const createWaveformPeakProcessor: ({ totalPeaks, samplesPerPeak, onProgress, progressIntervalInMs, now, }: WaveformPeakProcessorOptions) => WaveformPeakProcessor;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createWaveformPeakProcessor = exports.emitWaveformProgress = void 0;
|
|
4
|
+
const emitWaveformProgress = ({ completedPeaks, final, onProgress, peaks, totalPeaks, }) => {
|
|
5
|
+
onProgress === null || onProgress === void 0 ? void 0 : onProgress({
|
|
6
|
+
peaks,
|
|
7
|
+
completedPeaks,
|
|
8
|
+
totalPeaks,
|
|
9
|
+
final,
|
|
10
|
+
});
|
|
11
|
+
};
|
|
12
|
+
exports.emitWaveformProgress = emitWaveformProgress;
|
|
13
|
+
const createWaveformPeakProcessor = ({ totalPeaks, samplesPerPeak, onProgress, progressIntervalInMs, now, }) => {
|
|
14
|
+
const peaks = new Float32Array(totalPeaks);
|
|
15
|
+
let peakIndex = 0;
|
|
16
|
+
let peakMax = 0;
|
|
17
|
+
let sampleInPeak = 0;
|
|
18
|
+
let lastProgressAt = 0;
|
|
19
|
+
let lastProgressPeak = 0;
|
|
20
|
+
const emitProgress = (force) => {
|
|
21
|
+
const timestamp = now();
|
|
22
|
+
if (!force && peakIndex === lastProgressPeak && sampleInPeak === 0) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
if (!force && timestamp - lastProgressAt < progressIntervalInMs) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
lastProgressAt = timestamp;
|
|
29
|
+
lastProgressPeak = peakIndex;
|
|
30
|
+
(0, exports.emitWaveformProgress)({
|
|
31
|
+
peaks,
|
|
32
|
+
completedPeaks: peakIndex,
|
|
33
|
+
totalPeaks,
|
|
34
|
+
final: force,
|
|
35
|
+
onProgress,
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
peaks,
|
|
40
|
+
processSampleChunk: (floats, channels) => {
|
|
41
|
+
var _a;
|
|
42
|
+
const frameCount = Math.floor(floats.length / Math.max(1, channels));
|
|
43
|
+
for (let frame = 0; frame < frameCount; frame++) {
|
|
44
|
+
// `f32` copies are interleaved, so timing advances per frame.
|
|
45
|
+
let framePeak = 0;
|
|
46
|
+
for (let channel = 0; channel < channels; channel++) {
|
|
47
|
+
const sampleIndex = frame * channels + channel;
|
|
48
|
+
const abs = Math.abs((_a = floats[sampleIndex]) !== null && _a !== void 0 ? _a : 0);
|
|
49
|
+
if (abs > framePeak) {
|
|
50
|
+
framePeak = abs;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (framePeak > peakMax) {
|
|
54
|
+
peakMax = framePeak;
|
|
55
|
+
}
|
|
56
|
+
sampleInPeak++;
|
|
57
|
+
if (sampleInPeak >= samplesPerPeak) {
|
|
58
|
+
if (peakIndex < totalPeaks) {
|
|
59
|
+
peaks[peakIndex] = peakMax;
|
|
60
|
+
}
|
|
61
|
+
peakIndex++;
|
|
62
|
+
peakMax = 0;
|
|
63
|
+
sampleInPeak = 0;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
emitProgress(false);
|
|
67
|
+
},
|
|
68
|
+
finalize: () => {
|
|
69
|
+
if (sampleInPeak > 0 && peakIndex < totalPeaks) {
|
|
70
|
+
peaks[peakIndex] = peakMax;
|
|
71
|
+
peakIndex++;
|
|
72
|
+
}
|
|
73
|
+
emitProgress(true);
|
|
74
|
+
},
|
|
75
|
+
};
|
|
76
|
+
};
|
|
77
|
+
exports.createWaveformPeakProcessor = createWaveformPeakProcessor;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/// <reference lib="webworker" />
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
const draw_peaks_1 = require("./audio-waveform/draw-peaks");
|
|
5
|
+
const load_waveform_peaks_1 = require("./audio-waveform/load-waveform-peaks");
|
|
6
|
+
const slice_waveform_peaks_1 = require("./audio-waveform/slice-waveform-peaks");
|
|
7
|
+
const loop_display_1 = require("./loop-display");
|
|
8
|
+
let canvas = null;
|
|
9
|
+
let currentController = null;
|
|
10
|
+
let latestRequestId = 0;
|
|
11
|
+
const postError = (requestId, error) => {
|
|
12
|
+
const message = error instanceof Error ? error.message : 'Failed to render waveform';
|
|
13
|
+
const payload = {
|
|
14
|
+
type: 'error',
|
|
15
|
+
requestId,
|
|
16
|
+
message,
|
|
17
|
+
};
|
|
18
|
+
self.postMessage(payload);
|
|
19
|
+
};
|
|
20
|
+
const drawPartialWaveform = (message, peaks) => {
|
|
21
|
+
if (!canvas) {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const portionPeaks = (0, slice_waveform_peaks_1.sliceWaveformPeaks)({
|
|
25
|
+
durationInFrames: (0, loop_display_1.shouldTileLoopDisplay)(message.loopDisplay)
|
|
26
|
+
? message.loopDisplay.durationInFrames
|
|
27
|
+
: message.durationInFrames,
|
|
28
|
+
fps: message.fps,
|
|
29
|
+
peaks,
|
|
30
|
+
playbackRate: message.playbackRate,
|
|
31
|
+
startFrom: message.startFrom,
|
|
32
|
+
});
|
|
33
|
+
if (!(0, loop_display_1.shouldTileLoopDisplay)(message.loopDisplay)) {
|
|
34
|
+
(0, draw_peaks_1.drawBars)({
|
|
35
|
+
canvas,
|
|
36
|
+
peaks: portionPeaks,
|
|
37
|
+
color: 'rgba(255, 255, 255, 0.6)',
|
|
38
|
+
volume: message.volume,
|
|
39
|
+
width: message.width,
|
|
40
|
+
});
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
const loopWidth = (0, loop_display_1.getLoopDisplayWidth)({
|
|
44
|
+
visualizationWidth: message.width,
|
|
45
|
+
loopDisplay: message.loopDisplay,
|
|
46
|
+
});
|
|
47
|
+
const targetCanvas = new OffscreenCanvas(Math.max(1, Math.ceil(loopWidth)), message.height);
|
|
48
|
+
(0, draw_peaks_1.drawBars)({
|
|
49
|
+
canvas: targetCanvas,
|
|
50
|
+
peaks: portionPeaks,
|
|
51
|
+
color: 'rgba(255, 255, 255, 0.6)',
|
|
52
|
+
volume: message.volume,
|
|
53
|
+
width: targetCanvas.width,
|
|
54
|
+
});
|
|
55
|
+
const ctx = canvas.getContext('2d');
|
|
56
|
+
if (!ctx) {
|
|
57
|
+
throw new Error('Failed to get canvas context');
|
|
58
|
+
}
|
|
59
|
+
const pattern = ctx.createPattern(targetCanvas, 'repeat-x');
|
|
60
|
+
if (!pattern) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
pattern.setTransform(new DOMMatrix().scaleSelf(loopWidth / targetCanvas.width, 1));
|
|
64
|
+
ctx.clearRect(0, 0, message.width, message.height);
|
|
65
|
+
ctx.fillStyle = pattern;
|
|
66
|
+
ctx.fillRect(0, 0, message.width, message.height);
|
|
67
|
+
};
|
|
68
|
+
const renderWaveform = async (message) => {
|
|
69
|
+
if (!canvas) {
|
|
70
|
+
postError(message.requestId, new Error('Waveform canvas not initialized'));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
currentController === null || currentController === void 0 ? void 0 : currentController.abort();
|
|
75
|
+
currentController = controller;
|
|
76
|
+
latestRequestId = message.requestId;
|
|
77
|
+
try {
|
|
78
|
+
canvas.width = message.width;
|
|
79
|
+
canvas.height = message.height;
|
|
80
|
+
const peaks = await (0, load_waveform_peaks_1.loadWaveformPeaks)(message.src, controller.signal, {
|
|
81
|
+
onProgress: ({ peaks: nextPeaks }) => {
|
|
82
|
+
if (controller.signal.aborted ||
|
|
83
|
+
latestRequestId !== message.requestId) {
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
drawPartialWaveform(message, nextPeaks);
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
if (controller.signal.aborted || latestRequestId !== message.requestId) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
drawPartialWaveform(message, peaks);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (controller.signal.aborted || latestRequestId !== message.requestId) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
postError(message.requestId, error);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
self.addEventListener('message', (event) => {
|
|
102
|
+
const message = event.data;
|
|
103
|
+
if (message.type === 'init') {
|
|
104
|
+
canvas = message.canvas;
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (message.type === 'dispose') {
|
|
108
|
+
currentController === null || currentController === void 0 ? void 0 : currentController.abort();
|
|
109
|
+
currentController = null;
|
|
110
|
+
canvas = null;
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
renderWaveform(message);
|
|
114
|
+
});
|
|
@@ -0,0 +1,374 @@
|
|
|
1
|
+
// src/audio-waveform/parse-color.ts
|
|
2
|
+
var colorCache = new Map;
|
|
3
|
+
var parseColor = (color) => {
|
|
4
|
+
const cached = colorCache.get(color);
|
|
5
|
+
if (cached)
|
|
6
|
+
return cached;
|
|
7
|
+
const ctx = new OffscreenCanvas(1, 1).getContext("2d");
|
|
8
|
+
ctx.fillStyle = color;
|
|
9
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
10
|
+
const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
|
|
11
|
+
const result = [r, g, b, a];
|
|
12
|
+
colorCache.set(color, result);
|
|
13
|
+
return result;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
// src/audio-waveform/draw-peaks.ts
|
|
17
|
+
var CLIPPING_COLOR = "#FF7F50";
|
|
18
|
+
var drawBars = ({
|
|
19
|
+
canvas,
|
|
20
|
+
color,
|
|
21
|
+
peaks,
|
|
22
|
+
volume,
|
|
23
|
+
width
|
|
24
|
+
}) => {
|
|
25
|
+
const ctx = canvas.getContext("2d");
|
|
26
|
+
if (!ctx) {
|
|
27
|
+
throw new Error("Failed to get canvas context");
|
|
28
|
+
}
|
|
29
|
+
const { height } = canvas;
|
|
30
|
+
const w = canvas.width;
|
|
31
|
+
if (w === 0 || height === 0) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
ctx.clearRect(0, 0, w, height);
|
|
35
|
+
if (volume === 0)
|
|
36
|
+
return;
|
|
37
|
+
const [r, g, b, a] = parseColor(color);
|
|
38
|
+
const [cr, cg, cb, ca] = parseColor(CLIPPING_COLOR);
|
|
39
|
+
const imageData = ctx.createImageData(w, height);
|
|
40
|
+
const { data } = imageData;
|
|
41
|
+
const numBars = width;
|
|
42
|
+
for (let barIndex = 0;barIndex < numBars; barIndex++) {
|
|
43
|
+
const x = barIndex;
|
|
44
|
+
if (x >= w)
|
|
45
|
+
break;
|
|
46
|
+
const peakIndex = Math.floor(barIndex / numBars * peaks.length);
|
|
47
|
+
const peak = peaks[peakIndex] || 0;
|
|
48
|
+
const scaledPeak = peak * volume;
|
|
49
|
+
const halfBar = Math.max(0, Math.min(height / 2, scaledPeak * height / 2));
|
|
50
|
+
if (halfBar === 0)
|
|
51
|
+
continue;
|
|
52
|
+
const mid = height / 2;
|
|
53
|
+
const barY = Math.round(mid - halfBar);
|
|
54
|
+
const barEnd = Math.round(mid + halfBar);
|
|
55
|
+
const isClipping = scaledPeak > 1;
|
|
56
|
+
const clipTopEnd = isClipping ? Math.min(barY + 2, barEnd) : barY;
|
|
57
|
+
const clipBotStart = isClipping ? Math.max(barEnd - 2, barY) : barEnd;
|
|
58
|
+
for (let y = barY;y < clipTopEnd; y++) {
|
|
59
|
+
const idx = (y * w + x) * 4;
|
|
60
|
+
data[idx] = cr;
|
|
61
|
+
data[idx + 1] = cg;
|
|
62
|
+
data[idx + 2] = cb;
|
|
63
|
+
data[idx + 3] = ca;
|
|
64
|
+
}
|
|
65
|
+
for (let y = clipTopEnd;y < clipBotStart; y++) {
|
|
66
|
+
const idx = (y * w + x) * 4;
|
|
67
|
+
data[idx] = r;
|
|
68
|
+
data[idx + 1] = g;
|
|
69
|
+
data[idx + 2] = b;
|
|
70
|
+
data[idx + 3] = a;
|
|
71
|
+
}
|
|
72
|
+
for (let y = clipBotStart;y < barEnd; y++) {
|
|
73
|
+
const idx = (y * w + x) * 4;
|
|
74
|
+
data[idx] = cr;
|
|
75
|
+
data[idx + 1] = cg;
|
|
76
|
+
data[idx + 2] = cb;
|
|
77
|
+
data[idx + 3] = ca;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
ctx.putImageData(imageData, 0, 0);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// src/audio-waveform/load-waveform-peaks.ts
|
|
84
|
+
import { ALL_FORMATS, AudioSampleSink, Input, UrlSource } from "mediabunny";
|
|
85
|
+
|
|
86
|
+
// src/audio-waveform/constants.ts
|
|
87
|
+
var TARGET_SAMPLE_RATE = 100;
|
|
88
|
+
|
|
89
|
+
// src/audio-waveform/waveform-peak-processor.ts
|
|
90
|
+
var emitWaveformProgress = ({
|
|
91
|
+
completedPeaks,
|
|
92
|
+
final,
|
|
93
|
+
onProgress,
|
|
94
|
+
peaks,
|
|
95
|
+
totalPeaks
|
|
96
|
+
}) => {
|
|
97
|
+
onProgress?.({
|
|
98
|
+
peaks,
|
|
99
|
+
completedPeaks,
|
|
100
|
+
totalPeaks,
|
|
101
|
+
final
|
|
102
|
+
});
|
|
103
|
+
};
|
|
104
|
+
var createWaveformPeakProcessor = ({
|
|
105
|
+
totalPeaks,
|
|
106
|
+
samplesPerPeak,
|
|
107
|
+
onProgress,
|
|
108
|
+
progressIntervalInMs,
|
|
109
|
+
now
|
|
110
|
+
}) => {
|
|
111
|
+
const peaks = new Float32Array(totalPeaks);
|
|
112
|
+
let peakIndex = 0;
|
|
113
|
+
let peakMax = 0;
|
|
114
|
+
let sampleInPeak = 0;
|
|
115
|
+
let lastProgressAt = 0;
|
|
116
|
+
let lastProgressPeak = 0;
|
|
117
|
+
const emitProgress = (force) => {
|
|
118
|
+
const timestamp = now();
|
|
119
|
+
if (!force && peakIndex === lastProgressPeak && sampleInPeak === 0) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (!force && timestamp - lastProgressAt < progressIntervalInMs) {
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
lastProgressAt = timestamp;
|
|
126
|
+
lastProgressPeak = peakIndex;
|
|
127
|
+
emitWaveformProgress({
|
|
128
|
+
peaks,
|
|
129
|
+
completedPeaks: peakIndex,
|
|
130
|
+
totalPeaks,
|
|
131
|
+
final: force,
|
|
132
|
+
onProgress
|
|
133
|
+
});
|
|
134
|
+
};
|
|
135
|
+
return {
|
|
136
|
+
peaks,
|
|
137
|
+
processSampleChunk: (floats, channels) => {
|
|
138
|
+
const frameCount = Math.floor(floats.length / Math.max(1, channels));
|
|
139
|
+
for (let frame = 0;frame < frameCount; frame++) {
|
|
140
|
+
let framePeak = 0;
|
|
141
|
+
for (let channel = 0;channel < channels; channel++) {
|
|
142
|
+
const sampleIndex = frame * channels + channel;
|
|
143
|
+
const abs = Math.abs(floats[sampleIndex] ?? 0);
|
|
144
|
+
if (abs > framePeak) {
|
|
145
|
+
framePeak = abs;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
if (framePeak > peakMax) {
|
|
149
|
+
peakMax = framePeak;
|
|
150
|
+
}
|
|
151
|
+
sampleInPeak++;
|
|
152
|
+
if (sampleInPeak >= samplesPerPeak) {
|
|
153
|
+
if (peakIndex < totalPeaks) {
|
|
154
|
+
peaks[peakIndex] = peakMax;
|
|
155
|
+
}
|
|
156
|
+
peakIndex++;
|
|
157
|
+
peakMax = 0;
|
|
158
|
+
sampleInPeak = 0;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
emitProgress(false);
|
|
162
|
+
},
|
|
163
|
+
finalize: () => {
|
|
164
|
+
if (sampleInPeak > 0 && peakIndex < totalPeaks) {
|
|
165
|
+
peaks[peakIndex] = peakMax;
|
|
166
|
+
peakIndex++;
|
|
167
|
+
}
|
|
168
|
+
emitProgress(true);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
// src/audio-waveform/load-waveform-peaks.ts
|
|
174
|
+
var DEFAULT_PROGRESS_INTERVAL_IN_MS = 50;
|
|
175
|
+
var peaksCache = new Map;
|
|
176
|
+
async function loadWaveformPeaks(url, signal, options) {
|
|
177
|
+
const cached = peaksCache.get(url);
|
|
178
|
+
if (cached) {
|
|
179
|
+
emitWaveformProgress({
|
|
180
|
+
peaks: cached,
|
|
181
|
+
completedPeaks: cached.length,
|
|
182
|
+
totalPeaks: cached.length,
|
|
183
|
+
final: true,
|
|
184
|
+
onProgress: options?.onProgress
|
|
185
|
+
});
|
|
186
|
+
return cached;
|
|
187
|
+
}
|
|
188
|
+
const input = new Input({
|
|
189
|
+
formats: ALL_FORMATS,
|
|
190
|
+
source: new UrlSource(url)
|
|
191
|
+
});
|
|
192
|
+
try {
|
|
193
|
+
const audioTrack = await input.getPrimaryAudioTrack();
|
|
194
|
+
if (!audioTrack) {
|
|
195
|
+
return new Float32Array(0);
|
|
196
|
+
}
|
|
197
|
+
if (await audioTrack.isLive()) {
|
|
198
|
+
throw new Error("Live streams are not currently supported by Remotion. Sorry! Source: " + url);
|
|
199
|
+
}
|
|
200
|
+
if (await audioTrack.isRelativeToUnixEpoch()) {
|
|
201
|
+
throw new Error("Streams with UNIX timestamps are not currently supported by Remotion. Sorry! Source: " + url);
|
|
202
|
+
}
|
|
203
|
+
const sampleRate = await audioTrack.getSampleRate();
|
|
204
|
+
const durationInSeconds = await audioTrack.getDurationFromMetadata({ skipLiveWait: true }) ?? await audioTrack.computeDuration({ skipLiveWait: true });
|
|
205
|
+
const totalPeaks = Math.ceil(durationInSeconds * TARGET_SAMPLE_RATE);
|
|
206
|
+
const samplesPerPeak = Math.max(1, Math.floor(sampleRate / TARGET_SAMPLE_RATE));
|
|
207
|
+
const sink = new AudioSampleSink(audioTrack);
|
|
208
|
+
const processor = createWaveformPeakProcessor({
|
|
209
|
+
totalPeaks,
|
|
210
|
+
samplesPerPeak,
|
|
211
|
+
onProgress: options?.onProgress,
|
|
212
|
+
progressIntervalInMs: options?.progressIntervalInMs ?? DEFAULT_PROGRESS_INTERVAL_IN_MS,
|
|
213
|
+
now: () => Date.now()
|
|
214
|
+
});
|
|
215
|
+
for await (const sample of sink.samples()) {
|
|
216
|
+
if (signal.aborted) {
|
|
217
|
+
sample.close();
|
|
218
|
+
return new Float32Array(0);
|
|
219
|
+
}
|
|
220
|
+
const bytesNeeded = sample.allocationSize({
|
|
221
|
+
format: "f32",
|
|
222
|
+
planeIndex: 0
|
|
223
|
+
});
|
|
224
|
+
const floats = new Float32Array(bytesNeeded / 4);
|
|
225
|
+
sample.copyTo(floats, { format: "f32", planeIndex: 0 });
|
|
226
|
+
const channels = Math.max(1, sample.numberOfChannels);
|
|
227
|
+
sample.close();
|
|
228
|
+
processor.processSampleChunk(floats, channels);
|
|
229
|
+
}
|
|
230
|
+
processor.finalize();
|
|
231
|
+
const { peaks } = processor;
|
|
232
|
+
peaksCache.set(url, peaks);
|
|
233
|
+
return peaks;
|
|
234
|
+
} finally {
|
|
235
|
+
input.dispose();
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// src/audio-waveform/slice-waveform-peaks.ts
|
|
240
|
+
var sliceWaveformPeaks = ({
|
|
241
|
+
durationInFrames,
|
|
242
|
+
fps,
|
|
243
|
+
peaks,
|
|
244
|
+
playbackRate,
|
|
245
|
+
startFrom
|
|
246
|
+
}) => {
|
|
247
|
+
if (peaks.length === 0) {
|
|
248
|
+
return peaks;
|
|
249
|
+
}
|
|
250
|
+
const startTimeInSeconds = startFrom / fps;
|
|
251
|
+
const durationInSeconds = durationInFrames / fps * playbackRate;
|
|
252
|
+
const startPeakIndex = Math.floor(startTimeInSeconds * TARGET_SAMPLE_RATE);
|
|
253
|
+
const endPeakIndex = Math.ceil((startTimeInSeconds + durationInSeconds) * TARGET_SAMPLE_RATE);
|
|
254
|
+
return peaks.subarray(Math.max(0, startPeakIndex), Math.min(peaks.length, endPeakIndex));
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// src/loop-display.ts
|
|
258
|
+
var shouldTileLoopDisplay = (loopDisplay) => {
|
|
259
|
+
return loopDisplay !== undefined && loopDisplay.numberOfTimes > 1;
|
|
260
|
+
};
|
|
261
|
+
var getLoopDisplayWidth = ({
|
|
262
|
+
visualizationWidth,
|
|
263
|
+
loopDisplay
|
|
264
|
+
}) => {
|
|
265
|
+
if (!shouldTileLoopDisplay(loopDisplay)) {
|
|
266
|
+
return visualizationWidth;
|
|
267
|
+
}
|
|
268
|
+
return visualizationWidth / loopDisplay.numberOfTimes;
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
// src/audio-waveform-worker.ts
|
|
272
|
+
var canvas = null;
|
|
273
|
+
var currentController = null;
|
|
274
|
+
var latestRequestId = 0;
|
|
275
|
+
var postError = (requestId, error) => {
|
|
276
|
+
const message = error instanceof Error ? error.message : "Failed to render waveform";
|
|
277
|
+
const payload = {
|
|
278
|
+
type: "error",
|
|
279
|
+
requestId,
|
|
280
|
+
message
|
|
281
|
+
};
|
|
282
|
+
self.postMessage(payload);
|
|
283
|
+
};
|
|
284
|
+
var drawPartialWaveform = (message, peaks) => {
|
|
285
|
+
if (!canvas) {
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const portionPeaks = sliceWaveformPeaks({
|
|
289
|
+
durationInFrames: shouldTileLoopDisplay(message.loopDisplay) ? message.loopDisplay.durationInFrames : message.durationInFrames,
|
|
290
|
+
fps: message.fps,
|
|
291
|
+
peaks,
|
|
292
|
+
playbackRate: message.playbackRate,
|
|
293
|
+
startFrom: message.startFrom
|
|
294
|
+
});
|
|
295
|
+
if (!shouldTileLoopDisplay(message.loopDisplay)) {
|
|
296
|
+
drawBars({
|
|
297
|
+
canvas,
|
|
298
|
+
peaks: portionPeaks,
|
|
299
|
+
color: "rgba(255, 255, 255, 0.6)",
|
|
300
|
+
volume: message.volume,
|
|
301
|
+
width: message.width
|
|
302
|
+
});
|
|
303
|
+
return;
|
|
304
|
+
}
|
|
305
|
+
const loopWidth = getLoopDisplayWidth({
|
|
306
|
+
visualizationWidth: message.width,
|
|
307
|
+
loopDisplay: message.loopDisplay
|
|
308
|
+
});
|
|
309
|
+
const targetCanvas = new OffscreenCanvas(Math.max(1, Math.ceil(loopWidth)), message.height);
|
|
310
|
+
drawBars({
|
|
311
|
+
canvas: targetCanvas,
|
|
312
|
+
peaks: portionPeaks,
|
|
313
|
+
color: "rgba(255, 255, 255, 0.6)",
|
|
314
|
+
volume: message.volume,
|
|
315
|
+
width: targetCanvas.width
|
|
316
|
+
});
|
|
317
|
+
const ctx = canvas.getContext("2d");
|
|
318
|
+
if (!ctx) {
|
|
319
|
+
throw new Error("Failed to get canvas context");
|
|
320
|
+
}
|
|
321
|
+
const pattern = ctx.createPattern(targetCanvas, "repeat-x");
|
|
322
|
+
if (!pattern) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
pattern.setTransform(new DOMMatrix().scaleSelf(loopWidth / targetCanvas.width, 1));
|
|
326
|
+
ctx.clearRect(0, 0, message.width, message.height);
|
|
327
|
+
ctx.fillStyle = pattern;
|
|
328
|
+
ctx.fillRect(0, 0, message.width, message.height);
|
|
329
|
+
};
|
|
330
|
+
var renderWaveform = async (message) => {
|
|
331
|
+
if (!canvas) {
|
|
332
|
+
postError(message.requestId, new Error("Waveform canvas not initialized"));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
const controller = new AbortController;
|
|
336
|
+
currentController?.abort();
|
|
337
|
+
currentController = controller;
|
|
338
|
+
latestRequestId = message.requestId;
|
|
339
|
+
try {
|
|
340
|
+
canvas.width = message.width;
|
|
341
|
+
canvas.height = message.height;
|
|
342
|
+
const peaks = await loadWaveformPeaks(message.src, controller.signal, {
|
|
343
|
+
onProgress: ({ peaks: nextPeaks }) => {
|
|
344
|
+
if (controller.signal.aborted || latestRequestId !== message.requestId) {
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
drawPartialWaveform(message, nextPeaks);
|
|
348
|
+
}
|
|
349
|
+
});
|
|
350
|
+
if (controller.signal.aborted || latestRequestId !== message.requestId) {
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
drawPartialWaveform(message, peaks);
|
|
354
|
+
} catch (error) {
|
|
355
|
+
if (controller.signal.aborted || latestRequestId !== message.requestId) {
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
postError(message.requestId, error);
|
|
359
|
+
}
|
|
360
|
+
};
|
|
361
|
+
self.addEventListener("message", (event) => {
|
|
362
|
+
const message = event.data;
|
|
363
|
+
if (message.type === "init") {
|
|
364
|
+
canvas = message.canvas;
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
if (message.type === "dispose") {
|
|
368
|
+
currentController?.abort();
|
|
369
|
+
currentController = null;
|
|
370
|
+
canvas = null;
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
renderWaveform(message);
|
|
374
|
+
});
|
package/dist/esm/index.mjs
CHANGED
|
@@ -1,9 +1,267 @@
|
|
|
1
|
+
// src/audio-waveform/constants.ts
|
|
2
|
+
var TARGET_SAMPLE_RATE = 100;
|
|
3
|
+
// src/audio-waveform/parse-color.ts
|
|
4
|
+
var colorCache = new Map;
|
|
5
|
+
var parseColor = (color) => {
|
|
6
|
+
const cached = colorCache.get(color);
|
|
7
|
+
if (cached)
|
|
8
|
+
return cached;
|
|
9
|
+
const ctx = new OffscreenCanvas(1, 1).getContext("2d");
|
|
10
|
+
ctx.fillStyle = color;
|
|
11
|
+
ctx.fillRect(0, 0, 1, 1);
|
|
12
|
+
const [r, g, b, a] = ctx.getImageData(0, 0, 1, 1).data;
|
|
13
|
+
const result = [r, g, b, a];
|
|
14
|
+
colorCache.set(color, result);
|
|
15
|
+
return result;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/audio-waveform/draw-peaks.ts
|
|
19
|
+
var CLIPPING_COLOR = "#FF7F50";
|
|
20
|
+
var drawBars = ({
|
|
21
|
+
canvas,
|
|
22
|
+
color,
|
|
23
|
+
peaks,
|
|
24
|
+
volume,
|
|
25
|
+
width
|
|
26
|
+
}) => {
|
|
27
|
+
const ctx = canvas.getContext("2d");
|
|
28
|
+
if (!ctx) {
|
|
29
|
+
throw new Error("Failed to get canvas context");
|
|
30
|
+
}
|
|
31
|
+
const { height } = canvas;
|
|
32
|
+
const w = canvas.width;
|
|
33
|
+
if (w === 0 || height === 0) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
ctx.clearRect(0, 0, w, height);
|
|
37
|
+
if (volume === 0)
|
|
38
|
+
return;
|
|
39
|
+
const [r, g, b, a] = parseColor(color);
|
|
40
|
+
const [cr, cg, cb, ca] = parseColor(CLIPPING_COLOR);
|
|
41
|
+
const imageData = ctx.createImageData(w, height);
|
|
42
|
+
const { data } = imageData;
|
|
43
|
+
const numBars = width;
|
|
44
|
+
for (let barIndex = 0;barIndex < numBars; barIndex++) {
|
|
45
|
+
const x = barIndex;
|
|
46
|
+
if (x >= w)
|
|
47
|
+
break;
|
|
48
|
+
const peakIndex = Math.floor(barIndex / numBars * peaks.length);
|
|
49
|
+
const peak = peaks[peakIndex] || 0;
|
|
50
|
+
const scaledPeak = peak * volume;
|
|
51
|
+
const halfBar = Math.max(0, Math.min(height / 2, scaledPeak * height / 2));
|
|
52
|
+
if (halfBar === 0)
|
|
53
|
+
continue;
|
|
54
|
+
const mid = height / 2;
|
|
55
|
+
const barY = Math.round(mid - halfBar);
|
|
56
|
+
const barEnd = Math.round(mid + halfBar);
|
|
57
|
+
const isClipping = scaledPeak > 1;
|
|
58
|
+
const clipTopEnd = isClipping ? Math.min(barY + 2, barEnd) : barY;
|
|
59
|
+
const clipBotStart = isClipping ? Math.max(barEnd - 2, barY) : barEnd;
|
|
60
|
+
for (let y = barY;y < clipTopEnd; y++) {
|
|
61
|
+
const idx = (y * w + x) * 4;
|
|
62
|
+
data[idx] = cr;
|
|
63
|
+
data[idx + 1] = cg;
|
|
64
|
+
data[idx + 2] = cb;
|
|
65
|
+
data[idx + 3] = ca;
|
|
66
|
+
}
|
|
67
|
+
for (let y = clipTopEnd;y < clipBotStart; y++) {
|
|
68
|
+
const idx = (y * w + x) * 4;
|
|
69
|
+
data[idx] = r;
|
|
70
|
+
data[idx + 1] = g;
|
|
71
|
+
data[idx + 2] = b;
|
|
72
|
+
data[idx + 3] = a;
|
|
73
|
+
}
|
|
74
|
+
for (let y = clipBotStart;y < barEnd; y++) {
|
|
75
|
+
const idx = (y * w + x) * 4;
|
|
76
|
+
data[idx] = cr;
|
|
77
|
+
data[idx + 1] = cg;
|
|
78
|
+
data[idx + 2] = cb;
|
|
79
|
+
data[idx + 3] = ca;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
ctx.putImageData(imageData, 0, 0);
|
|
83
|
+
};
|
|
84
|
+
// src/audio-waveform/load-waveform-peaks.ts
|
|
85
|
+
import { ALL_FORMATS, AudioSampleSink, Input, UrlSource } from "mediabunny";
|
|
86
|
+
|
|
87
|
+
// src/audio-waveform/waveform-peak-processor.ts
|
|
88
|
+
var emitWaveformProgress = ({
|
|
89
|
+
completedPeaks,
|
|
90
|
+
final,
|
|
91
|
+
onProgress,
|
|
92
|
+
peaks,
|
|
93
|
+
totalPeaks
|
|
94
|
+
}) => {
|
|
95
|
+
onProgress?.({
|
|
96
|
+
peaks,
|
|
97
|
+
completedPeaks,
|
|
98
|
+
totalPeaks,
|
|
99
|
+
final
|
|
100
|
+
});
|
|
101
|
+
};
|
|
102
|
+
var createWaveformPeakProcessor = ({
|
|
103
|
+
totalPeaks,
|
|
104
|
+
samplesPerPeak,
|
|
105
|
+
onProgress,
|
|
106
|
+
progressIntervalInMs,
|
|
107
|
+
now
|
|
108
|
+
}) => {
|
|
109
|
+
const peaks = new Float32Array(totalPeaks);
|
|
110
|
+
let peakIndex = 0;
|
|
111
|
+
let peakMax = 0;
|
|
112
|
+
let sampleInPeak = 0;
|
|
113
|
+
let lastProgressAt = 0;
|
|
114
|
+
let lastProgressPeak = 0;
|
|
115
|
+
const emitProgress = (force) => {
|
|
116
|
+
const timestamp = now();
|
|
117
|
+
if (!force && peakIndex === lastProgressPeak && sampleInPeak === 0) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (!force && timestamp - lastProgressAt < progressIntervalInMs) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
lastProgressAt = timestamp;
|
|
124
|
+
lastProgressPeak = peakIndex;
|
|
125
|
+
emitWaveformProgress({
|
|
126
|
+
peaks,
|
|
127
|
+
completedPeaks: peakIndex,
|
|
128
|
+
totalPeaks,
|
|
129
|
+
final: force,
|
|
130
|
+
onProgress
|
|
131
|
+
});
|
|
132
|
+
};
|
|
133
|
+
return {
|
|
134
|
+
peaks,
|
|
135
|
+
processSampleChunk: (floats, channels) => {
|
|
136
|
+
const frameCount = Math.floor(floats.length / Math.max(1, channels));
|
|
137
|
+
for (let frame = 0;frame < frameCount; frame++) {
|
|
138
|
+
let framePeak = 0;
|
|
139
|
+
for (let channel = 0;channel < channels; channel++) {
|
|
140
|
+
const sampleIndex = frame * channels + channel;
|
|
141
|
+
const abs = Math.abs(floats[sampleIndex] ?? 0);
|
|
142
|
+
if (abs > framePeak) {
|
|
143
|
+
framePeak = abs;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
if (framePeak > peakMax) {
|
|
147
|
+
peakMax = framePeak;
|
|
148
|
+
}
|
|
149
|
+
sampleInPeak++;
|
|
150
|
+
if (sampleInPeak >= samplesPerPeak) {
|
|
151
|
+
if (peakIndex < totalPeaks) {
|
|
152
|
+
peaks[peakIndex] = peakMax;
|
|
153
|
+
}
|
|
154
|
+
peakIndex++;
|
|
155
|
+
peakMax = 0;
|
|
156
|
+
sampleInPeak = 0;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
emitProgress(false);
|
|
160
|
+
},
|
|
161
|
+
finalize: () => {
|
|
162
|
+
if (sampleInPeak > 0 && peakIndex < totalPeaks) {
|
|
163
|
+
peaks[peakIndex] = peakMax;
|
|
164
|
+
peakIndex++;
|
|
165
|
+
}
|
|
166
|
+
emitProgress(true);
|
|
167
|
+
}
|
|
168
|
+
};
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// src/audio-waveform/load-waveform-peaks.ts
|
|
172
|
+
var DEFAULT_PROGRESS_INTERVAL_IN_MS = 50;
|
|
173
|
+
var peaksCache = new Map;
|
|
174
|
+
async function loadWaveformPeaks(url, signal, options) {
|
|
175
|
+
const cached = peaksCache.get(url);
|
|
176
|
+
if (cached) {
|
|
177
|
+
emitWaveformProgress({
|
|
178
|
+
peaks: cached,
|
|
179
|
+
completedPeaks: cached.length,
|
|
180
|
+
totalPeaks: cached.length,
|
|
181
|
+
final: true,
|
|
182
|
+
onProgress: options?.onProgress
|
|
183
|
+
});
|
|
184
|
+
return cached;
|
|
185
|
+
}
|
|
186
|
+
const input = new Input({
|
|
187
|
+
formats: ALL_FORMATS,
|
|
188
|
+
source: new UrlSource(url)
|
|
189
|
+
});
|
|
190
|
+
try {
|
|
191
|
+
const audioTrack = await input.getPrimaryAudioTrack();
|
|
192
|
+
if (!audioTrack) {
|
|
193
|
+
return new Float32Array(0);
|
|
194
|
+
}
|
|
195
|
+
if (await audioTrack.isLive()) {
|
|
196
|
+
throw new Error("Live streams are not currently supported by Remotion. Sorry! Source: " + url);
|
|
197
|
+
}
|
|
198
|
+
if (await audioTrack.isRelativeToUnixEpoch()) {
|
|
199
|
+
throw new Error("Streams with UNIX timestamps are not currently supported by Remotion. Sorry! Source: " + url);
|
|
200
|
+
}
|
|
201
|
+
const sampleRate = await audioTrack.getSampleRate();
|
|
202
|
+
const durationInSeconds = await audioTrack.getDurationFromMetadata({ skipLiveWait: true }) ?? await audioTrack.computeDuration({ skipLiveWait: true });
|
|
203
|
+
const totalPeaks = Math.ceil(durationInSeconds * TARGET_SAMPLE_RATE);
|
|
204
|
+
const samplesPerPeak = Math.max(1, Math.floor(sampleRate / TARGET_SAMPLE_RATE));
|
|
205
|
+
const sink = new AudioSampleSink(audioTrack);
|
|
206
|
+
const processor = createWaveformPeakProcessor({
|
|
207
|
+
totalPeaks,
|
|
208
|
+
samplesPerPeak,
|
|
209
|
+
onProgress: options?.onProgress,
|
|
210
|
+
progressIntervalInMs: options?.progressIntervalInMs ?? DEFAULT_PROGRESS_INTERVAL_IN_MS,
|
|
211
|
+
now: () => Date.now()
|
|
212
|
+
});
|
|
213
|
+
for await (const sample of sink.samples()) {
|
|
214
|
+
if (signal.aborted) {
|
|
215
|
+
sample.close();
|
|
216
|
+
return new Float32Array(0);
|
|
217
|
+
}
|
|
218
|
+
const bytesNeeded = sample.allocationSize({
|
|
219
|
+
format: "f32",
|
|
220
|
+
planeIndex: 0
|
|
221
|
+
});
|
|
222
|
+
const floats = new Float32Array(bytesNeeded / 4);
|
|
223
|
+
sample.copyTo(floats, { format: "f32", planeIndex: 0 });
|
|
224
|
+
const channels = Math.max(1, sample.numberOfChannels);
|
|
225
|
+
sample.close();
|
|
226
|
+
processor.processSampleChunk(floats, channels);
|
|
227
|
+
}
|
|
228
|
+
processor.finalize();
|
|
229
|
+
const { peaks } = processor;
|
|
230
|
+
peaksCache.set(url, peaks);
|
|
231
|
+
return peaks;
|
|
232
|
+
} finally {
|
|
233
|
+
input.dispose();
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
// src/audio-waveform/make-audio-waveform-worker.ts
|
|
237
|
+
var makeAudioWaveformWorker = () => {
|
|
238
|
+
return new Worker(new URL("./audio-waveform-worker.mjs", import.meta.url), {
|
|
239
|
+
type: "module"
|
|
240
|
+
});
|
|
241
|
+
};
|
|
242
|
+
// src/audio-waveform/slice-waveform-peaks.ts
|
|
243
|
+
var sliceWaveformPeaks = ({
|
|
244
|
+
durationInFrames,
|
|
245
|
+
fps,
|
|
246
|
+
peaks,
|
|
247
|
+
playbackRate,
|
|
248
|
+
startFrom
|
|
249
|
+
}) => {
|
|
250
|
+
if (peaks.length === 0) {
|
|
251
|
+
return peaks;
|
|
252
|
+
}
|
|
253
|
+
const startTimeInSeconds = startFrom / fps;
|
|
254
|
+
const durationInSeconds = durationInFrames / fps * playbackRate;
|
|
255
|
+
const startPeakIndex = Math.floor(startTimeInSeconds * TARGET_SAMPLE_RATE);
|
|
256
|
+
const endPeakIndex = Math.ceil((startTimeInSeconds + durationInSeconds) * TARGET_SAMPLE_RATE);
|
|
257
|
+
return peaks.subarray(Math.max(0, startPeakIndex), Math.min(peaks.length, endPeakIndex));
|
|
258
|
+
};
|
|
1
259
|
// src/extract-frames.ts
|
|
2
260
|
import {
|
|
3
|
-
ALL_FORMATS,
|
|
4
|
-
Input,
|
|
261
|
+
ALL_FORMATS as ALL_FORMATS2,
|
|
262
|
+
Input as Input2,
|
|
5
263
|
InputDisposedError,
|
|
6
|
-
UrlSource,
|
|
264
|
+
UrlSource as UrlSource2,
|
|
7
265
|
VideoSampleSink
|
|
8
266
|
} from "mediabunny";
|
|
9
267
|
|
|
@@ -21,9 +279,9 @@ async function extractFrames({
|
|
|
21
279
|
onVideoSample,
|
|
22
280
|
signal
|
|
23
281
|
}) {
|
|
24
|
-
const input = new
|
|
25
|
-
formats:
|
|
26
|
-
source: new
|
|
282
|
+
const input = new Input2({
|
|
283
|
+
formats: ALL_FORMATS2,
|
|
284
|
+
source: new UrlSource2(src)
|
|
27
285
|
});
|
|
28
286
|
const dispose = () => {
|
|
29
287
|
input.dispose();
|
|
@@ -368,9 +626,12 @@ var resizeVideoFrame = ({
|
|
|
368
626
|
});
|
|
369
627
|
};
|
|
370
628
|
export {
|
|
629
|
+
sliceWaveformPeaks,
|
|
371
630
|
shouldTileLoopDisplay,
|
|
372
631
|
resizeVideoFrame,
|
|
373
632
|
makeFrameDatabaseKey,
|
|
633
|
+
makeAudioWaveformWorker,
|
|
634
|
+
loadWaveformPeaks,
|
|
374
635
|
getTimestampFromFrameDatabaseKey,
|
|
375
636
|
getLoopDisplayWidth,
|
|
376
637
|
getFrameDatabaseKeyPrefix,
|
|
@@ -381,9 +642,13 @@ export {
|
|
|
381
642
|
fillFrameWhereItFits,
|
|
382
643
|
extractFrames,
|
|
383
644
|
ensureSlots,
|
|
645
|
+
emitWaveformProgress,
|
|
384
646
|
drawSlot,
|
|
647
|
+
drawBars,
|
|
648
|
+
createWaveformPeakProcessor,
|
|
385
649
|
calculateTimestampSlots,
|
|
386
650
|
aspectRatioCache,
|
|
387
651
|
addFrameToCache,
|
|
388
|
-
WEBCODECS_TIMESCALE
|
|
652
|
+
WEBCODECS_TIMESCALE,
|
|
653
|
+
TARGET_SAMPLE_RATE
|
|
389
654
|
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
export type { AudioWaveformWorkerIncomingMessage, AudioWaveformWorkerOutgoingMessage, AudioWaveformWorkerRenderMessage, } from './audio-waveform/audio-waveform-worker-types';
|
|
2
|
+
export { TARGET_SAMPLE_RATE } from './audio-waveform/constants';
|
|
3
|
+
export { drawBars } from './audio-waveform/draw-peaks';
|
|
4
|
+
export { loadWaveformPeaks } from './audio-waveform/load-waveform-peaks';
|
|
5
|
+
export { makeAudioWaveformWorker } from './audio-waveform/make-audio-waveform-worker';
|
|
6
|
+
export { sliceWaveformPeaks } from './audio-waveform/slice-waveform-peaks';
|
|
7
|
+
export { createWaveformPeakProcessor, emitWaveformProgress, } from './audio-waveform/waveform-peak-processor';
|
|
1
8
|
export { extractFrames } from './extract-frames';
|
|
2
9
|
export type { ExtractFramesProps, ExtractFramesTimestampsInSecondsFn, } from './extract-frames';
|
|
3
10
|
export { addFrameToCache, aspectRatioCache, frameDatabase, getAspectRatioFromCache, getFrameDatabaseKeyPrefix, getTimestampFromFrameDatabaseKey, makeFrameDatabaseKey, } from './frame-database';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.resizeVideoFrame = exports.WEBCODECS_TIMESCALE = exports.getDurationOfOneFrame = exports.fillWithCachedFrames = exports.fillFrameWhereItFits = exports.ensureSlots = exports.drawSlot = exports.calculateTimestampSlots = exports.shouldTileLoopDisplay = exports.getLoopDisplayWidth = exports.makeFrameDatabaseKey = exports.getTimestampFromFrameDatabaseKey = exports.getFrameDatabaseKeyPrefix = exports.getAspectRatioFromCache = exports.frameDatabase = exports.aspectRatioCache = exports.addFrameToCache = exports.extractFrames = void 0;
|
|
3
|
+
exports.resizeVideoFrame = exports.WEBCODECS_TIMESCALE = exports.getDurationOfOneFrame = exports.fillWithCachedFrames = exports.fillFrameWhereItFits = exports.ensureSlots = exports.drawSlot = exports.calculateTimestampSlots = exports.shouldTileLoopDisplay = exports.getLoopDisplayWidth = exports.makeFrameDatabaseKey = exports.getTimestampFromFrameDatabaseKey = exports.getFrameDatabaseKeyPrefix = exports.getAspectRatioFromCache = exports.frameDatabase = exports.aspectRatioCache = exports.addFrameToCache = exports.extractFrames = exports.emitWaveformProgress = exports.createWaveformPeakProcessor = exports.sliceWaveformPeaks = exports.makeAudioWaveformWorker = exports.loadWaveformPeaks = exports.drawBars = exports.TARGET_SAMPLE_RATE = void 0;
|
|
4
|
+
const constants_1 = require("./audio-waveform/constants");
|
|
5
|
+
Object.defineProperty(exports, "TARGET_SAMPLE_RATE", { enumerable: true, get: function () { return constants_1.TARGET_SAMPLE_RATE; } });
|
|
6
|
+
const draw_peaks_1 = require("./audio-waveform/draw-peaks");
|
|
7
|
+
Object.defineProperty(exports, "drawBars", { enumerable: true, get: function () { return draw_peaks_1.drawBars; } });
|
|
8
|
+
const load_waveform_peaks_1 = require("./audio-waveform/load-waveform-peaks");
|
|
9
|
+
Object.defineProperty(exports, "loadWaveformPeaks", { enumerable: true, get: function () { return load_waveform_peaks_1.loadWaveformPeaks; } });
|
|
10
|
+
const make_audio_waveform_worker_1 = require("./audio-waveform/make-audio-waveform-worker");
|
|
11
|
+
Object.defineProperty(exports, "makeAudioWaveformWorker", { enumerable: true, get: function () { return make_audio_waveform_worker_1.makeAudioWaveformWorker; } });
|
|
12
|
+
const slice_waveform_peaks_1 = require("./audio-waveform/slice-waveform-peaks");
|
|
13
|
+
Object.defineProperty(exports, "sliceWaveformPeaks", { enumerable: true, get: function () { return slice_waveform_peaks_1.sliceWaveformPeaks; } });
|
|
14
|
+
const waveform_peak_processor_1 = require("./audio-waveform/waveform-peak-processor");
|
|
15
|
+
Object.defineProperty(exports, "createWaveformPeakProcessor", { enumerable: true, get: function () { return waveform_peak_processor_1.createWaveformPeakProcessor; } });
|
|
16
|
+
Object.defineProperty(exports, "emitWaveformProgress", { enumerable: true, get: function () { return waveform_peak_processor_1.emitWaveformProgress; } });
|
|
4
17
|
const extract_frames_1 = require("./extract-frames");
|
|
5
18
|
Object.defineProperty(exports, "extractFrames", { enumerable: true, get: function () { return extract_frames_1.extractFrames; } });
|
|
6
19
|
const frame_database_1 = require("./frame-database");
|
package/package.json
CHANGED
|
@@ -3,15 +3,15 @@
|
|
|
3
3
|
"url": "https://github.com/remotion-dev/remotion/tree/main/packages/timeline-utils"
|
|
4
4
|
},
|
|
5
5
|
"name": "@remotion/timeline-utils",
|
|
6
|
-
"version": "4.0.
|
|
6
|
+
"version": "4.0.462",
|
|
7
7
|
"description": "Internal utilities for rendering Remotion timelines",
|
|
8
8
|
"main": "dist/index.js",
|
|
9
|
-
"sideEffects": false,
|
|
10
9
|
"scripts": {
|
|
11
10
|
"formatting": "oxfmt src --check",
|
|
12
11
|
"format": "oxfmt src",
|
|
13
12
|
"lint": "eslint src",
|
|
14
|
-
"make": "tsgo -d && bun --env-file=../.env.bundle bundle.ts"
|
|
13
|
+
"make": "tsgo -d && bun --env-file=../.env.bundle bundle.ts",
|
|
14
|
+
"test": "bun test src"
|
|
15
15
|
},
|
|
16
16
|
"types": "dist/index.d.ts",
|
|
17
17
|
"module": "dist/esm/index.mjs",
|
|
@@ -21,10 +21,11 @@
|
|
|
21
21
|
"url": "https://github.com/remotion-dev/remotion/issues"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"mediabunny": "1.
|
|
24
|
+
"mediabunny": "1.45.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
|
-
"@
|
|
27
|
+
"@mediabunny/server": "1.45.0",
|
|
28
|
+
"@remotion/eslint-config-internal": "4.0.462",
|
|
28
29
|
"eslint": "9.19.0",
|
|
29
30
|
"@typescript/native-preview": "7.0.0-dev.20260217.1"
|
|
30
31
|
},
|
|
@@ -38,6 +39,19 @@
|
|
|
38
39
|
"module": "./dist/esm/index.mjs",
|
|
39
40
|
"import": "./dist/esm/index.mjs",
|
|
40
41
|
"require": "./dist/index.js"
|
|
42
|
+
},
|
|
43
|
+
"./audio-waveform-worker": {
|
|
44
|
+
"types": "./dist/audio-waveform-worker.d.ts",
|
|
45
|
+
"module": "./dist/esm/audio-waveform-worker.mjs",
|
|
46
|
+
"import": "./dist/esm/audio-waveform-worker.mjs",
|
|
47
|
+
"require": "./dist/audio-waveform-worker.js"
|
|
48
|
+
}
|
|
49
|
+
},
|
|
50
|
+
"typesVersions": {
|
|
51
|
+
">=1.0": {
|
|
52
|
+
"audio-waveform-worker": [
|
|
53
|
+
"./dist/audio-waveform-worker.d.ts"
|
|
54
|
+
]
|
|
41
55
|
}
|
|
42
56
|
}
|
|
43
57
|
}
|