@remotion/webcodecs 4.0.230 → 4.0.231

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/README.md CHANGED
@@ -15,4 +15,15 @@ Remove the `^` character from the version number to use the exact version.
15
15
 
16
16
  ## Usage
17
17
 
18
- This is an internal package and has no documentation.
18
+ See the [documentation](https://remotion.dev/webcodecs) for more information.
19
+
20
+ ## License
21
+ This package is licensed under the [/docs/license](Remotion License).
22
+ We consider a team of 4 or more people a "company".
23
+
24
+ **For "companies"**: A Remotion Company license needs to be obtained to use this package.
25
+ In a future version of `@remotion/webcodecs`, this package will also require the purchase of a newly created "WebCodecs Conversion Seat". [Get in touch](https://remotion.dev/contact) with us if you are planning to use this package.
26
+
27
+ **For individuals and teams up to 3**: You can use this package for free.
28
+
29
+ This is a short, non-binding explanation of our license. See the [https://remotion.dev/docs/license](License) itself for more details.
@@ -50,7 +50,7 @@ const createAudioEncoder = ({ onChunk, onError, codec, signal, config: audioEnco
50
50
  if (encoder.state === 'closed') {
51
51
  return;
52
52
  }
53
- await ioSynchronizer.waitFor({ unemitted: 2, _unprocessed: 2 });
53
+ await ioSynchronizer.waitFor({ unemitted: 20, _unprocessed: 20 });
54
54
  // @ts-expect-error - can have changed in the meanwhile
55
55
  if (encoder.state === 'closed') {
56
56
  return;
@@ -0,0 +1,2 @@
1
+ export declare const isFirefox: () => boolean;
2
+ export declare const isSafari: () => boolean;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.isSafari = exports.isFirefox = void 0;
4
+ const isFirefox = () => {
5
+ return navigator.userAgent.toLowerCase().indexOf('firefox') > -1;
6
+ };
7
+ exports.isFirefox = isFirefox;
8
+ const isSafari = () => {
9
+ return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
10
+ };
11
+ exports.isSafari = isSafari;
@@ -6,7 +6,7 @@ import type { LogLevel, Options, ParseMediaDynamicOptions, ParseMediaFields, Par
6
6
  import type { ConvertMediaAudioCodec, ConvertMediaContainer, ConvertMediaVideoCodec } from './codec-id';
7
7
  import { type ConvertMediaOnAudioTrackHandler } from './on-audio-track-handler';
8
8
  import { type ConvertMediaOnVideoTrackHandler } from './on-video-track-handler';
9
- export type ConvertMediaState = {
9
+ export type ConvertMediaProgress = {
10
10
  decodedVideoFrames: number;
11
11
  decodedAudioFrames: number;
12
12
  encodedVideoFrames: number;
@@ -19,17 +19,18 @@ export type ConvertMediaState = {
19
19
  export type ConvertMediaResult = {
20
20
  save: () => Promise<Blob>;
21
21
  remove: () => Promise<void>;
22
+ finalState: ConvertMediaProgress;
22
23
  };
23
- export type ConvertMediaOnMediaStateUpdate = (state: ConvertMediaState) => void;
24
+ export type ConvertMediaOnProgress = (state: ConvertMediaProgress) => void;
24
25
  export type ConvertMediaOnVideoFrame = (options: {
25
26
  frame: VideoFrame;
26
27
  track: VideoTrack;
27
28
  }) => Promise<VideoFrame> | VideoFrame;
28
- export declare const convertMedia: <F extends Options<ParseMediaFields>>({ src, onVideoFrame, onMediaStateUpdate: onMediaStateDoNoCallDirectly, audioCodec, container, videoCodec, signal: userPassedAbortSignal, onAudioTrack: userAudioResolver, onVideoTrack: userVideoResolver, reader, fields, logLevel, writer, ...more }: {
29
+ export declare const convertMedia: <F extends Options<ParseMediaFields>>({ src, onVideoFrame, onProgress: onProgressDoNotCallDirectly, audioCodec, container, videoCodec, signal: userPassedAbortSignal, onAudioTrack: userAudioResolver, onVideoTrack: userVideoResolver, reader, fields, logLevel, writer, progressIntervalInMs, ...more }: {
29
30
  src: ParseMediaOptions<F>["src"];
30
31
  container: ConvertMediaContainer;
31
32
  onVideoFrame?: ConvertMediaOnVideoFrame;
32
- onMediaStateUpdate?: ConvertMediaOnMediaStateUpdate;
33
+ onProgress?: ConvertMediaOnProgress;
33
34
  videoCodec?: ConvertMediaVideoCodec;
34
35
  audioCodec?: ConvertMediaAudioCodec;
35
36
  signal?: AbortSignal;
@@ -38,4 +39,5 @@ export declare const convertMedia: <F extends Options<ParseMediaFields>>({ src,
38
39
  reader?: ParseMediaOptions<F>["reader"];
39
40
  logLevel?: LogLevel;
40
41
  writer?: WriterInterface;
42
+ progressIntervalInMs?: number;
41
43
  } & ParseMediaDynamicOptions<F>) => Promise<ConvertMediaResult>;
@@ -12,10 +12,13 @@ const media_parser_1 = require("@remotion/media-parser");
12
12
  const auto_select_writer_1 = require("./auto-select-writer");
13
13
  const calculate_progress_1 = require("./calculate-progress");
14
14
  const error_cause_1 = __importDefault(require("./error-cause"));
15
+ const generate_output_filename_1 = require("./generate-output-filename");
15
16
  const on_audio_track_1 = require("./on-audio-track");
16
17
  const on_video_track_1 = require("./on-video-track");
18
+ const throttled_state_update_1 = require("./throttled-state-update");
17
19
  const with_resolvers_1 = require("./with-resolvers");
18
- const convertMedia = async function ({ src, onVideoFrame, onMediaStateUpdate: onMediaStateDoNoCallDirectly, audioCodec, container, videoCodec, signal: userPassedAbortSignal, onAudioTrack: userAudioResolver, onVideoTrack: userVideoResolver, reader, fields, logLevel = 'info', writer, ...more }) {
20
+ const convertMedia = async function ({ src, onVideoFrame, onProgress: onProgressDoNotCallDirectly, audioCodec, container, videoCodec, signal: userPassedAbortSignal, onAudioTrack: userAudioResolver, onVideoTrack: userVideoResolver, reader, fields, logLevel = 'info', writer, progressIntervalInMs, ...more }) {
21
+ var _a, _b;
19
22
  if (userPassedAbortSignal === null || userPassedAbortSignal === void 0 ? void 0 : userPassedAbortSignal.aborted) {
20
23
  return Promise.reject(new error_cause_1.default('Aborted'));
21
24
  }
@@ -37,49 +40,49 @@ const convertMedia = async function ({ src, onVideoFrame, onMediaStateUpdate: on
37
40
  abortConversion(new error_cause_1.default('Conversion aborted by user'));
38
41
  };
39
42
  userPassedAbortSignal === null || userPassedAbortSignal === void 0 ? void 0 : userPassedAbortSignal.addEventListener('abort', onUserAbort);
40
- const convertMediaState = {
41
- decodedAudioFrames: 0,
42
- decodedVideoFrames: 0,
43
- encodedVideoFrames: 0,
44
- encodedAudioFrames: 0,
45
- bytesWritten: 0,
46
- millisecondsWritten: 0,
47
- expectedOutputDurationInMs: null,
48
- overallProgress: 0,
49
- };
50
- const onMediaStateUpdate = (newState) => {
51
- if (controller.signal.aborted) {
52
- return;
53
- }
54
- onMediaStateDoNoCallDirectly === null || onMediaStateDoNoCallDirectly === void 0 ? void 0 : onMediaStateDoNoCallDirectly(newState);
55
- };
56
43
  const creator = container === 'webm'
57
44
  ? media_parser_1.MediaParserInternals.createMatroskaMedia
58
45
  : media_parser_1.MediaParserInternals.createIsoBaseMedia;
46
+ const throttledState = (0, throttled_state_update_1.throttledStateUpdate)({
47
+ updateFn: onProgressDoNotCallDirectly !== null && onProgressDoNotCallDirectly !== void 0 ? onProgressDoNotCallDirectly : null,
48
+ everyMilliseconds: progressIntervalInMs !== null && progressIntervalInMs !== void 0 ? progressIntervalInMs : 100,
49
+ signal: controller.signal,
50
+ });
59
51
  const state = await creator({
52
+ filename: (0, generate_output_filename_1.generateOutputFilename)(src, container),
60
53
  writer: await (0, auto_select_writer_1.autoSelectWriter)(writer, logLevel),
61
54
  onBytesProgress: (bytesWritten) => {
62
- convertMediaState.bytesWritten = bytesWritten;
63
- onMediaStateUpdate === null || onMediaStateUpdate === void 0 ? void 0 : onMediaStateUpdate(convertMediaState);
55
+ var _a;
56
+ (_a = throttledState.update) === null || _a === void 0 ? void 0 : _a.call(throttledState, (prevState) => {
57
+ return {
58
+ ...prevState,
59
+ bytesWritten,
60
+ };
61
+ });
64
62
  },
65
63
  onMillisecondsProgress: (millisecondsWritten) => {
66
- if (millisecondsWritten > convertMediaState.millisecondsWritten) {
67
- convertMediaState.millisecondsWritten = millisecondsWritten;
68
- convertMediaState.overallProgress = (0, calculate_progress_1.calculateProgress)({
69
- millisecondsWritten: convertMediaState.millisecondsWritten,
70
- expectedOutputDurationInMs: convertMediaState.expectedOutputDurationInMs,
71
- });
72
- onMediaStateUpdate === null || onMediaStateUpdate === void 0 ? void 0 : onMediaStateUpdate(convertMediaState);
73
- }
64
+ var _a;
65
+ (_a = throttledState.update) === null || _a === void 0 ? void 0 : _a.call(throttledState, (prevState) => {
66
+ if (millisecondsWritten > prevState.millisecondsWritten) {
67
+ return {
68
+ ...prevState,
69
+ millisecondsWritten,
70
+ overallProgress: (0, calculate_progress_1.calculateProgress)({
71
+ millisecondsWritten: prevState.millisecondsWritten,
72
+ expectedOutputDurationInMs: prevState.expectedOutputDurationInMs,
73
+ }),
74
+ };
75
+ }
76
+ return prevState;
77
+ });
74
78
  },
75
79
  logLevel,
76
80
  });
77
81
  const onVideoTrack = (0, on_video_track_1.makeVideoTrackHandler)({
78
82
  state,
79
83
  onVideoFrame: onVideoFrame !== null && onVideoFrame !== void 0 ? onVideoFrame : null,
80
- onMediaStateUpdate: onMediaStateUpdate !== null && onMediaStateUpdate !== void 0 ? onMediaStateUpdate : null,
84
+ onMediaStateUpdate: (_a = throttledState.update) !== null && _a !== void 0 ? _a : null,
81
85
  abortConversion,
82
- convertMediaState,
83
86
  controller,
84
87
  defaultVideoCodec: videoCodec !== null && videoCodec !== void 0 ? videoCodec : null,
85
88
  onVideoTrack: userVideoResolver !== null && userVideoResolver !== void 0 ? userVideoResolver : null,
@@ -90,8 +93,7 @@ const convertMedia = async function ({ src, onVideoFrame, onMediaStateUpdate: on
90
93
  abortConversion,
91
94
  defaultAudioCodec: audioCodec !== null && audioCodec !== void 0 ? audioCodec : null,
92
95
  controller,
93
- convertMediaState,
94
- onMediaStateUpdate: onMediaStateUpdate !== null && onMediaStateUpdate !== void 0 ? onMediaStateUpdate : null,
96
+ onMediaStateUpdate: (_b = throttledState.update) !== null && _b !== void 0 ? _b : null,
95
97
  state,
96
98
  onAudioTrack: userAudioResolver !== null && userAudioResolver !== void 0 ? userAudioResolver : null,
97
99
  logLevel,
@@ -110,6 +112,7 @@ const convertMedia = async function ({ src, onVideoFrame, onMediaStateUpdate: on
110
112
  reader,
111
113
  ...more,
112
114
  onDurationInSeconds: (durationInSeconds) => {
115
+ var _a;
113
116
  if (durationInSeconds === null) {
114
117
  return null;
115
118
  }
@@ -118,22 +121,33 @@ const convertMedia = async function ({ src, onVideoFrame, onMediaStateUpdate: on
118
121
  casted.onDurationInSeconds(durationInSeconds);
119
122
  }
120
123
  const expectedOutputDurationInMs = durationInSeconds * 1000;
121
- convertMediaState.expectedOutputDurationInMs = expectedOutputDurationInMs;
122
- convertMediaState.overallProgress = (0, calculate_progress_1.calculateProgress)({
123
- millisecondsWritten: convertMediaState.millisecondsWritten,
124
- expectedOutputDurationInMs,
124
+ (_a = throttledState.update) === null || _a === void 0 ? void 0 : _a.call(throttledState, (prevState) => {
125
+ return {
126
+ ...prevState,
127
+ expectedOutputDurationInMs,
128
+ overallProgress: (0, calculate_progress_1.calculateProgress)({
129
+ millisecondsWritten: prevState.millisecondsWritten,
130
+ expectedOutputDurationInMs,
131
+ }),
132
+ };
125
133
  });
126
- onMediaStateUpdate(convertMediaState);
127
134
  },
128
135
  })
129
136
  .then(() => {
130
137
  return state.waitForFinish();
131
138
  })
132
139
  .then(() => {
133
- resolve({ save: state.save, remove: state.remove });
140
+ resolve({
141
+ save: state.save,
142
+ remove: state.remove,
143
+ finalState: throttledState.get(),
144
+ });
134
145
  })
135
146
  .catch((err) => {
136
147
  reject(err);
148
+ })
149
+ .finally(() => {
150
+ throttledState.stopAndGetLastProgress();
137
151
  });
138
152
  return getPromiseToImmediatelyReturn().finally(() => {
139
153
  userPassedAbortSignal === null || userPassedAbortSignal === void 0 ? void 0 : userPassedAbortSignal.removeEventListener('abort', onUserAbort);
@@ -0,0 +1,9 @@
1
+ import type { ConvertMediaVideoCodec } from './codec-id';
2
+ export declare const needsToCorrectVideoFrame: ({ videoFrame, outputCodec, }: {
3
+ videoFrame: VideoFrame;
4
+ outputCodec: ConvertMediaVideoCodec;
5
+ }) => boolean;
6
+ export declare const convertToCorrectVideoFrame: ({ videoFrame, outputCodec, }: {
7
+ videoFrame: VideoFrame;
8
+ outputCodec: ConvertMediaVideoCodec;
9
+ }) => VideoFrame;
@@ -0,0 +1,28 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.convertToCorrectVideoFrame = exports.needsToCorrectVideoFrame = void 0;
4
+ const browser_quirks_1 = require("./browser-quirks");
5
+ const needsToCorrectVideoFrame = ({ videoFrame, outputCodec, }) => {
6
+ return (0, browser_quirks_1.isFirefox)() && videoFrame.format === 'BGRX' && outputCodec === 'h264';
7
+ };
8
+ exports.needsToCorrectVideoFrame = needsToCorrectVideoFrame;
9
+ const convertToCorrectVideoFrame = ({ videoFrame, outputCodec, }) => {
10
+ if (!(0, exports.needsToCorrectVideoFrame)({ videoFrame, outputCodec })) {
11
+ return videoFrame;
12
+ }
13
+ const canvas = new OffscreenCanvas(videoFrame.displayWidth, videoFrame.displayHeight);
14
+ canvas.width = videoFrame.displayWidth;
15
+ canvas.height = videoFrame.displayHeight;
16
+ const ctx = canvas.getContext('2d');
17
+ if (!ctx) {
18
+ throw new Error('Could not get 2d context');
19
+ }
20
+ ctx.drawImage(videoFrame, 0, 0);
21
+ return new VideoFrame(canvas, {
22
+ displayHeight: videoFrame.displayHeight,
23
+ displayWidth: videoFrame.displayWidth,
24
+ duration: videoFrame.duration,
25
+ timestamp: videoFrame.timestamp,
26
+ });
27
+ };
28
+ exports.convertToCorrectVideoFrame = convertToCorrectVideoFrame;
@@ -1,3 +1,18 @@
1
+ // src/set-remotion-imported.ts
2
+ import { VERSION } from "@remotion/media-parser";
3
+ var setRemotionImported = () => {
4
+ if (typeof globalThis === "undefined") {
5
+ return;
6
+ }
7
+ if (globalThis.remotion_imported) {
8
+ return;
9
+ }
10
+ globalThis.remotion_imported = VERSION;
11
+ if (typeof window !== "undefined") {
12
+ window.remotion_imported = `${VERSION}-webcodecs`;
13
+ }
14
+ };
15
+
1
16
  // src/log.ts
2
17
  import { MediaParserInternals } from "@remotion/media-parser";
3
18
  var { Log } = MediaParserInternals;
@@ -267,7 +282,7 @@ var createAudioEncoder = ({
267
282
  if (encoder.state === "closed") {
268
283
  return;
269
284
  }
270
- await ioSynchronizer.waitFor({ unemitted: 2, _unprocessed: 2 });
285
+ await ioSynchronizer.waitFor({ unemitted: 20, _unprocessed: 20 });
271
286
  if (encoder.state === "closed") {
272
287
  return;
273
288
  }
@@ -403,6 +418,14 @@ var getVideoDecoderConfigWithHardwareAcceleration = async (config) => {
403
418
  return null;
404
419
  };
405
420
 
421
+ // src/browser-quirks.ts
422
+ var isFirefox = () => {
423
+ return navigator.userAgent.toLowerCase().indexOf("firefox") > -1;
424
+ };
425
+ var isSafari = () => {
426
+ return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
427
+ };
428
+
406
429
  // src/choose-correct-avc1-profile.ts
407
430
  var chooseCorrectAvc1Profile = ({
408
431
  width,
@@ -451,7 +474,8 @@ var getVideoEncoderConfig = async ({
451
474
  const config = {
452
475
  codec: codec === "h264" ? chooseCorrectAvc1Profile({ fps, height, width }) : codec === "vp9" ? "vp09.00.10.08" : codec,
453
476
  height,
454
- width
477
+ width,
478
+ bitrate: isSafari() ? 3000000 : undefined
455
479
  };
456
480
  const hardware = {
457
481
  ...config,
@@ -544,6 +568,14 @@ var calculateProgress = ({
544
568
  // src/error-cause.ts
545
569
  var error_cause_default = Error;
546
570
 
571
+ // src/generate-output-filename.ts
572
+ var generateOutputFilename = (source, container) => {
573
+ const filename = typeof source === "string" ? source : source instanceof File ? source.name : "converted";
574
+ const behindSlash = filename.split("/").pop();
575
+ const withoutExtension = behindSlash.split(".").slice(0, -1).join(".");
576
+ return `${withoutExtension}.${container}`;
577
+ };
578
+
547
579
  // src/convert-encoded-chunk.ts
548
580
  var convertEncodedChunk = (chunk, trackId) => {
549
581
  const arr = new Uint8Array(chunk.byteLength);
@@ -614,7 +646,6 @@ var defaultOnAudioTrackHandler = async ({
614
646
  var makeAudioTrackHandler = ({
615
647
  state,
616
648
  defaultAudioCodec: audioCodec,
617
- convertMediaState,
618
649
  controller,
619
650
  abortConversion,
620
651
  onMediaStateUpdate,
@@ -652,8 +683,12 @@ var makeAudioTrackHandler = ({
652
683
  timescale: track.timescale,
653
684
  codecPrivate: track.codecPrivate
654
685
  });
655
- convertMediaState.encodedAudioFrames++;
656
- onMediaStateUpdate?.({ ...convertMediaState });
686
+ onMediaStateUpdate?.((prevState) => {
687
+ return {
688
+ ...prevState,
689
+ encodedAudioFrames: prevState.encodedAudioFrames + 1
690
+ };
691
+ });
657
692
  };
658
693
  }
659
694
  const audioEncoderConfig = await getAudioEncoderConfig({
@@ -697,8 +732,12 @@ var makeAudioTrackHandler = ({
697
732
  timescale: track.timescale,
698
733
  codecPrivate
699
734
  });
700
- convertMediaState.encodedAudioFrames++;
701
- onMediaStateUpdate?.({ ...convertMediaState });
735
+ onMediaStateUpdate?.((prevState) => {
736
+ return {
737
+ ...prevState,
738
+ encodedAudioFrames: prevState.encodedAudioFrames + 1
739
+ };
740
+ });
702
741
  },
703
742
  onError: (err) => {
704
743
  abortConversion(new error_cause_default(`Audio encoder of ${track.trackId} failed (see .cause of this error)`, {
@@ -713,8 +752,12 @@ var makeAudioTrackHandler = ({
713
752
  const audioDecoder = createAudioDecoder({
714
753
  onFrame: async (frame) => {
715
754
  await audioEncoder.encodeFrame(frame);
716
- convertMediaState.decodedAudioFrames++;
717
- onMediaStateUpdate?.(convertMediaState);
755
+ onMediaStateUpdate?.((prevState) => {
756
+ return {
757
+ ...prevState,
758
+ decodedAudioFrames: prevState.decodedAudioFrames + 1
759
+ };
760
+ });
718
761
  frame.close();
719
762
  },
720
763
  onError(error) {
@@ -783,21 +826,50 @@ var defaultOnVideoTrackHandler = async ({
783
826
  return Promise.resolve({ type: "fail" });
784
827
  };
785
828
 
829
+ // src/convert-to-correct-videoframe.ts
830
+ var needsToCorrectVideoFrame = ({
831
+ videoFrame,
832
+ outputCodec
833
+ }) => {
834
+ return isFirefox() && videoFrame.format === "BGRX" && outputCodec === "h264";
835
+ };
836
+ var convertToCorrectVideoFrame = ({
837
+ videoFrame,
838
+ outputCodec
839
+ }) => {
840
+ if (!needsToCorrectVideoFrame({ videoFrame, outputCodec })) {
841
+ return videoFrame;
842
+ }
843
+ const canvas = new OffscreenCanvas(videoFrame.displayWidth, videoFrame.displayHeight);
844
+ canvas.width = videoFrame.displayWidth;
845
+ canvas.height = videoFrame.displayHeight;
846
+ const ctx = canvas.getContext("2d");
847
+ if (!ctx) {
848
+ throw new Error("Could not get 2d context");
849
+ }
850
+ ctx.drawImage(videoFrame, 0, 0);
851
+ return new VideoFrame(canvas, {
852
+ displayHeight: videoFrame.displayHeight,
853
+ displayWidth: videoFrame.displayWidth,
854
+ duration: videoFrame.duration,
855
+ timestamp: videoFrame.timestamp
856
+ });
857
+ };
858
+
786
859
  // src/on-frame.ts
787
860
  var onFrame = async ({
788
861
  frame,
789
862
  onVideoFrame,
790
863
  videoEncoder,
791
- onMediaStateUpdate,
792
864
  track,
793
- convertMediaState
865
+ outputCodec
794
866
  }) => {
795
867
  const newFrame = onVideoFrame ? await onVideoFrame({ frame, track }) : frame;
796
- if (newFrame.codedHeight !== frame.codedHeight) {
797
- throw new Error(`Returned VideoFrame of track ${track.trackId} has different codedHeight (${newFrame.codedHeight}) than the input frame (${frame.codedHeight})`);
868
+ if (newFrame.codedHeight !== frame.displayHeight) {
869
+ throw new Error(`Returned VideoFrame of track ${track.trackId} has different codedHeight (${newFrame.codedHeight}) than the input frame displayHeight (${frame.displayHeight})`);
798
870
  }
799
- if (newFrame.codedWidth !== frame.codedWidth) {
800
- throw new Error(`Returned VideoFrame of track ${track.trackId} has different codedWidth (${newFrame.codedWidth}) than the input frame (${frame.codedWidth})`);
871
+ if (newFrame.codedWidth !== frame.displayWidth) {
872
+ throw new Error(`Returned VideoFrame of track ${track.trackId} has different codedWidth (${newFrame.codedWidth}) than the input frame displayWidth (${frame.displayWidth})`);
801
873
  }
802
874
  if (newFrame.displayWidth !== frame.displayWidth) {
803
875
  throw new Error(`Returned VideoFrame of track ${track.trackId} has different displayWidth (${newFrame.displayWidth}) than the input frame (${newFrame.displayHeight})`);
@@ -811,13 +883,18 @@ var onFrame = async ({
811
883
  if (newFrame.duration !== frame.duration) {
812
884
  throw new Error(`Returned VideoFrame of track ${track.trackId} has different duration (${newFrame.duration}) than the input frame (${newFrame.duration}). When calling new VideoFrame(), pass {duration: frame.duration} as second argument`);
813
885
  }
814
- await videoEncoder.encodeFrame(newFrame, newFrame.timestamp);
815
- convertMediaState.decodedVideoFrames++;
816
- onMediaStateUpdate?.({ ...convertMediaState });
817
- newFrame.close();
886
+ const fixedFrame = convertToCorrectVideoFrame({
887
+ videoFrame: newFrame,
888
+ outputCodec
889
+ });
890
+ await videoEncoder.encodeFrame(fixedFrame, fixedFrame.timestamp);
891
+ fixedFrame.close();
818
892
  if (frame !== newFrame) {
819
893
  frame.close();
820
894
  }
895
+ if (fixedFrame !== newFrame) {
896
+ fixedFrame.close();
897
+ }
821
898
  };
822
899
 
823
900
  // src/video-decoder.ts
@@ -910,7 +987,8 @@ var createVideoEncoder = ({
910
987
  onError,
911
988
  signal,
912
989
  config,
913
- logLevel
990
+ logLevel,
991
+ outputCodec
914
992
  }) => {
915
993
  if (signal.aborted) {
916
994
  throw new Error("Not creating video encoder, already aborted");
@@ -948,6 +1026,7 @@ var createVideoEncoder = ({
948
1026
  close();
949
1027
  };
950
1028
  signal.addEventListener("abort", onAbort);
1029
+ Log.verbose(logLevel, "Configuring video encoder", config);
951
1030
  encoder.configure(config);
952
1031
  let framesProcessed = 0;
953
1032
  const encodeFrame = async (frame) => {
@@ -955,14 +1034,14 @@ var createVideoEncoder = ({
955
1034
  return;
956
1035
  }
957
1036
  await ioSynchronizer.waitFor({
958
- unemitted: 2,
959
- _unprocessed: 2
1037
+ unemitted: 10,
1038
+ _unprocessed: 10
960
1039
  });
961
1040
  if (encoder.state === "closed") {
962
1041
  return;
963
1042
  }
964
1043
  const keyFrame = framesProcessed % 40 === 0;
965
- encoder.encode(frame, {
1044
+ encoder.encode(convertToCorrectVideoFrame({ videoFrame: frame, outputCodec }), {
966
1045
  keyFrame
967
1046
  });
968
1047
  ioSynchronizer.inputItem(frame.timestamp, keyFrame);
@@ -992,7 +1071,6 @@ var makeVideoTrackHandler = ({
992
1071
  onVideoFrame,
993
1072
  onMediaStateUpdate,
994
1073
  abortConversion,
995
- convertMediaState,
996
1074
  controller,
997
1075
  defaultVideoCodec,
998
1076
  onVideoTrack,
@@ -1033,8 +1111,12 @@ var makeVideoTrackHandler = ({
1033
1111
  timescale: track.timescale,
1034
1112
  codecPrivate: track.codecPrivate
1035
1113
  });
1036
- convertMediaState.decodedVideoFrames++;
1037
- onMediaStateUpdate?.({ ...convertMediaState });
1114
+ onMediaStateUpdate?.((prevState) => {
1115
+ return {
1116
+ ...prevState,
1117
+ decodedVideoFrames: prevState.decodedVideoFrames + 1
1118
+ };
1119
+ });
1038
1120
  };
1039
1121
  }
1040
1122
  const videoEncoderConfig = await getVideoEncoderConfig({
@@ -1071,8 +1153,12 @@ var makeVideoTrackHandler = ({
1071
1153
  timescale: track.timescale,
1072
1154
  codecPrivate: arrayBufferToUint8Array(metadata?.decoderConfig?.description ?? null)
1073
1155
  });
1074
- convertMediaState.encodedVideoFrames++;
1075
- onMediaStateUpdate?.({ ...convertMediaState });
1156
+ onMediaStateUpdate?.((prevState) => {
1157
+ return {
1158
+ ...prevState,
1159
+ encodedVideoFrames: prevState.encodedVideoFrames + 1
1160
+ };
1161
+ });
1076
1162
  },
1077
1163
  onError: (err) => {
1078
1164
  abortConversion(new error_cause_default(`Video encoder of track ${track.trackId} failed (see .cause of this error)`, {
@@ -1081,18 +1167,18 @@ var makeVideoTrackHandler = ({
1081
1167
  },
1082
1168
  signal: controller.signal,
1083
1169
  config: videoEncoderConfig,
1084
- logLevel
1170
+ logLevel,
1171
+ outputCodec: videoOperation.videoCodec
1085
1172
  });
1086
1173
  const videoDecoder = createVideoDecoder({
1087
1174
  config: videoDecoderConfig,
1088
1175
  onFrame: async (frame) => {
1089
1176
  await onFrame({
1090
- convertMediaState,
1091
1177
  frame,
1092
- onMediaStateUpdate,
1093
1178
  track,
1094
1179
  videoEncoder,
1095
- onVideoFrame
1180
+ onVideoFrame,
1181
+ outputCodec: videoOperation.videoCodec
1096
1182
  });
1097
1183
  },
1098
1184
  onError: (err) => {
@@ -1117,11 +1203,63 @@ var makeVideoTrackHandler = ({
1117
1203
  };
1118
1204
  };
1119
1205
 
1206
+ // src/throttled-state-update.ts
1207
+ var throttledStateUpdate = ({
1208
+ updateFn,
1209
+ everyMilliseconds,
1210
+ signal
1211
+ }) => {
1212
+ let currentState = {
1213
+ decodedAudioFrames: 0,
1214
+ decodedVideoFrames: 0,
1215
+ encodedVideoFrames: 0,
1216
+ encodedAudioFrames: 0,
1217
+ bytesWritten: 0,
1218
+ millisecondsWritten: 0,
1219
+ expectedOutputDurationInMs: null,
1220
+ overallProgress: 0
1221
+ };
1222
+ if (!updateFn) {
1223
+ return {
1224
+ get: () => currentState,
1225
+ update: null,
1226
+ stopAndGetLastProgress: () => {
1227
+ }
1228
+ };
1229
+ }
1230
+ let lastUpdated = null;
1231
+ const callUpdateIfChanged = () => {
1232
+ if (currentState === lastUpdated) {
1233
+ return;
1234
+ }
1235
+ updateFn(currentState);
1236
+ lastUpdated = currentState;
1237
+ };
1238
+ const interval = setInterval(() => {
1239
+ callUpdateIfChanged();
1240
+ }, everyMilliseconds);
1241
+ const onAbort = () => {
1242
+ clearInterval(interval);
1243
+ };
1244
+ signal.addEventListener("abort", onAbort, { once: true });
1245
+ return {
1246
+ get: () => currentState,
1247
+ update: (fn) => {
1248
+ currentState = fn(currentState);
1249
+ },
1250
+ stopAndGetLastProgress: () => {
1251
+ clearInterval(interval);
1252
+ signal.removeEventListener("abort", onAbort);
1253
+ return currentState;
1254
+ }
1255
+ };
1256
+ };
1257
+
1120
1258
  // src/convert-media.ts
1121
1259
  var convertMedia = async function({
1122
1260
  src,
1123
1261
  onVideoFrame,
1124
- onMediaStateUpdate: onMediaStateDoNoCallDirectly,
1262
+ onProgress: onProgressDoNotCallDirectly,
1125
1263
  audioCodec,
1126
1264
  container,
1127
1265
  videoCodec,
@@ -1132,6 +1270,7 @@ var convertMedia = async function({
1132
1270
  fields,
1133
1271
  logLevel = "info",
1134
1272
  writer,
1273
+ progressIntervalInMs,
1135
1274
  ...more
1136
1275
  }) {
1137
1276
  if (userPassedAbortSignal?.aborted) {
@@ -1155,47 +1294,45 @@ var convertMedia = async function({
1155
1294
  abortConversion(new error_cause_default("Conversion aborted by user"));
1156
1295
  };
1157
1296
  userPassedAbortSignal?.addEventListener("abort", onUserAbort);
1158
- const convertMediaState = {
1159
- decodedAudioFrames: 0,
1160
- decodedVideoFrames: 0,
1161
- encodedVideoFrames: 0,
1162
- encodedAudioFrames: 0,
1163
- bytesWritten: 0,
1164
- millisecondsWritten: 0,
1165
- expectedOutputDurationInMs: null,
1166
- overallProgress: 0
1167
- };
1168
- const onMediaStateUpdate = (newState) => {
1169
- if (controller.signal.aborted) {
1170
- return;
1171
- }
1172
- onMediaStateDoNoCallDirectly?.(newState);
1173
- };
1174
1297
  const creator = container === "webm" ? MediaParserInternals4.createMatroskaMedia : MediaParserInternals4.createIsoBaseMedia;
1298
+ const throttledState = throttledStateUpdate({
1299
+ updateFn: onProgressDoNotCallDirectly ?? null,
1300
+ everyMilliseconds: progressIntervalInMs ?? 100,
1301
+ signal: controller.signal
1302
+ });
1175
1303
  const state = await creator({
1304
+ filename: generateOutputFilename(src, container),
1176
1305
  writer: await autoSelectWriter(writer, logLevel),
1177
1306
  onBytesProgress: (bytesWritten) => {
1178
- convertMediaState.bytesWritten = bytesWritten;
1179
- onMediaStateUpdate?.(convertMediaState);
1307
+ throttledState.update?.((prevState) => {
1308
+ return {
1309
+ ...prevState,
1310
+ bytesWritten
1311
+ };
1312
+ });
1180
1313
  },
1181
1314
  onMillisecondsProgress: (millisecondsWritten) => {
1182
- if (millisecondsWritten > convertMediaState.millisecondsWritten) {
1183
- convertMediaState.millisecondsWritten = millisecondsWritten;
1184
- convertMediaState.overallProgress = calculateProgress({
1185
- millisecondsWritten: convertMediaState.millisecondsWritten,
1186
- expectedOutputDurationInMs: convertMediaState.expectedOutputDurationInMs
1187
- });
1188
- onMediaStateUpdate?.(convertMediaState);
1189
- }
1315
+ throttledState.update?.((prevState) => {
1316
+ if (millisecondsWritten > prevState.millisecondsWritten) {
1317
+ return {
1318
+ ...prevState,
1319
+ millisecondsWritten,
1320
+ overallProgress: calculateProgress({
1321
+ millisecondsWritten: prevState.millisecondsWritten,
1322
+ expectedOutputDurationInMs: prevState.expectedOutputDurationInMs
1323
+ })
1324
+ };
1325
+ }
1326
+ return prevState;
1327
+ });
1190
1328
  },
1191
1329
  logLevel
1192
1330
  });
1193
1331
  const onVideoTrack = makeVideoTrackHandler({
1194
1332
  state,
1195
1333
  onVideoFrame: onVideoFrame ?? null,
1196
- onMediaStateUpdate: onMediaStateUpdate ?? null,
1334
+ onMediaStateUpdate: throttledState.update ?? null,
1197
1335
  abortConversion,
1198
- convertMediaState,
1199
1336
  controller,
1200
1337
  defaultVideoCodec: videoCodec ?? null,
1201
1338
  onVideoTrack: userVideoResolver ?? null,
@@ -1206,8 +1343,7 @@ var convertMedia = async function({
1206
1343
  abortConversion,
1207
1344
  defaultAudioCodec: audioCodec ?? null,
1208
1345
  controller,
1209
- convertMediaState,
1210
- onMediaStateUpdate: onMediaStateUpdate ?? null,
1346
+ onMediaStateUpdate: throttledState.update ?? null,
1211
1347
  state,
1212
1348
  onAudioTrack: userAudioResolver ?? null,
1213
1349
  logLevel,
@@ -1234,24 +1370,36 @@ var convertMedia = async function({
1234
1370
  casted.onDurationInSeconds(durationInSeconds);
1235
1371
  }
1236
1372
  const expectedOutputDurationInMs = durationInSeconds * 1000;
1237
- convertMediaState.expectedOutputDurationInMs = expectedOutputDurationInMs;
1238
- convertMediaState.overallProgress = calculateProgress({
1239
- millisecondsWritten: convertMediaState.millisecondsWritten,
1240
- expectedOutputDurationInMs
1373
+ throttledState.update?.((prevState) => {
1374
+ return {
1375
+ ...prevState,
1376
+ expectedOutputDurationInMs,
1377
+ overallProgress: calculateProgress({
1378
+ millisecondsWritten: prevState.millisecondsWritten,
1379
+ expectedOutputDurationInMs
1380
+ })
1381
+ };
1241
1382
  });
1242
- onMediaStateUpdate(convertMediaState);
1243
1383
  }
1244
1384
  }).then(() => {
1245
1385
  return state.waitForFinish();
1246
1386
  }).then(() => {
1247
- resolve({ save: state.save, remove: state.remove });
1387
+ resolve({
1388
+ save: state.save,
1389
+ remove: state.remove,
1390
+ finalState: throttledState.get()
1391
+ });
1248
1392
  }).catch((err) => {
1249
1393
  reject(err);
1394
+ }).finally(() => {
1395
+ throttledState.stopAndGetLastProgress();
1250
1396
  });
1251
1397
  return getPromiseToImmediatelyReturn().finally(() => {
1252
1398
  userPassedAbortSignal?.removeEventListener("abort", onUserAbort);
1253
1399
  });
1254
1400
  };
1401
+ // src/index.ts
1402
+ setRemotionImported();
1255
1403
  export {
1256
1404
  getDefaultVideoCodec,
1257
1405
  getDefaultAudioCodec,
@@ -0,0 +1,2 @@
1
+ import type { ConvertMediaContainer } from './codec-id';
2
+ export declare const generateOutputFilename: (source: string | Blob, container: ConvertMediaContainer) => string;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateOutputFilename = void 0;
4
+ const generateOutputFilename = (source, container) => {
5
+ const filename = typeof source === 'string'
6
+ ? source
7
+ : source instanceof File
8
+ ? source.name
9
+ : 'converted';
10
+ const behindSlash = filename.split('/').pop();
11
+ const withoutExtension = behindSlash.split('.').slice(0, -1).join('.');
12
+ return `${withoutExtension}.${container}`;
13
+ };
14
+ exports.generateOutputFilename = generateOutputFilename;
package/dist/index.d.ts CHANGED
@@ -1,16 +1,16 @@
1
- export { WebCodecsAudioDecoder, createAudioDecoder } from './audio-decoder';
2
- export { WebCodecsAudioEncoder, createAudioEncoder } from './audio-encoder';
1
+ export { createAudioDecoder, WebCodecsAudioDecoder } from './audio-decoder';
2
+ export { createAudioEncoder, WebCodecsAudioEncoder } from './audio-encoder';
3
3
  export { canCopyAudioTrack } from './can-copy-audio-track';
4
4
  export { canCopyVideoTrack } from './can-copy-video-track';
5
5
  export { canReencodeAudioTrack } from './can-reencode-audio-track';
6
6
  export { canReencodeVideoTrack } from './can-reencode-video-track';
7
7
  export { ConvertMediaAudioCodec, ConvertMediaContainer, ConvertMediaVideoCodec, getAvailableAudioCodecs, getAvailableContainers, getAvailableVideoCodecs, } from './codec-id';
8
- export { ConvertMediaOnMediaStateUpdate, ConvertMediaOnVideoFrame, ConvertMediaResult, ConvertMediaState, convertMedia, } from './convert-media';
8
+ export { convertMedia, ConvertMediaOnProgress, ConvertMediaOnVideoFrame, ConvertMediaProgress, ConvertMediaResult, } from './convert-media';
9
9
  export { defaultOnAudioTrackHandler } from './default-on-audio-track-handler';
10
10
  export { defaultOnVideoTrackHandler } from './default-on-video-track-handler';
11
11
  export { getDefaultAudioCodec } from './get-default-audio-codec';
12
12
  export { getDefaultVideoCodec } from './get-default-video-codec';
13
13
  export { AudioOperation, ConvertMediaOnAudioTrackHandler, } from './on-audio-track-handler';
14
14
  export { ConvertMediaOnVideoTrackHandler, VideoOperation, } from './on-video-track-handler';
15
- export { WebCodecsVideoDecoder, createVideoDecoder } from './video-decoder';
16
- export { WebCodecsVideoEncoder, createVideoEncoder } from './video-encoder';
15
+ export { createVideoDecoder, WebCodecsVideoDecoder } from './video-decoder';
16
+ export { createVideoEncoder, WebCodecsVideoEncoder } from './video-encoder';
package/dist/index.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createVideoEncoder = exports.createVideoDecoder = exports.getDefaultVideoCodec = exports.getDefaultAudioCodec = exports.defaultOnVideoTrackHandler = exports.defaultOnAudioTrackHandler = exports.convertMedia = exports.getAvailableVideoCodecs = exports.getAvailableContainers = exports.getAvailableAudioCodecs = exports.canReencodeVideoTrack = exports.canReencodeAudioTrack = exports.canCopyVideoTrack = exports.canCopyAudioTrack = exports.createAudioEncoder = exports.createAudioDecoder = void 0;
4
+ const set_remotion_imported_1 = require("./set-remotion-imported");
4
5
  var audio_decoder_1 = require("./audio-decoder");
5
6
  Object.defineProperty(exports, "createAudioDecoder", { enumerable: true, get: function () { return audio_decoder_1.createAudioDecoder; } });
6
7
  var audio_encoder_1 = require("./audio-encoder");
@@ -31,3 +32,4 @@ var video_decoder_1 = require("./video-decoder");
31
32
  Object.defineProperty(exports, "createVideoDecoder", { enumerable: true, get: function () { return video_decoder_1.createVideoDecoder; } });
32
33
  var video_encoder_1 = require("./video-encoder");
33
34
  Object.defineProperty(exports, "createVideoEncoder", { enumerable: true, get: function () { return video_encoder_1.createVideoEncoder; } });
35
+ (0, set_remotion_imported_1.setRemotionImported)();
@@ -1,15 +1,14 @@
1
1
  import type { LogLevel, MediaFn, OnAudioTrack } from '@remotion/media-parser';
2
2
  import type { ConvertMediaAudioCodec, ConvertMediaContainer } from './codec-id';
3
- import type { ConvertMediaState } from './convert-media';
4
3
  import Error from './error-cause';
5
4
  import type { ConvertMediaOnAudioTrackHandler } from './on-audio-track-handler';
6
- export declare const makeAudioTrackHandler: ({ state, defaultAudioCodec: audioCodec, convertMediaState, controller, abortConversion, onMediaStateUpdate, onAudioTrack, logLevel, container, }: {
5
+ import type { ConvertMediaProgressFn } from './throttled-state-update';
6
+ export declare const makeAudioTrackHandler: ({ state, defaultAudioCodec: audioCodec, controller, abortConversion, onMediaStateUpdate, onAudioTrack, logLevel, container, }: {
7
7
  state: MediaFn;
8
8
  defaultAudioCodec: ConvertMediaAudioCodec | null;
9
- convertMediaState: ConvertMediaState;
10
9
  controller: AbortController;
11
10
  abortConversion: (errCause: Error) => void;
12
- onMediaStateUpdate: null | ((state: ConvertMediaState) => void);
11
+ onMediaStateUpdate: null | ConvertMediaProgressFn;
13
12
  onAudioTrack: ConvertMediaOnAudioTrackHandler | null;
14
13
  logLevel: LogLevel;
15
14
  container: ConvertMediaContainer;
@@ -12,7 +12,7 @@ const convert_encoded_chunk_1 = require("./convert-encoded-chunk");
12
12
  const default_on_audio_track_handler_1 = require("./default-on-audio-track-handler");
13
13
  const error_cause_1 = __importDefault(require("./error-cause"));
14
14
  const log_1 = require("./log");
15
- const makeAudioTrackHandler = ({ state, defaultAudioCodec: audioCodec, convertMediaState, controller, abortConversion, onMediaStateUpdate, onAudioTrack, logLevel, container, }) => async (track) => {
15
+ const makeAudioTrackHandler = ({ state, defaultAudioCodec: audioCodec, controller, abortConversion, onMediaStateUpdate, onAudioTrack, logLevel, container, }) => async (track) => {
16
16
  const audioOperation = await (onAudioTrack !== null && onAudioTrack !== void 0 ? onAudioTrack : default_on_audio_track_handler_1.defaultOnAudioTrackHandler)({
17
17
  defaultAudioCodec: audioCodec,
18
18
  track,
@@ -43,8 +43,12 @@ const makeAudioTrackHandler = ({ state, defaultAudioCodec: audioCodec, convertMe
43
43
  timescale: track.timescale,
44
44
  codecPrivate: track.codecPrivate,
45
45
  });
46
- convertMediaState.encodedAudioFrames++;
47
- onMediaStateUpdate === null || onMediaStateUpdate === void 0 ? void 0 : onMediaStateUpdate({ ...convertMediaState });
46
+ onMediaStateUpdate === null || onMediaStateUpdate === void 0 ? void 0 : onMediaStateUpdate((prevState) => {
47
+ return {
48
+ ...prevState,
49
+ encodedAudioFrames: prevState.encodedAudioFrames + 1,
50
+ };
51
+ });
48
52
  };
49
53
  }
50
54
  const audioEncoderConfig = await (0, audio_encoder_config_1.getAudioEncoderConfig)({
@@ -92,8 +96,12 @@ const makeAudioTrackHandler = ({ state, defaultAudioCodec: audioCodec, convertMe
92
96
  timescale: track.timescale,
93
97
  codecPrivate,
94
98
  });
95
- convertMediaState.encodedAudioFrames++;
96
- onMediaStateUpdate === null || onMediaStateUpdate === void 0 ? void 0 : onMediaStateUpdate({ ...convertMediaState });
99
+ onMediaStateUpdate === null || onMediaStateUpdate === void 0 ? void 0 : onMediaStateUpdate((prevState) => {
100
+ return {
101
+ ...prevState,
102
+ encodedAudioFrames: prevState.encodedAudioFrames + 1,
103
+ };
104
+ });
97
105
  },
98
106
  onError: (err) => {
99
107
  abortConversion(new error_cause_1.default(`Audio encoder of ${track.trackId} failed (see .cause of this error)`, {
@@ -108,8 +116,12 @@ const makeAudioTrackHandler = ({ state, defaultAudioCodec: audioCodec, convertMe
108
116
  const audioDecoder = (0, audio_decoder_1.createAudioDecoder)({
109
117
  onFrame: async (frame) => {
110
118
  await audioEncoder.encodeFrame(frame);
111
- convertMediaState.decodedAudioFrames++;
112
- onMediaStateUpdate === null || onMediaStateUpdate === void 0 ? void 0 : onMediaStateUpdate(convertMediaState);
119
+ onMediaStateUpdate === null || onMediaStateUpdate === void 0 ? void 0 : onMediaStateUpdate((prevState) => {
120
+ return {
121
+ ...prevState,
122
+ decodedAudioFrames: prevState.decodedAudioFrames + 1,
123
+ };
124
+ });
113
125
  frame.close();
114
126
  },
115
127
  onError(error) {
@@ -1,11 +1,11 @@
1
1
  import type { VideoTrack } from '@remotion/media-parser';
2
- import type { ConvertMediaOnMediaStateUpdate, ConvertMediaOnVideoFrame, ConvertMediaState } from './convert-media';
2
+ import type { ConvertMediaVideoCodec } from './codec-id';
3
+ import type { ConvertMediaOnVideoFrame } from './convert-media';
3
4
  import type { WebCodecsVideoEncoder } from './video-encoder';
4
- export declare const onFrame: ({ frame, onVideoFrame, videoEncoder, onMediaStateUpdate, track, convertMediaState, }: {
5
+ export declare const onFrame: ({ frame, onVideoFrame, videoEncoder, track, outputCodec, }: {
5
6
  frame: VideoFrame;
6
7
  onVideoFrame: ConvertMediaOnVideoFrame | null;
7
8
  videoEncoder: WebCodecsVideoEncoder;
8
- onMediaStateUpdate: ConvertMediaOnMediaStateUpdate | null;
9
9
  track: VideoTrack;
10
- convertMediaState: ConvertMediaState;
10
+ outputCodec: ConvertMediaVideoCodec;
11
11
  }) => Promise<void>;
package/dist/on-frame.js CHANGED
@@ -1,13 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.onFrame = void 0;
4
- const onFrame = async ({ frame, onVideoFrame, videoEncoder, onMediaStateUpdate, track, convertMediaState, }) => {
4
+ const convert_to_correct_videoframe_1 = require("./convert-to-correct-videoframe");
5
+ const onFrame = async ({ frame, onVideoFrame, videoEncoder, track, outputCodec, }) => {
5
6
  const newFrame = onVideoFrame ? await onVideoFrame({ frame, track }) : frame;
6
- if (newFrame.codedHeight !== frame.codedHeight) {
7
- throw new Error(`Returned VideoFrame of track ${track.trackId} has different codedHeight (${newFrame.codedHeight}) than the input frame (${frame.codedHeight})`);
7
+ if (newFrame.codedHeight !== frame.displayHeight) {
8
+ throw new Error(`Returned VideoFrame of track ${track.trackId} has different codedHeight (${newFrame.codedHeight}) than the input frame displayHeight (${frame.displayHeight})`);
8
9
  }
9
- if (newFrame.codedWidth !== frame.codedWidth) {
10
- throw new Error(`Returned VideoFrame of track ${track.trackId} has different codedWidth (${newFrame.codedWidth}) than the input frame (${frame.codedWidth})`);
10
+ if (newFrame.codedWidth !== frame.displayWidth) {
11
+ throw new Error(`Returned VideoFrame of track ${track.trackId} has different codedWidth (${newFrame.codedWidth}) than the input frame displayWidth (${frame.displayWidth})`);
11
12
  }
12
13
  if (newFrame.displayWidth !== frame.displayWidth) {
13
14
  throw new Error(`Returned VideoFrame of track ${track.trackId} has different displayWidth (${newFrame.displayWidth}) than the input frame (${newFrame.displayHeight})`);
@@ -21,12 +22,17 @@ const onFrame = async ({ frame, onVideoFrame, videoEncoder, onMediaStateUpdate,
21
22
  if (newFrame.duration !== frame.duration) {
22
23
  throw new Error(`Returned VideoFrame of track ${track.trackId} has different duration (${newFrame.duration}) than the input frame (${newFrame.duration}). When calling new VideoFrame(), pass {duration: frame.duration} as second argument`);
23
24
  }
24
- await videoEncoder.encodeFrame(newFrame, newFrame.timestamp);
25
- convertMediaState.decodedVideoFrames++;
26
- onMediaStateUpdate === null || onMediaStateUpdate === void 0 ? void 0 : onMediaStateUpdate({ ...convertMediaState });
27
- newFrame.close();
25
+ const fixedFrame = (0, convert_to_correct_videoframe_1.convertToCorrectVideoFrame)({
26
+ videoFrame: newFrame,
27
+ outputCodec,
28
+ });
29
+ await videoEncoder.encodeFrame(fixedFrame, fixedFrame.timestamp);
30
+ fixedFrame.close();
28
31
  if (frame !== newFrame) {
29
32
  frame.close();
30
33
  }
34
+ if (fixedFrame !== newFrame) {
35
+ fixedFrame.close();
36
+ }
31
37
  };
32
38
  exports.onFrame = onFrame;
@@ -1,14 +1,14 @@
1
1
  import type { LogLevel, MediaFn, OnVideoTrack } from '@remotion/media-parser';
2
2
  import type { ConvertMediaContainer, ConvertMediaVideoCodec } from './codec-id';
3
- import type { ConvertMediaOnMediaStateUpdate, ConvertMediaOnVideoFrame, ConvertMediaState } from './convert-media';
3
+ import type { ConvertMediaOnVideoFrame } from './convert-media';
4
4
  import Error from './error-cause';
5
5
  import type { ConvertMediaOnVideoTrackHandler } from './on-video-track-handler';
6
- export declare const makeVideoTrackHandler: ({ state, onVideoFrame, onMediaStateUpdate, abortConversion, convertMediaState, controller, defaultVideoCodec, onVideoTrack, logLevel, container, }: {
6
+ import type { ConvertMediaProgressFn } from './throttled-state-update';
7
+ export declare const makeVideoTrackHandler: ({ state, onVideoFrame, onMediaStateUpdate, abortConversion, controller, defaultVideoCodec, onVideoTrack, logLevel, container, }: {
7
8
  state: MediaFn;
8
9
  onVideoFrame: null | ConvertMediaOnVideoFrame;
9
- onMediaStateUpdate: null | ConvertMediaOnMediaStateUpdate;
10
+ onMediaStateUpdate: null | ConvertMediaProgressFn;
10
11
  abortConversion: (errCause: Error) => void;
11
- convertMediaState: ConvertMediaState;
12
12
  controller: AbortController;
13
13
  defaultVideoCodec: ConvertMediaVideoCodec | null;
14
14
  onVideoTrack: ConvertMediaOnVideoTrackHandler | null;
@@ -14,7 +14,7 @@ const video_decoder_1 = require("./video-decoder");
14
14
  const video_decoder_config_1 = require("./video-decoder-config");
15
15
  const video_encoder_1 = require("./video-encoder");
16
16
  const video_encoder_config_1 = require("./video-encoder-config");
17
- const makeVideoTrackHandler = ({ state, onVideoFrame, onMediaStateUpdate, abortConversion, convertMediaState, controller, defaultVideoCodec, onVideoTrack, logLevel, container, }) => async (track) => {
17
+ const makeVideoTrackHandler = ({ state, onVideoFrame, onMediaStateUpdate, abortConversion, controller, defaultVideoCodec, onVideoTrack, logLevel, container, }) => async (track) => {
18
18
  if (controller.signal.aborted) {
19
19
  throw new error_cause_1.default('Aborted');
20
20
  }
@@ -49,8 +49,12 @@ const makeVideoTrackHandler = ({ state, onVideoFrame, onMediaStateUpdate, abortC
49
49
  timescale: track.timescale,
50
50
  codecPrivate: track.codecPrivate,
51
51
  });
52
- convertMediaState.decodedVideoFrames++;
53
- onMediaStateUpdate === null || onMediaStateUpdate === void 0 ? void 0 : onMediaStateUpdate({ ...convertMediaState });
52
+ onMediaStateUpdate === null || onMediaStateUpdate === void 0 ? void 0 : onMediaStateUpdate((prevState) => {
53
+ return {
54
+ ...prevState,
55
+ decodedVideoFrames: prevState.decodedVideoFrames + 1,
56
+ };
57
+ });
54
58
  };
55
59
  }
56
60
  const videoEncoderConfig = await (0, video_encoder_config_1.getVideoEncoderConfig)({
@@ -88,8 +92,12 @@ const makeVideoTrackHandler = ({ state, onVideoFrame, onMediaStateUpdate, abortC
88
92
  timescale: track.timescale,
89
93
  codecPrivate: (0, arraybuffer_to_uint8_array_1.arrayBufferToUint8Array)(((_b = (_a = metadata === null || metadata === void 0 ? void 0 : metadata.decoderConfig) === null || _a === void 0 ? void 0 : _a.description) !== null && _b !== void 0 ? _b : null)),
90
94
  });
91
- convertMediaState.encodedVideoFrames++;
92
- onMediaStateUpdate === null || onMediaStateUpdate === void 0 ? void 0 : onMediaStateUpdate({ ...convertMediaState });
95
+ onMediaStateUpdate === null || onMediaStateUpdate === void 0 ? void 0 : onMediaStateUpdate((prevState) => {
96
+ return {
97
+ ...prevState,
98
+ encodedVideoFrames: prevState.encodedVideoFrames + 1,
99
+ };
100
+ });
93
101
  },
94
102
  onError: (err) => {
95
103
  abortConversion(new error_cause_1.default(`Video encoder of track ${track.trackId} failed (see .cause of this error)`, {
@@ -99,17 +107,17 @@ const makeVideoTrackHandler = ({ state, onVideoFrame, onMediaStateUpdate, abortC
99
107
  signal: controller.signal,
100
108
  config: videoEncoderConfig,
101
109
  logLevel,
110
+ outputCodec: videoOperation.videoCodec,
102
111
  });
103
112
  const videoDecoder = (0, video_decoder_1.createVideoDecoder)({
104
113
  config: videoDecoderConfig,
105
114
  onFrame: async (frame) => {
106
115
  await (0, on_frame_1.onFrame)({
107
- convertMediaState,
108
116
  frame,
109
- onMediaStateUpdate,
110
117
  track,
111
118
  videoEncoder,
112
119
  onVideoFrame,
120
+ outputCodec: videoOperation.videoCodec,
113
121
  });
114
122
  },
115
123
  onError: (err) => {
@@ -0,0 +1,6 @@
1
+ declare global {
2
+ interface Window {
3
+ remotion_imported: string | boolean;
4
+ }
5
+ }
6
+ export declare const setRemotionImported: () => void;
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.setRemotionImported = void 0;
4
+ const media_parser_1 = require("@remotion/media-parser");
5
+ // We set the `window.remotion_imported` variable for the sole purpose
6
+ // of being picked up by Wappalyzer.
7
+ // The Wappalyzer Chrome extension is used to detect the technologies
8
+ // used on websites, and it looks for this variable.
9
+ // Remotion is a customer of Wappalyzer and buys a list of domains
10
+ // where Remotion is used from time to time.
11
+ // Remotion uses this data to ensure companies which are required to get
12
+ // a company license for this pacakge are actually doing so.
13
+ const setRemotionImported = () => {
14
+ if (typeof globalThis === 'undefined') {
15
+ return;
16
+ }
17
+ if (globalThis.remotion_imported) {
18
+ return;
19
+ }
20
+ globalThis.remotion_imported = media_parser_1.VERSION;
21
+ if (typeof window !== 'undefined') {
22
+ window.remotion_imported = `${media_parser_1.VERSION}-webcodecs`;
23
+ }
24
+ };
25
+ exports.setRemotionImported = setRemotionImported;
@@ -0,0 +1,13 @@
1
+ import type { ConvertMediaOnProgress, ConvertMediaProgress } from './convert-media';
2
+ export type ConvertMediaProgressFn = (state: (prevState: ConvertMediaProgress) => ConvertMediaProgress) => void;
3
+ type ReturnType = {
4
+ get: () => ConvertMediaProgress;
5
+ update: ConvertMediaProgressFn | null;
6
+ stopAndGetLastProgress: () => void;
7
+ };
8
+ export declare const throttledStateUpdate: ({ updateFn, everyMilliseconds, signal, }: {
9
+ updateFn: ConvertMediaOnProgress | null;
10
+ everyMilliseconds: number;
11
+ signal: AbortSignal;
12
+ }) => ReturnType;
13
+ export {};
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.throttledStateUpdate = void 0;
4
+ const throttledStateUpdate = ({ updateFn, everyMilliseconds, signal, }) => {
5
+ let currentState = {
6
+ decodedAudioFrames: 0,
7
+ decodedVideoFrames: 0,
8
+ encodedVideoFrames: 0,
9
+ encodedAudioFrames: 0,
10
+ bytesWritten: 0,
11
+ millisecondsWritten: 0,
12
+ expectedOutputDurationInMs: null,
13
+ overallProgress: 0,
14
+ };
15
+ if (!updateFn) {
16
+ return {
17
+ get: () => currentState,
18
+ update: null,
19
+ stopAndGetLastProgress: () => { },
20
+ };
21
+ }
22
+ let lastUpdated = null;
23
+ const callUpdateIfChanged = () => {
24
+ if (currentState === lastUpdated) {
25
+ return;
26
+ }
27
+ updateFn(currentState);
28
+ lastUpdated = currentState;
29
+ };
30
+ const interval = setInterval(() => {
31
+ callUpdateIfChanged();
32
+ }, everyMilliseconds);
33
+ const onAbort = () => {
34
+ clearInterval(interval);
35
+ };
36
+ signal.addEventListener('abort', onAbort, { once: true });
37
+ return {
38
+ get: () => currentState,
39
+ update: (fn) => {
40
+ currentState = fn(currentState);
41
+ },
42
+ stopAndGetLastProgress: () => {
43
+ clearInterval(interval);
44
+ signal.removeEventListener('abort', onAbort);
45
+ return currentState;
46
+ },
47
+ };
48
+ };
49
+ exports.throttledStateUpdate = throttledStateUpdate;
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.getVideoEncoderConfig = void 0;
4
+ const browser_quirks_1 = require("./browser-quirks");
4
5
  const choose_correct_avc1_profile_1 = require("./choose-correct-avc1-profile");
5
6
  const getVideoEncoderConfig = async ({ codec, height, width, fps, }) => {
6
7
  if (typeof VideoEncoder === 'undefined') {
@@ -14,6 +15,7 @@ const getVideoEncoderConfig = async ({ codec, height, width, fps, }) => {
14
15
  : codec,
15
16
  height,
16
17
  width,
18
+ bitrate: (0, browser_quirks_1.isSafari)() ? 3000000 : undefined,
17
19
  };
18
20
  const hardware = {
19
21
  ...config,
@@ -1,14 +1,16 @@
1
1
  import type { LogLevel } from '@remotion/media-parser';
2
+ import type { ConvertMediaVideoCodec } from './codec-id';
2
3
  export type WebCodecsVideoEncoder = {
3
4
  encodeFrame: (videoFrame: VideoFrame, timestamp: number) => Promise<void>;
4
5
  waitForFinish: () => Promise<void>;
5
6
  close: () => void;
6
7
  flush: () => Promise<void>;
7
8
  };
8
- export declare const createVideoEncoder: ({ onChunk, onError, signal, config, logLevel, }: {
9
+ export declare const createVideoEncoder: ({ onChunk, onError, signal, config, logLevel, outputCodec, }: {
9
10
  onChunk: (chunk: EncodedVideoChunk, metadata: EncodedVideoChunkMetadata | null) => Promise<void>;
10
11
  onError: (error: DOMException) => void;
11
12
  signal: AbortSignal;
12
13
  config: VideoEncoderConfig;
13
14
  logLevel: LogLevel;
15
+ outputCodec: ConvertMediaVideoCodec;
14
16
  }) => WebCodecsVideoEncoder;
@@ -1,8 +1,10 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.createVideoEncoder = void 0;
4
+ const convert_to_correct_videoframe_1 = require("./convert-to-correct-videoframe");
4
5
  const io_synchronizer_1 = require("./io-manager/io-synchronizer");
5
- const createVideoEncoder = ({ onChunk, onError, signal, config, logLevel, }) => {
6
+ const log_1 = require("./log");
7
+ const createVideoEncoder = ({ onChunk, onError, signal, config, logLevel, outputCodec, }) => {
6
8
  if (signal.aborted) {
7
9
  throw new Error('Not creating video encoder, already aborted');
8
10
  }
@@ -44,6 +46,7 @@ const createVideoEncoder = ({ onChunk, onError, signal, config, logLevel, }) =>
44
46
  close();
45
47
  };
46
48
  signal.addEventListener('abort', onAbort);
49
+ log_1.Log.verbose(logLevel, 'Configuring video encoder', config);
47
50
  encoder.configure(config);
48
51
  let framesProcessed = 0;
49
52
  const encodeFrame = async (frame) => {
@@ -51,15 +54,16 @@ const createVideoEncoder = ({ onChunk, onError, signal, config, logLevel, }) =>
51
54
  return;
52
55
  }
53
56
  await ioSynchronizer.waitFor({
54
- unemitted: 2,
55
- _unprocessed: 2,
57
+ // Firefox stalls if too few frames are passed
58
+ unemitted: 10,
59
+ _unprocessed: 10,
56
60
  });
57
61
  // @ts-expect-error - can have changed in the meanwhile
58
62
  if (encoder.state === 'closed') {
59
63
  return;
60
64
  }
61
65
  const keyFrame = framesProcessed % 40 === 0;
62
- encoder.encode(frame, {
66
+ encoder.encode((0, convert_to_correct_videoframe_1.convertToCorrectVideoFrame)({ videoFrame: frame, outputCodec }), {
63
67
  keyFrame,
64
68
  });
65
69
  ioSynchronizer.inputItem(frame.timestamp, keyFrame);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@remotion/webcodecs",
3
- "version": "4.0.230",
3
+ "version": "4.0.231",
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "module": "dist/esm/index.mjs",
@@ -17,19 +17,20 @@
17
17
  "author": "Jonny Burger <jonny@remotion.dev>",
18
18
  "license": "Remotion License (See https://remotion.dev/docs/webcodecs#license)",
19
19
  "dependencies": {
20
- "@remotion/media-parser": "4.0.230"
20
+ "@remotion/media-parser": "4.0.231"
21
21
  },
22
22
  "peerDependencies": {},
23
23
  "devDependencies": {
24
24
  "@types/dom-webcodecs": "0.1.11",
25
25
  "eslint": "9.14.0",
26
- "@remotion/eslint-config-internal": "4.0.230"
26
+ "@remotion/eslint-config-internal": "4.0.231"
27
27
  },
28
28
  "keywords": [],
29
29
  "publishConfig": {
30
30
  "access": "public"
31
31
  },
32
32
  "description": "Media conversion in the browser",
33
+ "homepage": "https://remotion.dev/webcodecs",
33
34
  "scripts": {
34
35
  "formatting": "prettier src --check",
35
36
  "lint": "eslint src",