@remotion/webcodecs 4.0.327 → 4.0.330
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/esm/index.mjs +201 -17
- package/dist/extract-frames.d.ts +2 -1
- package/dist/extract-frames.js +47 -8
- package/dist/get-partial-audio-data.d.ts +8 -0
- package/dist/get-partial-audio-data.js +156 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/on-frame.d.ts +1 -1
- package/dist/on-frame.js +3 -3
- package/dist/reencode-video-track.js +2 -2
- package/dist/rotate-and-resize-video-frame.js +19 -8
- package/package.json +5 -5
package/dist/esm/index.mjs
CHANGED
|
@@ -366,22 +366,32 @@ var rotateAndResizeVideoFrame = ({
|
|
|
366
366
|
resizeOperation
|
|
367
367
|
}) => {
|
|
368
368
|
const normalized = normalizeVideoRotation(rotation);
|
|
369
|
-
|
|
369
|
+
const mustProcess = "rotation" in frame && frame.rotation !== 0;
|
|
370
|
+
if (normalized === 0 && resizeOperation === null && !mustProcess) {
|
|
370
371
|
return frame;
|
|
371
372
|
}
|
|
372
373
|
if (normalized % 90 !== 0) {
|
|
373
374
|
throw new Error("Only 90 degree rotations are supported");
|
|
374
375
|
}
|
|
375
|
-
const
|
|
376
|
+
const tentativeDimensions = calculateNewDimensionsFromRotateAndScale({
|
|
376
377
|
height: frame.displayHeight,
|
|
377
378
|
width: frame.displayWidth,
|
|
378
379
|
rotation,
|
|
379
380
|
needsToBeMultipleOfTwo,
|
|
380
381
|
resizeOperation
|
|
381
382
|
});
|
|
382
|
-
if (normalized === 0 && height === frame.displayHeight && width === frame.displayWidth) {
|
|
383
|
+
if (normalized === 0 && tentativeDimensions.height === frame.displayHeight && tentativeDimensions.width === frame.displayWidth && !mustProcess) {
|
|
383
384
|
return frame;
|
|
384
385
|
}
|
|
386
|
+
const frameRotation = frame.rotation ?? 0;
|
|
387
|
+
const canvasRotationToApply = normalizeVideoRotation(normalized - frameRotation);
|
|
388
|
+
const { width, height } = calculateNewDimensionsFromRotateAndScale({
|
|
389
|
+
height: frame.displayHeight,
|
|
390
|
+
width: frame.displayWidth,
|
|
391
|
+
rotation: canvasRotationToApply,
|
|
392
|
+
needsToBeMultipleOfTwo,
|
|
393
|
+
resizeOperation
|
|
394
|
+
});
|
|
385
395
|
const canvas = new OffscreenCanvas(width, height);
|
|
386
396
|
const ctx = canvas.getContext("2d");
|
|
387
397
|
if (!ctx) {
|
|
@@ -389,8 +399,6 @@ var rotateAndResizeVideoFrame = ({
|
|
|
389
399
|
}
|
|
390
400
|
canvas.width = width;
|
|
391
401
|
canvas.height = height;
|
|
392
|
-
const frameRotation = frame.rotation ?? 0;
|
|
393
|
-
const canvasRotationToApply = normalizeVideoRotation(normalized + frameRotation);
|
|
394
402
|
if (canvasRotationToApply === 90) {
|
|
395
403
|
ctx.translate(width, 0);
|
|
396
404
|
} else if (canvasRotationToApply === 180) {
|
|
@@ -404,7 +412,7 @@ var rotateAndResizeVideoFrame = ({
|
|
|
404
412
|
if (frame.displayHeight !== height || frame.displayWidth !== width) {
|
|
405
413
|
const dimensionsAfterRotate = calculateNewDimensionsFromRotate({
|
|
406
414
|
height: frame.displayHeight,
|
|
407
|
-
rotation,
|
|
415
|
+
rotation: canvasRotationToApply,
|
|
408
416
|
width: frame.displayWidth
|
|
409
417
|
});
|
|
410
418
|
ctx.scale(width / dimensionsAfterRotate.width, height / dimensionsAfterRotate.height);
|
|
@@ -4874,7 +4882,7 @@ var convertToCorrectVideoFrame = ({
|
|
|
4874
4882
|
};
|
|
4875
4883
|
|
|
4876
4884
|
// src/on-frame.ts
|
|
4877
|
-
var
|
|
4885
|
+
var processFrame = async ({
|
|
4878
4886
|
frame: unrotatedFrame,
|
|
4879
4887
|
onVideoFrame,
|
|
4880
4888
|
track,
|
|
@@ -5069,7 +5077,7 @@ var reencodeVideoTrack = async ({
|
|
|
5069
5077
|
if (videoOperation.type !== "reencode") {
|
|
5070
5078
|
throw new Error(`Video track with ID ${track.trackId} could not be resolved with a valid operation. Received ${JSON.stringify(videoOperation)}, but must be either "copy", "reencode", "drop" or "fail"`);
|
|
5071
5079
|
}
|
|
5072
|
-
const rotation = (videoOperation.rotate ?? rotate)
|
|
5080
|
+
const rotation = (videoOperation.rotate ?? rotate) + track.rotation;
|
|
5073
5081
|
const { height: newHeight, width: newWidth } = calculateNewDimensionsFromRotateAndScale({
|
|
5074
5082
|
width: track.codedWidth,
|
|
5075
5083
|
height: track.codedHeight,
|
|
@@ -5141,7 +5149,7 @@ var reencodeVideoTrack = async ({
|
|
|
5141
5149
|
},
|
|
5142
5150
|
onOutput: async (frame) => {
|
|
5143
5151
|
await controller._internals._mediaParserController._internals.checkForAbortAndPause();
|
|
5144
|
-
const processedFrame = await
|
|
5152
|
+
const processedFrame = await processFrame({
|
|
5145
5153
|
frame,
|
|
5146
5154
|
track,
|
|
5147
5155
|
onVideoFrame,
|
|
@@ -5594,7 +5602,7 @@ import {
|
|
|
5594
5602
|
import { parseMediaOnWebWorker } from "@remotion/media-parser/worker";
|
|
5595
5603
|
var internalExtractFrames = ({
|
|
5596
5604
|
src,
|
|
5597
|
-
onFrame
|
|
5605
|
+
onFrame,
|
|
5598
5606
|
signal,
|
|
5599
5607
|
timestampsInSeconds,
|
|
5600
5608
|
acknowledgeRemotionLicense,
|
|
@@ -5609,6 +5617,8 @@ var internalExtractFrames = ({
|
|
|
5609
5617
|
};
|
|
5610
5618
|
signal?.addEventListener("abort", abortListener, { once: true });
|
|
5611
5619
|
let dur = null;
|
|
5620
|
+
let lastFrame;
|
|
5621
|
+
let lastFrameEmitted;
|
|
5612
5622
|
parseMediaOnWebWorker({
|
|
5613
5623
|
src: new URL(src, window.location.href),
|
|
5614
5624
|
acknowledgeRemotionLicense,
|
|
@@ -5617,21 +5627,42 @@ var internalExtractFrames = ({
|
|
|
5617
5627
|
onDurationInSeconds(durationInSeconds) {
|
|
5618
5628
|
dur = durationInSeconds;
|
|
5619
5629
|
},
|
|
5620
|
-
onVideoTrack: async ({ track }) => {
|
|
5630
|
+
onVideoTrack: async ({ track, container }) => {
|
|
5621
5631
|
const timestampTargetsUnsorted = typeof timestampsInSeconds === "function" ? await timestampsInSeconds({
|
|
5622
5632
|
track,
|
|
5633
|
+
container,
|
|
5623
5634
|
durationInSeconds: dur
|
|
5624
5635
|
}) : timestampsInSeconds;
|
|
5625
5636
|
const timestampTargets = timestampTargetsUnsorted.sort((a, b) => a - b);
|
|
5637
|
+
if (timestampTargets.length === 0) {
|
|
5638
|
+
throw new Error("expected at least one timestamp to extract but found zero");
|
|
5639
|
+
}
|
|
5626
5640
|
controller.seek(timestampTargets[0]);
|
|
5627
5641
|
const decoder = createVideoDecoder({
|
|
5628
5642
|
onFrame: (frame) => {
|
|
5629
|
-
|
|
5630
|
-
|
|
5631
|
-
onFrame2(frame);
|
|
5632
|
-
} else {
|
|
5643
|
+
Log.trace(logLevel, "Received frame with timestamp", frame.timestamp);
|
|
5644
|
+
if (expectedFrames.length === 0) {
|
|
5633
5645
|
frame.close();
|
|
5646
|
+
return;
|
|
5647
|
+
}
|
|
5648
|
+
if (frame.timestamp < expectedFrames[0] - 1) {
|
|
5649
|
+
if (lastFrame) {
|
|
5650
|
+
lastFrame.close();
|
|
5651
|
+
}
|
|
5652
|
+
lastFrame = frame;
|
|
5653
|
+
return;
|
|
5654
|
+
}
|
|
5655
|
+
if (expectedFrames[0] + 6667 < frame.timestamp && lastFrame) {
|
|
5656
|
+
onFrame(lastFrame);
|
|
5657
|
+
lastFrameEmitted = lastFrame;
|
|
5658
|
+
expectedFrames.shift();
|
|
5659
|
+
lastFrame = frame;
|
|
5660
|
+
return;
|
|
5634
5661
|
}
|
|
5662
|
+
expectedFrames.shift();
|
|
5663
|
+
onFrame(frame);
|
|
5664
|
+
lastFrameEmitted = frame;
|
|
5665
|
+
lastFrame = frame;
|
|
5635
5666
|
},
|
|
5636
5667
|
onError: (e) => {
|
|
5637
5668
|
controller.abort();
|
|
@@ -5647,18 +5678,23 @@ var internalExtractFrames = ({
|
|
|
5647
5678
|
expectedFrames.push(timestampTargets.shift() * WEBCODECS_TIMESCALE);
|
|
5648
5679
|
while (queued.length > 0) {
|
|
5649
5680
|
const sam = queued.shift();
|
|
5681
|
+
if (!sam) {
|
|
5682
|
+
throw new Error("Sample is undefined");
|
|
5683
|
+
}
|
|
5650
5684
|
await decoder.waitForQueueToBeLessThan(10);
|
|
5685
|
+
Log.trace(logLevel, "Decoding sample", sam.timestamp);
|
|
5651
5686
|
await decoder.decode(sam);
|
|
5652
5687
|
}
|
|
5653
5688
|
};
|
|
5654
5689
|
return async (sample) => {
|
|
5655
5690
|
const nextTimestampWeWant = timestampTargets[0];
|
|
5691
|
+
Log.trace(logLevel, "Received sample with dts", sample.decodingTimestamp, "and cts", sample.timestamp);
|
|
5656
5692
|
if (sample.type === "key") {
|
|
5657
5693
|
await decoder.flush();
|
|
5658
5694
|
queued.length = 0;
|
|
5659
5695
|
}
|
|
5660
5696
|
queued.push(sample);
|
|
5661
|
-
if (sample.
|
|
5697
|
+
if (sample.decodingTimestamp >= timestampTargets[timestampTargets.length - 1] * WEBCODECS_TIMESCALE) {
|
|
5662
5698
|
await doProcess();
|
|
5663
5699
|
await decoder.flush();
|
|
5664
5700
|
controller.abort();
|
|
@@ -5667,7 +5703,7 @@ var internalExtractFrames = ({
|
|
|
5667
5703
|
if (nextTimestampWeWant === undefined) {
|
|
5668
5704
|
throw new Error("this should not happen");
|
|
5669
5705
|
}
|
|
5670
|
-
if (sample.
|
|
5706
|
+
if (sample.decodingTimestamp >= nextTimestampWeWant * WEBCODECS_TIMESCALE) {
|
|
5671
5707
|
await doProcess();
|
|
5672
5708
|
if (timestampTargets.length === 0) {
|
|
5673
5709
|
await decoder.flush();
|
|
@@ -5675,7 +5711,11 @@ var internalExtractFrames = ({
|
|
|
5675
5711
|
}
|
|
5676
5712
|
}
|
|
5677
5713
|
return async () => {
|
|
5714
|
+
await doProcess();
|
|
5678
5715
|
await decoder.flush();
|
|
5716
|
+
if (lastFrame && lastFrameEmitted !== lastFrame) {
|
|
5717
|
+
lastFrame.close();
|
|
5718
|
+
}
|
|
5679
5719
|
};
|
|
5680
5720
|
};
|
|
5681
5721
|
}
|
|
@@ -5688,6 +5728,9 @@ var internalExtractFrames = ({
|
|
|
5688
5728
|
resolvers.resolve();
|
|
5689
5729
|
}
|
|
5690
5730
|
}).finally(() => {
|
|
5731
|
+
if (lastFrame && lastFrameEmitted !== lastFrame) {
|
|
5732
|
+
lastFrame.close();
|
|
5733
|
+
}
|
|
5691
5734
|
signal?.removeEventListener("abort", abortListener);
|
|
5692
5735
|
});
|
|
5693
5736
|
return resolvers.promise;
|
|
@@ -5715,6 +5758,146 @@ var getAvailableAudioCodecs = ({
|
|
|
5715
5758
|
}
|
|
5716
5759
|
throw new Error(`Unsupported container: ${container}`);
|
|
5717
5760
|
};
|
|
5761
|
+
// src/get-partial-audio-data.ts
|
|
5762
|
+
import {
|
|
5763
|
+
hasBeenAborted as hasBeenAborted2,
|
|
5764
|
+
mediaParserController as mediaParserController3,
|
|
5765
|
+
parseMedia
|
|
5766
|
+
} from "@remotion/media-parser";
|
|
5767
|
+
var extractOverlappingAudioSamples = ({
|
|
5768
|
+
sample,
|
|
5769
|
+
fromSeconds,
|
|
5770
|
+
toSeconds,
|
|
5771
|
+
channelIndex,
|
|
5772
|
+
timescale: timescale2
|
|
5773
|
+
}) => {
|
|
5774
|
+
const chunkStartInSeconds = sample.timestamp / timescale2;
|
|
5775
|
+
const chunkDuration = sample.numberOfFrames / sample.sampleRate;
|
|
5776
|
+
const chunkEndInSeconds = chunkStartInSeconds + chunkDuration;
|
|
5777
|
+
const overlapStartSecond = Math.max(chunkStartInSeconds, fromSeconds);
|
|
5778
|
+
const overlapEndSecond = Math.min(chunkEndInSeconds, toSeconds);
|
|
5779
|
+
if (overlapStartSecond >= overlapEndSecond) {
|
|
5780
|
+
return null;
|
|
5781
|
+
}
|
|
5782
|
+
const { numberOfChannels } = sample;
|
|
5783
|
+
const samplesPerChannel = sample.numberOfFrames;
|
|
5784
|
+
let data;
|
|
5785
|
+
if (numberOfChannels === 1) {
|
|
5786
|
+
data = new Float32Array(sample.allocationSize({ format: "f32", planeIndex: 0 }));
|
|
5787
|
+
sample.copyTo(data, { format: "f32", planeIndex: 0 });
|
|
5788
|
+
} else {
|
|
5789
|
+
const allChannelsData = new Float32Array(sample.allocationSize({ format: "f32", planeIndex: 0 }));
|
|
5790
|
+
sample.copyTo(allChannelsData, { format: "f32", planeIndex: 0 });
|
|
5791
|
+
data = new Float32Array(samplesPerChannel);
|
|
5792
|
+
for (let i = 0;i < samplesPerChannel; i++) {
|
|
5793
|
+
data[i] = allChannelsData[i * numberOfChannels + channelIndex];
|
|
5794
|
+
}
|
|
5795
|
+
}
|
|
5796
|
+
const startSampleInChunk = Math.floor((overlapStartSecond - chunkStartInSeconds) * sample.sampleRate);
|
|
5797
|
+
const endSampleInChunk = Math.ceil((overlapEndSecond - chunkStartInSeconds) * sample.sampleRate);
|
|
5798
|
+
return data.slice(startSampleInChunk, endSampleInChunk);
|
|
5799
|
+
};
|
|
5800
|
+
var BUFFER_IN_SECONDS = 0.1;
|
|
5801
|
+
var getPartialAudioData = async ({
|
|
5802
|
+
src,
|
|
5803
|
+
fromSeconds,
|
|
5804
|
+
toSeconds,
|
|
5805
|
+
channelIndex,
|
|
5806
|
+
signal
|
|
5807
|
+
}) => {
|
|
5808
|
+
const controller = mediaParserController3();
|
|
5809
|
+
const audioSamples = [];
|
|
5810
|
+
if (signal.aborted) {
|
|
5811
|
+
throw new Error("Operation was aborted");
|
|
5812
|
+
}
|
|
5813
|
+
const { resolve: resolveAudioDecode, promise: audioDecodePromise } = Promise.withResolvers();
|
|
5814
|
+
const onAbort = () => {
|
|
5815
|
+
controller.abort();
|
|
5816
|
+
resolveAudioDecode();
|
|
5817
|
+
};
|
|
5818
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
5819
|
+
try {
|
|
5820
|
+
if (fromSeconds > 0) {
|
|
5821
|
+
controller.seek(fromSeconds);
|
|
5822
|
+
}
|
|
5823
|
+
await parseMedia({
|
|
5824
|
+
acknowledgeRemotionLicense: true,
|
|
5825
|
+
src,
|
|
5826
|
+
controller,
|
|
5827
|
+
onAudioTrack: ({ track }) => {
|
|
5828
|
+
if (signal.aborted) {
|
|
5829
|
+
return null;
|
|
5830
|
+
}
|
|
5831
|
+
const audioDecoder = createAudioDecoder({
|
|
5832
|
+
track,
|
|
5833
|
+
onFrame: (sample) => {
|
|
5834
|
+
if (signal.aborted) {
|
|
5835
|
+
sample.close();
|
|
5836
|
+
return;
|
|
5837
|
+
}
|
|
5838
|
+
const trimmedData = extractOverlappingAudioSamples({
|
|
5839
|
+
sample,
|
|
5840
|
+
fromSeconds,
|
|
5841
|
+
toSeconds,
|
|
5842
|
+
channelIndex,
|
|
5843
|
+
timescale: track.timescale
|
|
5844
|
+
});
|
|
5845
|
+
if (trimmedData) {
|
|
5846
|
+
audioSamples.push(trimmedData);
|
|
5847
|
+
}
|
|
5848
|
+
sample.close();
|
|
5849
|
+
},
|
|
5850
|
+
onError(error) {
|
|
5851
|
+
resolveAudioDecode();
|
|
5852
|
+
throw error;
|
|
5853
|
+
}
|
|
5854
|
+
});
|
|
5855
|
+
return async (sample) => {
|
|
5856
|
+
if (signal.aborted) {
|
|
5857
|
+
audioDecoder.close();
|
|
5858
|
+
controller.abort();
|
|
5859
|
+
return;
|
|
5860
|
+
}
|
|
5861
|
+
if (!audioDecoder) {
|
|
5862
|
+
throw new Error("No audio decoder found");
|
|
5863
|
+
}
|
|
5864
|
+
const fromSecondsWithBuffer = fromSeconds === 0 ? fromSeconds : fromSeconds + BUFFER_IN_SECONDS;
|
|
5865
|
+
const toSecondsWithBuffer = toSeconds - BUFFER_IN_SECONDS;
|
|
5866
|
+
const time = sample.timestamp / track.timescale;
|
|
5867
|
+
if (time < fromSecondsWithBuffer) {
|
|
5868
|
+
return;
|
|
5869
|
+
}
|
|
5870
|
+
if (time >= toSecondsWithBuffer) {
|
|
5871
|
+
audioDecoder.flush().then(() => {
|
|
5872
|
+
audioDecoder.close();
|
|
5873
|
+
resolveAudioDecode();
|
|
5874
|
+
});
|
|
5875
|
+
controller.abort();
|
|
5876
|
+
return;
|
|
5877
|
+
}
|
|
5878
|
+
await audioDecoder.waitForQueueToBeLessThan(10);
|
|
5879
|
+
audioDecoder.decode(sample);
|
|
5880
|
+
};
|
|
5881
|
+
}
|
|
5882
|
+
});
|
|
5883
|
+
} catch (err) {
|
|
5884
|
+
const isAbortedByTimeCutoff = hasBeenAborted2(err);
|
|
5885
|
+
if (!isAbortedByTimeCutoff && !signal.aborted) {
|
|
5886
|
+
throw err;
|
|
5887
|
+
}
|
|
5888
|
+
} finally {
|
|
5889
|
+
signal.removeEventListener("abort", onAbort);
|
|
5890
|
+
}
|
|
5891
|
+
await audioDecodePromise;
|
|
5892
|
+
const totalSamples = audioSamples.reduce((sum, sample) => sum + sample.length, 0);
|
|
5893
|
+
const result = new Float32Array(totalSamples);
|
|
5894
|
+
let offset = 0;
|
|
5895
|
+
for (const audioSample of audioSamples) {
|
|
5896
|
+
result.set(audioSample, offset);
|
|
5897
|
+
offset += audioSample.length;
|
|
5898
|
+
}
|
|
5899
|
+
return result;
|
|
5900
|
+
};
|
|
5718
5901
|
|
|
5719
5902
|
// src/index.ts
|
|
5720
5903
|
var WebCodecsInternals = {
|
|
@@ -5726,6 +5909,7 @@ setRemotionImported();
|
|
|
5726
5909
|
export {
|
|
5727
5910
|
webcodecsController,
|
|
5728
5911
|
rotateAndResizeVideoFrame,
|
|
5912
|
+
getPartialAudioData,
|
|
5729
5913
|
getDefaultVideoCodec,
|
|
5730
5914
|
getDefaultAudioCodec,
|
|
5731
5915
|
getAvailableVideoCodecs,
|
package/dist/extract-frames.d.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import type { MediaParserLogLevel, MediaParserVideoTrack } from '@remotion/media-parser';
|
|
1
|
+
import type { MediaParserContainer, MediaParserLogLevel, MediaParserVideoTrack } from '@remotion/media-parser';
|
|
2
2
|
export type ExtractFramesTimestampsInSecondsFn = (options: {
|
|
3
3
|
track: MediaParserVideoTrack;
|
|
4
|
+
container: MediaParserContainer;
|
|
4
5
|
durationInSeconds: number | null;
|
|
5
6
|
}) => Promise<number[]> | number[];
|
|
6
7
|
export declare const extractFrames: (options: {
|
package/dist/extract-frames.js
CHANGED
|
@@ -5,6 +5,7 @@ const media_parser_1 = require("@remotion/media-parser");
|
|
|
5
5
|
const worker_1 = require("@remotion/media-parser/worker");
|
|
6
6
|
const create_video_decoder_1 = require("./create-video-decoder");
|
|
7
7
|
const with_resolvers_1 = require("./create/with-resolvers");
|
|
8
|
+
const log_1 = require("./log");
|
|
8
9
|
const internalExtractFrames = ({ src, onFrame, signal, timestampsInSeconds, acknowledgeRemotionLicense, logLevel, }) => {
|
|
9
10
|
const controller = (0, media_parser_1.mediaParserController)();
|
|
10
11
|
const expectedFrames = [];
|
|
@@ -15,6 +16,8 @@ const internalExtractFrames = ({ src, onFrame, signal, timestampsInSeconds, ackn
|
|
|
15
16
|
};
|
|
16
17
|
signal?.addEventListener('abort', abortListener, { once: true });
|
|
17
18
|
let dur = null;
|
|
19
|
+
let lastFrame;
|
|
20
|
+
let lastFrameEmitted;
|
|
18
21
|
(0, worker_1.parseMediaOnWebWorker)({
|
|
19
22
|
src: new URL(src, window.location.href),
|
|
20
23
|
acknowledgeRemotionLicense,
|
|
@@ -23,24 +26,47 @@ const internalExtractFrames = ({ src, onFrame, signal, timestampsInSeconds, ackn
|
|
|
23
26
|
onDurationInSeconds(durationInSeconds) {
|
|
24
27
|
dur = durationInSeconds;
|
|
25
28
|
},
|
|
26
|
-
onVideoTrack: async ({ track }) => {
|
|
29
|
+
onVideoTrack: async ({ track, container }) => {
|
|
27
30
|
const timestampTargetsUnsorted = typeof timestampsInSeconds === 'function'
|
|
28
31
|
? await timestampsInSeconds({
|
|
29
32
|
track,
|
|
33
|
+
container,
|
|
30
34
|
durationInSeconds: dur,
|
|
31
35
|
})
|
|
32
36
|
: timestampsInSeconds;
|
|
33
37
|
const timestampTargets = timestampTargetsUnsorted.sort((a, b) => a - b);
|
|
38
|
+
if (timestampTargets.length === 0) {
|
|
39
|
+
throw new Error('expected at least one timestamp to extract but found zero');
|
|
40
|
+
}
|
|
34
41
|
controller.seek(timestampTargets[0]);
|
|
35
42
|
const decoder = (0, create_video_decoder_1.createVideoDecoder)({
|
|
36
43
|
onFrame: (frame) => {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
onFrame(frame);
|
|
40
|
-
}
|
|
41
|
-
else {
|
|
44
|
+
log_1.Log.trace(logLevel, 'Received frame with timestamp', frame.timestamp);
|
|
45
|
+
if (expectedFrames.length === 0) {
|
|
42
46
|
frame.close();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
if (frame.timestamp < expectedFrames[0] - 1) {
|
|
50
|
+
if (lastFrame) {
|
|
51
|
+
lastFrame.close();
|
|
52
|
+
}
|
|
53
|
+
lastFrame = frame;
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
// A WebM might have a timestamp of 67000 but we request 66666
|
|
57
|
+
// See a test with this problem in it-tests/rendering/frame-accuracy.test.ts
|
|
58
|
+
// Solution: We allow a 10.000ms - 3.333ms = 6.667ms difference between the requested timestamp and the actual timestamp
|
|
59
|
+
if (expectedFrames[0] + 6667 < frame.timestamp && lastFrame) {
|
|
60
|
+
onFrame(lastFrame);
|
|
61
|
+
lastFrameEmitted = lastFrame;
|
|
62
|
+
expectedFrames.shift();
|
|
63
|
+
lastFrame = frame;
|
|
64
|
+
return;
|
|
43
65
|
}
|
|
66
|
+
expectedFrames.shift();
|
|
67
|
+
onFrame(frame);
|
|
68
|
+
lastFrameEmitted = frame;
|
|
69
|
+
lastFrame = frame;
|
|
44
70
|
},
|
|
45
71
|
onError: (e) => {
|
|
46
72
|
controller.abort();
|
|
@@ -57,18 +83,23 @@ const internalExtractFrames = ({ src, onFrame, signal, timestampsInSeconds, ackn
|
|
|
57
83
|
expectedFrames.push(timestampTargets.shift() * media_parser_1.WEBCODECS_TIMESCALE);
|
|
58
84
|
while (queued.length > 0) {
|
|
59
85
|
const sam = queued.shift();
|
|
86
|
+
if (!sam) {
|
|
87
|
+
throw new Error('Sample is undefined');
|
|
88
|
+
}
|
|
60
89
|
await decoder.waitForQueueToBeLessThan(10);
|
|
90
|
+
log_1.Log.trace(logLevel, 'Decoding sample', sam.timestamp);
|
|
61
91
|
await decoder.decode(sam);
|
|
62
92
|
}
|
|
63
93
|
};
|
|
64
94
|
return async (sample) => {
|
|
65
95
|
const nextTimestampWeWant = timestampTargets[0];
|
|
96
|
+
log_1.Log.trace(logLevel, 'Received sample with dts', sample.decodingTimestamp, 'and cts', sample.timestamp);
|
|
66
97
|
if (sample.type === 'key') {
|
|
67
98
|
await decoder.flush();
|
|
68
99
|
queued.length = 0;
|
|
69
100
|
}
|
|
70
101
|
queued.push(sample);
|
|
71
|
-
if (sample.
|
|
102
|
+
if (sample.decodingTimestamp >=
|
|
72
103
|
timestampTargets[timestampTargets.length - 1] * media_parser_1.WEBCODECS_TIMESCALE) {
|
|
73
104
|
await doProcess();
|
|
74
105
|
await decoder.flush();
|
|
@@ -78,7 +109,8 @@ const internalExtractFrames = ({ src, onFrame, signal, timestampsInSeconds, ackn
|
|
|
78
109
|
if (nextTimestampWeWant === undefined) {
|
|
79
110
|
throw new Error('this should not happen');
|
|
80
111
|
}
|
|
81
|
-
if (sample.
|
|
112
|
+
if (sample.decodingTimestamp >=
|
|
113
|
+
nextTimestampWeWant * media_parser_1.WEBCODECS_TIMESCALE) {
|
|
82
114
|
await doProcess();
|
|
83
115
|
if (timestampTargets.length === 0) {
|
|
84
116
|
await decoder.flush();
|
|
@@ -86,7 +118,11 @@ const internalExtractFrames = ({ src, onFrame, signal, timestampsInSeconds, ackn
|
|
|
86
118
|
}
|
|
87
119
|
}
|
|
88
120
|
return async () => {
|
|
121
|
+
await doProcess();
|
|
89
122
|
await decoder.flush();
|
|
123
|
+
if (lastFrame && lastFrameEmitted !== lastFrame) {
|
|
124
|
+
lastFrame.close();
|
|
125
|
+
}
|
|
90
126
|
};
|
|
91
127
|
};
|
|
92
128
|
},
|
|
@@ -103,6 +139,9 @@ const internalExtractFrames = ({ src, onFrame, signal, timestampsInSeconds, ackn
|
|
|
103
139
|
}
|
|
104
140
|
})
|
|
105
141
|
.finally(() => {
|
|
142
|
+
if (lastFrame && lastFrameEmitted !== lastFrame) {
|
|
143
|
+
lastFrame.close();
|
|
144
|
+
}
|
|
106
145
|
signal?.removeEventListener('abort', abortListener);
|
|
107
146
|
});
|
|
108
147
|
return resolvers.promise;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export type GetPartialAudioDataProps = {
|
|
2
|
+
src: string;
|
|
3
|
+
fromSeconds: number;
|
|
4
|
+
toSeconds: number;
|
|
5
|
+
channelIndex: number;
|
|
6
|
+
signal: AbortSignal;
|
|
7
|
+
};
|
|
8
|
+
export declare const getPartialAudioData: ({ src, fromSeconds, toSeconds, channelIndex, signal, }: GetPartialAudioDataProps) => Promise<Float32Array>;
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getPartialAudioData = void 0;
|
|
4
|
+
const media_parser_1 = require("@remotion/media-parser");
|
|
5
|
+
const create_audio_decoder_1 = require("./create-audio-decoder");
|
|
6
|
+
/**
|
|
7
|
+
* Extract the portion of an audio chunk that overlaps with the requested time window
|
|
8
|
+
*/
|
|
9
|
+
const extractOverlappingAudioSamples = ({ sample, fromSeconds, toSeconds, channelIndex, timescale, }) => {
|
|
10
|
+
const chunkStartInSeconds = sample.timestamp / timescale;
|
|
11
|
+
const chunkDuration = sample.numberOfFrames / sample.sampleRate;
|
|
12
|
+
const chunkEndInSeconds = chunkStartInSeconds + chunkDuration;
|
|
13
|
+
// Calculate overlap with the requested window
|
|
14
|
+
const overlapStartSecond = Math.max(chunkStartInSeconds, fromSeconds);
|
|
15
|
+
const overlapEndSecond = Math.min(chunkEndInSeconds, toSeconds);
|
|
16
|
+
// Skip if no overlap with requested window
|
|
17
|
+
if (overlapStartSecond >= overlapEndSecond) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
// For multi-channel audio, we need to handle channels properly
|
|
21
|
+
const { numberOfChannels } = sample;
|
|
22
|
+
const samplesPerChannel = sample.numberOfFrames;
|
|
23
|
+
let data;
|
|
24
|
+
if (numberOfChannels === 1) {
|
|
25
|
+
// Mono audio
|
|
26
|
+
data = new Float32Array(sample.allocationSize({ format: 'f32', planeIndex: 0 }));
|
|
27
|
+
sample.copyTo(data, { format: 'f32', planeIndex: 0 });
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
// Multi-channel audio: extract specific channel
|
|
31
|
+
const allChannelsData = new Float32Array(sample.allocationSize({ format: 'f32', planeIndex: 0 }));
|
|
32
|
+
sample.copyTo(allChannelsData, { format: 'f32', planeIndex: 0 });
|
|
33
|
+
// Extract the specific channel (interleaved audio)
|
|
34
|
+
data = new Float32Array(samplesPerChannel);
|
|
35
|
+
for (let i = 0; i < samplesPerChannel; i++) {
|
|
36
|
+
data[i] = allChannelsData[i * numberOfChannels + channelIndex];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
// Calculate which samples to keep from this chunk
|
|
40
|
+
const startSampleInChunk = Math.floor((overlapStartSecond - chunkStartInSeconds) * sample.sampleRate);
|
|
41
|
+
const endSampleInChunk = Math.ceil((overlapEndSecond - chunkStartInSeconds) * sample.sampleRate);
|
|
42
|
+
// Only keep the samples we need
|
|
43
|
+
return data.slice(startSampleInChunk, endSampleInChunk);
|
|
44
|
+
};
|
|
45
|
+
// Small buffer to ensure we capture chunks that span across boundaries
|
|
46
|
+
// We need this because specified time window is not always aligned with the audio chunks
|
|
47
|
+
// so that we fetch a bit more, and then trim it down to the requested time window
|
|
48
|
+
const BUFFER_IN_SECONDS = 0.1;
|
|
49
|
+
const getPartialAudioData = async ({ src, fromSeconds, toSeconds, channelIndex, signal, }) => {
|
|
50
|
+
const controller = (0, media_parser_1.mediaParserController)();
|
|
51
|
+
// Collect audio samples
|
|
52
|
+
const audioSamples = [];
|
|
53
|
+
// Abort if the signal is already aborted
|
|
54
|
+
if (signal.aborted) {
|
|
55
|
+
throw new Error('Operation was aborted');
|
|
56
|
+
}
|
|
57
|
+
// Forward abort signal immediately to the controller
|
|
58
|
+
const { resolve: resolveAudioDecode, promise: audioDecodePromise } = Promise.withResolvers();
|
|
59
|
+
const onAbort = () => {
|
|
60
|
+
controller.abort();
|
|
61
|
+
resolveAudioDecode();
|
|
62
|
+
};
|
|
63
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
64
|
+
try {
|
|
65
|
+
if (fromSeconds > 0) {
|
|
66
|
+
controller.seek(fromSeconds);
|
|
67
|
+
}
|
|
68
|
+
await (0, media_parser_1.parseMedia)({
|
|
69
|
+
acknowledgeRemotionLicense: true,
|
|
70
|
+
src,
|
|
71
|
+
controller,
|
|
72
|
+
onAudioTrack: ({ track }) => {
|
|
73
|
+
if (signal.aborted) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
const audioDecoder = (0, create_audio_decoder_1.createAudioDecoder)({
|
|
77
|
+
track,
|
|
78
|
+
onFrame: (sample) => {
|
|
79
|
+
if (signal.aborted) {
|
|
80
|
+
sample.close();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const trimmedData = extractOverlappingAudioSamples({
|
|
84
|
+
sample,
|
|
85
|
+
fromSeconds,
|
|
86
|
+
toSeconds,
|
|
87
|
+
channelIndex,
|
|
88
|
+
timescale: track.timescale,
|
|
89
|
+
});
|
|
90
|
+
if (trimmedData) {
|
|
91
|
+
audioSamples.push(trimmedData);
|
|
92
|
+
}
|
|
93
|
+
sample.close();
|
|
94
|
+
},
|
|
95
|
+
onError(error) {
|
|
96
|
+
resolveAudioDecode();
|
|
97
|
+
throw error;
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
return async (sample) => {
|
|
101
|
+
if (signal.aborted) {
|
|
102
|
+
audioDecoder.close();
|
|
103
|
+
controller.abort();
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
if (!audioDecoder) {
|
|
107
|
+
throw new Error('No audio decoder found');
|
|
108
|
+
}
|
|
109
|
+
const fromSecondsWithBuffer = fromSeconds === 0 ? fromSeconds : fromSeconds + BUFFER_IN_SECONDS;
|
|
110
|
+
const toSecondsWithBuffer = toSeconds - BUFFER_IN_SECONDS;
|
|
111
|
+
// Convert timestamp using the track's timescale
|
|
112
|
+
const time = sample.timestamp / track.timescale;
|
|
113
|
+
// Skip samples that are before our requested start time (with buffer)
|
|
114
|
+
if (time < fromSecondsWithBuffer) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// Stop immediately when we reach our target time (with buffer)
|
|
118
|
+
if (time >= toSecondsWithBuffer) {
|
|
119
|
+
// wait until decoder is done
|
|
120
|
+
audioDecoder.flush().then(() => {
|
|
121
|
+
audioDecoder.close();
|
|
122
|
+
resolveAudioDecode();
|
|
123
|
+
});
|
|
124
|
+
controller.abort();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
await audioDecoder.waitForQueueToBeLessThan(10);
|
|
128
|
+
// we're waiting for the queue above anyway, enqueue in sync mode
|
|
129
|
+
audioDecoder.decode(sample);
|
|
130
|
+
};
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch (err) {
|
|
135
|
+
const isAbortedByTimeCutoff = (0, media_parser_1.hasBeenAborted)(err);
|
|
136
|
+
// Don't throw if we stopped the parsing ourselves
|
|
137
|
+
if (!isAbortedByTimeCutoff && !signal.aborted) {
|
|
138
|
+
throw err;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
finally {
|
|
142
|
+
// Clean up the event listener
|
|
143
|
+
signal.removeEventListener('abort', onAbort);
|
|
144
|
+
}
|
|
145
|
+
await audioDecodePromise;
|
|
146
|
+
// Simply concatenate all audio data since we've already trimmed each chunk
|
|
147
|
+
const totalSamples = audioSamples.reduce((sum, sample) => sum + sample.length, 0);
|
|
148
|
+
const result = new Float32Array(totalSamples);
|
|
149
|
+
let offset = 0;
|
|
150
|
+
for (const audioSample of audioSamples) {
|
|
151
|
+
result.set(audioSample, offset);
|
|
152
|
+
offset += audioSample.length;
|
|
153
|
+
}
|
|
154
|
+
return result;
|
|
155
|
+
};
|
|
156
|
+
exports.getPartialAudioData = getPartialAudioData;
|
package/dist/index.d.ts
CHANGED
|
@@ -23,6 +23,7 @@ export { getAvailableVideoCodecs } from './get-available-video-codecs';
|
|
|
23
23
|
export type { ConvertMediaVideoCodec } from './get-available-video-codecs';
|
|
24
24
|
export { getDefaultAudioCodec } from './get-default-audio-codec';
|
|
25
25
|
export { getDefaultVideoCodec } from './get-default-video-codec';
|
|
26
|
+
export { getPartialAudioData, GetPartialAudioDataProps, } from './get-partial-audio-data';
|
|
26
27
|
export type { AudioOperation, ConvertMediaOnAudioTrackHandler, } from './on-audio-track-handler';
|
|
27
28
|
export type { ConvertMediaOnVideoTrackHandler, VideoOperation, } from './on-video-track-handler';
|
|
28
29
|
export type { ResizeOperation } from './resizing/mode';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.WebCodecsInternals = exports.webcodecsController = exports.createVideoEncoder = exports.rotateAndResizeVideoFrame = exports.getDefaultVideoCodec = exports.getDefaultAudioCodec = exports.getAvailableVideoCodecs = exports.getAvailableContainers = exports.getAvailableAudioCodecs = exports.extractFrames = exports.defaultOnVideoTrackHandler = exports.defaultOnAudioTrackHandler = exports.createVideoDecoder = exports.createAudioDecoder = exports.convertMedia = exports.convertAudioData = exports.canReencodeVideoTrack = exports.canReencodeAudioTrack = exports.canCopyVideoTrack = exports.canCopyAudioTrack = exports.createAudioEncoder = void 0;
|
|
3
|
+
exports.WebCodecsInternals = exports.webcodecsController = exports.createVideoEncoder = exports.rotateAndResizeVideoFrame = exports.getPartialAudioData = exports.getDefaultVideoCodec = exports.getDefaultAudioCodec = exports.getAvailableVideoCodecs = exports.getAvailableContainers = exports.getAvailableAudioCodecs = exports.extractFrames = exports.defaultOnVideoTrackHandler = exports.defaultOnAudioTrackHandler = exports.createVideoDecoder = exports.createAudioDecoder = exports.convertMedia = exports.convertAudioData = exports.canReencodeVideoTrack = exports.canReencodeAudioTrack = exports.canCopyVideoTrack = exports.canCopyAudioTrack = exports.createAudioEncoder = void 0;
|
|
4
4
|
const rotate_and_resize_video_frame_1 = require("./rotate-and-resize-video-frame");
|
|
5
5
|
const rotation_1 = require("./rotation");
|
|
6
6
|
const set_remotion_imported_1 = require("./set-remotion-imported");
|
|
@@ -38,6 +38,8 @@ var get_default_audio_codec_1 = require("./get-default-audio-codec");
|
|
|
38
38
|
Object.defineProperty(exports, "getDefaultAudioCodec", { enumerable: true, get: function () { return get_default_audio_codec_1.getDefaultAudioCodec; } });
|
|
39
39
|
var get_default_video_codec_1 = require("./get-default-video-codec");
|
|
40
40
|
Object.defineProperty(exports, "getDefaultVideoCodec", { enumerable: true, get: function () { return get_default_video_codec_1.getDefaultVideoCodec; } });
|
|
41
|
+
var get_partial_audio_data_1 = require("./get-partial-audio-data");
|
|
42
|
+
Object.defineProperty(exports, "getPartialAudioData", { enumerable: true, get: function () { return get_partial_audio_data_1.getPartialAudioData; } });
|
|
41
43
|
var rotate_and_resize_video_frame_2 = require("./rotate-and-resize-video-frame");
|
|
42
44
|
Object.defineProperty(exports, "rotateAndResizeVideoFrame", { enumerable: true, get: function () { return rotate_and_resize_video_frame_2.rotateAndResizeVideoFrame; } });
|
|
43
45
|
var video_encoder_1 = require("./video-encoder");
|
package/dist/on-frame.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ import type { MediaParserVideoTrack } from '@remotion/media-parser';
|
|
|
2
2
|
import type { ConvertMediaOnVideoFrame } from './convert-media';
|
|
3
3
|
import type { ConvertMediaVideoCodec } from './get-available-video-codecs';
|
|
4
4
|
import type { ResizeOperation } from './resizing/mode';
|
|
5
|
-
export declare const
|
|
5
|
+
export declare const processFrame: ({ frame: unrotatedFrame, onVideoFrame, track, outputCodec, rotation, resizeOperation, }: {
|
|
6
6
|
frame: VideoFrame;
|
|
7
7
|
onVideoFrame: ConvertMediaOnVideoFrame | null;
|
|
8
8
|
track: MediaParserVideoTrack;
|
package/dist/on-frame.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.
|
|
3
|
+
exports.processFrame = void 0;
|
|
4
4
|
const browser_quirks_1 = require("./browser-quirks");
|
|
5
5
|
const convert_to_correct_videoframe_1 = require("./convert-to-correct-videoframe");
|
|
6
6
|
const rotate_and_resize_video_frame_1 = require("./rotate-and-resize-video-frame");
|
|
7
|
-
const
|
|
7
|
+
const processFrame = async ({ frame: unrotatedFrame, onVideoFrame, track, outputCodec, rotation, resizeOperation, }) => {
|
|
8
8
|
const rotated = (0, rotate_and_resize_video_frame_1.rotateAndResizeVideoFrame)({
|
|
9
9
|
rotation,
|
|
10
10
|
frame: unrotatedFrame,
|
|
@@ -43,4 +43,4 @@ const onFrame = async ({ frame: unrotatedFrame, onVideoFrame, track, outputCodec
|
|
|
43
43
|
}
|
|
44
44
|
return fixedFrame;
|
|
45
45
|
};
|
|
46
|
-
exports.
|
|
46
|
+
exports.processFrame = processFrame;
|
|
@@ -16,7 +16,7 @@ const reencodeVideoTrack = async ({ videoOperation, rotate, track, logLevel, abo
|
|
|
16
16
|
if (videoOperation.type !== 'reencode') {
|
|
17
17
|
throw new Error(`Video track with ID ${track.trackId} could not be resolved with a valid operation. Received ${JSON.stringify(videoOperation)}, but must be either "copy", "reencode", "drop" or "fail"`);
|
|
18
18
|
}
|
|
19
|
-
const rotation = (videoOperation.rotate ?? rotate)
|
|
19
|
+
const rotation = (videoOperation.rotate ?? rotate) + track.rotation;
|
|
20
20
|
const { height: newHeight, width: newWidth } = (0, rotation_1.calculateNewDimensionsFromRotateAndScale)({
|
|
21
21
|
width: track.codedWidth,
|
|
22
22
|
height: track.codedHeight,
|
|
@@ -88,7 +88,7 @@ const reencodeVideoTrack = async ({ videoOperation, rotate, track, logLevel, abo
|
|
|
88
88
|
},
|
|
89
89
|
onOutput: async (frame) => {
|
|
90
90
|
await controller._internals._mediaParserController._internals.checkForAbortAndPause();
|
|
91
|
-
const processedFrame = await (0, on_frame_1.
|
|
91
|
+
const processedFrame = await (0, on_frame_1.processFrame)({
|
|
92
92
|
frame,
|
|
93
93
|
track,
|
|
94
94
|
onVideoFrame,
|
|
@@ -8,14 +8,17 @@ const normalizeVideoRotation = (rotation) => {
|
|
|
8
8
|
exports.normalizeVideoRotation = normalizeVideoRotation;
|
|
9
9
|
const rotateAndResizeVideoFrame = ({ frame, rotation, needsToBeMultipleOfTwo = false, resizeOperation, }) => {
|
|
10
10
|
const normalized = (0, exports.normalizeVideoRotation)(rotation);
|
|
11
|
+
// In Chrome, there is "rotation", but we cannot put frames with VideoEncoder if they have a rotation.
|
|
12
|
+
// We have to draw them to a canvas and make a new frame without video rotation.
|
|
13
|
+
const mustProcess = 'rotation' in frame && frame.rotation !== 0;
|
|
11
14
|
// No resize, no rotation
|
|
12
|
-
if (normalized === 0 && resizeOperation === null) {
|
|
15
|
+
if (normalized === 0 && resizeOperation === null && !mustProcess) {
|
|
13
16
|
return frame;
|
|
14
17
|
}
|
|
15
18
|
if (normalized % 90 !== 0) {
|
|
16
19
|
throw new Error('Only 90 degree rotations are supported');
|
|
17
20
|
}
|
|
18
|
-
const
|
|
21
|
+
const tentativeDimensions = (0, rotation_1.calculateNewDimensionsFromRotateAndScale)({
|
|
19
22
|
height: frame.displayHeight,
|
|
20
23
|
width: frame.displayWidth,
|
|
21
24
|
rotation,
|
|
@@ -24,10 +27,21 @@ const rotateAndResizeVideoFrame = ({ frame, rotation, needsToBeMultipleOfTwo = f
|
|
|
24
27
|
});
|
|
25
28
|
// No rotation, and resize turned out to be same dimensions
|
|
26
29
|
if (normalized === 0 &&
|
|
27
|
-
height === frame.displayHeight &&
|
|
28
|
-
width === frame.displayWidth
|
|
30
|
+
tentativeDimensions.height === frame.displayHeight &&
|
|
31
|
+
tentativeDimensions.width === frame.displayWidth &&
|
|
32
|
+
!mustProcess) {
|
|
29
33
|
return frame;
|
|
30
34
|
}
|
|
35
|
+
// @ts-expect-error
|
|
36
|
+
const frameRotation = frame.rotation ?? 0;
|
|
37
|
+
const canvasRotationToApply = (0, exports.normalizeVideoRotation)(normalized - frameRotation);
|
|
38
|
+
const { width, height } = (0, rotation_1.calculateNewDimensionsFromRotateAndScale)({
|
|
39
|
+
height: frame.displayHeight,
|
|
40
|
+
width: frame.displayWidth,
|
|
41
|
+
rotation: canvasRotationToApply,
|
|
42
|
+
needsToBeMultipleOfTwo,
|
|
43
|
+
resizeOperation,
|
|
44
|
+
});
|
|
31
45
|
const canvas = new OffscreenCanvas(width, height);
|
|
32
46
|
const ctx = canvas.getContext('2d');
|
|
33
47
|
if (!ctx) {
|
|
@@ -35,9 +49,6 @@ const rotateAndResizeVideoFrame = ({ frame, rotation, needsToBeMultipleOfTwo = f
|
|
|
35
49
|
}
|
|
36
50
|
canvas.width = width;
|
|
37
51
|
canvas.height = height;
|
|
38
|
-
// @ts-expect-error
|
|
39
|
-
const frameRotation = frame.rotation ?? 0;
|
|
40
|
-
const canvasRotationToApply = (0, exports.normalizeVideoRotation)(normalized + frameRotation);
|
|
41
52
|
if (canvasRotationToApply === 90) {
|
|
42
53
|
ctx.translate(width, 0);
|
|
43
54
|
}
|
|
@@ -53,7 +64,7 @@ const rotateAndResizeVideoFrame = ({ frame, rotation, needsToBeMultipleOfTwo = f
|
|
|
53
64
|
if (frame.displayHeight !== height || frame.displayWidth !== width) {
|
|
54
65
|
const dimensionsAfterRotate = (0, rotation_1.calculateNewDimensionsFromRotate)({
|
|
55
66
|
height: frame.displayHeight,
|
|
56
|
-
rotation,
|
|
67
|
+
rotation: canvasRotationToApply,
|
|
57
68
|
width: frame.displayWidth,
|
|
58
69
|
});
|
|
59
70
|
ctx.scale(width / dimensionsAfterRotate.width, height / dimensionsAfterRotate.height);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@remotion/webcodecs",
|
|
3
|
-
"version": "4.0.
|
|
3
|
+
"version": "4.0.330",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"module": "dist/esm/index.mjs",
|
|
@@ -19,8 +19,8 @@
|
|
|
19
19
|
"author": "Jonny Burger <jonny@remotion.dev>",
|
|
20
20
|
"license": "Remotion License (See https://remotion.dev/docs/webcodecs#license)",
|
|
21
21
|
"dependencies": {
|
|
22
|
-
"@remotion/media-parser": "4.0.
|
|
23
|
-
"@remotion/licensing": "4.0.
|
|
22
|
+
"@remotion/media-parser": "4.0.330",
|
|
23
|
+
"@remotion/licensing": "4.0.330"
|
|
24
24
|
},
|
|
25
25
|
"peerDependencies": {},
|
|
26
26
|
"devDependencies": {
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"vite": "5.4.19",
|
|
30
30
|
"@playwright/test": "1.51.1",
|
|
31
31
|
"eslint": "9.19.0",
|
|
32
|
-
"@remotion/
|
|
33
|
-
"@remotion/
|
|
32
|
+
"@remotion/eslint-config-internal": "4.0.330",
|
|
33
|
+
"@remotion/example-videos": "4.0.330"
|
|
34
34
|
},
|
|
35
35
|
"keywords": [],
|
|
36
36
|
"publishConfig": {
|