refactor: simplify function signatures and reduce line breaks for readability

This commit is contained in:
Davide Mendolia
2026-05-24 11:53:44 +02:00
parent db1d6bfe96
commit c0b929d4b2
+39 -130
View File
@@ -19,11 +19,7 @@ function buildAV1CodecString(description?: BufferSource): string {
const bytes =
description instanceof ArrayBuffer
? new Uint8Array(description)
: new Uint8Array(
description.buffer,
description.byteOffset,
description.byteLength,
);
: new Uint8Array(description.buffer, description.byteOffset, description.byteLength);
// AV1CodecConfigurationRecord layout (4+ bytes):
// Byte 0: marker (1) | version (7)
@@ -91,24 +87,16 @@ const SCAN_UNBOUNDED_FALLBACK_SEC = 24 * 60 * 60;
* @param containerDuration Duration from the container-level metadata
* @param scannedDuration Duration derived from actual packet timestamps (ground truth)
*/
export function validateDuration(
containerDuration: number,
scannedDuration: number,
): number {
export function validateDuration(containerDuration: number, scannedDuration: number): number {
if (scannedDuration <= 0) {
// Zero scanned duration means corrupted/empty file — fall back to container
// (downstream shouldFailDecodeEndedEarly will catch truly empty files)
return Number.isFinite(containerDuration)
? Math.max(containerDuration, 0)
: 0;
return Number.isFinite(containerDuration) ? Math.max(containerDuration, 0) : 0;
}
if (!Number.isFinite(containerDuration) || containerDuration <= 0) {
return scannedDuration;
}
if (
Math.abs(containerDuration - scannedDuration) >
DURATION_DIVERGENCE_THRESHOLD_SEC
) {
if (Math.abs(containerDuration - scannedDuration) > DURATION_DIVERGENCE_THRESHOLD_SEC) {
return scannedDuration;
}
return containerDuration;
@@ -133,27 +121,16 @@ export function shouldFailDecodeEndedEarly({
return false;
}
if (
typeof streamDurationSec !== "number" ||
!Number.isFinite(streamDurationSec)
) {
if (typeof streamDurationSec !== "number" || !Number.isFinite(streamDurationSec)) {
return true;
}
const metadataTailSec = requiredEndSec - streamDurationSec;
const decodedNearStreamEnd =
Math.abs(lastDecodedFrameSec - streamDurationSec) <=
STREAM_DURATION_MATCH_TOLERANCE_SEC;
Math.abs(lastDecodedFrameSec - streamDurationSec) <= STREAM_DURATION_MATCH_TOLERANCE_SEC;
const maxTailSec = Math.max(
METADATA_TAIL_TOLERANCE_SEC,
requiredEndSec * 0.01,
);
if (
decodedNearStreamEnd &&
metadataTailSec > 0 &&
metadataTailSec <= maxTailSec
) {
const maxTailSec = Math.max(METADATA_TAIL_TOLERANCE_SEC, requiredEndSec * 0.01);
if (decodedNearStreamEnd && metadataTailSec > 0 && metadataTailSec <= maxTailSec) {
return false;
}
@@ -202,9 +179,7 @@ export class StreamingVideoDecoder {
private metadata: DecodedVideoInfo | null = null;
/** Routes to the appropriate loader based on whether the source is local or remote. */
private async loadSourceFile(
videoUrl: string,
): Promise<{ file: File; blob: Blob }> {
private async loadSourceFile(videoUrl: string): Promise<{ file: File; blob: Blob }> {
const isRemoteUrl = /^(https?:|blob:|data:)/i.test(videoUrl);
if (!isRemoteUrl && window.electronAPI) {
return this.withTimeout(
@@ -221,14 +196,10 @@ export class StreamingVideoDecoder {
}
/** Loads a local video file via the Electron IPC bridge. */
static async loadLocalSourceFile(
videoUrl: string,
): Promise<{ file: File; blob: Blob }> {
static async loadLocalSourceFile(videoUrl: string): Promise<{ file: File; blob: Blob }> {
const result = await window.electronAPI.readBinaryFile(videoUrl);
if (!result.success || !result.data) {
throw new Error(
result.message || result.error || "Failed to read source video",
);
throw new Error(result.message || result.error || "Failed to read source video");
}
const filename = (result.path || videoUrl).split(/[\\/]/).pop() || "video";
@@ -242,14 +213,10 @@ export class StreamingVideoDecoder {
}
/** Loads a remote or blob video URL via fetch. */
static async loadRemoteSourceFile(
videoUrl: string,
): Promise<{ file: File; blob: Blob }> {
static async loadRemoteSourceFile(videoUrl: string): Promise<{ file: File; blob: Blob }> {
const response = await fetch(videoUrl);
if (!response.ok) {
throw new Error(
`Failed to fetch source video: ${response.status} ${response.statusText}`,
);
throw new Error(`Failed to fetch source video: ${response.status} ${response.statusText}`);
}
const blob = await response.blob();
const filename = videoUrl.split("/").pop() || "video";
@@ -263,8 +230,7 @@ export class StreamingVideoDecoder {
const { file } = await this.loadSourceFile(videoUrl);
// Relative URL so it resolves correctly in both dev (http) and packaged (file://) builds
const wasmUrl = new URL("./wasm/web-demuxer.wasm", window.location.href)
.href;
const wasmUrl = new URL("./wasm/web-demuxer.wasm", window.location.href).href;
this.demuxer = new WebDemuxer({ wasmFilePath: wasmUrl });
await this.withTimeout(
this.demuxer.load(file),
@@ -277,9 +243,7 @@ export class StreamingVideoDecoder {
SOURCE_LOAD_TIMEOUT_MS,
"Timed out while reading video metadata.",
);
const videoStream = mediaInfo.streams.find(
(s) => s.codec_type_string === "video",
);
const videoStream = mediaInfo.streams.find((s) => s.codec_type_string === "video");
let frameRate = 60;
if (videoStream?.avg_frame_rate) {
@@ -291,9 +255,7 @@ export class StreamingVideoDecoder {
}
}
const audioStream = mediaInfo.streams.find(
(s) => s.codec_type_string === "audio",
);
const audioStream = mediaInfo.streams.find((s) => s.codec_type_string === "audio");
// Scan video packets to find the true content boundary.
// MediaRecorder (especially on Linux) writes unreliable container durations.
@@ -301,23 +263,14 @@ export class StreamingVideoDecoder {
// Pass explicit range because some containers are truncated without one.
// Sanitize because mediaInfo.duration can be NaN/Infinity (Chromium Linux bug),
// which would propagate into demuxer.read() as an invalid endpoint.
const containerDurationSec = Number.isFinite(mediaInfo.duration)
? mediaInfo.duration
: 0;
const containerDurationSec = Number.isFinite(mediaInfo.duration) ? mediaInfo.duration : 0;
const streamDurationSec =
typeof videoStream?.duration === "number" &&
Number.isFinite(videoStream.duration)
typeof videoStream?.duration === "number" && Number.isFinite(videoStream.duration)
? videoStream.duration
: 0;
const hintedDurationSec = Math.max(
containerDurationSec,
streamDurationSec,
0,
);
const hintedDurationSec = Math.max(containerDurationSec, streamDurationSec, 0);
const scanEndSec =
hintedDurationSec > 0
? hintedDurationSec + 0.5
: SCAN_UNBOUNDED_FALLBACK_SEC;
hintedDurationSec > 0 ? hintedDurationSec + 0.5 : SCAN_UNBOUNDED_FALLBACK_SEC;
let maxPacketEndUs = 0;
const scanReader = this.demuxer.read("video", 0, scanEndSec).getReader();
try {
@@ -335,18 +288,14 @@ export class StreamingVideoDecoder {
}
}
const scannedDuration = maxPacketEndUs / 1_000_000;
const validatedDuration = validateDuration(
mediaInfo.duration,
scannedDuration,
);
const validatedDuration = validateDuration(mediaInfo.duration, scannedDuration);
this.metadata = {
width: videoStream?.width || 1920,
height: videoStream?.height || 1080,
duration: validatedDuration,
streamDuration:
typeof videoStream?.duration === "number" &&
Number.isFinite(videoStream.duration)
typeof videoStream?.duration === "number" && Number.isFinite(videoStream.duration)
? videoStream.duration
: undefined,
frameRate,
@@ -379,14 +328,8 @@ export class StreamingVideoDecoder {
const decoderConfig = await this.demuxer.getDecoderConfig("video");
console.log(
"[StreamingVideoDecoder] decoderConfig.codec:",
decoderConfig.codec,
);
console.log(
"[StreamingVideoDecoder] decoderConfig.description:",
decoderConfig.description,
);
console.log("[StreamingVideoDecoder] decoderConfig.codec:", decoderConfig.codec);
console.log("[StreamingVideoDecoder] decoderConfig.description:", decoderConfig.description);
// web-demuxer may return bare four-character code strings ("av01", "vp08",
// "vp09", "avc1") that WebCodecs rejects. Normalize them to the short or
@@ -425,8 +368,7 @@ export class StreamingVideoDecoder {
const segmentOutputFrameCounts = segments.map((segment) =>
Math.ceil(
((segment.endSec - segment.startSec - EPSILON_SEC) / segment.speed) *
targetFrameRate,
((segment.endSec - segment.startSec - EPSILON_SEC) / segment.speed) * targetFrameRate,
),
);
const frameDurationUs = 1_000_000 / targetFrameRate;
@@ -468,9 +410,7 @@ export class StreamingVideoDecoder {
: decoderConfig;
try {
const support = await VideoDecoder.isConfigSupported(
preferredDecoderConfig,
);
const support = await VideoDecoder.isConfigSupported(preferredDecoderConfig);
console.log(
`[StreamingVideoDecoder] isConfigSupported for "${preferredDecoderConfig.codec}":`,
support.supported,
@@ -496,8 +436,7 @@ export class StreamingVideoDecoder {
const getNextFrame = (): Promise<VideoFrame | null> => {
if (decodeError) throw decodeError;
if (pendingFrames.length > 0)
return Promise.resolve(pendingFrames.shift()!);
if (pendingFrames.length > 0) return Promise.resolve(pendingFrames.shift()!);
if (decodeDone) return Promise.resolve(null);
return new Promise((resolve) => {
frameResolve = resolve;
@@ -561,18 +500,13 @@ export class StreamingVideoDecoder {
if (segmentFrameIndex >= segmentFrameCount) return false;
const sourceTimeSec =
segment.startSec +
(segmentFrameIndex / targetFrameRate) * segment.speed;
segment.startSec + (segmentFrameIndex / targetFrameRate) * segment.speed;
if (sourceTimeSec >= segment.endSec - EPSILON_SEC) return false;
const clone = new VideoFrame(heldFrame, {
timestamp: heldFrame.timestamp,
});
await onFrame(
clone,
exportFrameIndex * frameDurationUs,
sourceTimeSec * 1000,
);
await onFrame(clone, exportFrameIndex * frameDurationUs, sourceTimeSec * 1000);
segmentFrameIndex++;
exportFrameIndex++;
return true;
@@ -635,8 +569,7 @@ export class StreamingVideoDecoder {
}
const sourceTimeSec =
currentSegment.startSec +
(segmentFrameIndex / targetFrameRate) * currentSegment.speed;
currentSegment.startSec + (segmentFrameIndex / targetFrameRate) * currentSegment.speed;
if (sourceTimeSec >= currentSegment.endSec - EPSILON_SEC) {
break;
}
@@ -647,11 +580,7 @@ export class StreamingVideoDecoder {
const clone = new VideoFrame(heldFrame, {
timestamp: heldFrame.timestamp,
});
await onFrame(
clone,
exportFrameIndex * frameDurationUs,
sourceTimeSec * 1000,
);
await onFrame(clone, exportFrameIndex * frameDurationUs, sourceTimeSec * 1000);
segmentFrameIndex++;
exportFrameIndex++;
}
@@ -716,9 +645,7 @@ export class StreamingVideoDecoder {
})
) {
const decodedAtLabel =
lastDecodedFrameSec === null
? "no decoded frame"
: `${lastDecodedFrameSec.toFixed(3)}s`;
lastDecodedFrameSec === null ? "no decoded frame" : `${lastDecodedFrameSec.toFixed(3)}s`;
const message = `Decode ended early at ${decodedAtLabel} (needed ${requiredEndSec.toFixed(3)}s) – export may be slightly shorter than expected.`;
console.warn(`[StreamingVideoDecoder] ${message}`);
onWarning?.(message);
@@ -768,10 +695,7 @@ export class StreamingVideoDecoder {
speedRegions?: SpeedRegion[],
): { effectiveDuration: number; totalFrames: number } {
if (!this.metadata) throw new Error("Must call loadMetadata() first");
const trimSegments = this.computeSegments(
this.metadata.duration,
trimRegions,
);
const trimSegments = this.computeSegments(this.metadata.duration, trimRegions);
const segments = this.splitBySpeed(trimSegments, speedRegions);
return {
effectiveDuration: segments.reduce(
@@ -780,9 +704,7 @@ export class StreamingVideoDecoder {
),
totalFrames: segments.reduce((sum, seg) => {
const segDur = seg.endSec - seg.startSec - EPSILON_SEC;
return (
sum + Math.max(0, Math.ceil((segDur / seg.speed) * targetFrameRate))
);
return sum + Math.max(0, Math.ceil((segDur / seg.speed) * targetFrameRate));
}, 0),
};
}
@@ -798,15 +720,10 @@ export class StreamingVideoDecoder {
if (!speedRegions || speedRegions.length === 0)
return segments.map((s) => ({ ...s, speed: 1 }));
const result: Array<{ startSec: number; endSec: number; speed: number }> =
[];
const result: Array<{ startSec: number; endSec: number; speed: number }> = [];
for (const segment of segments) {
const overlapping = speedRegions
.filter(
(sr) =>
sr.startMs / 1000 < segment.endSec &&
sr.endMs / 1000 > segment.startSec,
)
.filter((sr) => sr.startMs / 1000 < segment.endSec && sr.endMs / 1000 > segment.startSec)
.sort((a, b) => a.startMs - b.startMs);
if (overlapping.length === 0) {
@@ -818,8 +735,7 @@ export class StreamingVideoDecoder {
for (const sr of overlapping) {
const srStart = Math.max(sr.startMs / 1000, segment.startSec);
const srEnd = Math.min(sr.endMs / 1000, segment.endSec);
if (cursor < srStart)
result.push({ startSec: cursor, endSec: srStart, speed: 1 });
if (cursor < srStart) result.push({ startSec: cursor, endSec: srStart, speed: 1 });
result.push({ startSec: srStart, endSec: srEnd, speed: sr.speed });
cursor = srEnd;
}
@@ -863,16 +779,9 @@ export class StreamingVideoDecoder {
}
/** Wraps a promise with a hard timeout, rejecting with `message` if it exceeds `timeoutMs`. */
private withTimeout<T>(
promise: Promise<T>,
timeoutMs: number,
message: string,
): Promise<T> {
private withTimeout<T>(promise: Promise<T>, timeoutMs: number, message: string): Promise<T> {
return new Promise<T>((resolve, reject) => {
const timer = window.setTimeout(
() => reject(new Error(message)),
timeoutMs,
);
const timer = window.setTimeout(() => reject(new Error(message)), timeoutMs);
promise.then(
(value) => {
window.clearTimeout(timer);