@remotion/web-renderer 4.0.402 → 4.0.403

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.
@@ -0,0 +1,3 @@
1
+ import type { CanRenderMediaOnWebOptions, CanRenderMediaOnWebResult } from './can-render-types';
2
+ export type { CanRenderIssue, CanRenderMediaOnWebOptions, CanRenderMediaOnWebResult, } from './can-render-types';
3
+ export declare const canRenderMediaOnWeb: (options: CanRenderMediaOnWebOptions) => Promise<CanRenderMediaOnWebResult>;
@@ -0,0 +1,27 @@
1
+ import type { WebRendererAudioCodec, WebRendererContainer, WebRendererQuality, WebRendererVideoCodec } from './mediabunny-mappings';
2
+ import type { WebRendererOutputTarget } from './output-target';
3
+ export type CanRenderIssueType = 'video-codec-unsupported' | 'audio-codec-unsupported' | 'webgl-unsupported' | 'webcodecs-unavailable' | 'container-codec-mismatch' | 'transparent-video-unsupported' | 'invalid-dimensions' | 'output-target-unsupported';
4
+ export type CanRenderIssue = {
5
+ type: CanRenderIssueType;
6
+ message: string;
7
+ severity: 'error' | 'warning';
8
+ };
9
+ export type CanRenderMediaOnWebResult = {
10
+ canRender: boolean;
11
+ issues: CanRenderIssue[];
12
+ resolvedVideoCodec: WebRendererVideoCodec;
13
+ resolvedAudioCodec: WebRendererAudioCodec | null;
14
+ resolvedOutputTarget: WebRendererOutputTarget;
15
+ };
16
+ export type CanRenderMediaOnWebOptions = {
17
+ container?: WebRendererContainer;
18
+ videoCodec?: WebRendererVideoCodec;
19
+ audioCodec?: WebRendererAudioCodec | null;
20
+ width: number;
21
+ height: number;
22
+ transparent?: boolean;
23
+ muted?: boolean;
24
+ videoBitrate?: number | WebRendererQuality;
25
+ audioBitrate?: number | WebRendererQuality;
26
+ outputTarget?: WebRendererOutputTarget | null;
27
+ };
@@ -0,0 +1,2 @@
1
+ import type { CanRenderIssue } from './can-render-types';
2
+ export declare const checkWebGLSupport: () => CanRenderIssue | null;
@@ -0,0 +1,9 @@
1
+ import { AudioSampleSource, type Quality } from 'mediabunny';
2
+ export declare const createAudioSampleSource: ({ muted, codec, bitrate, }: {
3
+ muted: boolean;
4
+ codec: "aac" | "alaw" | "flac" | "mp3" | "opus" | "pcm-f32" | "pcm-f32be" | "pcm-f64" | "pcm-f64be" | "pcm-s16" | "pcm-s16be" | "pcm-s24" | "pcm-s24be" | "pcm-s32" | "pcm-s32be" | "pcm-s8" | "pcm-u8" | "ulaw" | "vorbis" | null;
5
+ bitrate: number | Quality;
6
+ }) => {
7
+ audioSampleSource: AudioSampleSource;
8
+ [Symbol.dispose]: () => void;
9
+ } | null;
@@ -36,6 +36,327 @@ var __callDispose = (stack, error, hasError) => {
36
36
  return next();
37
37
  };
38
38
 
39
+ // src/can-render-media-on-web.ts
40
+ import { canEncodeVideo } from "mediabunny";
41
+
42
+ // src/can-use-webfs-target.ts
43
+ var canUseWebFsWriter = async () => {
44
+ if (!("storage" in navigator)) {
45
+ return false;
46
+ }
47
+ if (!("getDirectory" in navigator.storage)) {
48
+ return false;
49
+ }
50
+ try {
51
+ const directoryHandle = await navigator.storage.getDirectory();
52
+ const fileHandle = await directoryHandle.getFileHandle("remotion-probe-web-fs-support", {
53
+ create: true
54
+ });
55
+ const canUse = fileHandle.createWritable !== undefined;
56
+ return canUse;
57
+ } catch {
58
+ return false;
59
+ }
60
+ };
61
+
62
+ // src/check-webgl-support.ts
63
+ var checkWebGLSupport = () => {
64
+ try {
65
+ const canvas = new OffscreenCanvas(1, 1);
66
+ const gl = canvas.getContext("webgl2") || canvas.getContext("webgl");
67
+ if (!gl) {
68
+ return {
69
+ type: "webgl-unsupported",
70
+ message: "WebGL is not supported. 3D CSS transforms will fail.",
71
+ severity: "error"
72
+ };
73
+ }
74
+ return null;
75
+ } catch {
76
+ return {
77
+ type: "webgl-unsupported",
78
+ message: "WebGL is not supported. 3D CSS transforms will fail.",
79
+ severity: "error"
80
+ };
81
+ }
82
+ };
83
+
84
+ // src/mediabunny-mappings.ts
85
+ import {
86
+ Mp4OutputFormat,
87
+ QUALITY_HIGH,
88
+ QUALITY_LOW,
89
+ QUALITY_MEDIUM,
90
+ QUALITY_VERY_HIGH,
91
+ QUALITY_VERY_LOW,
92
+ WebMOutputFormat
93
+ } from "mediabunny";
94
+ var codecToMediabunnyCodec = (codec) => {
95
+ switch (codec) {
96
+ case "h264":
97
+ return "avc";
98
+ case "h265":
99
+ return "hevc";
100
+ case "vp8":
101
+ return "vp8";
102
+ case "vp9":
103
+ return "vp9";
104
+ case "av1":
105
+ return "av1";
106
+ default:
107
+ throw new Error(`Unsupported codec: ${codec}`);
108
+ }
109
+ };
110
+ var containerToMediabunnyContainer = (container) => {
111
+ switch (container) {
112
+ case "mp4":
113
+ return new Mp4OutputFormat;
114
+ case "webm":
115
+ return new WebMOutputFormat;
116
+ default:
117
+ throw new Error(`Unsupported container: ${container}`);
118
+ }
119
+ };
120
+ var getDefaultVideoCodecForContainer = (container) => {
121
+ switch (container) {
122
+ case "mp4":
123
+ return "h264";
124
+ case "webm":
125
+ return "vp8";
126
+ default:
127
+ throw new Error(`Unsupported container: ${container}`);
128
+ }
129
+ };
130
+ var getQualityForWebRendererQuality = (quality) => {
131
+ switch (quality) {
132
+ case "very-low":
133
+ return QUALITY_VERY_LOW;
134
+ case "low":
135
+ return QUALITY_LOW;
136
+ case "medium":
137
+ return QUALITY_MEDIUM;
138
+ case "high":
139
+ return QUALITY_HIGH;
140
+ case "very-high":
141
+ return QUALITY_VERY_HIGH;
142
+ default:
143
+ throw new Error(`Unsupported quality: ${quality}`);
144
+ }
145
+ };
146
+ var getMimeType = (container) => {
147
+ switch (container) {
148
+ case "mp4":
149
+ return "video/mp4";
150
+ case "webm":
151
+ return "video/webm";
152
+ default:
153
+ throw new Error(`Unsupported container: ${container}`);
154
+ }
155
+ };
156
+ var getDefaultAudioCodecForContainer = (container) => {
157
+ switch (container) {
158
+ case "mp4":
159
+ return "aac";
160
+ case "webm":
161
+ return "opus";
162
+ default:
163
+ throw new Error(`Unsupported container: ${container}`);
164
+ }
165
+ };
166
+ var WEB_RENDERER_VIDEO_CODECS = [
167
+ "h264",
168
+ "h265",
169
+ "vp8",
170
+ "vp9",
171
+ "av1"
172
+ ];
173
+ var getSupportedVideoCodecsForContainer = (container) => {
174
+ const format = containerToMediabunnyContainer(container);
175
+ const allSupported = format.getSupportedVideoCodecs();
176
+ return WEB_RENDERER_VIDEO_CODECS.filter((codec) => allSupported.includes(codecToMediabunnyCodec(codec)));
177
+ };
178
+ var WEB_RENDERER_AUDIO_CODECS = ["aac", "opus"];
179
+ var getSupportedAudioCodecsForContainer = (container) => {
180
+ const format = containerToMediabunnyContainer(container);
181
+ const allSupported = format.getSupportedAudioCodecs();
182
+ return WEB_RENDERER_AUDIO_CODECS.filter((codec) => allSupported.includes(codec));
183
+ };
184
+ var audioCodecToMediabunnyAudioCodec = (audioCodec) => {
185
+ return audioCodec;
186
+ };
187
+
188
+ // src/resolve-audio-codec.ts
189
+ import { canEncodeAudio } from "mediabunny";
190
+ var resolveAudioCodec = async (options) => {
191
+ const issues = [];
192
+ const { container, requestedCodec, userSpecifiedAudioCodec, bitrate } = options;
193
+ const audioCodec = requestedCodec ?? getDefaultAudioCodecForContainer(container);
194
+ const supportedAudioCodecs = getSupportedAudioCodecsForContainer(container);
195
+ if (!supportedAudioCodecs.includes(audioCodec)) {
196
+ issues.push({
197
+ type: "audio-codec-unsupported",
198
+ message: `Audio codec "${audioCodec}" is not supported for container "${container}". Supported: ${supportedAudioCodecs.join(", ")}`,
199
+ severity: "error"
200
+ });
201
+ return { codec: null, issues };
202
+ }
203
+ const mediabunnyAudioCodec = audioCodecToMediabunnyAudioCodec(audioCodec);
204
+ const canEncode = await canEncodeAudio(mediabunnyAudioCodec, { bitrate });
205
+ if (canEncode) {
206
+ return { codec: audioCodec, issues };
207
+ }
208
+ if (userSpecifiedAudioCodec) {
209
+ issues.push({
210
+ type: "audio-codec-unsupported",
211
+ message: `Audio codec "${audioCodec}" cannot be encoded by this browser. This is common for AAC on Firefox. Try using "opus" instead.`,
212
+ severity: "error"
213
+ });
214
+ return { codec: null, issues };
215
+ }
216
+ for (const fallbackCodec of supportedAudioCodecs) {
217
+ if (fallbackCodec !== audioCodec) {
218
+ const fallbackMediabunnyCodec = audioCodecToMediabunnyAudioCodec(fallbackCodec);
219
+ const canEncodeFallback = await canEncodeAudio(fallbackMediabunnyCodec, {
220
+ bitrate
221
+ });
222
+ if (canEncodeFallback) {
223
+ issues.push({
224
+ type: "audio-codec-unsupported",
225
+ message: `Falling back from audio codec "${audioCodec}" to "${fallbackCodec}" because the original codec cannot be encoded by this browser.`,
226
+ severity: "warning"
227
+ });
228
+ return { codec: fallbackCodec, issues };
229
+ }
230
+ }
231
+ }
232
+ issues.push({
233
+ type: "audio-codec-unsupported",
234
+ message: `No audio codec can be encoded by this browser for container "${container}".`,
235
+ severity: "error"
236
+ });
237
+ return { codec: null, issues };
238
+ };
239
+
240
+ // src/validate-dimensions.ts
241
+ var validateDimensions = (options) => {
242
+ const { width, height, codec } = options;
243
+ if (codec === "h264" || codec === "h265") {
244
+ if (width % 2 !== 0 || height % 2 !== 0) {
245
+ return {
246
+ type: "invalid-dimensions",
247
+ message: `${codec.toUpperCase()} codec requires width and height to be multiples of 2. Got ${width}x${height}`,
248
+ severity: "error"
249
+ };
250
+ }
251
+ }
252
+ return null;
253
+ };
254
+
255
+ // src/can-render-media-on-web.ts
256
+ var canRenderMediaOnWeb = async (options) => {
257
+ const issues = [];
258
+ if (typeof VideoEncoder === "undefined") {
259
+ issues.push({
260
+ type: "webcodecs-unavailable",
261
+ message: "WebCodecs API is not available in this browser. A modern browser with WebCodecs support is required.",
262
+ severity: "error"
263
+ });
264
+ }
265
+ const container = options.container ?? "mp4";
266
+ const videoCodec = options.videoCodec ?? getDefaultVideoCodecForContainer(container);
267
+ const transparent = options.transparent ?? false;
268
+ const muted = options.muted ?? false;
269
+ const { width, height } = options;
270
+ const resolvedVideoBitrate = typeof options.videoBitrate === "number" ? options.videoBitrate : getQualityForWebRendererQuality(options.videoBitrate ?? "medium");
271
+ const resolvedAudioBitrate = typeof options.audioBitrate === "number" ? options.audioBitrate : getQualityForWebRendererQuality(options.audioBitrate ?? "medium");
272
+ const format = containerToMediabunnyContainer(container);
273
+ if (!format.getSupportedCodecs().includes(codecToMediabunnyCodec(videoCodec))) {
274
+ issues.push({
275
+ type: "container-codec-mismatch",
276
+ message: `Codec ${videoCodec} is not supported for container ${container}`,
277
+ severity: "error"
278
+ });
279
+ }
280
+ const dimensionIssue = validateDimensions({ width, height, codec: videoCodec });
281
+ if (dimensionIssue) {
282
+ issues.push(dimensionIssue);
283
+ }
284
+ const canEncodeVideoResult = await canEncodeVideo(codecToMediabunnyCodec(videoCodec), { bitrate: resolvedVideoBitrate });
285
+ if (!canEncodeVideoResult) {
286
+ issues.push({
287
+ type: "video-codec-unsupported",
288
+ message: `Video codec "${videoCodec}" cannot be encoded by this browser`,
289
+ severity: "error"
290
+ });
291
+ }
292
+ if (transparent && !["vp8", "vp9"].includes(videoCodec)) {
293
+ issues.push({
294
+ type: "transparent-video-unsupported",
295
+ message: `Transparent video requires VP8 or VP9 codec with WebM container. ${videoCodec} does not support alpha channel.`,
296
+ severity: "error"
297
+ });
298
+ }
299
+ let resolvedAudioCodec = null;
300
+ if (!muted) {
301
+ const audioResult = await resolveAudioCodec({
302
+ container,
303
+ requestedCodec: options.audioCodec,
304
+ userSpecifiedAudioCodec: options.audioCodec !== undefined && options.audioCodec !== null,
305
+ bitrate: resolvedAudioBitrate
306
+ });
307
+ resolvedAudioCodec = audioResult.codec;
308
+ issues.push(...audioResult.issues);
309
+ }
310
+ const webglIssue = checkWebGLSupport();
311
+ if (webglIssue) {
312
+ issues.push(webglIssue);
313
+ }
314
+ const canUseWebFs = await canUseWebFsWriter();
315
+ let resolvedOutputTarget;
316
+ if (options.outputTarget === "web-fs") {
317
+ if (!canUseWebFs) {
318
+ issues.push({
319
+ type: "output-target-unsupported",
320
+ message: 'The "web-fs" output target is not supported in this browser. The File System Access API is required.',
321
+ severity: "error"
322
+ });
323
+ }
324
+ resolvedOutputTarget = "web-fs";
325
+ } else if (options.outputTarget === "arraybuffer") {
326
+ resolvedOutputTarget = "arraybuffer";
327
+ } else {
328
+ resolvedOutputTarget = canUseWebFs ? "web-fs" : "arraybuffer";
329
+ }
330
+ return {
331
+ canRender: issues.filter((i) => i.severity === "error").length === 0,
332
+ issues,
333
+ resolvedVideoCodec: videoCodec,
334
+ resolvedAudioCodec,
335
+ resolvedOutputTarget
336
+ };
337
+ };
338
+ // src/get-encodable-codecs.ts
339
+ import {
340
+ getEncodableAudioCodecs as mediabunnyGetEncodableAudioCodecs,
341
+ getEncodableVideoCodecs as mediabunnyGetEncodableVideoCodecs
342
+ } from "mediabunny";
343
+ var getEncodableVideoCodecs = async (container, options) => {
344
+ const supported = getSupportedVideoCodecsForContainer(container);
345
+ const mediabunnyCodecs = supported.map(codecToMediabunnyCodec);
346
+ const resolvedBitrate = options?.videoBitrate ? typeof options.videoBitrate === "number" ? options.videoBitrate : getQualityForWebRendererQuality(options.videoBitrate) : undefined;
347
+ const encodable = await mediabunnyGetEncodableVideoCodecs(mediabunnyCodecs, {
348
+ bitrate: resolvedBitrate
349
+ });
350
+ return supported.filter((c) => encodable.includes(codecToMediabunnyCodec(c)));
351
+ };
352
+ var getEncodableAudioCodecs = async (container, options) => {
353
+ const supported = getSupportedAudioCodecsForContainer(container);
354
+ const resolvedBitrate = options?.audioBitrate ? typeof options.audioBitrate === "number" ? options.audioBitrate : getQualityForWebRendererQuality(options.audioBitrate) : undefined;
355
+ const encodable = await mediabunnyGetEncodableAudioCodecs(supported, {
356
+ bitrate: resolvedBitrate
357
+ });
358
+ return supported.filter((c) => encodable.includes(c));
359
+ };
39
360
  // src/render-media-on-web.tsx
40
361
  import { BufferTarget, StreamTarget } from "mediabunny";
41
362
  import { Internals as Internals7 } from "remotion";
@@ -167,24 +488,21 @@ var onlyInlineAudio = ({
167
488
  });
168
489
  };
169
490
 
170
- // src/can-use-webfs-target.ts
171
- var canUseWebFsWriter = async () => {
172
- if (!("storage" in navigator)) {
173
- return false;
174
- }
175
- if (!("getDirectory" in navigator.storage)) {
176
- return false;
177
- }
178
- try {
179
- const directoryHandle = await navigator.storage.getDirectory();
180
- const fileHandle = await directoryHandle.getFileHandle("remotion-probe-web-fs-support", {
181
- create: true
182
- });
183
- const canUse = fileHandle.createWritable !== undefined;
184
- return canUse;
185
- } catch {
186
- return false;
491
+ // src/create-audio-sample-source.ts
492
+ import { AudioSampleSource } from "mediabunny";
493
+ var createAudioSampleSource = ({
494
+ muted,
495
+ codec,
496
+ bitrate
497
+ }) => {
498
+ if (muted || codec === null) {
499
+ return null;
187
500
  }
501
+ const audioSampleSource = new AudioSampleSource({
502
+ codec,
503
+ bitrate
504
+ });
505
+ return { audioSampleSource, [Symbol.dispose]: () => audioSampleSource.close() };
188
506
  };
189
507
 
190
508
  // src/create-scaffold.tsx
@@ -398,51 +716,6 @@ var getRealFrameRange = (durationInFrames, frameRange) => {
398
716
  return frameRange;
399
717
  };
400
718
 
401
- // src/get-audio-sample-source.ts
402
- import {
403
- AudioSampleSource
404
- } from "mediabunny";
405
-
406
- // src/get-audio-encoding-config.ts
407
- import {
408
- canEncodeAudio,
409
- QUALITY_MEDIUM
410
- } from "mediabunny";
411
- var getDefaultAudioEncodingConfig = async () => {
412
- const preferredDefaultAudioEncodingConfig = {
413
- codec: "aac",
414
- bitrate: QUALITY_MEDIUM
415
- };
416
- if (await canEncodeAudio(preferredDefaultAudioEncodingConfig.codec, preferredDefaultAudioEncodingConfig)) {
417
- return preferredDefaultAudioEncodingConfig;
418
- }
419
- const backupDefaultAudioEncodingConfig = {
420
- codec: "opus",
421
- bitrate: QUALITY_MEDIUM
422
- };
423
- if (await canEncodeAudio(backupDefaultAudioEncodingConfig.codec, backupDefaultAudioEncodingConfig)) {
424
- return backupDefaultAudioEncodingConfig;
425
- }
426
- return null;
427
- };
428
-
429
- // src/get-audio-sample-source.ts
430
- var addAudioSampleSource = async ({
431
- muted,
432
- output
433
- }) => {
434
- if (muted) {
435
- return null;
436
- }
437
- const defaultAudioEncodingConfig = await getDefaultAudioEncodingConfig();
438
- if (!defaultAudioEncodingConfig) {
439
- throw new Error("No default audio encoding config found");
440
- }
441
- const audioSampleSource = new AudioSampleSource(defaultAudioEncodingConfig);
442
- output.addAudioTrack(audioSampleSource);
443
- return { audioSampleSource, [Symbol.dispose]: () => audioSampleSource.close() };
444
- };
445
-
446
719
  // src/internal-state.ts
447
720
  var makeInternalState = () => {
448
721
  let drawnPrecomposedPixels = 0;
@@ -513,79 +786,6 @@ var makeVideoSampleSourceCleanup = (encodingConfig) => {
513
786
  };
514
787
  };
515
788
 
516
- // src/mediabunny-mappings.ts
517
- import {
518
- Mp4OutputFormat,
519
- QUALITY_HIGH,
520
- QUALITY_LOW,
521
- QUALITY_MEDIUM as QUALITY_MEDIUM2,
522
- QUALITY_VERY_HIGH,
523
- QUALITY_VERY_LOW,
524
- WebMOutputFormat
525
- } from "mediabunny";
526
- var codecToMediabunnyCodec = (codec) => {
527
- switch (codec) {
528
- case "h264":
529
- return "avc";
530
- case "h265":
531
- return "hevc";
532
- case "vp8":
533
- return "vp8";
534
- case "vp9":
535
- return "vp9";
536
- case "av1":
537
- return "av1";
538
- default:
539
- throw new Error(`Unsupported codec: ${codec}`);
540
- }
541
- };
542
- var containerToMediabunnyContainer = (container) => {
543
- switch (container) {
544
- case "mp4":
545
- return new Mp4OutputFormat;
546
- case "webm":
547
- return new WebMOutputFormat;
548
- default:
549
- throw new Error(`Unsupported container: ${container}`);
550
- }
551
- };
552
- var getDefaultVideoCodecForContainer = (container) => {
553
- switch (container) {
554
- case "mp4":
555
- return "h264";
556
- case "webm":
557
- return "vp8";
558
- default:
559
- throw new Error(`Unsupported container: ${container}`);
560
- }
561
- };
562
- var getQualityForWebRendererQuality = (quality) => {
563
- switch (quality) {
564
- case "very-low":
565
- return QUALITY_VERY_LOW;
566
- case "low":
567
- return QUALITY_LOW;
568
- case "medium":
569
- return QUALITY_MEDIUM2;
570
- case "high":
571
- return QUALITY_HIGH;
572
- case "very-high":
573
- return QUALITY_VERY_HIGH;
574
- default:
575
- throw new Error(`Unsupported quality: ${quality}`);
576
- }
577
- };
578
- var getMimeType = (container) => {
579
- switch (container) {
580
- case "mp4":
581
- return "video/mp4";
582
- case "webm":
583
- return "video/webm";
584
- default:
585
- throw new Error(`Unsupported container: ${container}`);
586
- }
587
- };
588
-
589
789
  // src/render-operations-queue.ts
590
790
  var onlyOneRenderAtATimeQueue = {
591
791
  ref: Promise.resolve()
@@ -3432,6 +3632,8 @@ var internalRenderMediaOnWeb = async ({
3432
3632
  mediaCacheSizeInBytes,
3433
3633
  schema,
3434
3634
  videoCodec: codec,
3635
+ audioCodec: unresolvedAudioCodec,
3636
+ audioBitrate,
3435
3637
  container,
3436
3638
  signal,
3437
3639
  onProgress,
@@ -3456,6 +3658,23 @@ var internalRenderMediaOnWeb = async ({
3456
3658
  if (codec && !format.getSupportedCodecs().includes(codecToMediabunnyCodec(codec))) {
3457
3659
  return Promise.reject(new Error(`Codec ${codec} is not supported for container ${container}`));
3458
3660
  }
3661
+ const resolvedAudioBitrate = typeof audioBitrate === "number" ? audioBitrate : getQualityForWebRendererQuality(audioBitrate);
3662
+ let finalAudioCodec = null;
3663
+ if (!muted) {
3664
+ const audioResult = await resolveAudioCodec({
3665
+ container,
3666
+ requestedCodec: unresolvedAudioCodec,
3667
+ userSpecifiedAudioCodec: unresolvedAudioCodec !== undefined && unresolvedAudioCodec !== null,
3668
+ bitrate: resolvedAudioBitrate
3669
+ });
3670
+ for (const issue of audioResult.issues) {
3671
+ if (issue.severity === "error") {
3672
+ return Promise.reject(new Error(issue.message));
3673
+ }
3674
+ Internals7.Log.warn({ logLevel, tag: "@remotion/web-renderer" }, issue.message);
3675
+ }
3676
+ finalAudioCodec = audioResult.codec;
3677
+ }
3459
3678
  const resolved = await Internals7.resolveVideoConfig({
3460
3679
  calculateMetadata: composition.calculateMetadata ?? null,
3461
3680
  signal: signal ?? new AbortController().signal,
@@ -3524,10 +3743,14 @@ var internalRenderMediaOnWeb = async ({
3524
3743
  alpha: transparent ? "keep" : "discard"
3525
3744
  }), 0);
3526
3745
  outputWithCleanup.output.addVideoTrack(videoSampleSource.videoSampleSource);
3527
- const audioSampleSource = __using(__stack, await addAudioSampleSource({
3746
+ const audioSampleSource = __using(__stack, createAudioSampleSource({
3528
3747
  muted,
3529
- output: outputWithCleanup.output
3748
+ codec: finalAudioCodec ? audioCodecToMediabunnyAudioCodec(finalAudioCodec) : null,
3749
+ bitrate: resolvedAudioBitrate
3530
3750
  }), 0);
3751
+ if (audioSampleSource) {
3752
+ outputWithCleanup.output.addAudioTrack(audioSampleSource.audioSampleSource);
3753
+ }
3531
3754
  await outputWithCleanup.output.start();
3532
3755
  if (signal?.aborted) {
3533
3756
  throw new Error("renderMediaOnWeb() was cancelled");
@@ -3680,6 +3903,8 @@ var renderMediaOnWeb = (options) => {
3680
3903
  schema: options.schema ?? undefined,
3681
3904
  mediaCacheSizeInBytes: options.mediaCacheSizeInBytes ?? null,
3682
3905
  videoCodec: codec,
3906
+ audioCodec: options.audioCodec ?? null,
3907
+ audioBitrate: options.audioBitrate ?? "medium",
3683
3908
  container,
3684
3909
  signal: options.signal ?? null,
3685
3910
  onProgress: options.onProgress ?? null,
@@ -3815,5 +4040,12 @@ var renderStillOnWeb = (options) => {
3815
4040
  };
3816
4041
  export {
3817
4042
  renderStillOnWeb,
3818
- renderMediaOnWeb
4043
+ renderMediaOnWeb,
4044
+ getSupportedVideoCodecsForContainer,
4045
+ getSupportedAudioCodecsForContainer,
4046
+ getEncodableVideoCodecs,
4047
+ getEncodableAudioCodecs,
4048
+ getDefaultVideoCodecForContainer,
4049
+ getDefaultAudioCodecForContainer,
4050
+ canRenderMediaOnWeb
3819
4051
  };
@@ -0,0 +1,9 @@
1
+ import { type WebRendererAudioCodec, type WebRendererContainer, type WebRendererQuality, type WebRendererVideoCodec } from './mediabunny-mappings';
2
+ export type GetEncodableVideoCodecsOptions = {
3
+ videoBitrate?: number | WebRendererQuality;
4
+ };
5
+ export type GetEncodableAudioCodecsOptions = {
6
+ audioBitrate?: number | WebRendererQuality;
7
+ };
8
+ export declare const getEncodableVideoCodecs: (container: WebRendererContainer, options?: GetEncodableVideoCodecsOptions | undefined) => Promise<WebRendererVideoCodec[]>;
9
+ export declare const getEncodableAudioCodecs: (container: WebRendererContainer, options?: GetEncodableAudioCodecsOptions | undefined) => Promise<WebRendererAudioCodec[]>;
package/dist/index.d.ts CHANGED
@@ -1,7 +1,11 @@
1
1
  import './symbol-dispose';
2
2
  export type { EmittedArtifact, WebRendererOnArtifact } from './artifact';
3
+ export { canRenderMediaOnWeb } from './can-render-media-on-web';
4
+ export type { CanRenderIssue, CanRenderMediaOnWebOptions, CanRenderMediaOnWebResult, } from './can-render-media-on-web';
3
5
  export type { FrameRange } from './frame-range';
4
- export type { WebRendererContainer, WebRendererQuality, WebRendererVideoCodec, } from './mediabunny-mappings';
6
+ export { getEncodableAudioCodecs, getEncodableVideoCodecs, type GetEncodableAudioCodecsOptions, type GetEncodableVideoCodecsOptions, } from './get-encodable-codecs';
7
+ export { getDefaultAudioCodecForContainer, getDefaultVideoCodecForContainer, getSupportedAudioCodecsForContainer, getSupportedVideoCodecsForContainer, } from './mediabunny-mappings';
8
+ export type { WebRendererAudioCodec, WebRendererContainer, WebRendererQuality, WebRendererVideoCodec, } from './mediabunny-mappings';
5
9
  export type { WebRendererOutputTarget } from './output-target';
6
10
  export { renderMediaOnWeb } from './render-media-on-web';
7
11
  export type { RenderMediaOnWebOptions, RenderMediaOnWebProgress, RenderMediaOnWebProgressCallback, RenderMediaOnWebResult, } from './render-media-on-web';
@@ -2,9 +2,14 @@ import type { Quality } from 'mediabunny';
2
2
  import { type OutputFormat } from 'mediabunny';
3
3
  export type WebRendererVideoCodec = 'h264' | 'h265' | 'vp8' | 'vp9' | 'av1';
4
4
  export type WebRendererContainer = 'mp4' | 'webm';
5
+ export type WebRendererAudioCodec = 'aac' | 'opus';
5
6
  export type WebRendererQuality = 'very-low' | 'low' | 'medium' | 'high' | 'very-high';
6
7
  export declare const codecToMediabunnyCodec: (codec: WebRendererVideoCodec) => "av1" | "avc" | "hevc" | "vp8" | "vp9";
7
8
  export declare const containerToMediabunnyContainer: (container: WebRendererContainer) => OutputFormat;
8
9
  export declare const getDefaultVideoCodecForContainer: (container: WebRendererContainer) => WebRendererVideoCodec;
9
10
  export declare const getQualityForWebRendererQuality: (quality: WebRendererQuality) => Quality;
10
11
  export declare const getMimeType: (container: WebRendererContainer) => string;
12
+ export declare const getDefaultAudioCodecForContainer: (container: WebRendererContainer) => WebRendererAudioCodec;
13
+ export declare const getSupportedVideoCodecsForContainer: (container: WebRendererContainer) => WebRendererVideoCodec[];
14
+ export declare const getSupportedAudioCodecsForContainer: (container: WebRendererContainer) => WebRendererAudioCodec[];
15
+ export declare const audioCodecToMediabunnyAudioCodec: (audioCodec: WebRendererAudioCodec) => "aac" | "alaw" | "flac" | "mp3" | "opus" | "pcm-f32" | "pcm-f32be" | "pcm-f64" | "pcm-f64be" | "pcm-s16" | "pcm-s16be" | "pcm-s24" | "pcm-s24be" | "pcm-s32" | "pcm-s32be" | "pcm-s8" | "pcm-u8" | "ulaw" | "vorbis";
@@ -3,7 +3,7 @@ import type { AnyZodObject, z } from 'zod';
3
3
  import { type WebRendererOnArtifact } from './artifact';
4
4
  import { type FrameRange } from './frame-range';
5
5
  import type { InternalState } from './internal-state';
6
- import type { WebRendererContainer, WebRendererQuality } from './mediabunny-mappings';
6
+ import type { WebRendererAudioCodec, WebRendererContainer, WebRendererQuality } from './mediabunny-mappings';
7
7
  import { type WebRendererVideoCodec } from './mediabunny-mappings';
8
8
  import type { WebRendererOutputTarget } from './output-target';
9
9
  import type { CompositionCalculateMetadataOrExplicit } from './props-if-has-props';
@@ -35,6 +35,8 @@ type OptionalRenderMediaOnWebOptions<Schema extends AnyZodObject> = {
35
35
  schema: Schema | undefined;
36
36
  mediaCacheSizeInBytes: number | null;
37
37
  videoCodec: WebRendererVideoCodec;
38
+ audioCodec: WebRendererAudioCodec | null;
39
+ audioBitrate: number | WebRendererQuality;
38
40
  container: WebRendererContainer;
39
41
  signal: AbortSignal | null;
40
42
  onProgress: RenderMediaOnWebProgressCallback | null;
@@ -0,0 +1,119 @@
1
+ /** Image format for encoded screenshot output. */
2
+ export type ImageFormat = 'png' | 'jpeg' | 'webp';
3
+ /** Options for rendering a screenshot to canvas. */
4
+ export interface RenderOptions {
5
+ /**
6
+ * Canvas background color. Set to `null` (or omit) for a transparent canvas.
7
+ * When provided, the string is passed directly to `fillStyle`.
8
+ */
9
+ backgroundColor?: string | null;
10
+ /**
11
+ * Optional existing canvas to render into.
12
+ * When omitted, a new canvas element is created.
13
+ */
14
+ canvas?: HTMLCanvasElement;
15
+ /**
16
+ * Rendering scale factor. Defaults to `window.devicePixelRatio` (or `1`).
17
+ */
18
+ scale?: number;
19
+ /**
20
+ * Crop origin X (CSS pixels) relative to the element's left edge.
21
+ * Defaults to the element's left edge.
22
+ */
23
+ x?: number;
24
+ /**
25
+ * Crop origin Y (CSS pixels) relative to the element's top edge.
26
+ * Defaults to the element's top edge.
27
+ */
28
+ y?: number;
29
+ /**
30
+ * Output width in CSS pixels. Defaults to the element's width.
31
+ */
32
+ width?: number;
33
+ /**
34
+ * Output height in CSS pixels. Defaults to the element's height.
35
+ */
36
+ height?: number;
37
+ /**
38
+ * Controls how `position: fixed` elements outside the captured subtree are
39
+ * handled.
40
+ *
41
+ * - `none` – ignore all fixed elements outside `element`.
42
+ * - `intersecting` – include fixed elements whose bounding rect intersects the capture rect.
43
+ * - `all` – include all fixed elements that overlap the viewport.
44
+ */
45
+ includeFixed?: 'none' | 'intersecting' | 'all';
46
+ }
47
+ /** Options for encoding a canvas to an image format. */
48
+ export interface EncodeOptions {
49
+ /**
50
+ * Image format to encode. Defaults to `'png'`.
51
+ */
52
+ format?: ImageFormat;
53
+ /**
54
+ * Image quality for lossy formats (`jpeg`, `webp`). A number between `0` and `1`.
55
+ * Ignored for `png`. Defaults to `0.92`.
56
+ */
57
+ quality?: number;
58
+ }
59
+ /** Combined options for one-shot screenshot methods that render and encode. */
60
+ export type ScreenshotOptions = RenderOptions & EncodeOptions;
61
+ /**
62
+ * A promise-like object representing a screenshot capture. The underlying
63
+ * render happens once; subsequent calls to `.canvas()`, `.blob()`, or `.url()`
64
+ * reuse the same rendered canvas.
65
+ */
66
+ export interface ScreenshotTask extends Promise<HTMLCanvasElement> {
67
+ /** Returns the rendered canvas. */
68
+ canvas(): Promise<HTMLCanvasElement>;
69
+ /** Encodes the rendered canvas to a Blob. */
70
+ blob(options?: EncodeOptions): Promise<Blob>;
71
+ /**
72
+ * Encodes the rendered canvas to a Blob and creates an object URL.
73
+ * Remember to call `URL.revokeObjectURL(url)` when done to avoid memory leaks.
74
+ */
75
+ url(options?: EncodeOptions): Promise<string>;
76
+ }
77
+ /**
78
+ * Renders a DOM element into a canvas using modern browser features.
79
+ *
80
+ * Returns a `ScreenshotTask` that is both a Promise and provides methods
81
+ * to encode the rendered canvas. The underlying render happens once;
82
+ * subsequent calls to `.canvas()`, `.blob()`, or `.url()` reuse the same result.
83
+ *
84
+ * This implementation targets evergreen browsers only and assumes a real DOM +
85
+ * Canvas2D environment (not Node.js).
86
+ *
87
+ * @example
88
+ * // Capture handle pattern - render once, encode multiple ways
89
+ * const shot = screenshot(element, { scale: 2 })
90
+ * const canvas = await shot.canvas()
91
+ * const pngBlob = await shot.blob({ format: 'png' })
92
+ * const webpUrl = await shot.url({ format: 'webp', quality: 0.9 })
93
+ *
94
+ * @example
95
+ * // Direct await returns the canvas
96
+ * const canvas = await screenshot(element)
97
+ *
98
+ * @example
99
+ * // One-shot convenience methods
100
+ * const canvas = await screenshot.canvas(element, { scale: 2 })
101
+ * const blob = await screenshot.blob(element, { format: 'jpeg', quality: 0.85 })
102
+ * const url = await screenshot.url(element, { format: 'png' })
103
+ */
104
+ declare function screenshot(target: Element | string, options?: RenderOptions): ScreenshotTask;
105
+ declare namespace screenshot {
106
+ var canvas: (target: string | Element, options?: RenderOptions | undefined) => Promise<HTMLCanvasElement>;
107
+ }
108
+ declare namespace screenshot {
109
+ var blob: (target: string | Element, options?: ScreenshotOptions | undefined) => Promise<Blob>;
110
+ }
111
+ declare namespace screenshot {
112
+ var url: (target: string | Element, options?: ScreenshotOptions | undefined) => Promise<string>;
113
+ }
114
+ export { screenshot };
115
+ declare global {
116
+ interface CanvasRenderingContext2D {
117
+ _filterPolyfillValue?: string;
118
+ }
119
+ }
@@ -0,0 +1,13 @@
1
+ import { type Quality } from 'mediabunny';
2
+ import type { CanRenderIssue } from './can-render-types';
3
+ import { type WebRendererAudioCodec, type WebRendererContainer } from './mediabunny-mappings';
4
+ export type ResolveAudioCodecResult = {
5
+ codec: WebRendererAudioCodec | null;
6
+ issues: CanRenderIssue[];
7
+ };
8
+ export declare const resolveAudioCodec: (options: {
9
+ container: WebRendererContainer;
10
+ requestedCodec: WebRendererAudioCodec | null | undefined;
11
+ userSpecifiedAudioCodec: boolean;
12
+ bitrate: number | Quality;
13
+ }) => Promise<ResolveAudioCodecResult>;
@@ -0,0 +1,7 @@
1
+ import type { CanRenderIssue } from './can-render-types';
2
+ import type { WebRendererVideoCodec } from './mediabunny-mappings';
3
+ export declare const validateDimensions: (options: {
4
+ width: number;
5
+ height: number;
6
+ codec: WebRendererVideoCodec;
7
+ }) => CanRenderIssue | null;
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "url": "https://github.com/remotion-dev/remotion/tree/main/packages/web-renderer"
4
4
  },
5
5
  "name": "@remotion/web-renderer",
6
- "version": "4.0.402",
6
+ "version": "4.0.403",
7
7
  "main": "dist/index.js",
8
8
  "type": "module",
9
9
  "sideEffects": false,
@@ -18,14 +18,14 @@
18
18
  "author": "Remotion <jonny@remotion.dev>",
19
19
  "license": "UNLICENSED",
20
20
  "dependencies": {
21
- "@remotion/licensing": "4.0.402",
22
- "remotion": "4.0.402",
21
+ "@remotion/licensing": "4.0.403",
22
+ "remotion": "4.0.403",
23
23
  "mediabunny": "1.27.3"
24
24
  },
25
25
  "devDependencies": {
26
- "@remotion/eslint-config-internal": "4.0.402",
27
- "@remotion/player": "4.0.402",
28
- "@remotion/media": "4.0.402",
26
+ "@remotion/eslint-config-internal": "4.0.403",
27
+ "@remotion/player": "4.0.403",
28
+ "@remotion/media": "4.0.403",
29
29
  "@typescript/native-preview": "7.0.0-dev.20260105.1",
30
30
  "@vitejs/plugin-react": "4.1.0",
31
31
  "@vitest/browser-playwright": "4.0.9",