@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.
- package/dist/audio-extraction/extract-audio.js +15 -8
- package/dist/convert-audiodata/convert-audiodata.js +5 -4
- package/dist/esm/index.mjs +68 -23
- package/dist/extract-frame-and-audio.js +13 -1
- package/dist/media-player.js +0 -6
- package/dist/video/video-for-rendering.js +2 -8
- package/dist/video-extraction/extract-frame-via-broadcast-channel.d.ts +1 -1
- package/dist/video-extraction/extract-frame-via-broadcast-channel.js +3 -7
- package/dist/video-extraction/rotate-frame.d.ts +4 -0
- package/dist/video-extraction/rotate-frame.js +34 -0
- package/dist/video-extraction/to-video-frame-fixed-rotation.d.ts +14 -0
- package/dist/video-extraction/to-video-frame-fixed-rotation.js +41 -0
- package/package.json +4 -4
|
@@ -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
|
-
|
|
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
|
-
|
|
5
|
-
|
|
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 (
|
|
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
|
};
|
package/dist/esm/index.mjs
CHANGED
|
@@ -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
|
-
|
|
2742
|
-
|
|
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 (
|
|
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
|
-
|
|
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:
|
|
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
|
|
3167
|
-
|
|
3168
|
-
|
|
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
|
|
4073
|
-
context.canvas.height = imageBitmap
|
|
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:
|
|
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
|
};
|
package/dist/media-player.js
CHANGED
|
@@ -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
|
-
|
|
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 |
|
|
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
|
|
60
|
-
|
|
61
|
-
|
|
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,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.
|
|
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.
|
|
25
|
-
"remotion": "4.0.
|
|
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.
|
|
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",
|