@remotion/media 4.0.373 → 4.0.375

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.
@@ -1,6 +1,7 @@
1
1
  import { audioManager } from '../caches';
2
2
  import { combineAudioDataAndClosePrevious } from '../convert-audiodata/combine-audiodata';
3
- import { convertAudioData } from '../convert-audiodata/convert-audiodata';
3
+ import { convertAudioData, fixFloatingPoint, } from '../convert-audiodata/convert-audiodata';
4
+ import { TARGET_NUMBER_OF_CHANNELS, TARGET_SAMPLE_RATE, } from '../convert-audiodata/resample-audiodata';
4
5
  import { getSink } from '../get-sink';
5
6
  import { getTimeInSeconds } from '../get-time-in-seconds';
6
7
  const extractAudioInternal = async ({ src, timeInSeconds: unloopedTimeInSeconds, durationInSeconds: durationNotYetApplyingPlaybackRate, logLevel, loop, playbackRate, audioStreamIndex, trimBefore, trimAfter, fps, }) => {
@@ -44,7 +45,6 @@ const extractAudioInternal = async ({ src, timeInSeconds: unloopedTimeInSeconds,
44
45
  const durationInSeconds = durationNotYetApplyingPlaybackRate * playbackRate;
45
46
  const samples = await sampleIterator.getSamples(timeInSeconds, durationInSeconds);
46
47
  audioManager.logOpenFrames();
47
- const trimStartToleranceInSeconds = 0.002;
48
48
  const audioDataArray = [];
49
49
  for (let i = 0; i < samples.length; i++) {
50
50
  const sample = samples[i];
@@ -64,14 +64,18 @@ const extractAudioInternal = async ({ src, timeInSeconds: unloopedTimeInSeconds,
64
64
  // amount of samples to shave from start and end
65
65
  let trimStartInSeconds = 0;
66
66
  let trimEndInSeconds = 0;
67
+ let leadingSilence = null;
67
68
  if (isFirstSample) {
68
- trimStartInSeconds = timeInSeconds - sample.timestamp;
69
- if (trimStartInSeconds < 0 &&
70
- trimStartInSeconds > -trimStartToleranceInSeconds) {
71
- trimStartInSeconds = 0;
72
- }
69
+ trimStartInSeconds = fixFloatingPoint(timeInSeconds - sample.timestamp);
73
70
  if (trimStartInSeconds < 0) {
74
- throw new Error(`trimStartInSeconds is negative: ${trimStartInSeconds}. ${JSON.stringify({ timeInSeconds, ts: sample.timestamp, d: sample.duration, isFirstSample, isLastSample, durationInSeconds, i, st: samples.map((s) => s.timestamp) })}`);
71
+ const silenceFrames = Math.ceil(fixFloatingPoint(-trimStartInSeconds * TARGET_SAMPLE_RATE));
72
+ leadingSilence = {
73
+ data: new Int16Array(silenceFrames * TARGET_NUMBER_OF_CHANNELS),
74
+ numberOfFrames: silenceFrames,
75
+ timestamp: timeInSeconds * 1000000,
76
+ durationInMicroSeconds: (silenceFrames / TARGET_SAMPLE_RATE) * 1000000,
77
+ };
78
+ trimStartInSeconds = 0;
75
79
  }
76
80
  }
77
81
  if (isLastSample) {
@@ -93,6 +97,9 @@ const extractAudioInternal = async ({ src, timeInSeconds: unloopedTimeInSeconds,
93
97
  if (audioData.numberOfFrames === 0) {
94
98
  continue;
95
99
  }
100
+ if (leadingSilence) {
101
+ audioDataArray.push(leadingSilence);
102
+ }
96
103
  audioDataArray.push(audioData);
97
104
  }
98
105
  if (audioDataArray.length === 0) {
@@ -1,11 +1,12 @@
1
1
  import { resampleAudioData, TARGET_NUMBER_OF_CHANNELS, TARGET_SAMPLE_RATE, } from './resample-audiodata';
2
2
  const FORMAT = 's16';
3
3
  export const fixFloatingPoint = (value) => {
4
- if (value % 1 < 0.0000001) {
5
- return Math.floor(value);
4
+ const decimal = Math.abs(value % 1);
5
+ if (decimal < 0.0000001) {
6
+ return value < 0 ? Math.ceil(value) : Math.floor(value);
6
7
  }
7
- if (value % 1 > 0.9999999) {
8
- return Math.ceil(value);
8
+ if (decimal > 0.9999999) {
9
+ return value < 0 ? Math.floor(value) : Math.ceil(value);
9
10
  }
10
11
  return value;
11
12
  };
@@ -994,11 +994,6 @@ class MediaPlayer {
994
994
  if (currentPlaybackTime === newTime) {
995
995
  return;
996
996
  }
997
- const newAudioSyncAnchor = this.sharedAudioContext.currentTime - newTime / (this.playbackRate * this.globalPlaybackRate);
998
- const diff = Math.abs(newAudioSyncAnchor - this.audioSyncAnchor);
999
- if (diff > 0.04) {
1000
- this.setPlaybackTime(newTime, this.playbackRate * this.globalPlaybackRate);
1001
- }
1002
997
  await this.videoIteratorManager?.seek({
1003
998
  newTime,
1004
999
  nonce
@@ -2738,11 +2733,12 @@ var getMaxVideoCacheSize = (logLevel) => {
2738
2733
  // src/convert-audiodata/convert-audiodata.ts
2739
2734
  var FORMAT = "s16";
2740
2735
  var fixFloatingPoint2 = (value) => {
2741
- if (value % 1 < 0.0000001) {
2742
- return Math.floor(value);
2736
+ const decimal = Math.abs(value % 1);
2737
+ if (decimal < 0.0000001) {
2738
+ return value < 0 ? Math.ceil(value) : Math.floor(value);
2743
2739
  }
2744
- if (value % 1 > 0.9999999) {
2745
- return Math.ceil(value);
2740
+ if (decimal > 0.9999999) {
2741
+ return value < 0 ? Math.floor(value) : Math.ceil(value);
2746
2742
  }
2747
2743
  return value;
2748
2744
  };
@@ -2897,7 +2893,6 @@ var extractAudioInternal = async ({
2897
2893
  const durationInSeconds = durationNotYetApplyingPlaybackRate * playbackRate;
2898
2894
  const samples = await sampleIterator.getSamples(timeInSeconds, durationInSeconds);
2899
2895
  audioManager.logOpenFrames();
2900
- const trimStartToleranceInSeconds = 0.002;
2901
2896
  const audioDataArray = [];
2902
2897
  for (let i = 0;i < samples.length; i++) {
2903
2898
  const sample = samples[i];
@@ -2912,13 +2907,18 @@ var extractAudioInternal = async ({
2912
2907
  const audioDataRaw = sample.toAudioData();
2913
2908
  let trimStartInSeconds = 0;
2914
2909
  let trimEndInSeconds = 0;
2910
+ let leadingSilence = null;
2915
2911
  if (isFirstSample) {
2916
- trimStartInSeconds = timeInSeconds - sample.timestamp;
2917
- if (trimStartInSeconds < 0 && trimStartInSeconds > -trimStartToleranceInSeconds) {
2918
- trimStartInSeconds = 0;
2919
- }
2912
+ trimStartInSeconds = fixFloatingPoint2(timeInSeconds - sample.timestamp);
2920
2913
  if (trimStartInSeconds < 0) {
2921
- throw new Error(`trimStartInSeconds is negative: ${trimStartInSeconds}. ${JSON.stringify({ timeInSeconds, ts: sample.timestamp, d: sample.duration, isFirstSample, isLastSample, durationInSeconds, i, st: samples.map((s) => s.timestamp) })}`);
2914
+ const silenceFrames = Math.ceil(fixFloatingPoint2(-trimStartInSeconds * TARGET_SAMPLE_RATE));
2915
+ leadingSilence = {
2916
+ data: new Int16Array(silenceFrames * TARGET_NUMBER_OF_CHANNELS),
2917
+ numberOfFrames: silenceFrames,
2918
+ timestamp: timeInSeconds * 1e6,
2919
+ durationInMicroSeconds: silenceFrames / TARGET_SAMPLE_RATE * 1e6
2920
+ };
2921
+ trimStartInSeconds = 0;
2922
2922
  }
2923
2923
  }
2924
2924
  if (isLastSample) {
@@ -2936,6 +2936,9 @@ var extractAudioInternal = async ({
2936
2936
  if (audioData.numberOfFrames === 0) {
2937
2937
  continue;
2938
2938
  }
2939
+ if (leadingSilence) {
2940
+ audioDataArray.push(leadingSilence);
2941
+ }
2939
2942
  audioDataArray.push(audioData);
2940
2943
  }
2941
2944
  if (audioDataArray.length === 0) {
@@ -3023,6 +3026,39 @@ var extractFrame = (params) => {
3023
3026
  return queue2;
3024
3027
  };
3025
3028
 
3029
+ // src/video-extraction/rotate-frame.ts
3030
+ var rotateFrame = async ({
3031
+ frame,
3032
+ rotation
3033
+ }) => {
3034
+ if (rotation === 0) {
3035
+ const directBitmap = await createImageBitmap(frame);
3036
+ frame.close();
3037
+ return directBitmap;
3038
+ }
3039
+ const width = rotation === 90 || rotation === 270 ? frame.displayHeight : frame.displayWidth;
3040
+ const height = rotation === 90 || rotation === 270 ? frame.displayWidth : frame.displayHeight;
3041
+ const canvas = new OffscreenCanvas(width, height);
3042
+ const ctx = canvas.getContext("2d");
3043
+ if (!ctx) {
3044
+ throw new Error("Could not get 2d context");
3045
+ }
3046
+ canvas.width = width;
3047
+ canvas.height = height;
3048
+ if (rotation === 90) {
3049
+ ctx.translate(width, 0);
3050
+ } else if (rotation === 180) {
3051
+ ctx.translate(width, height);
3052
+ } else if (rotation === 270) {
3053
+ ctx.translate(0, height);
3054
+ }
3055
+ ctx.rotate(rotation * (Math.PI / 180));
3056
+ ctx.drawImage(frame, 0, 0);
3057
+ const bitmap = await createImageBitmap(canvas);
3058
+ frame.close();
3059
+ return bitmap;
3060
+ };
3061
+
3026
3062
  // src/extract-frame-and-audio.ts
3027
3063
  var extractFrameAndAudio = async ({
3028
3064
  src,
@@ -3093,9 +3129,20 @@ var extractFrameAndAudio = async ({
3093
3129
  durationInSeconds: frame?.type === "success" ? frame.durationInSeconds : null
3094
3130
  };
3095
3131
  }
3132
+ if (!frame?.frame) {
3133
+ return {
3134
+ type: "success",
3135
+ frame: null,
3136
+ audio: audio?.data ?? null,
3137
+ durationInSeconds: audio?.durationInSeconds ?? null
3138
+ };
3139
+ }
3096
3140
  return {
3097
3141
  type: "success",
3098
- frame: frame?.frame?.toVideoFrame() ?? null,
3142
+ frame: await rotateFrame({
3143
+ frame: frame.frame.toVideoFrame(),
3144
+ rotation: frame.frame.rotation
3145
+ }),
3099
3146
  audio: audio?.data ?? null,
3100
3147
  durationInSeconds: audio?.durationInSeconds ?? null
3101
3148
  };
@@ -3163,10 +3210,9 @@ if (typeof window !== "undefined" && window.remotion_broadcastChannel && window.
3163
3210
  return;
3164
3211
  }
3165
3212
  const { frame, audio, durationInSeconds } = result;
3166
- const videoFrame = frame;
3167
- const imageBitmap = videoFrame ? await createImageBitmap(videoFrame) : null;
3168
- if (videoFrame) {
3169
- videoFrame.close();
3213
+ const imageBitmap = frame ? await createImageBitmap(frame) : null;
3214
+ if (frame) {
3215
+ frame.close();
3170
3216
  }
3171
3217
  const response = {
3172
3218
  type: "response-success",
@@ -3176,7 +3222,6 @@ if (typeof window !== "undefined" && window.remotion_broadcastChannel && window.
3176
3222
  durationInSeconds: durationInSeconds ?? null
3177
3223
  };
3178
3224
  window.remotion_broadcastChannel.postMessage(response);
3179
- videoFrame?.close();
3180
3225
  } catch (error) {
3181
3226
  const response = {
3182
3227
  type: "response-error",
@@ -4069,8 +4114,8 @@ var VideoForRendering = ({
4069
4114
  if (!context) {
4070
4115
  return;
4071
4116
  }
4072
- context.canvas.width = imageBitmap instanceof ImageBitmap ? imageBitmap.width : imageBitmap.displayWidth;
4073
- context.canvas.height = imageBitmap instanceof ImageBitmap ? imageBitmap.height : imageBitmap.displayHeight;
4117
+ context.canvas.width = imageBitmap.width;
4118
+ context.canvas.height = imageBitmap.height;
4074
4119
  context.canvas.style.aspectRatio = `${context.canvas.width} / ${context.canvas.height}`;
4075
4120
  context.drawImage(imageBitmap, 0, 0);
4076
4121
  imageBitmap.close();
@@ -1,6 +1,7 @@
1
1
  import { extractAudio } from './audio-extraction/extract-audio';
2
2
  import { isNetworkError } from './is-network-error';
3
3
  import { extractFrame } from './video-extraction/extract-frame';
4
+ import { rotateFrame } from './video-extraction/rotate-frame';
4
5
  export const extractFrameAndAudio = async ({ src, timeInSeconds, logLevel, durationInSeconds, playbackRate, includeAudio, includeVideo, loop, audioStreamIndex, trimAfter, trimBefore, fps, }) => {
5
6
  try {
6
7
  const [frame, audio] = await Promise.all([
@@ -61,9 +62,20 @@ export const extractFrameAndAudio = async ({ src, timeInSeconds, logLevel, durat
61
62
  durationInSeconds: frame?.type === 'success' ? frame.durationInSeconds : null,
62
63
  };
63
64
  }
65
+ if (!frame?.frame) {
66
+ return {
67
+ type: 'success',
68
+ frame: null,
69
+ audio: audio?.data ?? null,
70
+ durationInSeconds: audio?.durationInSeconds ?? null,
71
+ };
72
+ }
64
73
  return {
65
74
  type: 'success',
66
- frame: frame?.frame?.toVideoFrame() ?? null,
75
+ frame: await rotateFrame({
76
+ frame: frame.frame.toVideoFrame(),
77
+ rotation: frame.frame.rotation,
78
+ }),
67
79
  audio: audio?.data ?? null,
68
80
  durationInSeconds: audio?.durationInSeconds ?? null,
69
81
  };
@@ -228,12 +228,6 @@ export class MediaPlayer {
228
228
  if (currentPlaybackTime === newTime) {
229
229
  return;
230
230
  }
231
- const newAudioSyncAnchor = this.sharedAudioContext.currentTime -
232
- newTime / (this.playbackRate * this.globalPlaybackRate);
233
- const diff = Math.abs(newAudioSyncAnchor - this.audioSyncAnchor);
234
- if (diff > 0.04) {
235
- this.setPlaybackTime(newTime, this.playbackRate * this.globalPlaybackRate);
236
- }
237
231
  await this.videoIteratorManager?.seek({
238
232
  newTime,
239
233
  nonce,
@@ -120,14 +120,8 @@ export const VideoForRendering = ({ volume: volumeProp, playbackRate, src, muted
120
120
  if (!context) {
121
121
  return;
122
122
  }
123
- context.canvas.width =
124
- imageBitmap instanceof ImageBitmap
125
- ? imageBitmap.width
126
- : imageBitmap.displayWidth;
127
- context.canvas.height =
128
- imageBitmap instanceof ImageBitmap
129
- ? imageBitmap.height
130
- : imageBitmap.displayHeight;
123
+ context.canvas.width = imageBitmap.width;
124
+ context.canvas.height = imageBitmap.height;
131
125
  context.canvas.style.aspectRatio = `${context.canvas.width} / ${context.canvas.height}`;
132
126
  context.drawImage(imageBitmap, 0, 0);
133
127
  imageBitmap.close();
@@ -2,7 +2,7 @@ import { type LogLevel } from 'remotion';
2
2
  import type { PcmS16AudioData } from '../convert-audiodata/convert-audiodata';
3
3
  export type ExtractFrameViaBroadcastChannelResult = {
4
4
  type: 'success';
5
- frame: ImageBitmap | VideoFrame | null;
5
+ frame: ImageBitmap | null;
6
6
  audio: PcmS16AudioData | null;
7
7
  durationInSeconds: number | null;
8
8
  } | {
@@ -56,12 +56,9 @@ if (typeof window !== 'undefined' &&
56
56
  return;
57
57
  }
58
58
  const { frame, audio, durationInSeconds } = result;
59
- const videoFrame = frame;
60
- const imageBitmap = videoFrame
61
- ? await createImageBitmap(videoFrame)
62
- : null;
63
- if (videoFrame) {
64
- videoFrame.close();
59
+ const imageBitmap = frame ? await createImageBitmap(frame) : null;
60
+ if (frame) {
61
+ frame.close();
65
62
  }
66
63
  const response = {
67
64
  type: 'response-success',
@@ -71,7 +68,6 @@ if (typeof window !== 'undefined' &&
71
68
  durationInSeconds: durationInSeconds ?? null,
72
69
  };
73
70
  window.remotion_broadcastChannel.postMessage(response);
74
- videoFrame?.close();
75
71
  }
76
72
  catch (error) {
77
73
  const response = {
@@ -0,0 +1,4 @@
1
+ export declare const rotateFrame: ({ frame, rotation, }: {
2
+ frame: VideoFrame;
3
+ rotation: number;
4
+ }) => Promise<ImageBitmap>;
@@ -0,0 +1,34 @@
1
+ export const rotateFrame = async ({ frame, rotation, }) => {
2
+ if (rotation === 0) {
3
+ const directBitmap = await createImageBitmap(frame);
4
+ frame.close();
5
+ return directBitmap;
6
+ }
7
+ const width = rotation === 90 || rotation === 270
8
+ ? frame.displayHeight
9
+ : frame.displayWidth;
10
+ const height = rotation === 90 || rotation === 270
11
+ ? frame.displayWidth
12
+ : frame.displayHeight;
13
+ const canvas = new OffscreenCanvas(width, height);
14
+ const ctx = canvas.getContext('2d');
15
+ if (!ctx) {
16
+ throw new Error('Could not get 2d context');
17
+ }
18
+ canvas.width = width;
19
+ canvas.height = height;
20
+ if (rotation === 90) {
21
+ ctx.translate(width, 0);
22
+ }
23
+ else if (rotation === 180) {
24
+ ctx.translate(width, height);
25
+ }
26
+ else if (rotation === 270) {
27
+ ctx.translate(0, height);
28
+ }
29
+ ctx.rotate(rotation * (Math.PI / 180));
30
+ ctx.drawImage(frame, 0, 0);
31
+ const bitmap = await createImageBitmap(canvas);
32
+ frame.close();
33
+ return bitmap;
34
+ };
@@ -0,0 +1,14 @@
1
+ import type { VideoSample } from 'mediabunny';
2
+ /**
3
+ * Once we convert a VideoSample to a VideoFrame, we lose the rotation
4
+ * https://github.com/Vanilagy/mediabunny/pull/212
5
+ * This will be fixed in Mediabunny v2, but for now, we need to manually fix it.
6
+ *
7
+ * I'm actually wondering if your PR is actually a breaking change
8
+ I would say it kinda is actually
9
+ Because, previously only the VideoSample had rotation but the video frame you got from .toVideoFrame() was unrotated. Now, the resulting VideoFrame will be rotated, so drawing it to a canvas will behave differently. To me, this is a breaking change
10
+ People's old code that manually handled the rotation will break here
11
+ So I think this is actually a PR for v2
12
+ And for Remotion, you can do a temporary workaround fix by cloning the VideoFrame and overriding rotation that way, then closing the old frame, then transferring the cloned frame
13
+ */
14
+ export declare const toVideoFrameFixedRotation: (videoSample: VideoSample) => VideoFrame;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Once we convert a VideoSample to a VideoFrame, we lose the rotation
3
+ * https://github.com/Vanilagy/mediabunny/pull/212
4
+ * This will be fixed in Mediabunny v2, but for now, we need to manually fix it.
5
+ *
6
+ * I'm actually wondering if your PR is actually a breaking change
7
+ I would say it kinda is actually
8
+ Because, previously only the VideoSample had rotation but the video frame you got from .toVideoFrame() was unrotated. Now, the resulting VideoFrame will be rotated, so drawing it to a canvas will behave differently. To me, this is a breaking change
9
+ People's old code that manually handled the rotation will break here
10
+ So I think this is actually a PR for v2
11
+ And for Remotion, you can do a temporary workaround fix by cloning the VideoFrame and overriding rotation that way, then closing the old frame, then transferring the cloned frame
12
+ */
13
+ export const toVideoFrameFixedRotation = (videoSample) => {
14
+ const frame = videoSample.toVideoFrame();
15
+ if (videoSample.rotation === 0) {
16
+ return frame;
17
+ }
18
+ const canvas = new OffscreenCanvas(width, height);
19
+ const ctx = canvas.getContext('2d');
20
+ if (!ctx) {
21
+ throw new Error('Could not get 2d context');
22
+ }
23
+ canvas.width = width;
24
+ canvas.height = height;
25
+ if (canvasRotationToApply === 90) {
26
+ ctx.translate(width, 0);
27
+ }
28
+ else if (canvasRotationToApply === 180) {
29
+ ctx.translate(width, height);
30
+ }
31
+ else if (canvasRotationToApply === 270) {
32
+ ctx.translate(0, height);
33
+ }
34
+ console.log('sample rotation', videoSample.rotation);
35
+ // @ts-expect-error - rotation is not a known property of VideoFrameInit
36
+ const fixedFrame = new VideoFrame(frame, { rotation: videoSample.rotation });
37
+ frame.close();
38
+ // @ts-expect-error - rotation is not a known property of VideoFrameInit
39
+ console.log('fixed frame rotation', fixedFrame.rotation);
40
+ return fixedFrame;
41
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remotion/media",
3
- "version": "4.0.373",
3
+ "version": "4.0.375",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "module": "dist/esm/index.mjs",
@@ -21,15 +21,15 @@
21
21
  "make": "tsc -d && bun --env-file=../.env.bundle bundle.ts"
22
22
  },
23
23
  "dependencies": {
24
- "mediabunny": "1.24.3",
25
- "remotion": "4.0.373"
24
+ "mediabunny": "1.24.5",
25
+ "remotion": "4.0.375"
26
26
  },
27
27
  "peerDependencies": {
28
28
  "react": ">=16.8.0",
29
29
  "react-dom": ">=16.8.0"
30
30
  },
31
31
  "devDependencies": {
32
- "@remotion/eslint-config-internal": "4.0.373",
32
+ "@remotion/eslint-config-internal": "4.0.375",
33
33
  "@vitest/browser-webdriverio": "4.0.7",
34
34
  "eslint": "9.19.0",
35
35
  "react": "19.0.0",