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.
This commit is contained in:
@@ -19,9 +19,9 @@ export class AudioProcessor {
|
||||
demuxer: WebDemuxer,
|
||||
muxer: VideoMuxer,
|
||||
videoUrl: string,
|
||||
trimRegions?: TrimRegion[],
|
||||
speedRegions?: SpeedRegion[],
|
||||
validatedDurationSec?: number,
|
||||
trimRegions: TrimRegion[] | undefined,
|
||||
speedRegions: SpeedRegion[] | undefined,
|
||||
validatedDurationSec: number,
|
||||
): Promise<void> {
|
||||
const sortedTrims = trimRegions ? [...trimRegions].sort((a, b) => a.startMs - b.startMs) : [];
|
||||
const sortedSpeedRegions = speedRegions
|
||||
@@ -46,10 +46,9 @@ export class AudioProcessor {
|
||||
}
|
||||
|
||||
// No speed edits: keep the original demux/decode/encode path with trim timestamp remap.
|
||||
const readEndSec =
|
||||
typeof validatedDurationSec === "number" && Number.isFinite(validatedDurationSec)
|
||||
? validatedDurationSec + 0.5
|
||||
: undefined;
|
||||
// The +0.5s buffer mirrors streamingDecoder.decodeAll's read window so the trim-only
|
||||
// and speed-aware paths agree on how far to read past the validated duration boundary.
|
||||
const readEndSec = validatedDurationSec + 0.5;
|
||||
await this.processTrimOnlyAudio(demuxer, muxer, sortedTrims, readEndSec);
|
||||
}
|
||||
|
||||
@@ -193,7 +192,7 @@ export class AudioProcessor {
|
||||
videoUrl: string,
|
||||
trimRegions: TrimRegion[],
|
||||
speedRegions: SpeedRegion[],
|
||||
validatedDurationSec?: number,
|
||||
validatedDurationSec: number,
|
||||
): Promise<Blob> {
|
||||
const media = document.createElement("audio");
|
||||
media.src = videoUrl;
|
||||
@@ -230,7 +229,7 @@ export class AudioProcessor {
|
||||
// Skip past any initial trim region(s) before recording starts to avoid
|
||||
// capturing trimmed audio during the first rAF frames of playback.
|
||||
// Loops to handle back-to-back or overlapping trims at t=0.
|
||||
const effectiveEnd = validatedDurationSec ?? media.duration;
|
||||
const effectiveEnd = validatedDurationSec;
|
||||
let startPosition = 0;
|
||||
for (let i = 0; i <= trimRegions.length; i++) {
|
||||
const activeTrim = this.findActiveTrimRegion(startPosition * 1000, trimRegions);
|
||||
@@ -287,7 +286,7 @@ export class AudioProcessor {
|
||||
|
||||
// Stop playback at validated duration — browser's media.duration
|
||||
// may be inflated from bad container metadata.
|
||||
if (validatedDurationSec !== undefined && media.currentTime >= validatedDurationSec) {
|
||||
if (media.currentTime >= validatedDurationSec) {
|
||||
media.pause();
|
||||
cleanup();
|
||||
resolve();
|
||||
@@ -299,10 +298,7 @@ export class AudioProcessor {
|
||||
|
||||
if (activeTrimRegion && !media.paused && !media.ended) {
|
||||
const skipToTime = activeTrimRegion.endMs / 1000;
|
||||
if (
|
||||
skipToTime >= media.duration ||
|
||||
(validatedDurationSec !== undefined && skipToTime >= validatedDurationSec)
|
||||
) {
|
||||
if (skipToTime >= media.duration || skipToTime >= validatedDurationSec) {
|
||||
media.pause();
|
||||
cleanup();
|
||||
resolve();
|
||||
@@ -379,7 +375,10 @@ export class AudioProcessor {
|
||||
}
|
||||
|
||||
if (!recordedBlobPromise) {
|
||||
return new Blob([], { type: "audio/webm" });
|
||||
// Invariant: either an early return above fires, or startAudioRecording ran and
|
||||
// populated recordedBlobPromise before the playback Promise resolved. Reaching
|
||||
// here means that contract was broken — fail loud instead of returning silence.
|
||||
throw new Error("Audio recorder finished without assigning recordedBlobPromise");
|
||||
}
|
||||
const recordedBlob = await recordedBlobPromise;
|
||||
if (this.cancelled) {
|
||||
|
||||
Reference in New Issue
Block a user