Skip to content

Commit 6d20328

Browse files
authored
Merge pull request #6170 from remotion-dev/feature/web-renderer-audio-codecs
2 parents 634375c + f9d54b6 commit 6d20328

17 files changed

+812
-185
lines changed

packages/docs/docs/web-renderer/render-media-on-web.mdx

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,34 @@ _boolean_
147147

148148
If set to `true`, no audio will be included in the output video.
149149

150+
### `audioCodec?`
151+
152+
_string_ <TsType type="WebRendererAudioCodec" source="@remotion/web-renderer" />
153+
154+
Which codec should be used to encode the audio.
155+
Default depends on the container:
156+
157+
- For `mp4` container: `aac`
158+
- For `webm` container: `opus`
159+
160+
:::note
161+
AAC encoding is not supported in Firefox. If you don't specify an audio codec and the default cannot be encoded by the browser, Remotion will automatically fall back to a supported codec.
162+
163+
If you explicitly specify `aac` and render in Firefox, the render will fail with an error.
164+
:::
165+
166+
### `audioBitrate?`
167+
168+
_number | string_ <TsType type="WebRendererQuality" source="@remotion/web-renderer" />
169+
170+
Controls the quality and file size of the audio. Can be either a number representing the bitrate in bits per second, or one of the preset quality levels:
171+
172+
- `"very-low"`: Smallest file size, lowest quality
173+
- `"low"`: Small file size, lower quality
174+
- `"medium"`: Balanced file size and quality (default)
175+
- `"high"`: Larger file size, higher quality
176+
- `"very-high"`: Largest file size, highest quality
177+
150178
### `delayRenderTimeoutInMilliseconds?`
151179

152180
_number_

packages/studio/src/components/Menu/MenuSubItem.tsx

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,7 @@ export const MenuSubItem: React.FC<{
6969
readonly setSubMenuActivated: React.Dispatch<
7070
React.SetStateAction<SubMenuActivated>
7171
>;
72+
readonly disabled?: boolean;
7273
}> = ({
7374
label,
7475
leaveLeftSpace,
@@ -82,6 +83,7 @@ export const MenuSubItem: React.FC<{
8283
onQuitMenu,
8384
subMenuActivated,
8485
setSubMenuActivated,
86+
disabled,
8587
}) => {
8688
const [hovered, setHovered] = useState(false);
8789
const ref = useRef<HTMLDivElement>(null);
@@ -95,12 +97,18 @@ export const MenuSubItem: React.FC<{
9597
const style = useMemo((): React.CSSProperties => {
9698
return {
9799
...container,
98-
backgroundColor: selected ? CLEAR_HOVER : 'transparent',
100+
backgroundColor: selected && !disabled ? CLEAR_HOVER : 'transparent',
101+
opacity: disabled ? 0.5 : 1,
102+
cursor: disabled ? 'not-allowed' : 'default',
99103
};
100-
}, [selected]);
104+
}, [selected, disabled]);
101105

102106
const onPointerUp = useCallback(
103107
(e: PointerEvent<HTMLDivElement>) => {
108+
if (disabled) {
109+
return;
110+
}
111+
104112
if (subMenu) {
105113
setSubMenuActivated('with-mouse');
106114
setHovered(true);
@@ -109,13 +117,17 @@ export const MenuSubItem: React.FC<{
109117

110118
onActionChosen(id, e);
111119
},
112-
[id, onActionChosen, setSubMenuActivated, subMenu],
120+
[disabled, id, onActionChosen, setSubMenuActivated, subMenu],
113121
);
114122

115123
const onPointerEnter = useCallback(() => {
124+
if (disabled) {
125+
return;
126+
}
127+
116128
onItemSelected(id);
117129
setHovered(true);
118-
}, [id, onItemSelected]);
130+
}, [disabled, id, onItemSelected]);
119131

120132
const onPointerLeave = useCallback(() => {
121133
setHovered(false);

packages/studio/src/components/NewComposition/ComboBox.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ export type SelectionItem = {
6262
leftItem: React.ReactNode;
6363
subMenu: SubMenu | null;
6464
quickSwitcherLabel: string | null;
65+
disabled?: boolean;
6566
};
6667

6768
export type ComboboxValue = DividerItem | SelectionItem;

packages/studio/src/components/NewComposition/MenuContent.tsx

Lines changed: 20 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,10 @@ export const MenuContent: React.FC<{
7272
setSelectedItem(id);
7373
}, []);
7474

75+
const isItemSelectable = useCallback((v: ComboboxValue) => {
76+
return v.type !== 'divider' && !v.disabled;
77+
}, []);
78+
7579
const onArrowUp = useCallback(() => {
7680
setSelectedItem((prevItem) => {
7781
if (prevItem === null) {
@@ -84,41 +88,41 @@ export const MenuContent: React.FC<{
8488
}
8589

8690
const previousItems = values.filter(
87-
(v, i) => i < index && v.type !== 'divider',
91+
(v, i) => i < index && isItemSelectable(v),
8892
);
8993
if (previousItems.length > 0) {
9094
return previousItems[previousItems.length - 1].id as MenuId;
9195
}
9296

93-
const firstNonDivider = values.find((v) => v.type !== 'divider');
94-
if (firstNonDivider) {
95-
return firstNonDivider.id as MenuId;
97+
const firstSelectable = values.find((v) => isItemSelectable(v));
98+
if (firstSelectable) {
99+
return firstSelectable.id as MenuId;
96100
}
97101

98102
throw new Error('could not find previous item');
99103
});
100-
}, [topItemCanBeUnselected, values]);
104+
}, [topItemCanBeUnselected, values, isItemSelectable]);
101105

102106
const onArrowDown = useCallback(() => {
103107
setSelectedItem((prevItem) => {
104108
const index = values.findIndex((val) => val.id === prevItem);
105-
const nextItem = values.find((v, i) => i > index && v.type !== 'divider');
109+
const nextItem = values.find((v, i) => i > index && isItemSelectable(v));
106110
if (nextItem) {
107111
return nextItem.id;
108112
}
109113

110-
const lastNonDivider = values
114+
const lastSelectable = values
111115
.slice()
112116
.reverse()
113-
.find((v) => v.type !== 'divider');
117+
.find((v) => isItemSelectable(v));
114118

115-
if (lastNonDivider) {
116-
return lastNonDivider.id;
119+
if (lastSelectable) {
120+
return lastSelectable.id;
117121
}
118122

119123
throw new Error('could not find next item');
120124
});
121-
}, [values]);
125+
}, [values, isItemSelectable]);
122126

123127
const onEnter = useCallback(() => {
124128
if (selectedItem === null) {
@@ -134,6 +138,10 @@ export const MenuContent: React.FC<{
134138
throw new Error('cannot find divider');
135139
}
136140

141+
if (item.disabled) {
142+
return;
143+
}
144+
137145
if (item.subMenu) {
138146
return setSubMenuActivated('without-mouse');
139147
}
@@ -342,6 +350,7 @@ export const MenuContent: React.FC<{
342350
onNextMenu={onNextMenu}
343351
subMenuActivated={subMenuActivated}
344352
setSubMenuActivated={setSubMenuActivated}
353+
disabled={item.disabled}
345354
/>
346355
);
347356
})}

packages/studio/src/components/RenderModal/WebRenderModal.tsx

Lines changed: 51 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,16 @@ import {getDefaultOutLocation} from '@remotion/studio-shared';
33
import type {
44
RenderMediaOnWebProgress,
55
RenderStillOnWebImageFormat,
6+
WebRendererAudioCodec,
67
WebRendererContainer,
78
WebRendererQuality,
89
WebRendererVideoCodec,
910
} from '@remotion/web-renderer';
10-
import {renderMediaOnWeb, renderStillOnWeb} from '@remotion/web-renderer';
11+
import {
12+
getDefaultAudioCodecForContainer,
13+
renderMediaOnWeb,
14+
renderStillOnWeb,
15+
} from '@remotion/web-renderer';
1116
import {useCallback, useContext, useMemo, useState} from 'react';
1217
import {ShortcutHint} from '../../error-overlay/remotion-overlay/ShortcutHint';
1318
import {AudioIcon} from '../../icons/audio';
@@ -42,6 +47,8 @@ import {
4247
ResolveCompositionBeforeModal,
4348
ResolvedCompositionContext,
4449
} from './ResolveCompositionBeforeModal';
50+
import {useEncodableAudioCodecs} from './use-encodable-audio-codecs';
51+
import {useEncodableVideoCodecs} from './use-encodable-video-codecs';
4552
import {WebRendererExperimentalBadge} from './WebRendererExperimentalBadge';
4653
import {WebRenderModalAdvanced} from './WebRenderModalAdvanced';
4754
import {WebRenderModalAudio} from './WebRenderModalAudio';
@@ -185,6 +192,9 @@ const WebRenderModal: React.FC<WebRenderModalProps> = ({
185192
// Video-specific state
186193
const [codec, setCodec] = useState<WebRendererVideoCodec>('h264');
187194
const [container, setContainer] = useState<WebRendererContainer>('mp4');
195+
const [audioCodec, setAudioCodec] = useState<WebRendererAudioCodec>('aac');
196+
const [audioBitrate, setAudioBitrate] =
197+
useState<WebRendererQuality>('medium');
188198
const [videoBitrate, setVideoBitrate] = useState<WebRendererQuality>('high');
189199
const [hardwareAcceleration, setHardwareAcceleration] = useState<
190200
'no-preference' | 'prefer-hardware' | 'prefer-software'
@@ -203,6 +213,25 @@ const WebRenderModal: React.FC<WebRenderModalProps> = ({
203213

204214
const [licenseKey, setLicenseKey] = useState(initialLicenseKey);
205215

216+
const encodableAudioCodecs = useEncodableAudioCodecs(container);
217+
const encodableVideoCodecs = useEncodableVideoCodecs(container);
218+
219+
const effectiveAudioCodec = useMemo((): WebRendererAudioCodec => {
220+
if (encodableAudioCodecs.includes(audioCodec)) {
221+
return audioCodec;
222+
}
223+
224+
return encodableAudioCodecs[0] ?? audioCodec;
225+
}, [audioCodec, encodableAudioCodecs]);
226+
227+
const effectiveVideoCodec = useMemo((): WebRendererVideoCodec => {
228+
if (encodableVideoCodecs.includes(codec)) {
229+
return codec;
230+
}
231+
232+
return encodableVideoCodecs[0] ?? codec;
233+
}, [codec, encodableVideoCodecs]);
234+
206235
const finalEndFrame = useMemo(() => {
207236
if (endFrame === null) {
208237
return resolvedComposition.durationInFrames - 1;
@@ -258,6 +287,7 @@ const WebRenderModal: React.FC<WebRenderModalProps> = ({
258287
const setContainerFormat = useCallback(
259288
(newContainer: WebRendererContainer) => {
260289
setContainer(newContainer);
290+
setAudioCodec(getDefaultAudioCodecForContainer(newContainer));
261291
setOutName((prev) => {
262292
const newFileName = getStringBeforeSuffix(prev) + '.' + newContainer;
263293
return newFileName;
@@ -274,7 +304,7 @@ const WebRenderModal: React.FC<WebRenderModalProps> = ({
274304
const newFileName = getStringBeforeSuffix(prev) + '.' + container;
275305
return newFileName;
276306
});
277-
} else {
307+
} else if (newMode === 'still') {
278308
setOutName((prev) => {
279309
const newFileName = getStringBeforeSuffix(prev) + '.' + imageFormat;
280310
return newFileName;
@@ -461,7 +491,9 @@ const WebRenderModal: React.FC<WebRenderModalProps> = ({
461491
delayRenderTimeoutInMilliseconds: delayRenderTimeout,
462492
mediaCacheSizeInBytes,
463493
logLevel,
464-
videoCodec: codec,
494+
videoCodec: effectiveVideoCodec,
495+
audioCodec: effectiveAudioCodec,
496+
audioBitrate,
465497
container,
466498
videoBitrate,
467499
hardwareAcceleration,
@@ -495,7 +527,9 @@ const WebRenderModal: React.FC<WebRenderModalProps> = ({
495527
delayRenderTimeout,
496528
mediaCacheSizeInBytes,
497529
logLevel,
498-
codec,
530+
effectiveVideoCodec,
531+
effectiveAudioCodec,
532+
audioBitrate,
499533
container,
500534
videoBitrate,
501535
hardwareAcceleration,
@@ -623,8 +657,9 @@ const WebRenderModal: React.FC<WebRenderModalProps> = ({
623657
onFrameSetDirectly={onFrameSetDirectly}
624658
container={container}
625659
setContainerFormat={setContainerFormat}
626-
codec={codec}
627660
setCodec={setCodec}
661+
encodableVideoCodecs={encodableVideoCodecs}
662+
effectiveVideoCodec={effectiveVideoCodec}
628663
startFrame={finalStartFrame}
629664
setStartFrame={setStartFrame}
630665
endFrame={finalEndFrame}
@@ -659,7 +694,17 @@ const WebRenderModal: React.FC<WebRenderModalProps> = ({
659694
setTransparent={setTransparent}
660695
/>
661696
) : tab === 'audio' ? (
662-
<WebRenderModalAudio muted={muted} setMuted={setMuted} />
697+
<WebRenderModalAudio
698+
muted={muted}
699+
setMuted={setMuted}
700+
audioCodec={audioCodec}
701+
setAudioCodec={setAudioCodec}
702+
audioBitrate={audioBitrate}
703+
setAudioBitrate={setAudioBitrate}
704+
container={container}
705+
encodableCodecs={encodableAudioCodecs}
706+
effectiveAudioCodec={effectiveAudioCodec}
707+
/>
663708
) : tab === 'advanced' ? (
664709
<WebRenderModalAdvanced
665710
renderMode={renderMode}

0 commit comments

Comments
 (0)