-
Notifications
You must be signed in to change notification settings - Fork 2.4k
Updates for exporting video .mp4 type #438
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -6,8 +6,15 @@ const AUDIO_BITRATE = 128_000; | |
| const DECODE_BACKPRESSURE_LIMIT = 20; | ||
| const MIN_SPEED_REGION_DELTA_MS = 0.0001; | ||
|
|
||
| export type TargetAudioCodec = "aac" | "opus"; | ||
|
|
||
| export class AudioProcessor { | ||
| private cancelled = false; | ||
| private targetCodec: TargetAudioCodec; | ||
|
|
||
| constructor(targetCodec: TargetAudioCodec) { | ||
| this.targetCodec = targetCodec; | ||
| } | ||
|
|
||
| /** | ||
| * Audio export has two modes: | ||
|
|
@@ -132,16 +139,24 @@ export class AudioProcessor { | |
| const sampleRate = audioConfig.sampleRate || 48000; | ||
| const channels = audioConfig.numberOfChannels || 2; | ||
|
|
||
| const encodeConfig: AudioEncoderConfig = { | ||
| codec: "opus", | ||
| sampleRate, | ||
| numberOfChannels: channels, | ||
| bitrate: AUDIO_BITRATE, | ||
| }; | ||
| const encodeConfig: AudioEncoderConfig = | ||
| this.targetCodec === "aac" | ||
| ? { | ||
| codec: "mp4a.40.2", | ||
| sampleRate, | ||
| numberOfChannels: channels, | ||
| bitrate: AUDIO_BITRATE, | ||
| } | ||
| : { | ||
| codec: "opus", | ||
| sampleRate, | ||
| numberOfChannels: channels, | ||
| bitrate: AUDIO_BITRATE, | ||
| }; | ||
|
|
||
| const encodeSupport = await AudioEncoder.isConfigSupported(encodeConfig); | ||
| if (!encodeSupport.supported) { | ||
| console.warn("[AudioProcessor] Opus encoding not supported, skipping audio"); | ||
| console.warn("[AudioProcessor] Audio encoding not supported, skipping audio"); | ||
| for (const frame of decodedFrames) frame.close(); | ||
| return; | ||
| } | ||
|
|
@@ -314,28 +329,7 @@ export class AudioProcessor { | |
|
|
||
| try { | ||
| await demuxer.load(file); | ||
| const audioConfig = (await demuxer.getDecoderConfig("audio")) as AudioDecoderConfig; | ||
| const reader = (demuxer.read("audio") as ReadableStream<EncodedAudioChunk>).getReader(); | ||
| let isFirstChunk = true; | ||
|
|
||
| try { | ||
| while (!this.cancelled) { | ||
| const { done, value: chunk } = await reader.read(); | ||
| if (done || !chunk) break; | ||
| if (isFirstChunk) { | ||
| await muxer.addAudioChunk(chunk, { decoderConfig: audioConfig }); | ||
| isFirstChunk = false; | ||
| } else { | ||
| await muxer.addAudioChunk(chunk); | ||
| } | ||
| } | ||
| } finally { | ||
| try { | ||
| await reader.cancel(); | ||
| } catch { | ||
| /* reader already closed */ | ||
| } | ||
| } | ||
| await this.processTrimOnlyAudio(demuxer, muxer, [], undefined); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This makes speed-region exports much heavier in memory. Line 332 now sends the rendered blob through |
||
| } finally { | ||
| try { | ||
| demuxer.destroy(); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import type { WebDemuxer } from "web-demuxer"; | ||
| import type { | ||
| AnnotationRegion, | ||
| CropRegion, | ||
|
|
@@ -10,12 +11,13 @@ import type { | |
| import { AsyncVideoFrameQueue } from "./asyncVideoFrameQueue"; | ||
| import { AudioProcessor } from "./audioEncoder"; | ||
| import { FrameRenderer } from "./frameRenderer"; | ||
| import { VideoMuxer } from "./muxer"; | ||
| import { type Mp4AudioCodec, VideoMuxer } from "./muxer"; | ||
| import { StreamingVideoDecoder } from "./streamingDecoder"; | ||
| import type { ExportConfig, ExportProgress, ExportResult } from "./types"; | ||
|
|
||
| const ENCODER_STALL_TIMEOUT_MS = 15_000; | ||
| const ENCODER_FLUSH_TIMEOUT_MS = 20_000; | ||
| const AUDIO_BITRATE = 128_000; | ||
|
|
||
| interface VideoExporterConfig extends ExportConfig { | ||
| videoUrl: string; | ||
|
|
@@ -66,6 +68,40 @@ export class VideoExporter { | |
| this.config = config; | ||
| } | ||
|
|
||
| private async pickAudioCodec(demuxer: WebDemuxer | null): Promise<Mp4AudioCodec | null> { | ||
| if (!demuxer) return null; | ||
|
|
||
| let audioConfig: AudioDecoderConfig; | ||
| try { | ||
| audioConfig = (await demuxer.getDecoderConfig("audio")) as AudioDecoderConfig; | ||
| } catch { | ||
| return null; | ||
| } | ||
|
|
||
| const sampleRate = audioConfig.sampleRate || 48000; | ||
| const channels = audioConfig.numberOfChannels || 2; | ||
|
|
||
| const aacConfig: AudioEncoderConfig = { | ||
| codec: "mp4a.40.2", | ||
| sampleRate, | ||
| numberOfChannels: channels, | ||
| bitrate: AUDIO_BITRATE, | ||
| }; | ||
| const aacSupport = await AudioEncoder.isConfigSupported(aacConfig); | ||
| if (aacSupport.supported) return "aac"; | ||
|
|
||
| const opusConfig: AudioEncoderConfig = { | ||
| codec: "opus", | ||
| sampleRate, | ||
| numberOfChannels: channels, | ||
| bitrate: AUDIO_BITRATE, | ||
| }; | ||
| const opusSupport = await AudioEncoder.isConfigSupported(opusConfig); | ||
| if (opusSupport.supported) return "opus"; | ||
|
|
||
| return null; | ||
| } | ||
|
Comment on lines
+71
to
+103
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
echo "video exporter codec probing:"
rg -n -C3 'pickAudioCodec|AudioEncoder\.isConfigSupported|return "aac"|return "opus"|return null' src/lib/exporter/videoExporter.ts
echo '---'
echo "audio processor decode/encode gates:"
rg -n -C3 'AudioDecoder\.isConfigSupported|Audio codec not supported|Audio encoding not supported' src/lib/exporter/audioEncoder.ts
echo '---'
echo "hard failure on missing audio codec:"
rg -n -C2 'Source has audio, but no supported audio encoder was found for MP4 export' src/lib/exporter/videoExporter.tsRepository: siddharthvaddem/openscreen Length of output: 2198 Add decoder gate to The codec picker at lines 71–103 only probes Suggested fix:
🤖 Prompt for AI Agents |
||
|
|
||
| async export(): Promise<ExportResult> { | ||
| const encoderPreferences = this.getEncoderPreferences(); | ||
| let lastError: Error | null = null; | ||
|
|
@@ -153,7 +189,14 @@ export class VideoExporter { | |
| await this.initializeEncoder(encoderPreference); | ||
|
|
||
| const hasAudio = videoInfo.hasAudio; | ||
| const muxer = new VideoMuxer(this.config, hasAudio); | ||
| const audioCodec = hasAudio ? await this.pickAudioCodec(streamingDecoder.getDemuxer()) : null; | ||
| if (hasAudio && !audioCodec) { | ||
| throw new Error( | ||
| "Source has audio, but no supported audio encoder was found for MP4 export.", | ||
| ); | ||
|
Comment on lines
+193
to
+196
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This new hard failure blocks the entire MP4 export whenever the input has an audio stream but neither AAC nor Opus encoding is supported in the runtime. That is a functional regression for environments with limited WebCodecs audio support, where a video-only export is still possible and was previously handled by skipping audio in Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| const muxer = new VideoMuxer(this.config, audioCodec); | ||
| this.muxer = muxer; | ||
| await muxer.initialize(); | ||
|
|
||
|
|
@@ -335,11 +378,11 @@ export class VideoExporter { | |
| phase: "finalizing", | ||
| }); | ||
|
|
||
| if (hasAudio && !this.cancelled) { | ||
| if (audioCodec && !this.cancelled) { | ||
| const demuxer = streamingDecoder.getDemuxer(); | ||
| if (demuxer) { | ||
| console.log("[VideoExporter] Processing audio track..."); | ||
| this.audioProcessor = new AudioProcessor(); | ||
| this.audioProcessor = new AudioProcessor(audioCodec); | ||
| await this.audioProcessor.process( | ||
| demuxer, | ||
| muxer, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Routing
muxRenderedAudioBlobthroughprocessTrimOnlyAudiochanges this path from chunk streaming to full decode/re-encode buffering, becauseprocessTrimOnlyAudioaccumulates all decodedAudioDataframes before encoding/muxing. For long videos with speed regions, this can drive memory usage to uncompressed PCM scale and cause export stalls/OOMs that did not occur with the previous streamed chunk path.Useful? React with 👍 / 👎.