Skip to content

Commit dd8c001

Browse files
committed
refactor: require validatedDurationSec in AudioProcessor, drop fallbacks
AudioProcessor.process and renderPitchPreservedTimelineAudio accepted validatedDurationSec as optional, so the speed-aware path fell back to media.duration when it was absent. HTMLMediaElement.duration can be Infinity for the same MediaRecorder/Chromium Linux containers this PR targets, which would make effectiveEnd and the playback stop checks unreliable. The only caller (VideoExporter.process) already threads streamingDecoder's validatedDuration through, so make the parameter required. Drop the media.duration fallback, the Number.isFinite guard on readEndSec, and the two `!== undefined` checks in the tick loop. While here: - Document that +0.5 on readEndSec mirrors streamingDecoder.decodeAll's read window so trim-only and speed-aware paths stay in sync. - Replace the unreachable silent-blob fallback at the end of renderPitchPreservedTimelineAudio with a loud invariant throw, so a broken recorder contract surfaces instead of yielding empty audio.
1 parent 0c01db7 commit dd8c001

1 file changed

Lines changed: 14 additions & 15 deletions

File tree

src/lib/exporter/audioEncoder.ts

Lines changed: 14 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -19,9 +19,9 @@ export class AudioProcessor {
1919
demuxer: WebDemuxer,
2020
muxer: VideoMuxer,
2121
videoUrl: string,
22-
trimRegions?: TrimRegion[],
23-
speedRegions?: SpeedRegion[],
24-
validatedDurationSec?: number,
22+
trimRegions: TrimRegion[] | undefined,
23+
speedRegions: SpeedRegion[] | undefined,
24+
validatedDurationSec: number,
2525
): Promise<void> {
2626
const sortedTrims = trimRegions ? [...trimRegions].sort((a, b) => a.startMs - b.startMs) : [];
2727
const sortedSpeedRegions = speedRegions
@@ -46,10 +46,9 @@ export class AudioProcessor {
4646
}
4747

4848
// No speed edits: keep the original demux/decode/encode path with trim timestamp remap.
49-
const readEndSec =
50-
typeof validatedDurationSec === "number" && Number.isFinite(validatedDurationSec)
51-
? validatedDurationSec + 0.5
52-
: undefined;
49+
// The +0.5s buffer mirrors streamingDecoder.decodeAll's read window so the trim-only
50+
// and speed-aware paths agree on how far to read past the validated duration boundary.
51+
const readEndSec = validatedDurationSec + 0.5;
5352
await this.processTrimOnlyAudio(demuxer, muxer, sortedTrims, readEndSec);
5453
}
5554

@@ -193,7 +192,7 @@ export class AudioProcessor {
193192
videoUrl: string,
194193
trimRegions: TrimRegion[],
195194
speedRegions: SpeedRegion[],
196-
validatedDurationSec?: number,
195+
validatedDurationSec: number,
197196
): Promise<Blob> {
198197
const media = document.createElement("audio");
199198
media.src = videoUrl;
@@ -230,7 +229,7 @@ export class AudioProcessor {
230229
// Skip past any initial trim region(s) before recording starts to avoid
231230
// capturing trimmed audio during the first rAF frames of playback.
232231
// Loops to handle back-to-back or overlapping trims at t=0.
233-
const effectiveEnd = validatedDurationSec ?? media.duration;
232+
const effectiveEnd = validatedDurationSec;
234233
let startPosition = 0;
235234
for (let i = 0; i <= trimRegions.length; i++) {
236235
const activeTrim = this.findActiveTrimRegion(startPosition * 1000, trimRegions);
@@ -287,7 +286,7 @@ export class AudioProcessor {
287286

288287
// Stop playback at validated duration — browser's media.duration
289288
// may be inflated from bad container metadata.
290-
if (validatedDurationSec !== undefined && media.currentTime >= validatedDurationSec) {
289+
if (media.currentTime >= validatedDurationSec) {
291290
media.pause();
292291
cleanup();
293292
resolve();
@@ -299,10 +298,7 @@ export class AudioProcessor {
299298

300299
if (activeTrimRegion && !media.paused && !media.ended) {
301300
const skipToTime = activeTrimRegion.endMs / 1000;
302-
if (
303-
skipToTime >= media.duration ||
304-
(validatedDurationSec !== undefined && skipToTime >= validatedDurationSec)
305-
) {
301+
if (skipToTime >= media.duration || skipToTime >= validatedDurationSec) {
306302
media.pause();
307303
cleanup();
308304
resolve();
@@ -379,7 +375,10 @@ export class AudioProcessor {
379375
}
380376

381377
if (!recordedBlobPromise) {
382-
return new Blob([], { type: "audio/webm" });
378+
// Invariant: either an early return above fires, or startAudioRecording ran and
379+
// populated recordedBlobPromise before the playback Promise resolved. Reaching
380+
// here means that contract was broken — fail loud instead of returning silence.
381+
throw new Error("Audio recorder finished without assigning recordedBlobPromise");
383382
}
384383
const recordedBlob = await recordedBlobPromise;
385384
if (this.cancelled) {

0 commit comments

Comments
 (0)