@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.
@@ -366,22 +366,32 @@ var rotateAndResizeVideoFrame = ({
366
366
  resizeOperation
367
367
  }) => {
368
368
  const normalized = normalizeVideoRotation(rotation);
369
- if (normalized === 0 && resizeOperation === null) {
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 { height, width } = calculateNewDimensionsFromRotateAndScale({
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 onFrame = async ({
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) - track.rotation;
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 onFrame({
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: onFrame2,
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
- if (frame.timestamp >= expectedFrames[0] - 1) {
5630
- expectedFrames.shift();
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.timestamp >= timestampTargets[timestampTargets.length - 1] * WEBCODECS_TIMESCALE) {
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.timestamp >= nextTimestampWeWant * WEBCODECS_TIMESCALE) {
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,
@@ -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: {
@@ -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
- if (frame.timestamp >= expectedFrames[0] - 1) {
38
- expectedFrames.shift();
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.timestamp >=
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.timestamp >= nextTimestampWeWant * media_parser_1.WEBCODECS_TIMESCALE) {
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");
@@ -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 onFrame: ({ frame: unrotatedFrame, onVideoFrame, track, outputCodec, rotation, resizeOperation, }: {
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.onFrame = void 0;
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 onFrame = async ({ frame: unrotatedFrame, onVideoFrame, track, outputCodec, rotation, resizeOperation, }) => {
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.onFrame = onFrame;
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) - track.rotation;
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.onFrame)({
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 { height, width } = (0, rotation_1.calculateNewDimensionsFromRotateAndScale)({
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.327",
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.327",
23
- "@remotion/licensing": "4.0.327"
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/example-videos": "4.0.327",
33
- "@remotion/eslint-config-internal": "4.0.327"
32
+ "@remotion/eslint-config-internal": "4.0.330",
33
+ "@remotion/example-videos": "4.0.330"
34
34
  },
35
35
  "keywords": [],
36
36
  "publishConfig": {