mirror of
https://github.com/siddharthvaddem/openscreen.git
synced 2026-11-01 20:32:20 +08:00
refactor: simplify function signatures and reduce line breaks for readability
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user