From 03926c3b2e7e7638addefc0d5d37f13d020f7f30 Mon Sep 17 00:00:00 2001 From: zbcoding Date: Thu, 3 Sep 2026 17:25:36 -0500 Subject: [PATCH 1/6] fix(export): resolve the audio codec against what the platform can encode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every mp4 export template asks for AAC, and the encoder took that as a demand. In WebCodecs, AAC is the platform's encoder — AudioToolbox on macOS, Media Foundation on Windows — so Chromium on Linux has none, and an export failed after the video track had already started encoding. The codec asked for is now a preference: resolveAudioCodec keeps it when the container takes it and the browser can encode it, and otherwise picks the container's next encodable choice (Opus, which mp4 takes and Chromium bundles everywhere). It is resolved at the three places an export starts — the export panel, the dapi handler, and the encoder itself — so the progress panel, the echoed dapi config, and the file all name the same codec. The audio picker offers only codecs this machine can actually encode. Nothing changes where AAC exists: it is encodable, so it is what is used. --- .../inspector/export-templates.ts | 1 + .../sidebar-right/inspector/export.tsx | 45 ++++++++++++++--- apps/web/src/context/dapi/export.ts | 26 ++++++++-- apps/web/src/context/export.tsx | 24 ++++++++- packages/encoder/src/codecs.ts | 50 +++++++++++++++++++ packages/encoder/src/encoder.ts | 23 ++++++++- packages/encoder/src/format.ts | 13 +++-- packages/encoder/src/index.ts | 1 + packages/encoder/src/interfaces.ts | 4 +- reference/export.md | 4 +- 10 files changed, 172 insertions(+), 19 deletions(-) create mode 100644 packages/encoder/src/codecs.ts diff --git a/apps/web/src/components/sidebar-right/inspector/export-templates.ts b/apps/web/src/components/sidebar-right/inspector/export-templates.ts index 56f96473..03deda29 100644 --- a/apps/web/src/components/sidebar-right/inspector/export-templates.ts +++ b/apps/web/src/components/sidebar-right/inspector/export-templates.ts @@ -23,6 +23,7 @@ export const RESOLUTION_OPTIONS: number[] = [720, 1080, 1440, 2160]; export const VIDEO_CODEC_OPTIONS: VideoCodec[] = ["avc", "hevc", "vp9", "av1", "vp8"]; export const VIDEO_FORMAT_OPTIONS: ContainerFormat[] = ["mp4", "webm", "ogg", "mov"]; export const FRAME_RATE_OPTIONS: number[] = [24, 25, 29.97, 30, 48, 50, 59.94, 60]; +/** The codecs the audio picker offers, less the ones a machine cannot encode (see `ExportPanel`). */ export const AUDIO_CODEC_OPTIONS: AudioCodec[] = ["aac", "opus"]; export const SAMPLE_RATE_OPTIONS: number[] = [44100, 48000, 96000]; diff --git a/apps/web/src/components/sidebar-right/inspector/export.tsx b/apps/web/src/components/sidebar-right/inspector/export.tsx index 76dcb346..be8b4648 100644 --- a/apps/web/src/components/sidebar-right/inspector/export.tsx +++ b/apps/web/src/components/sidebar-right/inspector/export.tsx @@ -3,8 +3,8 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ import { Show, createMemo, createResource, createSignal } from "solid-js"; -import { canEncodeVideo } from "mediabunny"; -import { computeOutputSize } from "@diffusionstudio/encoder"; +import { canEncodeVideo, getEncodableAudioCodecs } from "mediabunny"; +import { audioCodecsForFormat, computeOutputSize, resolveAudioCodec } from "@diffusionstudio/encoder"; import { PanelSection } from "@/components/ui/panel-section"; import { Button } from "@/components/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; @@ -158,6 +158,28 @@ export function ExportPanel(props: ExportPanelProps) { ({ codec, bitrate, width, height }) => canEncodeVideo(codec, { width, height, bitrate }), ); + // Which audio codecs are worth offering: those the container takes and + // this browser can encode. AAC is a platform encoder in WebCodecs + // (AudioToolbox on macOS, Media Foundation on Windows) and simply absent + // on Linux, so offering it there would only fail the export. + const [audioCodecs] = createResource( + () => { + const current = settings(); + return { + format: current?.format ?? ("mp4" as const), + sampleRate: current?.audio?.sampleRate, + bitrate: current?.audio?.bitrate, + }; + }, + async ({ format, ...options }) => { + const [supported, encodable] = await Promise.all([ + audioCodecsForFormat(format), + getEncodableAudioCodecs([...AUDIO_CODEC_OPTIONS], options), + ]); + return encodable.filter((codec) => supported.includes(codec)); + }, + ); + // Audio-only exports always encode const exportSupported = createMemo(() => { const current = settings(); @@ -180,14 +202,21 @@ export function ExportPanel(props: ExportPanelProps) { void config()?.setExport(entity(), value); }; - // Replaces the settings with a preset's, wholesale. - const applyTemplate = (id: string) => { + // Replaces the settings with a preset's, wholesale. A preset names AAC for + // mp4, which is a preference rather than a demand — what is written is the + // codec this browser can encode into the container. + const applyTemplate = async (id: string) => { const next = templateSettings(id); - if (next) write(next); + if (!next) return; + const codec = await resolveAudioCodec(next.format, next.audio?.codec, { + sampleRate: next.audio?.sampleRate, + bitrate: next.audio?.bitrate, + }); + write(codec ? { ...next, audio: { ...next.audio, codec } } : next); }; const addSettings = () => { - applyTemplate(DEFAULT_EXPORT_TEMPLATE_ID); + void applyTemplate(DEFAULT_EXPORT_TEMPLATE_ID); setIsInspectorOpen(true); }; @@ -294,6 +323,7 @@ export function ExportPanel(props: ExportPanelProps) { settings={current()} resolutionOptions={resolutionOptions()} selectedResolution={selectedResolution()} + audioCodecOptions={audioCodecs() ?? AUDIO_CODEC_OPTIONS} anchorRef={inspectorAnchorRef} onClose={() => setIsInspectorOpen(false)} onSelectTemplate={applyTemplate} @@ -309,6 +339,7 @@ type ExportInspectorProps = { settings: ProjectExportConfig; resolutionOptions: ResolutionOption[]; selectedResolution: ResolutionOption | null; + audioCodecOptions: AudioCodec[]; anchorRef: HTMLDivElement | undefined; onClose: () => void; onSelectTemplate: (id: string) => void; @@ -507,7 +538,7 @@ function ExportInspector(props: ExportInspectorProps) {