import { execFile } from "node:child_process"; import { rewriteAssetPath } from "@hyperframes/parsers/asset-paths"; import { findFfBinary } from "@hyperframes/parsers/ff-binaries"; import { cleanAssetUrl, isRemoteOrInlineUrl, maskNonScannableRanges, resolveExistingLocalAsset, } from "@hyperframes/parsers/asset-resolution"; import type { HyperframeLintFinding } from "./types.js"; /** Structurally compatible with `project.ts`'s (unexported) `HtmlSource` — * duplicated as a shape, not imported, to avoid a circular import between * this file and `project.ts` (which imports `lintHevcPreviewCodec` below). */ interface HtmlSourceLike { html: string; compSrcPath?: string; } const PROBE_TIMEOUT_MS = 4000; // Bounds concurrent ffprobe child processes for compositions referencing many videos. const PROBE_CONCURRENCY = 8; function execFileAsync(file: string, args: string[]): Promise { return new Promise((resolvePromise, reject) => { execFile(file, args, { timeout: PROBE_TIMEOUT_MS }, (error, stdout) => { if (error) reject(error); else resolvePromise(stdout.toString()); }); }); } function hasHevcStream(json: unknown): boolean { if (typeof json !== "object" || json === null) return false; const streams = Reflect.get(json, "streams"); if (!Array.isArray(streams)) return false; return streams.some((stream) => { if (typeof stream !== "object" || stream === null) return false; return Reflect.get(stream, "codec_name") === "hevc"; }); } // Best-effort: any failure (ffprobe missing, times out, non-video file, // unparsable output) resolves to "not HEVC" rather than throwing. This rule // must never fail lint/check just because ffprobe isn't installed. async function probeIsHevc(ffprobePath: string, filePath: string): Promise { try { const stdout = await execFileAsync(ffprobePath, [ "-v", "error", "-select_streams", "v:0", "-show_entries", "stream=codec_name", "-of", "json", filePath, ]); return hasHevcStream(JSON.parse(stdout)); } catch { return false; } } /** * Collects local `