@hyperframes/studio-server 0.8.141 → 0.8.143
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-DSVQ2U7J.js → chunk-32IH7WBY.js} +36 -14
- package/dist/chunk-32IH7WBY.js.map +1 -0
- package/dist/{chunk-QP7LKFU2.js → chunk-6EFY2WPN.js} +2 -2
- package/dist/{chunk-5VCIZYUE.js → chunk-AYKLBPP2.js} +3 -3
- package/dist/{chunk-CLWFZQUU.js → chunk-JJ4N3PXT.js} +4 -1
- package/dist/chunk-JJ4N3PXT.js.map +1 -0
- package/dist/{chunk-74JVVCWL.js → chunk-RN33UALN.js} +16 -18
- package/dist/chunk-RN33UALN.js.map +1 -0
- package/dist/helpers/loudness.js +4 -1
- package/dist/helpers/mediaCodecMap.js +1 -1
- package/dist/helpers/mediaProxyPreview.js +3 -3
- package/dist/helpers/proxyTranscoder.js +2 -2
- package/dist/helpers/sourceMutation.js +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +279 -72
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
- package/dist/chunk-74JVVCWL.js.map +0 -1
- package/dist/chunk-CLWFZQUU.js.map +0 -1
- package/dist/chunk-DSVQ2U7J.js.map +0 -1
- /package/dist/{chunk-QP7LKFU2.js.map → chunk-6EFY2WPN.js.map} +0 -0
- /package/dist/{chunk-5VCIZYUE.js.map → chunk-AYKLBPP2.js.map} +0 -0
|
@@ -1,3 +1,7 @@
|
|
|
1
|
+
import {
|
|
2
|
+
resolveWithinProject
|
|
3
|
+
} from "./chunk-DTKK7TKL.js";
|
|
4
|
+
|
|
1
5
|
// src/helpers/loudness.ts
|
|
2
6
|
import { execFile } from "child_process";
|
|
3
7
|
import { isAbsolute, relative, resolve } from "path";
|
|
@@ -5,6 +9,25 @@ import { promisify } from "util";
|
|
|
5
9
|
import { isAudibleVideoElement } from "@hyperframes/core/audible-video";
|
|
6
10
|
import { formatAudioGain, MAX_AUDIO_GAIN_DB } from "@hyperframes/core/audio-gain";
|
|
7
11
|
import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
|
|
12
|
+
import { readMediaOffsetSeconds } from "@hyperframes/parsers/media-duration";
|
|
13
|
+
|
|
14
|
+
// src/helpers/requestSubPath.ts
|
|
15
|
+
import { existsSync } from "fs";
|
|
16
|
+
import { decodedUrlPath, decodeWellFormedEscapes } from "@hyperframes/parsers/asset-paths";
|
|
17
|
+
import { decodeWellFormedEscapes as decodeWellFormedEscapes2 } from "@hyperframes/parsers/asset-paths";
|
|
18
|
+
function requestSubPath(url, route) {
|
|
19
|
+
const routeSegments = route.split("/");
|
|
20
|
+
const segments = new URL(url).pathname.split("/");
|
|
21
|
+
const start = segments.indexOf(routeSegments[0] ?? "");
|
|
22
|
+
return decodeWellFormedEscapes(segments.slice(start + routeSegments.length).join("/"));
|
|
23
|
+
}
|
|
24
|
+
function requestedProjectPath(projectDir, field) {
|
|
25
|
+
const trimmed = field.trim().replace(/^[.]\//, "");
|
|
26
|
+
const named = resolveWithinProject(projectDir, trimmed);
|
|
27
|
+
return named && existsSync(named) ? trimmed : decodedUrlPath(trimmed);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// src/helpers/loudness.ts
|
|
8
31
|
var execFileAsync = promisify(execFile);
|
|
9
32
|
var FFMPEG_TIMEOUT_MS = 12e4;
|
|
10
33
|
function authoredNumber(raw, fallback, attribute, id, strictlyPositive = false) {
|
|
@@ -149,6 +172,13 @@ function requiredAudioIdentity(attributes) {
|
|
|
149
172
|
if (!src) throw new Error(`Audio #${id} has no src`);
|
|
150
173
|
return { id, src };
|
|
151
174
|
}
|
|
175
|
+
function authoredMediaStart(attributes, id) {
|
|
176
|
+
return readMediaOffsetSeconds((name) => {
|
|
177
|
+
const raw = attributes.get(name)?.value;
|
|
178
|
+
if (name === "data-media-start") authoredNumber(raw, 0, name, id);
|
|
179
|
+
return raw;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
152
182
|
function hasBareAttribute(html, range, name) {
|
|
153
183
|
const unquoted = html.slice(range.start, range.end).replace(/"[^"]*"|'[^']*'/g, '""');
|
|
154
184
|
return new RegExp(`\\s${name}(?=[\\s/>=])`, "i").test(unquoted);
|
|
@@ -173,12 +203,7 @@ function mediaTagFromRange(html, range) {
|
|
|
173
203
|
id,
|
|
174
204
|
src,
|
|
175
205
|
volume: authoredNumber(volumeAttribute?.value, 1, "data-volume", id),
|
|
176
|
-
mediaStart:
|
|
177
|
-
attributes.get("data-media-start")?.value,
|
|
178
|
-
0,
|
|
179
|
-
"data-media-start",
|
|
180
|
-
id
|
|
181
|
-
),
|
|
206
|
+
mediaStart: authoredMediaStart(attributes, id),
|
|
182
207
|
duration: authoredDuration(attributes, id),
|
|
183
208
|
playbackRate: authoredNumber(
|
|
184
209
|
attributes.get("data-playback-rate")?.value,
|
|
@@ -205,13 +230,7 @@ function audioTags(html) {
|
|
|
205
230
|
return tags;
|
|
206
231
|
}
|
|
207
232
|
function resolveLocalAudioPath(projectDir, src) {
|
|
208
|
-
const
|
|
209
|
-
let decoded = "";
|
|
210
|
-
try {
|
|
211
|
-
decoded = decodeURIComponent(withoutSuffix);
|
|
212
|
-
} catch {
|
|
213
|
-
throw new Error(`Audio source must be a local project file: ${src}`);
|
|
214
|
-
}
|
|
233
|
+
const decoded = requestedProjectPath(projectDir, src);
|
|
215
234
|
if (!decoded || isAbsolute(decoded) || /^(?:[a-z][a-z\d+.-]*:|\/\/)/i.test(decoded)) {
|
|
216
235
|
throw new Error(`Audio source must be a local project file: ${src}`);
|
|
217
236
|
}
|
|
@@ -346,6 +365,9 @@ function absoluteLoudnessPlan(track, targetLufs = DEFAULT_TARGET_LUFS) {
|
|
|
346
365
|
}
|
|
347
366
|
|
|
348
367
|
export {
|
|
368
|
+
requestSubPath,
|
|
369
|
+
requestedProjectPath,
|
|
370
|
+
decodeWellFormedEscapes2 as decodeWellFormedEscapes,
|
|
349
371
|
audioTags,
|
|
350
372
|
resolveLocalAudioPath,
|
|
351
373
|
parseEbur128Summary,
|
|
@@ -359,4 +381,4 @@ export {
|
|
|
359
381
|
NORMALIZE_TRUE_PEAK_CEILING_DBFS,
|
|
360
382
|
absoluteLoudnessPlan
|
|
361
383
|
};
|
|
362
|
-
//# sourceMappingURL=chunk-
|
|
384
|
+
//# sourceMappingURL=chunk-32IH7WBY.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/helpers/loudness.ts","../src/helpers/requestSubPath.ts"],"sourcesContent":["import { execFile } from \"node:child_process\";\nimport { isAbsolute, relative, resolve } from \"node:path\";\nimport { promisify } from \"node:util\";\nimport { isAudibleVideoElement } from \"@hyperframes/core/audible-video\";\nimport { formatAudioGain, MAX_AUDIO_GAIN_DB } from \"@hyperframes/core/audio-gain\";\nimport { findFfBinary } from \"@hyperframes/parsers/ff-binaries\";\nimport { readMediaOffsetSeconds } from \"@hyperframes/parsers/media-duration\";\nimport { requestedProjectPath } from \"./requestSubPath.js\";\n\nconst execFileAsync = promisify(execFile);\n\nconst FFMPEG_TIMEOUT_MS = 120_000;\n\ninterface AttributeSpan {\n value: string;\n valueStart: number;\n valueEnd: number;\n}\n\ninterface TagRange {\n start: number;\n end: number;\n}\n\ntype MediaTagName = \"audio\" | \"video\";\n\ninterface MediaTagRange extends TagRange {\n name: MediaTagName;\n}\n\ninterface ParsedStartTag extends TagRange {\n closing: boolean;\n name: string;\n}\n\ninterface ScanResult {\n cursor: number;\n mediaRange: MediaTagRange | null;\n}\n\nexport interface AudioTag {\n tag: MediaTagName;\n id: string;\n src: string;\n volume: number;\n mediaStart: number;\n duration: number | null;\n playbackRate: number;\n start: number;\n end: number;\n volumeAttribute: AttributeSpan | null;\n}\n\nexport interface LoudnessMeasurement {\n integratedLufs: number;\n truePeakDbfs: number;\n}\n\nexport interface NormalizationTrack extends LoudnessMeasurement {\n id: string;\n volume: number;\n}\n\nexport interface AudioNormalizationPlan {\n referenceId: string;\n targetId: string;\n referenceLufs: number;\n targetLufs: number;\n previousVolume: number;\n volume: number;\n gainDb: number;\n changeDb: number;\n projectedLufs: number;\n projectedTruePeakDbfs: number;\n}\n\nfunction authoredNumber(\n raw: string | undefined,\n fallback: number,\n attribute: string,\n id: string,\n strictlyPositive = false,\n): number {\n if (raw === undefined) return fallback;\n const value = Number(raw);\n if (!Number.isFinite(value) || (strictlyPositive ? value <= 0 : value < 0)) {\n throw new Error(`Audio #${id} has an invalid ${attribute}: ${raw}`);\n }\n return value;\n}\n\nfunction attributeFromMatch(\n match: RegExpMatchArray,\n absoluteOffset: number,\n): [string, AttributeSpan] | null {\n if (match.index === undefined) return null;\n const name = match[1]?.toLowerCase();\n const value = matchedAttributeValue(match);\n if (!name || value === undefined) return null;\n\n const whole = match[0];\n const localValueStart = attributeValueStart(whole);\n const valueStart = absoluteOffset + match.index + localValueStart;\n return [name, { value, valueStart, valueEnd: valueStart + value.length }];\n}\n\nfunction matchedAttributeValue(match: RegExpMatchArray): string | undefined {\n return match[2] ?? match[3] ?? match[4];\n}\n\nfunction attributeValueStart(attribute: string): number {\n let cursor = attribute.indexOf(\"=\") + 1;\n while (/\\s/.test(attribute[cursor] ?? \"\")) cursor += 1;\n const quote = attribute[cursor];\n return quote === '\"' || quote === \"'\" ? cursor + 1 : cursor;\n}\n\nfunction attributesInTag(html: string, from: number, to: number): Map<string, AttributeSpan> {\n const tag = html.slice(from, to);\n const nameEnd = tag.search(/\\s|\\/?\\s*>/);\n const attributes = new Map<string, AttributeSpan>();\n if (nameEnd < 0) return attributes;\n\n const pattern = /([^\\s=/>]+)\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s\"'=<>`]+))/g;\n for (const match of tag.slice(nameEnd).matchAll(pattern)) {\n const attribute = attributeFromMatch(match, from + nameEnd);\n if (attribute) attributes.set(...attribute);\n }\n return attributes;\n}\n\nfunction startTagEnd(html: string, start: number): number {\n let quote = \"\";\n let end = start + 1;\n for (; end < html.length; end += 1) {\n const char = html[end];\n if (quote) {\n if (char === quote) quote = \"\";\n } else if (char === '\"' || char === \"'\") {\n quote = char;\n } else if (char === \">\") {\n return end + 1;\n }\n }\n throw new Error(\"Unterminated HTML start tag\");\n}\n\nfunction commentEnd(html: string, start: number): number {\n const end = html.indexOf(\"-->\", start + 4);\n if (end < 0) throw new Error(\"Unterminated HTML comment\");\n return end + 3;\n}\n\nconst isSpaceAt = (html: string, index: number) => /\\s/.test(html.charAt(index));\nconst isWordAt = (html: string, index: number) => /\\w/.test(html.charAt(index));\n\nfunction skipSpaces(html: string, index: number): number {\n let cursor = index;\n while (cursor < html.length && isSpaceAt(html, cursor)) cursor++;\n return cursor;\n}\n\nfunction parsedStartTag(html: string, start: number): ParsedStartTag | null {\n let cursor = skipSpaces(html, start + 1);\n const closing = html.charAt(cursor) === \"/\";\n if (closing) cursor = skipSpaces(html, cursor + 1);\n const nameStart = cursor;\n if (!/[a-z]/i.test(html.charAt(cursor))) return null;\n while (/[a-z\\d:-]/i.test(html.charAt(cursor + 1))) cursor++;\n let nameEnd = cursor + 1;\n while (nameEnd > nameStart && isWordAt(html, nameEnd - 1) === isWordAt(html, nameEnd)) nameEnd--;\n if (nameEnd === nameStart) return null;\n return {\n start,\n end: startTagEnd(html, start),\n closing,\n name: html.slice(nameStart, nameEnd).toLowerCase(),\n };\n}\n\nfunction rawTextElementEnd(html: string, lower: string, tag: ParsedStartTag): number | null {\n if (tag.closing || (tag.name !== \"script\" && tag.name !== \"style\")) return null;\n const closeStart = lower.indexOf(`</${tag.name}`, tag.end);\n if (closeStart < 0) throw new Error(`Unterminated <${tag.name}> element`);\n return startTagEnd(html, closeStart);\n}\n\nfunction scanMarkup(html: string, lower: string, start: number): ScanResult {\n if (html.startsWith(\"<!--\", start)) {\n return { cursor: commentEnd(html, start), mediaRange: null };\n }\n const tag = parsedStartTag(html, start);\n if (!tag) {\n const marker = html[start + 1];\n const cursor = marker === \"!\" || marker === \"?\" ? startTagEnd(html, start) : start + 1;\n return { cursor, mediaRange: null };\n }\n const rawTextEnd = rawTextElementEnd(html, lower, tag);\n if (rawTextEnd !== null) return { cursor: rawTextEnd, mediaRange: null };\n return { cursor: tag.end, mediaRange: mediaRangeOf(tag) };\n}\n\nfunction mediaRangeOf(tag: ParsedStartTag): MediaTagRange | null {\n if (tag.closing) return null;\n if (tag.name === \"audio\" || tag.name === \"video\") {\n return { start: tag.start, end: tag.end, name: tag.name };\n }\n return null;\n}\n\nfunction mediaTagRanges(html: string): MediaTagRange[] {\n const lower = html.toLowerCase();\n const ranges: MediaTagRange[] = [];\n let cursor = 0;\n while (cursor < html.length) {\n const start = html.indexOf(\"<\", cursor);\n if (start < 0) break;\n const scanned = scanMarkup(html, lower, start);\n if (scanned.mediaRange) ranges.push(scanned.mediaRange);\n cursor = scanned.cursor;\n }\n return ranges;\n}\n\nfunction trimmedAttribute(attributes: Map<string, AttributeSpan>, name: string): string {\n return attributes.get(name)?.value.trim() ?? \"\";\n}\n\n/**\n * How much of the source the composition actually plays.\n *\n * `data-duration` is the explicit trim; absent, `data-end` still bounds the\n * clip's timeline window, and the parser, the runtime and the render mixer all\n * honour it. Measuring the whole file for a `data-end`-trimmed clip reads a\n * loudness the composition never plays — and `--write` then \"corrects\" an\n * already-matched clip by tens of dB.\n */\nfunction authoredDuration(attributes: Map<string, AttributeSpan>, id: string): number | null {\n const raw = attributes.get(\"data-duration\")?.value;\n if (raw !== undefined) return authoredNumber(raw, 0, \"data-duration\", id, true);\n\n const rawEnd = attributes.get(\"data-end\")?.value;\n if (rawEnd === undefined) return null;\n const end = authoredNumber(rawEnd, 0, \"data-end\", id, true);\n const start = authoredNumber(attributes.get(\"data-start\")?.value, 0, \"data-start\", id);\n return end > start ? end - start : null;\n}\n\nfunction requiredAudioIdentity(attributes: Map<string, AttributeSpan>) {\n const id = trimmedAttribute(attributes, \"id\");\n const src = trimmedAttribute(attributes, \"src\");\n if (!id) throw new Error(\"Every normalized <audio> element needs an id\");\n if (!src) throw new Error(`Audio #${id} has no src`);\n return { id, src };\n}\n\nfunction authoredMediaStart(attributes: Map<string, AttributeSpan>, id: string): number {\n return readMediaOffsetSeconds((name) => {\n const raw = attributes.get(name)?.value;\n if (name === \"data-media-start\") authoredNumber(raw, 0, name, id);\n return raw;\n });\n}\n\n/** Boolean attributes carry no `=`, so the value map cannot see them. */\nfunction hasBareAttribute(html: string, range: TagRange, name: string): boolean {\n const unquoted = html.slice(range.start, range.end).replace(/\"[^\"]*\"|'[^']*'/g, '\"\"');\n return new RegExp(`\\\\s${name}(?=[\\\\s/>=])`, \"i\").test(unquoted);\n}\n\nfunction isAudibleVideoTag(\n html: string,\n range: TagRange,\n attributes: Map<string, AttributeSpan>,\n): boolean {\n return isAudibleVideoElement({\n tagName: \"video\",\n hasAttribute: (name) => attributes.has(name) || hasBareAttribute(html, range, name),\n getAttribute: (name) => attributes.get(name)?.value ?? null,\n });\n}\n\n/**\n * A video joins the scan only when it carries sound and can be addressed.\n * Silent footage, and a video no command could name, are skipped rather than\n * failing a run that is about some other clip.\n */\nfunction inScopeVideo(\n html: string,\n range: TagRange,\n attributes: Map<string, AttributeSpan>,\n): boolean {\n return (\n isAudibleVideoTag(html, range, attributes) &&\n trimmedAttribute(attributes, \"id\") !== \"\" &&\n trimmedAttribute(attributes, \"src\") !== \"\"\n );\n}\n\nfunction mediaTagFromRange(html: string, range: MediaTagRange): AudioTag | null {\n const attributes = attributesInTag(html, range.start, range.end);\n if (range.name === \"video\" && !inScopeVideo(html, range, attributes)) return null;\n const { id, src } = requiredAudioIdentity(attributes);\n const volumeAttribute = attributes.get(\"data-volume\") ?? null;\n return {\n tag: range.name,\n id,\n src,\n volume: authoredNumber(volumeAttribute?.value, 1, \"data-volume\", id),\n mediaStart: authoredMediaStart(attributes, id),\n duration: authoredDuration(attributes, id),\n playbackRate: authoredNumber(\n attributes.get(\"data-playback-rate\")?.value,\n 1,\n \"data-playback-rate\",\n id,\n true,\n ),\n start: range.start,\n end: range.end,\n volumeAttribute,\n };\n}\n\n/** Read the authored clips with sound — `<audio>` and audible `<video>` — without reserializing. */\nexport function audioTags(html: string): AudioTag[] {\n const seen = new Set<string>();\n const tags: AudioTag[] = [];\n for (const range of mediaTagRanges(html)) {\n const tag = mediaTagFromRange(html, range);\n if (!tag) continue;\n if (seen.has(tag.id)) throw new Error(`Duplicate audio id \"${tag.id}\"`);\n seen.add(tag.id);\n tags.push(tag);\n }\n return tags;\n}\n\n/** Resolve only a file contained by the project. Remote and traversal sources are not measured. */\nexport function resolveLocalAudioPath(projectDir: string, src: string): string {\n const decoded = requestedProjectPath(projectDir, src);\n if (!decoded || isAbsolute(decoded) || /^(?:[a-z][a-z\\d+.-]*:|\\/\\/)/i.test(decoded)) {\n throw new Error(`Audio source must be a local project file: ${src}`);\n }\n const root = resolve(projectDir);\n const file = resolve(root, decoded);\n const rel = relative(root, file);\n if (!rel || rel === \"..\" || rel.startsWith(`..${process.platform === \"win32\" ? \"\\\\\" : \"/\"}`)) {\n throw new Error(`Audio source must be a local project file: ${src}`);\n }\n return file;\n}\n\n/** Parse FFmpeg's final EBU R128 summary, not its per-window log lines. */\nexport function parseEbur128Summary(stderr: string): LoudnessMeasurement {\n const summaryAt = stderr.lastIndexOf(\"Summary:\");\n const summary = summaryAt >= 0 ? stderr.slice(summaryAt) : \"\";\n const integrated = summary.match(/\\bI:\\s*(-?\\d+(?:\\.\\d+)?)\\s+LUFS\\b/);\n const peak = summary.match(/\\bPeak:\\s*(-?\\d+(?:\\.\\d+)?)\\s+dBFS\\b/);\n const integratedLufs = Number(integrated?.[1]);\n const truePeakDbfs = Number(peak?.[1]);\n if (!Number.isFinite(integratedLufs)) {\n throw new Error(\"FFmpeg did not report integrated loudness for this audio stream\");\n }\n if (!Number.isFinite(truePeakDbfs)) {\n throw new Error(\"FFmpeg did not report true peak for this audio stream\");\n }\n return { integratedLufs, truePeakDbfs };\n}\n\nexport function gainDb(volume: number): number {\n return volume > 0 ? 20 * Math.log10(volume) : Number.NEGATIVE_INFINITY;\n}\n\n/** Calculate the target's absolute authored gain while keeping the reference unchanged. */\nexport function audioNormalizationPlan(\n referenceTrack: NormalizationTrack,\n targetTrack: NormalizationTrack,\n): AudioNormalizationPlan {\n if (!(referenceTrack.volume > 0) || !(targetTrack.volume > 0)) {\n throw new Error(\"Muted audio cannot be used for loudness matching\");\n }\n const referenceLufs = referenceTrack.integratedLufs + gainDb(referenceTrack.volume);\n const targetLufs = targetTrack.integratedLufs + gainDb(targetTrack.volume);\n const wantedGainDb = referenceLufs - targetTrack.integratedLufs;\n if (wantedGainDb > MAX_AUDIO_GAIN_DB + 1e-9) {\n throw new Error(\n `Matching #${targetTrack.id} needs ${wantedGainDb.toFixed(1)} dB, beyond the +${MAX_AUDIO_GAIN_DB} dB authoring ceiling. ` +\n `A gap this large belongs in the source file, not the mixer — mixer gain raises the noise floor with the signal. ` +\n `Normalize ${targetTrack.id}'s asset offline (e.g. ffmpeg loudnorm), or lower #${referenceTrack.id} instead.`,\n );\n }\n const volume = 10 ** (wantedGainDb / 20);\n const projectedTruePeakDbfs = targetTrack.truePeakDbfs + wantedGainDb;\n if (projectedTruePeakDbfs > 0) {\n throw new Error(\n `Matching #${targetTrack.id} would clip at +${projectedTruePeakDbfs.toFixed(1)} dBFS; limit or preprocess the source first`,\n );\n }\n return {\n referenceId: referenceTrack.id,\n targetId: targetTrack.id,\n referenceLufs,\n targetLufs,\n previousVolume: targetTrack.volume,\n volume,\n gainDb: wantedGainDb,\n changeDb: wantedGainDb - gainDb(targetTrack.volume),\n projectedLufs: targetTrack.integratedLufs + wantedGainDb,\n projectedTruePeakDbfs,\n };\n}\n\n/** Patch one authored attribute in place so scripts/styles/comments remain byte-stable. */\nexport function updateAudioVolume(html: string, id: string, volume: number): string {\n const tag = audioTags(html).find((candidate) => candidate.id === id);\n if (!tag) throw new Error(`Audio #${id} was not found`);\n const value = formatAudioGain(volume);\n if (tag.volumeAttribute) {\n return (\n html.slice(0, tag.volumeAttribute.valueStart) +\n value +\n html.slice(tag.volumeAttribute.valueEnd)\n );\n }\n\n let insertion = tag.end - 1;\n let tail = insertion - 1;\n while (tail >= tag.start && /\\s/.test(html[tail] ?? \"\")) tail -= 1;\n if (html[tail] === \"/\") {\n insertion = tail;\n while (insertion > tag.start && /\\s/.test(html[insertion - 1] ?? \"\")) insertion -= 1;\n } else {\n while (insertion > tag.start && /\\s/.test(html[insertion - 1] ?? \"\")) insertion -= 1;\n }\n return html.slice(0, insertion) + ` data-volume=\"${value}\"` + html.slice(insertion);\n}\n\n/**\n * FFmpeg arguments that measure exactly the window the composition plays.\n *\n * `-ss` / `-t` go BEFORE `-i`, as INPUT options. After `-i` they bound the\n * output, and with `-f null` there is no real output to bound: ffmpeg keeps\n * feeding the filter graph past the limit, so ebur128 integrates audio the clip\n * never plays. Measured on a 12 s file whose first 4 s — the played window — is\n * -61.8 LUFS: output-side `-t 4` reported -33.8 LUFS, input-side reports -61.8,\n * the same as physically cutting the file first.\n */\nexport function loudnessMeasureArgs(file: string, tag: MeasuredWindow): string[] {\n const args = [\"-hide_banner\", \"-nostats\"];\n if (tag.mediaStart > 0) args.push(\"-ss\", String(tag.mediaStart));\n const sourceSpan = tag.duration === null ? null : tag.duration * (tag.playbackRate ?? 1);\n if (sourceSpan !== null && sourceSpan > 0) args.push(\"-t\", String(sourceSpan));\n args.push(\"-i\", file, \"-map\", \"0:a:0\", \"-vn\", \"-af\", \"ebur128=peak=true\", \"-f\", \"null\", \"-\");\n return args;\n}\n\nexport type MeasuredWindow = Pick<AudioTag, \"mediaStart\" | \"duration\"> &\n Partial<Pick<AudioTag, \"playbackRate\">>;\n\nexport async function measureAudio(\n ffmpegPath: string,\n file: string,\n tag: MeasuredWindow,\n): Promise<LoudnessMeasurement> {\n const result = await execFileAsync(ffmpegPath, loudnessMeasureArgs(file, tag), {\n encoding: \"utf8\",\n maxBuffer: 16 * 1024 * 1024,\n timeout: FFMPEG_TIMEOUT_MS,\n windowsHide: true,\n });\n return parseEbur128Summary(result.stderr);\n}\n\nexport function requiredFfmpeg(): string {\n const ffmpegPath = findFfBinary(\"ffmpeg\", { configuredMustExist: true });\n if (!ffmpegPath) throw new Error(\"FFmpeg is required to measure integrated loudness\");\n return ffmpegPath;\n}\n\nexport const DEFAULT_TARGET_LUFS = -16;\nexport const NORMALIZE_TRUE_PEAK_CEILING_DBFS = -1.5;\n/** EBU R128's absolute gate: a window this quiet has no integrated loudness to match. */\nconst SILENCE_LUFS = -70;\n\nexport type LoudnessLimit = \"gain-ceiling\" | \"true-peak\";\n\nexport interface AbsoluteLoudnessPlan {\n targetId: string;\n targetLufs: number;\n sourceLufs: number;\n previousVolume: number;\n volume: number;\n gainDb: number;\n changeDb: number;\n projectedLufs: number;\n projectedTruePeakDbfs: number;\n limitedBy: LoudnessLimit | null;\n}\n\n/**\n * The authored gain that brings one clip's played window to an absolute\n * loudness. Unlike reference matching it never refuses: past the +12 dB\n * authoring ceiling or the true-peak ceiling it applies the most that fits and\n * names which limit stopped it.\n */\nexport function absoluteLoudnessPlan(\n track: NormalizationTrack,\n targetLufs: number = DEFAULT_TARGET_LUFS,\n): AbsoluteLoudnessPlan {\n if (track.integratedLufs <= SILENCE_LUFS) {\n throw new Error(`#${track.id} is silent in the played window; there is nothing to normalize`);\n }\n const wantedGainDb = targetLufs - track.integratedLufs;\n const peakRoomDb = NORMALIZE_TRUE_PEAK_CEILING_DBFS - track.truePeakDbfs;\n const limits: Array<[LoudnessLimit | null, number]> = [\n [null, wantedGainDb],\n [\"gain-ceiling\", MAX_AUDIO_GAIN_DB],\n [\"true-peak\", peakRoomDb],\n ];\n const [limitedBy, appliedGainDb] = limits.reduce((lowest, candidate) =>\n candidate[1] < lowest[1] - 1e-9 ? candidate : lowest,\n );\n const previousGainDb = track.volume > 0 ? gainDb(track.volume) : 0;\n return {\n targetId: track.id,\n targetLufs,\n sourceLufs: track.integratedLufs,\n previousVolume: track.volume,\n volume: 10 ** (appliedGainDb / 20),\n gainDb: appliedGainDb,\n changeDb: appliedGainDb - previousGainDb,\n projectedLufs: track.integratedLufs + appliedGainDb,\n projectedTruePeakDbfs: track.truePeakDbfs + appliedGainDb,\n limitedBy,\n };\n}\n","import { existsSync } from \"node:fs\";\nimport { decodedUrlPath, decodeWellFormedEscapes } from \"@hyperframes/parsers/asset-paths\";\nimport { resolveWithinProject } from \"./safePath.js\";\nexport { decodeWellFormedEscapes } from \"@hyperframes/parsers/asset-paths\";\n\n// The decoded path after `route` (\"projects/:id/preview\", \"composition\"), cut by segment from the raw URL:\n// Hono's c.req.path leaves %40 %25 %23 %26 %3F encoded, so cutting a decoded prefix out of it misses.\nexport function requestSubPath(url: string, route: string): string {\n const routeSegments = route.split(\"/\");\n const segments = new URL(url).pathname.split(\"/\");\n const start = segments.indexOf(routeSegments[0] ?? \"\");\n return decodeWellFormedEscapes(segments.slice(start + routeSegments.length).join(\"/\"));\n}\n\n// A project file a request names: the literal name when that file exists, otherwise the field read as a URL.\nexport function requestedProjectPath(projectDir: string, field: string): string {\n const trimmed = field.trim().replace(/^[.]\\//, \"\");\n const named = resolveWithinProject(projectDir, trimmed);\n return named && existsSync(named) ? trimmed : decodedUrlPath(trimmed);\n}\n"],"mappings":";;;;;AAAA,SAAS,gBAAgB;AACzB,SAAS,YAAY,UAAU,eAAe;AAC9C,SAAS,iBAAiB;AAC1B,SAAS,6BAA6B;AACtC,SAAS,iBAAiB,yBAAyB;AACnD,SAAS,oBAAoB;AAC7B,SAAS,8BAA8B;;;ACNvC,SAAS,kBAAkB;AAC3B,SAAS,gBAAgB,+BAA+B;AAExD,SAAS,2BAAAA,gCAA+B;AAIjC,SAAS,eAAe,KAAa,OAAuB;AACjE,QAAM,gBAAgB,MAAM,MAAM,GAAG;AACrC,QAAM,WAAW,IAAI,IAAI,GAAG,EAAE,SAAS,MAAM,GAAG;AAChD,QAAM,QAAQ,SAAS,QAAQ,cAAc,CAAC,KAAK,EAAE;AACrD,SAAO,wBAAwB,SAAS,MAAM,QAAQ,cAAc,MAAM,EAAE,KAAK,GAAG,CAAC;AACvF;AAGO,SAAS,qBAAqB,YAAoB,OAAuB;AAC9E,QAAM,UAAU,MAAM,KAAK,EAAE,QAAQ,UAAU,EAAE;AACjD,QAAM,QAAQ,qBAAqB,YAAY,OAAO;AACtD,SAAO,SAAS,WAAW,KAAK,IAAI,UAAU,eAAe,OAAO;AACtE;;;ADVA,IAAM,gBAAgB,UAAU,QAAQ;AAExC,IAAM,oBAAoB;AAiE1B,SAAS,eACP,KACA,UACA,WACA,IACA,mBAAmB,OACX;AACR,MAAI,QAAQ,OAAW,QAAO;AAC9B,QAAM,QAAQ,OAAO,GAAG;AACxB,MAAI,CAAC,OAAO,SAAS,KAAK,MAAM,mBAAmB,SAAS,IAAI,QAAQ,IAAI;AAC1E,UAAM,IAAI,MAAM,UAAU,EAAE,mBAAmB,SAAS,KAAK,GAAG,EAAE;AAAA,EACpE;AACA,SAAO;AACT;AAEA,SAAS,mBACP,OACA,gBACgC;AAChC,MAAI,MAAM,UAAU,OAAW,QAAO;AACtC,QAAM,OAAO,MAAM,CAAC,GAAG,YAAY;AACnC,QAAM,QAAQ,sBAAsB,KAAK;AACzC,MAAI,CAAC,QAAQ,UAAU,OAAW,QAAO;AAEzC,QAAM,QAAQ,MAAM,CAAC;AACrB,QAAM,kBAAkB,oBAAoB,KAAK;AACjD,QAAM,aAAa,iBAAiB,MAAM,QAAQ;AAClD,SAAO,CAAC,MAAM,EAAE,OAAO,YAAY,UAAU,aAAa,MAAM,OAAO,CAAC;AAC1E;AAEA,SAAS,sBAAsB,OAA6C;AAC1E,SAAO,MAAM,CAAC,KAAK,MAAM,CAAC,KAAK,MAAM,CAAC;AACxC;AAEA,SAAS,oBAAoB,WAA2B;AACtD,MAAI,SAAS,UAAU,QAAQ,GAAG,IAAI;AACtC,SAAO,KAAK,KAAK,UAAU,MAAM,KAAK,EAAE,EAAG,WAAU;AACrD,QAAM,QAAQ,UAAU,MAAM;AAC9B,SAAO,UAAU,OAAO,UAAU,MAAM,SAAS,IAAI;AACvD;AAEA,SAAS,gBAAgB,MAAc,MAAc,IAAwC;AAC3F,QAAM,MAAM,KAAK,MAAM,MAAM,EAAE;AAC/B,QAAM,UAAU,IAAI,OAAO,YAAY;AACvC,QAAM,aAAa,oBAAI,IAA2B;AAClD,MAAI,UAAU,EAAG,QAAO;AAExB,QAAM,UAAU;AAChB,aAAW,SAAS,IAAI,MAAM,OAAO,EAAE,SAAS,OAAO,GAAG;AACxD,UAAM,YAAY,mBAAmB,OAAO,OAAO,OAAO;AAC1D,QAAI,UAAW,YAAW,IAAI,GAAG,SAAS;AAAA,EAC5C;AACA,SAAO;AACT;AAEA,SAAS,YAAY,MAAc,OAAuB;AACxD,MAAI,QAAQ;AACZ,MAAI,MAAM,QAAQ;AAClB,SAAO,MAAM,KAAK,QAAQ,OAAO,GAAG;AAClC,UAAM,OAAO,KAAK,GAAG;AACrB,QAAI,OAAO;AACT,UAAI,SAAS,MAAO,SAAQ;AAAA,IAC9B,WAAW,SAAS,OAAO,SAAS,KAAK;AACvC,cAAQ;AAAA,IACV,WAAW,SAAS,KAAK;AACvB,aAAO,MAAM;AAAA,IACf;AAAA,EACF;AACA,QAAM,IAAI,MAAM,6BAA6B;AAC/C;AAEA,SAAS,WAAW,MAAc,OAAuB;AACvD,QAAM,MAAM,KAAK,QAAQ,OAAO,QAAQ,CAAC;AACzC,MAAI,MAAM,EAAG,OAAM,IAAI,MAAM,2BAA2B;AACxD,SAAO,MAAM;AACf;AAEA,IAAM,YAAY,CAAC,MAAc,UAAkB,KAAK,KAAK,KAAK,OAAO,KAAK,CAAC;AAC/E,IAAM,WAAW,CAAC,MAAc,UAAkB,KAAK,KAAK,KAAK,OAAO,KAAK,CAAC;AAE9E,SAAS,WAAW,MAAc,OAAuB;AACvD,MAAI,SAAS;AACb,SAAO,SAAS,KAAK,UAAU,UAAU,MAAM,MAAM,EAAG;AACxD,SAAO;AACT;AAEA,SAAS,eAAe,MAAc,OAAsC;AAC1E,MAAI,SAAS,WAAW,MAAM,QAAQ,CAAC;AACvC,QAAM,UAAU,KAAK,OAAO,MAAM,MAAM;AACxC,MAAI,QAAS,UAAS,WAAW,MAAM,SAAS,CAAC;AACjD,QAAM,YAAY;AAClB,MAAI,CAAC,SAAS,KAAK,KAAK,OAAO,MAAM,CAAC,EAAG,QAAO;AAChD,SAAO,aAAa,KAAK,KAAK,OAAO,SAAS,CAAC,CAAC,EAAG;AACnD,MAAI,UAAU,SAAS;AACvB,SAAO,UAAU,aAAa,SAAS,MAAM,UAAU,CAAC,MAAM,SAAS,MAAM,OAAO,EAAG;AACvF,MAAI,YAAY,UAAW,QAAO;AAClC,SAAO;AAAA,IACL;AAAA,IACA,KAAK,YAAY,MAAM,KAAK;AAAA,IAC5B;AAAA,IACA,MAAM,KAAK,MAAM,WAAW,OAAO,EAAE,YAAY;AAAA,EACnD;AACF;AAEA,SAAS,kBAAkB,MAAc,OAAe,KAAoC;AAC1F,MAAI,IAAI,WAAY,IAAI,SAAS,YAAY,IAAI,SAAS,QAAU,QAAO;AAC3E,QAAM,aAAa,MAAM,QAAQ,KAAK,IAAI,IAAI,IAAI,IAAI,GAAG;AACzD,MAAI,aAAa,EAAG,OAAM,IAAI,MAAM,iBAAiB,IAAI,IAAI,WAAW;AACxE,SAAO,YAAY,MAAM,UAAU;AACrC;AAEA,SAAS,WAAW,MAAc,OAAe,OAA2B;AAC1E,MAAI,KAAK,WAAW,QAAQ,KAAK,GAAG;AAClC,WAAO,EAAE,QAAQ,WAAW,MAAM,KAAK,GAAG,YAAY,KAAK;AAAA,EAC7D;AACA,QAAM,MAAM,eAAe,MAAM,KAAK;AACtC,MAAI,CAAC,KAAK;AACR,UAAM,SAAS,KAAK,QAAQ,CAAC;AAC7B,UAAM,SAAS,WAAW,OAAO,WAAW,MAAM,YAAY,MAAM,KAAK,IAAI,QAAQ;AACrF,WAAO,EAAE,QAAQ,YAAY,KAAK;AAAA,EACpC;AACA,QAAM,aAAa,kBAAkB,MAAM,OAAO,GAAG;AACrD,MAAI,eAAe,KAAM,QAAO,EAAE,QAAQ,YAAY,YAAY,KAAK;AACvE,SAAO,EAAE,QAAQ,IAAI,KAAK,YAAY,aAAa,GAAG,EAAE;AAC1D;AAEA,SAAS,aAAa,KAA2C;AAC/D,MAAI,IAAI,QAAS,QAAO;AACxB,MAAI,IAAI,SAAS,WAAW,IAAI,SAAS,SAAS;AAChD,WAAO,EAAE,OAAO,IAAI,OAAO,KAAK,IAAI,KAAK,MAAM,IAAI,KAAK;AAAA,EAC1D;AACA,SAAO;AACT;AAEA,SAAS,eAAe,MAA+B;AACrD,QAAM,QAAQ,KAAK,YAAY;AAC/B,QAAM,SAA0B,CAAC;AACjC,MAAI,SAAS;AACb,SAAO,SAAS,KAAK,QAAQ;AAC3B,UAAM,QAAQ,KAAK,QAAQ,KAAK,MAAM;AACtC,QAAI,QAAQ,EAAG;AACf,UAAM,UAAU,WAAW,MAAM,OAAO,KAAK;AAC7C,QAAI,QAAQ,WAAY,QAAO,KAAK,QAAQ,UAAU;AACtD,aAAS,QAAQ;AAAA,EACnB;AACA,SAAO;AACT;AAEA,SAAS,iBAAiB,YAAwC,MAAsB;AACtF,SAAO,WAAW,IAAI,IAAI,GAAG,MAAM,KAAK,KAAK;AAC/C;AAWA,SAAS,iBAAiB,YAAwC,IAA2B;AAC3F,QAAM,MAAM,WAAW,IAAI,eAAe,GAAG;AAC7C,MAAI,QAAQ,OAAW,QAAO,eAAe,KAAK,GAAG,iBAAiB,IAAI,IAAI;AAE9E,QAAM,SAAS,WAAW,IAAI,UAAU,GAAG;AAC3C,MAAI,WAAW,OAAW,QAAO;AACjC,QAAM,MAAM,eAAe,QAAQ,GAAG,YAAY,IAAI,IAAI;AAC1D,QAAM,QAAQ,eAAe,WAAW,IAAI,YAAY,GAAG,OAAO,GAAG,cAAc,EAAE;AACrF,SAAO,MAAM,QAAQ,MAAM,QAAQ;AACrC;AAEA,SAAS,sBAAsB,YAAwC;AACrE,QAAM,KAAK,iBAAiB,YAAY,IAAI;AAC5C,QAAM,MAAM,iBAAiB,YAAY,KAAK;AAC9C,MAAI,CAAC,GAAI,OAAM,IAAI,MAAM,8CAA8C;AACvE,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,UAAU,EAAE,aAAa;AACnD,SAAO,EAAE,IAAI,IAAI;AACnB;AAEA,SAAS,mBAAmB,YAAwC,IAAoB;AACtF,SAAO,uBAAuB,CAAC,SAAS;AACtC,UAAM,MAAM,WAAW,IAAI,IAAI,GAAG;AAClC,QAAI,SAAS,mBAAoB,gBAAe,KAAK,GAAG,MAAM,EAAE;AAChE,WAAO;AAAA,EACT,CAAC;AACH;AAGA,SAAS,iBAAiB,MAAc,OAAiB,MAAuB;AAC9E,QAAM,WAAW,KAAK,MAAM,MAAM,OAAO,MAAM,GAAG,EAAE,QAAQ,oBAAoB,IAAI;AACpF,SAAO,IAAI,OAAO,MAAM,IAAI,gBAAgB,GAAG,EAAE,KAAK,QAAQ;AAChE;AAEA,SAAS,kBACP,MACA,OACA,YACS;AACT,SAAO,sBAAsB;AAAA,IAC3B,SAAS;AAAA,IACT,cAAc,CAAC,SAAS,WAAW,IAAI,IAAI,KAAK,iBAAiB,MAAM,OAAO,IAAI;AAAA,IAClF,cAAc,CAAC,SAAS,WAAW,IAAI,IAAI,GAAG,SAAS;AAAA,EACzD,CAAC;AACH;AAOA,SAAS,aACP,MACA,OACA,YACS;AACT,SACE,kBAAkB,MAAM,OAAO,UAAU,KACzC,iBAAiB,YAAY,IAAI,MAAM,MACvC,iBAAiB,YAAY,KAAK,MAAM;AAE5C;AAEA,SAAS,kBAAkB,MAAc,OAAuC;AAC9E,QAAM,aAAa,gBAAgB,MAAM,MAAM,OAAO,MAAM,GAAG;AAC/D,MAAI,MAAM,SAAS,WAAW,CAAC,aAAa,MAAM,OAAO,UAAU,EAAG,QAAO;AAC7E,QAAM,EAAE,IAAI,IAAI,IAAI,sBAAsB,UAAU;AACpD,QAAM,kBAAkB,WAAW,IAAI,aAAa,KAAK;AACzD,SAAO;AAAA,IACL,KAAK,MAAM;AAAA,IACX;AAAA,IACA;AAAA,IACA,QAAQ,eAAe,iBAAiB,OAAO,GAAG,eAAe,EAAE;AAAA,IACnE,YAAY,mBAAmB,YAAY,EAAE;AAAA,IAC7C,UAAU,iBAAiB,YAAY,EAAE;AAAA,IACzC,cAAc;AAAA,MACZ,WAAW,IAAI,oBAAoB,GAAG;AAAA,MACtC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IACF;AAAA,IACA,OAAO,MAAM;AAAA,IACb,KAAK,MAAM;AAAA,IACX;AAAA,EACF;AACF;AAGO,SAAS,UAAU,MAA0B;AAClD,QAAM,OAAO,oBAAI,IAAY;AAC7B,QAAM,OAAmB,CAAC;AAC1B,aAAW,SAAS,eAAe,IAAI,GAAG;AACxC,UAAM,MAAM,kBAAkB,MAAM,KAAK;AACzC,QAAI,CAAC,IAAK;AACV,QAAI,KAAK,IAAI,IAAI,EAAE,EAAG,OAAM,IAAI,MAAM,uBAAuB,IAAI,EAAE,GAAG;AACtE,SAAK,IAAI,IAAI,EAAE;AACf,SAAK,KAAK,GAAG;AAAA,EACf;AACA,SAAO;AACT;AAGO,SAAS,sBAAsB,YAAoB,KAAqB;AAC7E,QAAM,UAAU,qBAAqB,YAAY,GAAG;AACpD,MAAI,CAAC,WAAW,WAAW,OAAO,KAAK,+BAA+B,KAAK,OAAO,GAAG;AACnF,UAAM,IAAI,MAAM,8CAA8C,GAAG,EAAE;AAAA,EACrE;AACA,QAAM,OAAO,QAAQ,UAAU;AAC/B,QAAM,OAAO,QAAQ,MAAM,OAAO;AAClC,QAAM,MAAM,SAAS,MAAM,IAAI;AAC/B,MAAI,CAAC,OAAO,QAAQ,QAAQ,IAAI,WAAW,KAAK,QAAQ,aAAa,UAAU,OAAO,GAAG,EAAE,GAAG;AAC5F,UAAM,IAAI,MAAM,8CAA8C,GAAG,EAAE;AAAA,EACrE;AACA,SAAO;AACT;AAGO,SAAS,oBAAoB,QAAqC;AACvE,QAAM,YAAY,OAAO,YAAY,UAAU;AAC/C,QAAM,UAAU,aAAa,IAAI,OAAO,MAAM,SAAS,IAAI;AAC3D,QAAM,aAAa,QAAQ,MAAM,mCAAmC;AACpE,QAAM,OAAO,QAAQ,MAAM,sCAAsC;AACjE,QAAM,iBAAiB,OAAO,aAAa,CAAC,CAAC;AAC7C,QAAM,eAAe,OAAO,OAAO,CAAC,CAAC;AACrC,MAAI,CAAC,OAAO,SAAS,cAAc,GAAG;AACpC,UAAM,IAAI,MAAM,iEAAiE;AAAA,EACnF;AACA,MAAI,CAAC,OAAO,SAAS,YAAY,GAAG;AAClC,UAAM,IAAI,MAAM,uDAAuD;AAAA,EACzE;AACA,SAAO,EAAE,gBAAgB,aAAa;AACxC;AAEO,SAAS,OAAO,QAAwB;AAC7C,SAAO,SAAS,IAAI,KAAK,KAAK,MAAM,MAAM,IAAI,OAAO;AACvD;AAGO,SAAS,uBACd,gBACA,aACwB;AACxB,MAAI,EAAE,eAAe,SAAS,MAAM,EAAE,YAAY,SAAS,IAAI;AAC7D,UAAM,IAAI,MAAM,kDAAkD;AAAA,EACpE;AACA,QAAM,gBAAgB,eAAe,iBAAiB,OAAO,eAAe,MAAM;AAClF,QAAM,aAAa,YAAY,iBAAiB,OAAO,YAAY,MAAM;AACzE,QAAM,eAAe,gBAAgB,YAAY;AACjD,MAAI,eAAe,oBAAoB,MAAM;AAC3C,UAAM,IAAI;AAAA,MACR,aAAa,YAAY,EAAE,UAAU,aAAa,QAAQ,CAAC,CAAC,oBAAoB,iBAAiB,yJAElF,YAAY,EAAE,sDAAsD,eAAe,EAAE;AAAA,IACtG;AAAA,EACF;AACA,QAAM,SAAS,OAAO,eAAe;AACrC,QAAM,wBAAwB,YAAY,eAAe;AACzD,MAAI,wBAAwB,GAAG;AAC7B,UAAM,IAAI;AAAA,MACR,aAAa,YAAY,EAAE,mBAAmB,sBAAsB,QAAQ,CAAC,CAAC;AAAA,IAChF;AAAA,EACF;AACA,SAAO;AAAA,IACL,aAAa,eAAe;AAAA,IAC5B,UAAU,YAAY;AAAA,IACtB;AAAA,IACA;AAAA,IACA,gBAAgB,YAAY;AAAA,IAC5B;AAAA,IACA,QAAQ;AAAA,IACR,UAAU,eAAe,OAAO,YAAY,MAAM;AAAA,IAClD,eAAe,YAAY,iBAAiB;AAAA,IAC5C;AAAA,EACF;AACF;AAGO,SAAS,kBAAkB,MAAc,IAAY,QAAwB;AAClF,QAAM,MAAM,UAAU,IAAI,EAAE,KAAK,CAAC,cAAc,UAAU,OAAO,EAAE;AACnE,MAAI,CAAC,IAAK,OAAM,IAAI,MAAM,UAAU,EAAE,gBAAgB;AACtD,QAAM,QAAQ,gBAAgB,MAAM;AACpC,MAAI,IAAI,iBAAiB;AACvB,WACE,KAAK,MAAM,GAAG,IAAI,gBAAgB,UAAU,IAC5C,QACA,KAAK,MAAM,IAAI,gBAAgB,QAAQ;AAAA,EAE3C;AAEA,MAAI,YAAY,IAAI,MAAM;AAC1B,MAAI,OAAO,YAAY;AACvB,SAAO,QAAQ,IAAI,SAAS,KAAK,KAAK,KAAK,IAAI,KAAK,EAAE,EAAG,SAAQ;AACjE,MAAI,KAAK,IAAI,MAAM,KAAK;AACtB,gBAAY;AACZ,WAAO,YAAY,IAAI,SAAS,KAAK,KAAK,KAAK,YAAY,CAAC,KAAK,EAAE,EAAG,cAAa;AAAA,EACrF,OAAO;AACL,WAAO,YAAY,IAAI,SAAS,KAAK,KAAK,KAAK,YAAY,CAAC,KAAK,EAAE,EAAG,cAAa;AAAA,EACrF;AACA,SAAO,KAAK,MAAM,GAAG,SAAS,IAAI,iBAAiB,KAAK,MAAM,KAAK,MAAM,SAAS;AACpF;AAYO,SAAS,oBAAoB,MAAc,KAA+B;AAC/E,QAAM,OAAO,CAAC,gBAAgB,UAAU;AACxC,MAAI,IAAI,aAAa,EAAG,MAAK,KAAK,OAAO,OAAO,IAAI,UAAU,CAAC;AAC/D,QAAM,aAAa,IAAI,aAAa,OAAO,OAAO,IAAI,YAAY,IAAI,gBAAgB;AACtF,MAAI,eAAe,QAAQ,aAAa,EAAG,MAAK,KAAK,MAAM,OAAO,UAAU,CAAC;AAC7E,OAAK,KAAK,MAAM,MAAM,QAAQ,SAAS,OAAO,OAAO,qBAAqB,MAAM,QAAQ,GAAG;AAC3F,SAAO;AACT;AAKA,eAAsB,aACpB,YACA,MACA,KAC8B;AAC9B,QAAM,SAAS,MAAM,cAAc,YAAY,oBAAoB,MAAM,GAAG,GAAG;AAAA,IAC7E,UAAU;AAAA,IACV,WAAW,KAAK,OAAO;AAAA,IACvB,SAAS;AAAA,IACT,aAAa;AAAA,EACf,CAAC;AACD,SAAO,oBAAoB,OAAO,MAAM;AAC1C;AAEO,SAAS,iBAAyB;AACvC,QAAM,aAAa,aAAa,UAAU,EAAE,qBAAqB,KAAK,CAAC;AACvE,MAAI,CAAC,WAAY,OAAM,IAAI,MAAM,mDAAmD;AACpF,SAAO;AACT;AAEO,IAAM,sBAAsB;AAC5B,IAAM,mCAAmC;AAEhD,IAAM,eAAe;AAuBd,SAAS,qBACd,OACA,aAAqB,qBACC;AACtB,MAAI,MAAM,kBAAkB,cAAc;AACxC,UAAM,IAAI,MAAM,IAAI,MAAM,EAAE,gEAAgE;AAAA,EAC9F;AACA,QAAM,eAAe,aAAa,MAAM;AACxC,QAAM,aAAa,mCAAmC,MAAM;AAC5D,QAAM,SAAgD;AAAA,IACpD,CAAC,MAAM,YAAY;AAAA,IACnB,CAAC,gBAAgB,iBAAiB;AAAA,IAClC,CAAC,aAAa,UAAU;AAAA,EAC1B;AACA,QAAM,CAAC,WAAW,aAAa,IAAI,OAAO;AAAA,IAAO,CAAC,QAAQ,cACxD,UAAU,CAAC,IAAI,OAAO,CAAC,IAAI,OAAO,YAAY;AAAA,EAChD;AACA,QAAM,iBAAiB,MAAM,SAAS,IAAI,OAAO,MAAM,MAAM,IAAI;AACjE,SAAO;AAAA,IACL,UAAU,MAAM;AAAA,IAChB;AAAA,IACA,YAAY,MAAM;AAAA,IAClB,gBAAgB,MAAM;AAAA,IACtB,QAAQ,OAAO,gBAAgB;AAAA,IAC/B,QAAQ;AAAA,IACR,UAAU,gBAAgB;AAAA,IAC1B,eAAe,MAAM,iBAAiB;AAAA,IACtC,uBAAuB,MAAM,eAAe;AAAA,IAC5C;AAAA,EACF;AACF;","names":["decodeWellFormedEscapes"]}
|
|
@@ -2,7 +2,7 @@ import {
|
|
|
2
2
|
PROXY_VARIANT_CONFIG,
|
|
3
3
|
probeFirstFrameColour,
|
|
4
4
|
probeMediaMetadata
|
|
5
|
-
} from "./chunk-
|
|
5
|
+
} from "./chunk-RN33UALN.js";
|
|
6
6
|
import {
|
|
7
7
|
mkdirWithinProject,
|
|
8
8
|
realProjectRoot,
|
|
@@ -491,4 +491,4 @@ export {
|
|
|
491
491
|
proxyActivityMark,
|
|
492
492
|
resolveProxy
|
|
493
493
|
};
|
|
494
|
-
//# sourceMappingURL=chunk-
|
|
494
|
+
//# sourceMappingURL=chunk-6EFY2WPN.js.map
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PROXY_PARAMS_VERSION,
|
|
3
3
|
resolveProxy
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-6EFY2WPN.js";
|
|
5
5
|
import {
|
|
6
6
|
createMediaCodecProbeCache,
|
|
7
7
|
proxyVariantFor,
|
|
8
8
|
recordProxyPrewarm,
|
|
9
9
|
scanProjectMediaCodecMap,
|
|
10
10
|
shouldPrewarmProxy
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-RN33UALN.js";
|
|
12
12
|
|
|
13
13
|
// src/helpers/mediaProxyPreview.ts
|
|
14
14
|
import { resolve } from "path";
|
|
@@ -64,4 +64,4 @@ export {
|
|
|
64
64
|
injectMediaCodecMapIntoHtml,
|
|
65
65
|
injectMediaCodecMap
|
|
66
66
|
};
|
|
67
|
-
//# sourceMappingURL=chunk-
|
|
67
|
+
//# sourceMappingURL=chunk-AYKLBPP2.js.map
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
// src/helpers/sourceMutation.ts
|
|
2
|
+
import { replacementTimelineAssetId } from "@hyperframes/core/timeline-asset-id";
|
|
2
3
|
import { parseHTML } from "linkedom";
|
|
3
4
|
import { removeElementWithGsapCascade } from "@hyperframes/parsers";
|
|
4
5
|
import { readMediaOffsetSeconds, readPlaybackRate } from "@hyperframes/parsers/media-duration";
|
|
@@ -242,6 +243,8 @@ function patchElementInHtml(source, target, operations, takenIds) {
|
|
|
242
243
|
if (!isAllowedHtmlAttribute(op.property)) break;
|
|
243
244
|
if (op.value != null) {
|
|
244
245
|
if (!isSafeAttributeValue(op.property, op.value)) break;
|
|
246
|
+
const replacementId = op.property === "src" ? replacementTimelineAssetId(document, opTarget, op.value) : null;
|
|
247
|
+
if (replacementId !== null) opTarget.setAttribute("id", replacementId);
|
|
245
248
|
opTarget.setAttribute(op.property, op.value);
|
|
246
249
|
} else {
|
|
247
250
|
opTarget.removeAttribute(op.property);
|
|
@@ -519,4 +522,4 @@ export {
|
|
|
519
522
|
unwrapElementsFromHtml,
|
|
520
523
|
relinkSplitHalvesInHtml
|
|
521
524
|
};
|
|
522
|
-
//# sourceMappingURL=chunk-
|
|
525
|
+
//# sourceMappingURL=chunk-JJ4N3PXT.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/helpers/sourceMutation.ts","../src/helpers/sourceStyleMutation.ts"],"sourcesContent":["import { replacementTimelineAssetId } from \"@hyperframes/core/timeline-asset-id\";\nimport { parseHTML } from \"linkedom\";\nimport { removeElementWithGsapCascade } from \"@hyperframes/parsers\";\nimport { readMediaOffsetSeconds, readPlaybackRate } from \"@hyperframes/parsers/media-duration\";\nimport postcss from \"postcss\";\nimport selectorParser from \"postcss-selector-parser\";\nimport { isAllowedHtmlAttribute, isSafeAttributeValue } from \"@hyperframes/core/html-attr-safety\";\nimport { sanitizeRichTextChildren } from \"@hyperframes/core/rich-text-sanitize\";\nimport {\n EXCLUDED_TAGS,\n ensureHfIds,\n mintHfId,\n walkCompositionDescendants,\n} from \"@hyperframes/parsers/hf-ids\";\nimport { readClipTiming, writeClipTiming } from \"@hyperframes/core/composition-contract\";\nimport { relinkSplitHalves } from \"@hyperframes/core/media-link\";\nimport { parseStyleDecls, patchStyleAttrString } from \"./sourceStyleMutation.js\";\n\nexport interface SourceMutationTarget {\n id?: string | null;\n hfId?: string;\n selector?: string;\n selectorIndex?: number;\n}\n\nexport function parseSourceDocument(raw: string): {\n document: Document;\n wrappedFragment: boolean;\n} {\n const source = ensureHfIds(raw);\n const hasDocumentShell = /<!doctype|<html[\\s>]/i.test(source);\n if (hasDocumentShell) {\n return { document: parseHTML(source).document, wrappedFragment: false };\n }\n return {\n document: parseHTML(`<!DOCTYPE html><html><head></head><body>${source}</body></html>`).document,\n wrappedFragment: true,\n };\n}\n\nfunction duplicateCssRulesForId(document: Document, originalId: string, newId: string): void {\n const idToken = `#${originalId}`;\n const transform = selectorParser((selectors) => {\n selectors.walkIds((node) => {\n if (node.value === originalId) node.value = newId;\n });\n });\n for (const styleEl of document.querySelectorAll(\"style\")) {\n const css = styleEl.textContent ?? \"\";\n let root: postcss.Root;\n try {\n root = postcss.parse(css);\n } catch {\n continue;\n }\n const clones: postcss.Rule[] = [];\n root.walkRules((rule) => {\n if (!rule.selector.includes(idToken)) return;\n const newSelector = transform.processSync(rule.selector);\n if (newSelector === rule.selector) return;\n const clone = rule.clone({ selector: newSelector });\n clones.push(clone);\n });\n if (clones.length > 0) {\n for (const c of clones) root.append(c);\n styleEl.textContent = root.toString();\n }\n }\n}\n\nfunction querySelectorAllWithTemplates(root: Document | Element, selector: string): Element[] {\n const matches = Array.from(root.querySelectorAll(selector));\n if (matches.length > 0) return matches;\n // querySelectorAll doesn't traverse <template> content in linkedom.\n // Search directly on each template element (NOT .content — removing from\n // .content's DocumentFragment doesn't update the serialized output).\n // Recurse so NESTED templates resolve too — ensureHfIds and the SDK's\n // querySelectorAllDeep descend nested composition templates, so ids exist at\n // any template depth; a one-level search here would silently no-op\n // server-side ops on those ids while the SDK resolves them.\n const templates = Array.from(root.querySelectorAll(\"template\"));\n for (const tmpl of templates) {\n const inner = querySelectorAllWithTemplates(tmpl, selector);\n if (inner.length > 0) return inner;\n }\n return [];\n}\n\n// Prevent CSS attribute-selector injection via a crafted hfId: escape\n// backslashes first, then double-quotes. Keeps a malformed/hostile value from\n// breaking out of the `[data-hf-id=\"…\"]` selector once callers beyond the\n// internal mint contract (R2+ user flows) pass values here.\nfunction escapeCssAttrValue(value: string): string {\n return value.replace(/\\\\/g, \"\\\\\\\\\").replace(/\"/g, '\\\\\"');\n}\n\nfunction findByHfId(document: Document, hfId: string): Element | null {\n try {\n const matches = querySelectorAllWithTemplates(\n document,\n `[data-hf-id=\"${escapeCssAttrValue(hfId)}\"]`,\n );\n if (matches.length > 1) {\n // The mint contract guarantees uniqueness; a duplicate means upstream\n // id drift. Don't silently patch an arbitrary one — surface it.\n // eslint-disable-next-line no-console\n console.warn(\n `sourceMutation: data-hf-id \"${hfId}\" matched ${matches.length} elements; using the first. ids must be unique per document.`,\n );\n }\n return matches[0] ?? null;\n } catch {\n // Malformed selector despite escaping — let the caller fall back.\n return null;\n }\n}\n\nexport function findTargetElement(\n document: Document,\n target: SourceMutationTarget,\n): Element | null {\n if (target.hfId) {\n const el = findByHfId(document, target.hfId);\n if (el) return el;\n }\n\n if (target.id) {\n const byId = document.getElementById(target.id);\n if (byId) return byId;\n }\n\n if (!target.selector) return null;\n try {\n const matches = querySelectorAllWithTemplates(document, target.selector);\n return matches[target.selectorIndex ?? 0] ?? null;\n } catch {\n return null;\n }\n}\n\n/**\n * Removes every target in one parse and one serialization. A target nested inside one already\n * removed no longer matches, which is a normal outcome rather than a failure.\n */\nexport function removeElementsFromHtml(\n source: string,\n targets: readonly SourceMutationTarget[],\n): string {\n const { document, wrappedFragment } = parseSourceDocument(source);\n let removed = false;\n for (const target of targets) {\n const element = findTargetElement(document, target);\n if (!element) continue;\n removeElementWithGsapCascade(document, element);\n removed = true;\n }\n if (!removed) return source;\n return wrappedFragment ? document.body.innerHTML || \"\" : document.toString();\n}\n\nexport function removeElementFromHtml(source: string, target: SourceMutationTarget): string {\n return removeElementsFromHtml(source, [target]);\n}\n\nexport function isHTMLElement(el: Node): el is HTMLElement {\n const HTMLEl = el.ownerDocument?.defaultView?.HTMLElement;\n return HTMLEl ? el instanceof HTMLEl : el.nodeType === 1 && \"style\" in el;\n}\n\nexport function dedupeClonedCompositionId(document: Document, clone: Element): void {\n const compositionId = clone.getAttribute(\"data-composition-id\");\n if (!compositionId) return;\n const usedCompositionIds = new Set(\n querySelectorAllWithTemplates(document, \"[data-composition-id]\").map((node) =>\n node.getAttribute(\"data-composition-id\"),\n ),\n );\n const base = `${compositionId}-split`;\n let nextCompositionId = base;\n let suffix = 2;\n while (usedCompositionIds.has(nextCompositionId)) nextCompositionId = `${base}-${suffix++}`;\n clone.setAttribute(\"data-composition-id\", nextCompositionId);\n}\n\nexport interface PatchOperation {\n /** `ensure-id` keeps the element's id, else writes `value` made unique in this file and `takenIds`. */\n type:\n | \"inline-style\"\n | \"attribute\"\n | \"html-attribute\"\n | \"text-content\"\n | \"rich-text\"\n | \"ensure-id\";\n property: string;\n value: string | null;\n childSelector?: string;\n childIndex?: number;\n}\n\ninterface ResolvedPatchOperation {\n op: PatchOperation;\n target: HTMLElement;\n}\n\nfunction resolveOperationTarget(parent: HTMLElement, op: PatchOperation): HTMLElement | null {\n if (op.childSelector === undefined) return parent;\n try {\n const child = parent.querySelectorAll(op.childSelector)[op.childIndex ?? 0] ?? null;\n return child && isHTMLElement(child) ? child : null;\n } catch {\n return null;\n }\n}\n\n/**\n * Give the elements a rich-text patch just introduced their stable ids, here,\n * in the bytes about to be written and handed back.\n *\n * Otherwise the next preview request mints them and writes the file a second\n * time, after Studio has already recorded the edit in its history. The recorded\n * \"after\" stops matching disk, the content check refuses, and undo reports the\n * file as changed outside Studio — for every colour applied to a run of\n * characters and every text layer added. The clip split stamps its own clone\n * for exactly this reason.\n *\n * Minted one element at a time with the same function `ensureHfIds` uses, so\n * these ids are the ones the next pass would have assigned. Not `ensureHfIds`\n * itself: it takes a whole document, and handing it this element's markup would\n * put the markup back as one.\n */\nfunction stampNewChildIds(parent: Element): void {\n const assigned = new Set<string>();\n const root = parent.ownerDocument?.body ?? parent;\n walkCompositionDescendants(root, (el) => {\n const id = el.getAttribute(\"data-hf-id\");\n if (id) assigned.add(id);\n });\n for (const el of parent.querySelectorAll(\"*\")) {\n if (el.getAttribute(\"data-hf-id\")) continue;\n if (EXCLUDED_TAGS.has(el.tagName.toLowerCase())) continue;\n el.setAttribute(\"data-hf-id\", mintHfId(el, assigned));\n }\n}\n\n// fallow-ignore-next-line complexity\nexport function patchElementInHtml(\n source: string,\n target: SourceMutationTarget,\n operations: PatchOperation[],\n takenIds?: ReadonlySet<string>,\n): { html: string; matched: boolean; elementId?: string | null } {\n const { document, wrappedFragment } = parseSourceDocument(source);\n const el = findTargetElement(document, target);\n if (!el || !isHTMLElement(el)) return { html: source, matched: false };\n const htmlEl = el;\n const originalHtml = wrappedFragment ? document.body.innerHTML || \"\" : document.toString();\n\n const resolved: ResolvedPatchOperation[] = [];\n for (const op of operations) {\n const opTarget = resolveOperationTarget(htmlEl, op);\n if (!opTarget) return { html: source, matched: false };\n resolved.push({ op, target: opTarget });\n }\n\n for (const { op, target: opTarget } of resolved) {\n switch (op.type) {\n case \"inline-style\":\n // linkedom's CSSStyleDeclaration does not support CSS custom properties\n // (--foo) or newer individual transform properties (translate, rotate,\n // scale) via style.setProperty(). Manipulate the style attribute string\n // directly so all property names survive the round-trip.\n {\n const raw = opTarget.getAttribute(\"style\") ?? \"\";\n const patched = patchStyleAttrString(raw, op.property, op.value);\n opTarget.setAttribute(\"style\", patched);\n }\n break;\n case \"attribute\":\n {\n const fullAttr = op.property.startsWith(\"data-\") ? op.property : `data-${op.property}`;\n if (op.value != null) {\n opTarget.setAttribute(fullAttr, op.value);\n } else {\n opTarget.removeAttribute(fullAttr);\n }\n }\n break;\n case \"html-attribute\":\n if (!isAllowedHtmlAttribute(op.property)) break;\n if (op.value != null) {\n if (!isSafeAttributeValue(op.property, op.value)) break;\n const replacementId =\n op.property === \"src\" ? replacementTimelineAssetId(document, opTarget, op.value) : null;\n if (replacementId !== null) opTarget.setAttribute(\"id\", replacementId);\n opTarget.setAttribute(op.property, op.value);\n } else {\n opTarget.removeAttribute(op.property);\n }\n break;\n case \"ensure-id\":\n if (opTarget.getAttribute(\"id\") || !op.value) break;\n opTarget.setAttribute(\"id\", nextUniqueId(document, op.value, takenIds));\n break;\n case \"text-content\":\n if (op.value != null) {\n const inner = opTarget.children.length === 1 ? opTarget.firstElementChild : null;\n const textTarget = inner && isHTMLElement(inner) ? inner : opTarget;\n textTarget.textContent = op.value;\n }\n break;\n // The one operation that can write markup, so the one that has to check\n // it. Assigned first and sanitised after, rather than sanitising a\n // string: parsing is what turns a payload into the tree the allowlist\n // can actually judge, and linkedom never runs anything it parses.\n case \"rich-text\":\n if (op.value != null) {\n opTarget.innerHTML = op.value;\n sanitizeRichTextChildren(opTarget);\n stampNewChildIds(opTarget);\n }\n break;\n }\n }\n\n const html = wrappedFragment ? document.body.innerHTML || \"\" : document.toString();\n const elementId = htmlEl.getAttribute(\"id\");\n if (html === originalHtml) return { html: source, matched: true, elementId };\n return { html: ensureHfIds(html), matched: true, elementId };\n}\n\nexport function nextUniqueId(\n document: Document,\n base: string,\n takenIds?: ReadonlySet<string>,\n): string {\n let id = base;\n let suffix = 2;\n while (document.getElementById(id) || takenIds?.has(id)) id = `${base}-${suffix++}`;\n return id;\n}\n\n/** Whether each target exists in `source`; the document is parsed once however many targets ask. */\nexport function probeElementsInSource(\n source: string,\n targets: readonly SourceMutationTarget[],\n): boolean[] {\n const { document } = parseSourceDocument(source);\n return targets.map((target) => {\n if (!target.id && !target.hfId && !target.selector) return false;\n const el = findTargetElement(document, target);\n return el != null && isHTMLElement(el);\n });\n}\n\nexport function probeElementInSource(source: string, target: SourceMutationTarget): boolean {\n return probeElementsInSource(source, [target])[0] ?? false;\n}\n\nexport interface SplitElementResult {\n html: string;\n matched: boolean;\n newId: string | null;\n}\n\nfunction resolveElementTiming(el: Element): {\n start: number;\n duration: number;\n} {\n const timing = readClipTiming(el);\n return { start: timing.start ?? 0, duration: timing.duration ?? 0 };\n}\n\nfunction setElementDuration(\n el: Element,\n start: number,\n duration: number,\n trackIndex?: number,\n): void {\n writeClipTiming(el, {\n start: Math.round(start * 1000) / 1000,\n duration: Math.round(duration * 1000) / 1000,\n ...(trackIndex != null ? { trackIndex } : {}),\n });\n}\n\n// fallow-ignore-next-line complexity\nexport function splitElementInHtml(\n source: string,\n target: SourceMutationTarget,\n splitTime: number,\n newId: string,\n fallbackTiming?: {\n start: number;\n duration: number;\n playbackStart?: number;\n playbackRate?: number;\n stampPlaybackStart?: boolean;\n // The element's current resolved track (authored, or the runtime's\n // positional-index fallback when unauthored). Stamped onto both halves so\n // inserting the clone can't shift either one to a different row — see\n // parseAuthoredTrack's fallback in core/runtime/timeline.ts.\n track?: number;\n },\n): SplitElementResult {\n const { document, wrappedFragment } = parseSourceDocument(source);\n const el = findTargetElement(document, target);\n if (!el || !isHTMLElement(el)) return { html: source, matched: false, newId: null };\n\n const timing = resolveElementTiming(el);\n let { start, duration } = timing;\n // GSAP-animated elements carry their timing in the script, not in data-* attrs,\n // so the source has no authored duration. Fall back to the store's (GSAP-derived)\n // range — the runtime windows visibility off data-start/data-duration regardless\n // of class, so stamping both halves below makes each half show only in its window.\n if (duration <= 0 && fallbackTiming && fallbackTiming.duration > 0) {\n start = fallbackTiming.start;\n duration = fallbackTiming.duration;\n }\n if (duration <= 0 || splitTime <= start || splitTime >= start + duration) {\n return { html: source, matched: false, newId: null };\n }\n\n newId = nextUniqueId(document, newId);\n\n const firstDuration = splitTime - start;\n const secondDuration = duration - firstDuration;\n\n const clone = el.cloneNode(true);\n if (!isHTMLElement(clone)) return { html: source, matched: false, newId: null };\n clone.setAttribute(\"id\", newId);\n dedupeClonedCompositionId(document, clone);\n clone.removeAttribute(\"data-hf-id\");\n // Descendants carry their own data-hf-id; leaving them duplicates the id of\n // every nested node (e.g. an inner <span>), so strip them on the clone too.\n for (const node of clone.querySelectorAll(\"[data-hf-id]\")) node.removeAttribute(\"data-hf-id\");\n setElementDuration(clone, splitTime, secondDuration, fallbackTiming?.track);\n\n // Keep the \"clip\" class — the runtime uses it to control visibility\n // based on data-start/data-duration timing.\n\n // A split creates two views over the same media source. Even an untrimmed\n // audio/video element needs an explicit zero in-point stamped on the first\n // half so the second half can advance from it instead of restarting at zero.\n const playbackStartAttr = el.hasAttribute(\"data-playback-start\")\n ? \"data-playback-start\"\n : el.hasAttribute(\"data-media-start\")\n ? \"data-media-start\"\n : fallbackTiming?.stampPlaybackStart\n ? \"data-playback-start\"\n : el.matches(\"audio, video\")\n ? \"data-media-start\"\n : null;\n if (playbackStartAttr) {\n const readAttr = (name: string) => el.getAttribute(name);\n const authoredTrim = el.getAttribute(playbackStartAttr);\n const currentTrim =\n authoredTrim !== null\n ? readMediaOffsetSeconds(readAttr)\n : (fallbackTiming?.playbackStart ?? 0);\n const rate = readPlaybackRate(readAttr, fallbackTiming?.playbackRate);\n if (authoredTrim === null || Number(authoredTrim) !== currentTrim)\n el.setAttribute(playbackStartAttr, String(Math.round(currentTrim * 1000) / 1000));\n clone.setAttribute(\n playbackStartAttr,\n String(Math.round((currentTrim + firstDuration * rate) * 1000) / 1000),\n );\n }\n\n // Duplicate CSS rules targeting the original ID so the clone inherits the same styles.\n const originalId = el.getAttribute(\"id\");\n if (originalId) {\n duplicateCssRulesForId(document, originalId, newId);\n }\n\n // Trim the original element's duration. A GSAP element had no data-start; stamp\n // it so the runtime windows the first half (visibility selects on [data-start]).\n setElementDuration(el, start, firstDuration, fallbackTiming?.track);\n\n // Insert clone after original\n if (el.nextSibling) {\n el.parentElement!.insertBefore(clone, el.nextSibling);\n } else {\n el.parentElement!.appendChild(clone);\n }\n\n const html = wrappedFragment ? document.body.innerHTML || \"\" : document.toString();\n return {\n // The split owns its new nodes' stable ids. Leaving the clone unstamped makes\n // the next preview request persist different bytes after history is recorded.\n html: ensureHfIds(html),\n matched: true,\n newId,\n };\n}\n\n// --- Element grouping -------------------------------------------------------\n// A group is a real `<div data-hf-group=\"…\">` wrapping its members in the DOM.\n// Wrapping rebases each member's left/top so its absolute position is unchanged:\n// the wrapper sits at the selection bbox top-left, and each child's new left/top\n// is its old left/top minus the wrapper origin (computed client-side, where live\n// layout is available, and passed in via `rebases`). GSAP x/y, CSS translate and\n// --hf-studio-offset vars are deltas relative to flow position and stay untouched.\n\nexport interface WrapElementsResult {\n html: string;\n matched: boolean;\n groupId: string | null;\n error?: string;\n}\n\nexport interface UnwrapElementsResult {\n html: string;\n unwrapped: boolean;\n /** The unwrapped wrapper's id, so callers can strip GSAP that targeted it\n * (the wrapper is gone; a leftover `gsap.set(\"#id\")` would throw at runtime). */\n unwrappedGroupId?: string;\n /** Members (id'd children) with their absolute layout centres (post un-rebase),\n * so the caller can BAKE the group's GSAP transform into each member before\n * stripping it — otherwise the group's moves are lost on ungroup. */\n members?: Array<{ id: string; cx: number; cy: number }>;\n /** The wrapper's layout centre — the pivot for baking the group's rotation/scale. */\n groupCenter?: { cx: number; cy: number };\n}\n\nexport interface ElementRebase {\n target: SourceMutationTarget;\n left: number;\n top: number;\n /** The member's current resolved track (authored, or the runtime's\n * positional-index fallback). Stamped explicitly so moving it into the\n * wrapper can't shift its computed row — same hazard split closes. */\n track?: number;\n}\n\nfunction getInlineStylePx(el: Element, property: string): number {\n const style = (isHTMLElement(el) ? el.getAttribute(\"style\") : null) ?? \"\";\n const { props } = parseStyleDecls(style);\n const raw = props.get(property);\n if (!raw) return 0;\n const n = parseFloat(raw);\n return Number.isFinite(n) ? n : 0;\n}\n\nfunction setInlineLeftTop(el: HTMLElement, left: number, top: number): void {\n let style = el.getAttribute(\"style\") ?? \"\";\n style = patchStyleAttrString(style, \"left\", `${left}px`);\n style = patchStyleAttrString(style, \"top\", `${top}px`);\n el.setAttribute(\"style\", style);\n}\n\n// Slug the group name (\"Group 1\" → \"group-1\") into a unique, valid element id.\nfunction uniqueGroupDomId(document: Document, groupId: string): string {\n const base =\n groupId\n .trim()\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, \"-\")\n // Normalization above leaves at most one hyphen at either edge.\n .replace(/^-|-$/g, \"\") || \"group\";\n return nextUniqueId(document, base);\n}\n\n// fallow-ignore-next-line complexity\nexport function wrapElementsInHtml(\n source: string,\n targets: SourceMutationTarget[],\n groupId: string,\n bbox: { left: number; top: number; width: number; height: number },\n rebases: ElementRebase[],\n): WrapElementsResult {\n const { document, wrappedFragment } = parseSourceDocument(source);\n if (targets.length === 0) {\n return { html: source, matched: false, groupId: null, error: \"no targets\" };\n }\n\n // Resolve + dedupe by element ref (two targets may point at the same node).\n const els: HTMLElement[] = [];\n const seen = new Set<Element>();\n for (const target of targets) {\n const el = findTargetElement(document, target);\n if (!el || !isHTMLElement(el) || seen.has(el)) continue;\n seen.add(el);\n els.push(el);\n }\n if (els.length === 0) {\n return { html: source, matched: false, groupId: null, error: \"no targets matched\" };\n }\n\n // P1: require a single common parent (LCA multi-parent wrapping is P2).\n const parent = els[0]?.parentElement;\n if (!parent || els.some((el) => el.parentElement !== parent)) {\n return {\n html: source,\n matched: false,\n groupId: null,\n error: \"grouped elements must share a single parent\",\n };\n }\n\n // Order members by their position in the parent (= z-order / stacking order).\n const memberSet = new Set<Element>(els);\n const ordered = Array.from(parent.children).filter((c): c is HTMLElement => memberSet.has(c));\n\n // Map each member to its rebased left/top and track (resolved against the same document).\n const rebaseByEl = new Map<Element, { left: number; top: number; track?: number }>();\n for (const rebase of rebases) {\n const el = findTargetElement(document, rebase.target);\n if (el) rebaseByEl.set(el, { left: rebase.left, top: rebase.top, track: rebase.track });\n }\n\n const wrapper = document.createElement(\"div\");\n wrapper.setAttribute(\"data-hf-group\", groupId);\n // A real `id` (slug of the group name) makes the wrapper a first-class node in the\n // clip manifest / timeline parent-map (both keyed by id) and a clean GSAP target —\n // without it the wrapper is invisible to the timeline and breaks child enumeration.\n wrapper.setAttribute(\"id\", uniqueGroupDomId(document, groupId));\n // Adopt the topmost member's stacking level. A group is one stacking unit, so a\n // non-member interleaved between two selected members can't stay \"between\" them\n // once they unify. Matching Figma/Sketch, the group lifts to the topmost selected\n // layer: the wrapper goes at the LAST member's slot and carries the max member\n // z-index — so an interleaved non-member falls below the group instead of hoisting\n // above it, and explicit member z-indexes are honored.\n const memberZIndexes = ordered\n .map((el) =>\n Number.parseInt(\n parseStyleDecls(el.getAttribute(\"style\") ?? \"\").props.get(\"z-index\") ?? \"\",\n 10,\n ),\n )\n .filter((z) => Number.isFinite(z));\n const maxZ = memberZIndexes.length > 0 ? Math.max(...memberZIndexes) : null;\n wrapper.setAttribute(\n \"style\",\n `position: absolute; left: ${bbox.left}px; top: ${bbox.top}px; width: ${bbox.width}px; height: ${bbox.height}px` +\n (maxZ !== null ? `; z-index: ${maxZ}` : \"\"),\n );\n\n // Insert the wrapper at the topmost member's slot, then move members into it.\n parent.insertBefore(wrapper, ordered[ordered.length - 1] ?? null);\n for (const el of ordered) {\n const rebase = rebaseByEl.get(el);\n if (rebase) setInlineLeftTop(el, rebase.left, rebase.top);\n if (rebase?.track != null) writeClipTiming(el, { trackIndex: Math.round(rebase.track) });\n wrapper.appendChild(el); // appendChild moves the node, preserving order\n }\n\n return {\n html: wrappedFragment ? document.body.innerHTML || \"\" : document.toString(),\n matched: true,\n groupId,\n };\n}\n\nexport interface UnwrapChildTrack {\n target: SourceMutationTarget;\n /** The child's current resolved track, same hazard and fix as wrap's members. */\n track?: number;\n}\n\n// Only children actually inside the group and given a resolved track qualify —\n// same hazard and fix as wrap's members, scoped to this group's own children.\nfunction buildChildTrackMap(\n document: Document,\n group: Element,\n childTracks: UnwrapChildTrack[],\n): Map<Element, number> {\n const trackByEl = new Map<Element, number>();\n for (const entry of childTracks) {\n const el = findTargetElement(document, entry.target);\n if (el && group.contains(el) && entry.track != null) trackByEl.set(el, entry.track);\n }\n return trackByEl;\n}\n\n// Undoes the wrap-side rebase (child absolute = child rebased + wrapper\n// origin), stamps each child's resolved track where one was given, and moves\n// every child back into the parent ahead of the wrapper — preserving order.\nfunction relocateGroupChildren(\n group: Element,\n parent: Element,\n wLeft: number,\n wTop: number,\n trackByEl: Map<Element, number>,\n): Array<{ id: string; cx: number; cy: number }> {\n const members: Array<{ id: string; cx: number; cy: number }> = [];\n for (const child of Array.from(group.children)) {\n if (isHTMLElement(child)) {\n const newLeft = getInlineStylePx(child, \"left\") + wLeft;\n const newTop = getInlineStylePx(child, \"top\") + wTop;\n setInlineLeftTop(child, newLeft, newTop);\n const track = trackByEl.get(child);\n if (track != null) writeClipTiming(child, { trackIndex: Math.round(track) });\n if (child.id) {\n members.push({\n id: child.id,\n cx: newLeft + getInlineStylePx(child, \"width\") / 2,\n cy: newTop + getInlineStylePx(child, \"height\") / 2,\n });\n }\n }\n parent.insertBefore(child, group);\n }\n return members;\n}\n\nexport function unwrapElementsFromHtml(\n source: string,\n groupTarget: SourceMutationTarget,\n childTracks: UnwrapChildTrack[] = [],\n): UnwrapElementsResult {\n const { document, wrappedFragment } = parseSourceDocument(source);\n const group = findTargetElement(document, groupTarget);\n if (!group || !isHTMLElement(group)) return { html: source, unwrapped: false };\n // Shape guard mirroring the wrap-side contract: only ever dissolve an actual\n // group wrapper. A stale/desynced selection that resolves to a plain <div>\n // would otherwise be unwrapped — rebasing its children by the parent's origin\n // (silent corruption). Wrap enforces invariants; unwrap must too.\n if (!group.hasAttribute(\"data-hf-group\")) return { html: source, unwrapped: false };\n\n const parent = group.parentElement;\n if (!parent) return { html: source, unwrapped: false };\n\n const trackByEl = buildChildTrackMap(document, group, childTracks);\n\n // Undo the rebase: child absolute position = child (rebased) + wrapper origin.\n const wLeft = getInlineStylePx(group, \"left\");\n const wTop = getInlineStylePx(group, \"top\");\n const groupCenter = {\n cx: wLeft + getInlineStylePx(group, \"width\") / 2,\n cy: wTop + getInlineStylePx(group, \"height\") / 2,\n };\n\n const members = relocateGroupChildren(group, parent, wLeft, wTop, trackByEl);\n const groupId = group.id || undefined;\n group.remove();\n\n return {\n html: wrappedFragment ? document.body.innerHTML || \"\" : document.toString(),\n unwrapped: true,\n unwrappedGroupId: groupId,\n members,\n groupCenter,\n };\n}\n\n/** After a cut, give each linked group's right halves their own `data-link` and `data-sync-origin`. */\nexport function relinkSplitHalvesInHtml(source: string, rightHalfIds: readonly string[]): string {\n const { document, wrappedFragment } = parseSourceDocument(source);\n const carriesPairing = (id: string) => {\n const el = document.getElementById(id);\n return Boolean(el?.hasAttribute(\"data-link\") || el?.hasAttribute(\"data-sync-origin\"));\n };\n if (!rightHalfIds.some(carriesPairing)) {\n return source;\n }\n relinkSplitHalves(document, rightHalfIds);\n return wrappedFragment ? document.body.innerHTML || \"\" : document.toString();\n}\n","// fallow-ignore-next-line complexity\nexport function parseStyleDecls(style: string): { props: Map<string, string>; order: string[] } {\n const props = new Map<string, string>();\n const order: string[] = [];\n let i = 0;\n while (i < style.length) {\n let depth = 0;\n let inSingle = false;\n let inDouble = false;\n const start = i;\n while (i < style.length) {\n const ch = style[i];\n if (ch === \"'\" && !inDouble) inSingle = !inSingle;\n else if (ch === '\"' && !inSingle) inDouble = !inDouble;\n else if (!inSingle && !inDouble) {\n if (ch === \"(\") depth++;\n else if (ch === \")\") depth = Math.max(0, depth - 1);\n else if (ch === \";\" && depth === 0) break;\n }\n i++;\n }\n const decl = style.slice(start, i).trim();\n i++;\n if (!decl) continue;\n const colon = decl.indexOf(\":\");\n if (colon < 0) continue;\n const key = decl.slice(0, colon).trim();\n const val = decl.slice(colon + 1).trim();\n if (!key) continue;\n if (!props.has(key)) order.push(key);\n props.set(key, val);\n }\n return { props, order };\n}\n\nfunction serializeStyleDecls(props: Map<string, string>, order: string[]): string {\n return order\n .map((k) => `${k}: ${props.get(k) ?? \"\"}`)\n .filter((d) => d.trim())\n .join(\"; \");\n}\n\nexport function patchStyleAttrString(\n style: string,\n property: string,\n value: string | null,\n): string {\n const { props, order } = parseStyleDecls(style);\n if (value === null) {\n props.delete(property);\n const idx = order.indexOf(property);\n if (idx >= 0) order.splice(idx, 1);\n } else {\n if (!props.has(property)) order.push(property);\n props.set(property, value);\n }\n return serializeStyleDecls(props, order);\n}\n"],"mappings":";AAAA,SAAS,kCAAkC;AAC3C,SAAS,iBAAiB;AAC1B,SAAS,oCAAoC;AAC7C,SAAS,wBAAwB,wBAAwB;AACzD,OAAO,aAAa;AACpB,OAAO,oBAAoB;AAC3B,SAAS,wBAAwB,4BAA4B;AAC7D,SAAS,gCAAgC;AACzC;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OACK;AACP,SAAS,gBAAgB,uBAAuB;AAChD,SAAS,yBAAyB;;;ACd3B,SAAS,gBAAgB,OAAgE;AAC9F,QAAM,QAAQ,oBAAI,IAAoB;AACtC,QAAM,QAAkB,CAAC;AACzB,MAAI,IAAI;AACR,SAAO,IAAI,MAAM,QAAQ;AACvB,QAAI,QAAQ;AACZ,QAAI,WAAW;AACf,QAAI,WAAW;AACf,UAAM,QAAQ;AACd,WAAO,IAAI,MAAM,QAAQ;AACvB,YAAM,KAAK,MAAM,CAAC;AAClB,UAAI,OAAO,OAAO,CAAC,SAAU,YAAW,CAAC;AAAA,eAChC,OAAO,OAAO,CAAC,SAAU,YAAW,CAAC;AAAA,eACrC,CAAC,YAAY,CAAC,UAAU;AAC/B,YAAI,OAAO,IAAK;AAAA,iBACP,OAAO,IAAK,SAAQ,KAAK,IAAI,GAAG,QAAQ,CAAC;AAAA,iBACzC,OAAO,OAAO,UAAU,EAAG;AAAA,MACtC;AACA;AAAA,IACF;AACA,UAAM,OAAO,MAAM,MAAM,OAAO,CAAC,EAAE,KAAK;AACxC;AACA,QAAI,CAAC,KAAM;AACX,UAAM,QAAQ,KAAK,QAAQ,GAAG;AAC9B,QAAI,QAAQ,EAAG;AACf,UAAM,MAAM,KAAK,MAAM,GAAG,KAAK,EAAE,KAAK;AACtC,UAAM,MAAM,KAAK,MAAM,QAAQ,CAAC,EAAE,KAAK;AACvC,QAAI,CAAC,IAAK;AACV,QAAI,CAAC,MAAM,IAAI,GAAG,EAAG,OAAM,KAAK,GAAG;AACnC,UAAM,IAAI,KAAK,GAAG;AAAA,EACpB;AACA,SAAO,EAAE,OAAO,MAAM;AACxB;AAEA,SAAS,oBAAoB,OAA4B,OAAyB;AAChF,SAAO,MACJ,IAAI,CAAC,MAAM,GAAG,CAAC,KAAK,MAAM,IAAI,CAAC,KAAK,EAAE,EAAE,EACxC,OAAO,CAAC,MAAM,EAAE,KAAK,CAAC,EACtB,KAAK,IAAI;AACd;AAEO,SAAS,qBACd,OACA,UACA,OACQ;AACR,QAAM,EAAE,OAAO,MAAM,IAAI,gBAAgB,KAAK;AAC9C,MAAI,UAAU,MAAM;AAClB,UAAM,OAAO,QAAQ;AACrB,UAAM,MAAM,MAAM,QAAQ,QAAQ;AAClC,QAAI,OAAO,EAAG,OAAM,OAAO,KAAK,CAAC;AAAA,EACnC,OAAO;AACL,QAAI,CAAC,MAAM,IAAI,QAAQ,EAAG,OAAM,KAAK,QAAQ;AAC7C,UAAM,IAAI,UAAU,KAAK;AAAA,EAC3B;AACA,SAAO,oBAAoB,OAAO,KAAK;AACzC;;;ADhCO,SAAS,oBAAoB,KAGlC;AACA,QAAM,SAAS,YAAY,GAAG;AAC9B,QAAM,mBAAmB,wBAAwB,KAAK,MAAM;AAC5D,MAAI,kBAAkB;AACpB,WAAO,EAAE,UAAU,UAAU,MAAM,EAAE,UAAU,iBAAiB,MAAM;AAAA,EACxE;AACA,SAAO;AAAA,IACL,UAAU,UAAU,2CAA2C,MAAM,gBAAgB,EAAE;AAAA,IACvF,iBAAiB;AAAA,EACnB;AACF;AAEA,SAAS,uBAAuB,UAAoB,YAAoB,OAAqB;AAC3F,QAAM,UAAU,IAAI,UAAU;AAC9B,QAAM,YAAY,eAAe,CAAC,cAAc;AAC9C,cAAU,QAAQ,CAAC,SAAS;AAC1B,UAAI,KAAK,UAAU,WAAY,MAAK,QAAQ;AAAA,IAC9C,CAAC;AAAA,EACH,CAAC;AACD,aAAW,WAAW,SAAS,iBAAiB,OAAO,GAAG;AACxD,UAAM,MAAM,QAAQ,eAAe;AACnC,QAAI;AACJ,QAAI;AACF,aAAO,QAAQ,MAAM,GAAG;AAAA,IAC1B,QAAQ;AACN;AAAA,IACF;AACA,UAAM,SAAyB,CAAC;AAChC,SAAK,UAAU,CAAC,SAAS;AACvB,UAAI,CAAC,KAAK,SAAS,SAAS,OAAO,EAAG;AACtC,YAAM,cAAc,UAAU,YAAY,KAAK,QAAQ;AACvD,UAAI,gBAAgB,KAAK,SAAU;AACnC,YAAM,QAAQ,KAAK,MAAM,EAAE,UAAU,YAAY,CAAC;AAClD,aAAO,KAAK,KAAK;AAAA,IACnB,CAAC;AACD,QAAI,OAAO,SAAS,GAAG;AACrB,iBAAW,KAAK,OAAQ,MAAK,OAAO,CAAC;AACrC,cAAQ,cAAc,KAAK,SAAS;AAAA,IACtC;AAAA,EACF;AACF;AAEA,SAAS,8BAA8B,MAA0B,UAA6B;AAC5F,QAAM,UAAU,MAAM,KAAK,KAAK,iBAAiB,QAAQ,CAAC;AAC1D,MAAI,QAAQ,SAAS,EAAG,QAAO;AAQ/B,QAAM,YAAY,MAAM,KAAK,KAAK,iBAAiB,UAAU,CAAC;AAC9D,aAAW,QAAQ,WAAW;AAC5B,UAAM,QAAQ,8BAA8B,MAAM,QAAQ;AAC1D,QAAI,MAAM,SAAS,EAAG,QAAO;AAAA,EAC/B;AACA,SAAO,CAAC;AACV;AAMA,SAAS,mBAAmB,OAAuB;AACjD,SAAO,MAAM,QAAQ,OAAO,MAAM,EAAE,QAAQ,MAAM,KAAK;AACzD;AAEA,SAAS,WAAW,UAAoB,MAA8B;AACpE,MAAI;AACF,UAAM,UAAU;AAAA,MACd;AAAA,MACA,gBAAgB,mBAAmB,IAAI,CAAC;AAAA,IAC1C;AACA,QAAI,QAAQ,SAAS,GAAG;AAItB,cAAQ;AAAA,QACN,+BAA+B,IAAI,aAAa,QAAQ,MAAM;AAAA,MAChE;AAAA,IACF;AACA,WAAO,QAAQ,CAAC,KAAK;AAAA,EACvB,QAAQ;AAEN,WAAO;AAAA,EACT;AACF;AAEO,SAAS,kBACd,UACA,QACgB;AAChB,MAAI,OAAO,MAAM;AACf,UAAM,KAAK,WAAW,UAAU,OAAO,IAAI;AAC3C,QAAI,GAAI,QAAO;AAAA,EACjB;AAEA,MAAI,OAAO,IAAI;AACb,UAAM,OAAO,SAAS,eAAe,OAAO,EAAE;AAC9C,QAAI,KAAM,QAAO;AAAA,EACnB;AAEA,MAAI,CAAC,OAAO,SAAU,QAAO;AAC7B,MAAI;AACF,UAAM,UAAU,8BAA8B,UAAU,OAAO,QAAQ;AACvE,WAAO,QAAQ,OAAO,iBAAiB,CAAC,KAAK;AAAA,EAC/C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMO,SAAS,uBACd,QACA,SACQ;AACR,QAAM,EAAE,UAAU,gBAAgB,IAAI,oBAAoB,MAAM;AAChE,MAAI,UAAU;AACd,aAAW,UAAU,SAAS;AAC5B,UAAM,UAAU,kBAAkB,UAAU,MAAM;AAClD,QAAI,CAAC,QAAS;AACd,iCAA6B,UAAU,OAAO;AAC9C,cAAU;AAAA,EACZ;AACA,MAAI,CAAC,QAAS,QAAO;AACrB,SAAO,kBAAkB,SAAS,KAAK,aAAa,KAAK,SAAS,SAAS;AAC7E;AAEO,SAAS,sBAAsB,QAAgB,QAAsC;AAC1F,SAAO,uBAAuB,QAAQ,CAAC,MAAM,CAAC;AAChD;AAEO,SAAS,cAAc,IAA6B;AACzD,QAAM,SAAS,GAAG,eAAe,aAAa;AAC9C,SAAO,SAAS,cAAc,SAAS,GAAG,aAAa,KAAK,WAAW;AACzE;AAEO,SAAS,0BAA0B,UAAoB,OAAsB;AAClF,QAAM,gBAAgB,MAAM,aAAa,qBAAqB;AAC9D,MAAI,CAAC,cAAe;AACpB,QAAM,qBAAqB,IAAI;AAAA,IAC7B,8BAA8B,UAAU,uBAAuB,EAAE;AAAA,MAAI,CAAC,SACpE,KAAK,aAAa,qBAAqB;AAAA,IACzC;AAAA,EACF;AACA,QAAM,OAAO,GAAG,aAAa;AAC7B,MAAI,oBAAoB;AACxB,MAAI,SAAS;AACb,SAAO,mBAAmB,IAAI,iBAAiB,EAAG,qBAAoB,GAAG,IAAI,IAAI,QAAQ;AACzF,QAAM,aAAa,uBAAuB,iBAAiB;AAC7D;AAsBA,SAAS,uBAAuB,QAAqB,IAAwC;AAC3F,MAAI,GAAG,kBAAkB,OAAW,QAAO;AAC3C,MAAI;AACF,UAAM,QAAQ,OAAO,iBAAiB,GAAG,aAAa,EAAE,GAAG,cAAc,CAAC,KAAK;AAC/E,WAAO,SAAS,cAAc,KAAK,IAAI,QAAQ;AAAA,EACjD,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAkBA,SAAS,iBAAiB,QAAuB;AAC/C,QAAM,WAAW,oBAAI,IAAY;AACjC,QAAM,OAAO,OAAO,eAAe,QAAQ;AAC3C,6BAA2B,MAAM,CAAC,OAAO;AACvC,UAAM,KAAK,GAAG,aAAa,YAAY;AACvC,QAAI,GAAI,UAAS,IAAI,EAAE;AAAA,EACzB,CAAC;AACD,aAAW,MAAM,OAAO,iBAAiB,GAAG,GAAG;AAC7C,QAAI,GAAG,aAAa,YAAY,EAAG;AACnC,QAAI,cAAc,IAAI,GAAG,QAAQ,YAAY,CAAC,EAAG;AACjD,OAAG,aAAa,cAAc,SAAS,IAAI,QAAQ,CAAC;AAAA,EACtD;AACF;AAGO,SAAS,mBACd,QACA,QACA,YACA,UAC+D;AAC/D,QAAM,EAAE,UAAU,gBAAgB,IAAI,oBAAoB,MAAM;AAChE,QAAM,KAAK,kBAAkB,UAAU,MAAM;AAC7C,MAAI,CAAC,MAAM,CAAC,cAAc,EAAE,EAAG,QAAO,EAAE,MAAM,QAAQ,SAAS,MAAM;AACrE,QAAM,SAAS;AACf,QAAM,eAAe,kBAAkB,SAAS,KAAK,aAAa,KAAK,SAAS,SAAS;AAEzF,QAAM,WAAqC,CAAC;AAC5C,aAAW,MAAM,YAAY;AAC3B,UAAM,WAAW,uBAAuB,QAAQ,EAAE;AAClD,QAAI,CAAC,SAAU,QAAO,EAAE,MAAM,QAAQ,SAAS,MAAM;AACrD,aAAS,KAAK,EAAE,IAAI,QAAQ,SAAS,CAAC;AAAA,EACxC;AAEA,aAAW,EAAE,IAAI,QAAQ,SAAS,KAAK,UAAU;AAC/C,YAAQ,GAAG,MAAM;AAAA,MACf,KAAK;AAKH;AACE,gBAAM,MAAM,SAAS,aAAa,OAAO,KAAK;AAC9C,gBAAM,UAAU,qBAAqB,KAAK,GAAG,UAAU,GAAG,KAAK;AAC/D,mBAAS,aAAa,SAAS,OAAO;AAAA,QACxC;AACA;AAAA,MACF,KAAK;AACH;AACE,gBAAM,WAAW,GAAG,SAAS,WAAW,OAAO,IAAI,GAAG,WAAW,QAAQ,GAAG,QAAQ;AACpF,cAAI,GAAG,SAAS,MAAM;AACpB,qBAAS,aAAa,UAAU,GAAG,KAAK;AAAA,UAC1C,OAAO;AACL,qBAAS,gBAAgB,QAAQ;AAAA,UACnC;AAAA,QACF;AACA;AAAA,MACF,KAAK;AACH,YAAI,CAAC,uBAAuB,GAAG,QAAQ,EAAG;AAC1C,YAAI,GAAG,SAAS,MAAM;AACpB,cAAI,CAAC,qBAAqB,GAAG,UAAU,GAAG,KAAK,EAAG;AAClD,gBAAM,gBACJ,GAAG,aAAa,QAAQ,2BAA2B,UAAU,UAAU,GAAG,KAAK,IAAI;AACrF,cAAI,kBAAkB,KAAM,UAAS,aAAa,MAAM,aAAa;AACrE,mBAAS,aAAa,GAAG,UAAU,GAAG,KAAK;AAAA,QAC7C,OAAO;AACL,mBAAS,gBAAgB,GAAG,QAAQ;AAAA,QACtC;AACA;AAAA,MACF,KAAK;AACH,YAAI,SAAS,aAAa,IAAI,KAAK,CAAC,GAAG,MAAO;AAC9C,iBAAS,aAAa,MAAM,aAAa,UAAU,GAAG,OAAO,QAAQ,CAAC;AACtE;AAAA,MACF,KAAK;AACH,YAAI,GAAG,SAAS,MAAM;AACpB,gBAAM,QAAQ,SAAS,SAAS,WAAW,IAAI,SAAS,oBAAoB;AAC5E,gBAAM,aAAa,SAAS,cAAc,KAAK,IAAI,QAAQ;AAC3D,qBAAW,cAAc,GAAG;AAAA,QAC9B;AACA;AAAA;AAAA;AAAA;AAAA;AAAA,MAKF,KAAK;AACH,YAAI,GAAG,SAAS,MAAM;AACpB,mBAAS,YAAY,GAAG;AACxB,mCAAyB,QAAQ;AACjC,2BAAiB,QAAQ;AAAA,QAC3B;AACA;AAAA,IACJ;AAAA,EACF;AAEA,QAAM,OAAO,kBAAkB,SAAS,KAAK,aAAa,KAAK,SAAS,SAAS;AACjF,QAAM,YAAY,OAAO,aAAa,IAAI;AAC1C,MAAI,SAAS,aAAc,QAAO,EAAE,MAAM,QAAQ,SAAS,MAAM,UAAU;AAC3E,SAAO,EAAE,MAAM,YAAY,IAAI,GAAG,SAAS,MAAM,UAAU;AAC7D;AAEO,SAAS,aACd,UACA,MACA,UACQ;AACR,MAAI,KAAK;AACT,MAAI,SAAS;AACb,SAAO,SAAS,eAAe,EAAE,KAAK,UAAU,IAAI,EAAE,EAAG,MAAK,GAAG,IAAI,IAAI,QAAQ;AACjF,SAAO;AACT;AAGO,SAAS,sBACd,QACA,SACW;AACX,QAAM,EAAE,SAAS,IAAI,oBAAoB,MAAM;AAC/C,SAAO,QAAQ,IAAI,CAAC,WAAW;AAC7B,QAAI,CAAC,OAAO,MAAM,CAAC,OAAO,QAAQ,CAAC,OAAO,SAAU,QAAO;AAC3D,UAAM,KAAK,kBAAkB,UAAU,MAAM;AAC7C,WAAO,MAAM,QAAQ,cAAc,EAAE;AAAA,EACvC,CAAC;AACH;AAEO,SAAS,qBAAqB,QAAgB,QAAuC;AAC1F,SAAO,sBAAsB,QAAQ,CAAC,MAAM,CAAC,EAAE,CAAC,KAAK;AACvD;AAQA,SAAS,qBAAqB,IAG5B;AACA,QAAM,SAAS,eAAe,EAAE;AAChC,SAAO,EAAE,OAAO,OAAO,SAAS,GAAG,UAAU,OAAO,YAAY,EAAE;AACpE;AAEA,SAAS,mBACP,IACA,OACA,UACA,YACM;AACN,kBAAgB,IAAI;AAAA,IAClB,OAAO,KAAK,MAAM,QAAQ,GAAI,IAAI;AAAA,IAClC,UAAU,KAAK,MAAM,WAAW,GAAI,IAAI;AAAA,IACxC,GAAI,cAAc,OAAO,EAAE,WAAW,IAAI,CAAC;AAAA,EAC7C,CAAC;AACH;AAGO,SAAS,mBACd,QACA,QACA,WACA,OACA,gBAYoB;AACpB,QAAM,EAAE,UAAU,gBAAgB,IAAI,oBAAoB,MAAM;AAChE,QAAM,KAAK,kBAAkB,UAAU,MAAM;AAC7C,MAAI,CAAC,MAAM,CAAC,cAAc,EAAE,EAAG,QAAO,EAAE,MAAM,QAAQ,SAAS,OAAO,OAAO,KAAK;AAElF,QAAM,SAAS,qBAAqB,EAAE;AACtC,MAAI,EAAE,OAAO,SAAS,IAAI;AAK1B,MAAI,YAAY,KAAK,kBAAkB,eAAe,WAAW,GAAG;AAClE,YAAQ,eAAe;AACvB,eAAW,eAAe;AAAA,EAC5B;AACA,MAAI,YAAY,KAAK,aAAa,SAAS,aAAa,QAAQ,UAAU;AACxE,WAAO,EAAE,MAAM,QAAQ,SAAS,OAAO,OAAO,KAAK;AAAA,EACrD;AAEA,UAAQ,aAAa,UAAU,KAAK;AAEpC,QAAM,gBAAgB,YAAY;AAClC,QAAM,iBAAiB,WAAW;AAElC,QAAM,QAAQ,GAAG,UAAU,IAAI;AAC/B,MAAI,CAAC,cAAc,KAAK,EAAG,QAAO,EAAE,MAAM,QAAQ,SAAS,OAAO,OAAO,KAAK;AAC9E,QAAM,aAAa,MAAM,KAAK;AAC9B,4BAA0B,UAAU,KAAK;AACzC,QAAM,gBAAgB,YAAY;AAGlC,aAAW,QAAQ,MAAM,iBAAiB,cAAc,EAAG,MAAK,gBAAgB,YAAY;AAC5F,qBAAmB,OAAO,WAAW,gBAAgB,gBAAgB,KAAK;AAQ1E,QAAM,oBAAoB,GAAG,aAAa,qBAAqB,IAC3D,wBACA,GAAG,aAAa,kBAAkB,IAChC,qBACA,gBAAgB,qBACd,wBACA,GAAG,QAAQ,cAAc,IACvB,qBACA;AACV,MAAI,mBAAmB;AACrB,UAAM,WAAW,CAAC,SAAiB,GAAG,aAAa,IAAI;AACvD,UAAM,eAAe,GAAG,aAAa,iBAAiB;AACtD,UAAM,cACJ,iBAAiB,OACb,uBAAuB,QAAQ,IAC9B,gBAAgB,iBAAiB;AACxC,UAAM,OAAO,iBAAiB,UAAU,gBAAgB,YAAY;AACpE,QAAI,iBAAiB,QAAQ,OAAO,YAAY,MAAM;AACpD,SAAG,aAAa,mBAAmB,OAAO,KAAK,MAAM,cAAc,GAAI,IAAI,GAAI,CAAC;AAClF,UAAM;AAAA,MACJ;AAAA,MACA,OAAO,KAAK,OAAO,cAAc,gBAAgB,QAAQ,GAAI,IAAI,GAAI;AAAA,IACvE;AAAA,EACF;AAGA,QAAM,aAAa,GAAG,aAAa,IAAI;AACvC,MAAI,YAAY;AACd,2BAAuB,UAAU,YAAY,KAAK;AAAA,EACpD;AAIA,qBAAmB,IAAI,OAAO,eAAe,gBAAgB,KAAK;AAGlE,MAAI,GAAG,aAAa;AAClB,OAAG,cAAe,aAAa,OAAO,GAAG,WAAW;AAAA,EACtD,OAAO;AACL,OAAG,cAAe,YAAY,KAAK;AAAA,EACrC;AAEA,QAAM,OAAO,kBAAkB,SAAS,KAAK,aAAa,KAAK,SAAS,SAAS;AACjF,SAAO;AAAA;AAAA;AAAA,IAGL,MAAM,YAAY,IAAI;AAAA,IACtB,SAAS;AAAA,IACT;AAAA,EACF;AACF;AAyCA,SAAS,iBAAiB,IAAa,UAA0B;AAC/D,QAAM,SAAS,cAAc,EAAE,IAAI,GAAG,aAAa,OAAO,IAAI,SAAS;AACvE,QAAM,EAAE,MAAM,IAAI,gBAAgB,KAAK;AACvC,QAAM,MAAM,MAAM,IAAI,QAAQ;AAC9B,MAAI,CAAC,IAAK,QAAO;AACjB,QAAM,IAAI,WAAW,GAAG;AACxB,SAAO,OAAO,SAAS,CAAC,IAAI,IAAI;AAClC;AAEA,SAAS,iBAAiB,IAAiB,MAAc,KAAmB;AAC1E,MAAI,QAAQ,GAAG,aAAa,OAAO,KAAK;AACxC,UAAQ,qBAAqB,OAAO,QAAQ,GAAG,IAAI,IAAI;AACvD,UAAQ,qBAAqB,OAAO,OAAO,GAAG,GAAG,IAAI;AACrD,KAAG,aAAa,SAAS,KAAK;AAChC;AAGA,SAAS,iBAAiB,UAAoB,SAAyB;AACrE,QAAM,OACJ,QACG,KAAK,EACL,YAAY,EACZ,QAAQ,eAAe,GAAG,EAE1B,QAAQ,UAAU,EAAE,KAAK;AAC9B,SAAO,aAAa,UAAU,IAAI;AACpC;AAGO,SAAS,mBACd,QACA,SACA,SACA,MACA,SACoB;AACpB,QAAM,EAAE,UAAU,gBAAgB,IAAI,oBAAoB,MAAM;AAChE,MAAI,QAAQ,WAAW,GAAG;AACxB,WAAO,EAAE,MAAM,QAAQ,SAAS,OAAO,SAAS,MAAM,OAAO,aAAa;AAAA,EAC5E;AAGA,QAAM,MAAqB,CAAC;AAC5B,QAAM,OAAO,oBAAI,IAAa;AAC9B,aAAW,UAAU,SAAS;AAC5B,UAAM,KAAK,kBAAkB,UAAU,MAAM;AAC7C,QAAI,CAAC,MAAM,CAAC,cAAc,EAAE,KAAK,KAAK,IAAI,EAAE,EAAG;AAC/C,SAAK,IAAI,EAAE;AACX,QAAI,KAAK,EAAE;AAAA,EACb;AACA,MAAI,IAAI,WAAW,GAAG;AACpB,WAAO,EAAE,MAAM,QAAQ,SAAS,OAAO,SAAS,MAAM,OAAO,qBAAqB;AAAA,EACpF;AAGA,QAAM,SAAS,IAAI,CAAC,GAAG;AACvB,MAAI,CAAC,UAAU,IAAI,KAAK,CAAC,OAAO,GAAG,kBAAkB,MAAM,GAAG;AAC5D,WAAO;AAAA,MACL,MAAM;AAAA,MACN,SAAS;AAAA,MACT,SAAS;AAAA,MACT,OAAO;AAAA,IACT;AAAA,EACF;AAGA,QAAM,YAAY,IAAI,IAAa,GAAG;AACtC,QAAM,UAAU,MAAM,KAAK,OAAO,QAAQ,EAAE,OAAO,CAAC,MAAwB,UAAU,IAAI,CAAC,CAAC;AAG5F,QAAM,aAAa,oBAAI,IAA4D;AACnF,aAAW,UAAU,SAAS;AAC5B,UAAM,KAAK,kBAAkB,UAAU,OAAO,MAAM;AACpD,QAAI,GAAI,YAAW,IAAI,IAAI,EAAE,MAAM,OAAO,MAAM,KAAK,OAAO,KAAK,OAAO,OAAO,MAAM,CAAC;AAAA,EACxF;AAEA,QAAM,UAAU,SAAS,cAAc,KAAK;AAC5C,UAAQ,aAAa,iBAAiB,OAAO;AAI7C,UAAQ,aAAa,MAAM,iBAAiB,UAAU,OAAO,CAAC;AAO9D,QAAM,iBAAiB,QACpB;AAAA,IAAI,CAAC,OACJ,OAAO;AAAA,MACL,gBAAgB,GAAG,aAAa,OAAO,KAAK,EAAE,EAAE,MAAM,IAAI,SAAS,KAAK;AAAA,MACxE;AAAA,IACF;AAAA,EACF,EACC,OAAO,CAAC,MAAM,OAAO,SAAS,CAAC,CAAC;AACnC,QAAM,OAAO,eAAe,SAAS,IAAI,KAAK,IAAI,GAAG,cAAc,IAAI;AACvE,UAAQ;AAAA,IACN;AAAA,IACA,6BAA6B,KAAK,IAAI,YAAY,KAAK,GAAG,cAAc,KAAK,KAAK,eAAe,KAAK,MAAM,QACzG,SAAS,OAAO,cAAc,IAAI,KAAK;AAAA,EAC5C;AAGA,SAAO,aAAa,SAAS,QAAQ,QAAQ,SAAS,CAAC,KAAK,IAAI;AAChE,aAAW,MAAM,SAAS;AACxB,UAAM,SAAS,WAAW,IAAI,EAAE;AAChC,QAAI,OAAQ,kBAAiB,IAAI,OAAO,MAAM,OAAO,GAAG;AACxD,QAAI,QAAQ,SAAS,KAAM,iBAAgB,IAAI,EAAE,YAAY,KAAK,MAAM,OAAO,KAAK,EAAE,CAAC;AACvF,YAAQ,YAAY,EAAE;AAAA,EACxB;AAEA,SAAO;AAAA,IACL,MAAM,kBAAkB,SAAS,KAAK,aAAa,KAAK,SAAS,SAAS;AAAA,IAC1E,SAAS;AAAA,IACT;AAAA,EACF;AACF;AAUA,SAAS,mBACP,UACA,OACA,aACsB;AACtB,QAAM,YAAY,oBAAI,IAAqB;AAC3C,aAAW,SAAS,aAAa;AAC/B,UAAM,KAAK,kBAAkB,UAAU,MAAM,MAAM;AACnD,QAAI,MAAM,MAAM,SAAS,EAAE,KAAK,MAAM,SAAS,KAAM,WAAU,IAAI,IAAI,MAAM,KAAK;AAAA,EACpF;AACA,SAAO;AACT;AAKA,SAAS,sBACP,OACA,QACA,OACA,MACA,WAC+C;AAC/C,QAAM,UAAyD,CAAC;AAChE,aAAW,SAAS,MAAM,KAAK,MAAM,QAAQ,GAAG;AAC9C,QAAI,cAAc,KAAK,GAAG;AACxB,YAAM,UAAU,iBAAiB,OAAO,MAAM,IAAI;AAClD,YAAM,SAAS,iBAAiB,OAAO,KAAK,IAAI;AAChD,uBAAiB,OAAO,SAAS,MAAM;AACvC,YAAM,QAAQ,UAAU,IAAI,KAAK;AACjC,UAAI,SAAS,KAAM,iBAAgB,OAAO,EAAE,YAAY,KAAK,MAAM,KAAK,EAAE,CAAC;AAC3E,UAAI,MAAM,IAAI;AACZ,gBAAQ,KAAK;AAAA,UACX,IAAI,MAAM;AAAA,UACV,IAAI,UAAU,iBAAiB,OAAO,OAAO,IAAI;AAAA,UACjD,IAAI,SAAS,iBAAiB,OAAO,QAAQ,IAAI;AAAA,QACnD,CAAC;AAAA,MACH;AAAA,IACF;AACA,WAAO,aAAa,OAAO,KAAK;AAAA,EAClC;AACA,SAAO;AACT;AAEO,SAAS,uBACd,QACA,aACA,cAAkC,CAAC,GACb;AACtB,QAAM,EAAE,UAAU,gBAAgB,IAAI,oBAAoB,MAAM;AAChE,QAAM,QAAQ,kBAAkB,UAAU,WAAW;AACrD,MAAI,CAAC,SAAS,CAAC,cAAc,KAAK,EAAG,QAAO,EAAE,MAAM,QAAQ,WAAW,MAAM;AAK7E,MAAI,CAAC,MAAM,aAAa,eAAe,EAAG,QAAO,EAAE,MAAM,QAAQ,WAAW,MAAM;AAElF,QAAM,SAAS,MAAM;AACrB,MAAI,CAAC,OAAQ,QAAO,EAAE,MAAM,QAAQ,WAAW,MAAM;AAErD,QAAM,YAAY,mBAAmB,UAAU,OAAO,WAAW;AAGjE,QAAM,QAAQ,iBAAiB,OAAO,MAAM;AAC5C,QAAM,OAAO,iBAAiB,OAAO,KAAK;AAC1C,QAAM,cAAc;AAAA,IAClB,IAAI,QAAQ,iBAAiB,OAAO,OAAO,IAAI;AAAA,IAC/C,IAAI,OAAO,iBAAiB,OAAO,QAAQ,IAAI;AAAA,EACjD;AAEA,QAAM,UAAU,sBAAsB,OAAO,QAAQ,OAAO,MAAM,SAAS;AAC3E,QAAM,UAAU,MAAM,MAAM;AAC5B,QAAM,OAAO;AAEb,SAAO;AAAA,IACL,MAAM,kBAAkB,SAAS,KAAK,aAAa,KAAK,SAAS,SAAS;AAAA,IAC1E,WAAW;AAAA,IACX,kBAAkB;AAAA,IAClB;AAAA,IACA;AAAA,EACF;AACF;AAGO,SAAS,wBAAwB,QAAgB,cAAyC;AAC/F,QAAM,EAAE,UAAU,gBAAgB,IAAI,oBAAoB,MAAM;AAChE,QAAM,iBAAiB,CAAC,OAAe;AACrC,UAAM,KAAK,SAAS,eAAe,EAAE;AACrC,WAAO,QAAQ,IAAI,aAAa,WAAW,KAAK,IAAI,aAAa,kBAAkB,CAAC;AAAA,EACtF;AACA,MAAI,CAAC,aAAa,KAAK,cAAc,GAAG;AACtC,WAAO;AAAA,EACT;AACA,oBAAkB,UAAU,YAAY;AACxC,SAAO,kBAAkB,SAAS,KAAK,aAAa,KAAK,SAAS,SAAS;AAC7E;","names":[]}
|
|
@@ -3,15 +3,15 @@ import {
|
|
|
3
3
|
} from "./chunk-DTKK7TKL.js";
|
|
4
4
|
|
|
5
5
|
// src/helpers/mediaCodecMap.ts
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { scanHtmlOpeningTags, decodeAuthoredAttribute } from "@hyperframes/parsers";
|
|
7
|
+
import { statSync } from "fs";
|
|
8
|
+
import { sep } from "path";
|
|
8
9
|
import { rewriteAssetPath } from "@hyperframes/parsers/asset-paths";
|
|
9
10
|
import {
|
|
10
11
|
cleanAssetUrl,
|
|
11
12
|
isRemoteOrInlineUrl,
|
|
12
13
|
isUnresolvedAssetPlaceholder,
|
|
13
|
-
|
|
14
|
-
resolveLocalAssetCandidates
|
|
14
|
+
resolveExistingLocalAsset as resolveAsset
|
|
15
15
|
} from "@hyperframes/parsers/asset-resolution";
|
|
16
16
|
|
|
17
17
|
// src/helpers/mediaMetadata.ts
|
|
@@ -272,24 +272,22 @@ async function probeAssetCodec(filePath, runner, cache = defaultProbeCache) {
|
|
|
272
272
|
rememberProbeResult(cache, filePath, { target, mtimeMs: stat.mtimeMs, size: stat.size, facts });
|
|
273
273
|
return facts;
|
|
274
274
|
}
|
|
275
|
-
var VIDEO_SRC_RE = /<video\b[^>]*\bsrc\s*=\s*["']([^"']+)["'][^>]*>/gi;
|
|
276
275
|
function resolveExistingLocalAsset(projectDir, url) {
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
return { resolvedPath, rootRelativePathname: `/${rootRelative}` };
|
|
276
|
+
const asset = resolveAsset(projectDir, url);
|
|
277
|
+
if (!asset) return null;
|
|
278
|
+
return {
|
|
279
|
+
resolvedPath: asset.resolved,
|
|
280
|
+
rootRelativePathname: `/${asset.rootRelativePath.split(sep).join("/")}`
|
|
281
|
+
};
|
|
284
282
|
}
|
|
285
283
|
function collectLocalVideoAssets(projectDir, htmlSources) {
|
|
286
284
|
const candidates = /* @__PURE__ */ new Map();
|
|
287
285
|
for (const { html, compSrcPath } of htmlSources) {
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
const rawSrc =
|
|
286
|
+
for (const tag of scanHtmlOpeningTags(html)) {
|
|
287
|
+
if (tag.name !== "video" || !tag.closed) continue;
|
|
288
|
+
const attribute = tag.attributes.find((attr) => attr.name === "src");
|
|
289
|
+
if (attribute?.kind !== "value") continue;
|
|
290
|
+
const rawSrc = decodeAuthoredAttribute(attribute.value);
|
|
293
291
|
if (isUnresolvedAssetPlaceholder(rawSrc)) continue;
|
|
294
292
|
const src = cleanAssetUrl(rawSrc);
|
|
295
293
|
if (!src || isRemoteOrInlineUrl(src)) continue;
|
|
@@ -346,4 +344,4 @@ export {
|
|
|
346
344
|
probeAssetCodec,
|
|
347
345
|
scanProjectMediaCodecMap
|
|
348
346
|
};
|
|
349
|
-
//# sourceMappingURL=chunk-
|
|
347
|
+
//# sourceMappingURL=chunk-RN33UALN.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/helpers/mediaCodecMap.ts","../src/helpers/mediaMetadata.ts"],"sourcesContent":["import { scanHtmlOpeningTags, decodeAuthoredAttribute } from \"@hyperframes/parsers\";\nimport { statSync } from \"node:fs\";\nimport { realpath } from \"./safePath.js\";\nimport { sep } from \"node:path\";\nimport { rewriteAssetPath } from \"@hyperframes/parsers/asset-paths\";\nimport {\n cleanAssetUrl,\n isRemoteOrInlineUrl,\n isUnresolvedAssetPlaceholder,\n resolveExistingLocalAsset as resolveAsset,\n} from \"@hyperframes/parsers/asset-resolution\";\nimport { pixelFormatHasAlpha, probeMediaMetadata, type FfprobeRunner } from \"./mediaMetadata.js\";\n\n/**\n * One reusable answer to \"what codec is this asset, and is it browser-hostile?\",\n * built on top of `mediaMetadata.ts`'s ffprobe-backed prober so studio-server\n * probes each asset once instead of running a second prober.\n */\n\nexport interface AssetCodecFacts {\n codecName: string;\n browserHostile: boolean;\n /** Coarse `canPlayType()` input; `null` when not applicable (safe codec) or\n * when no representative mime exists (ProRes: browsers never decode it, so\n * the runtime always proxies rather than probing `canPlayType`). */\n representativeMime: string | null;\n /** Source carries an alpha channel (ffprobe pix_fmt). Alpha sources use a\n * VP8/WebM proxy so their transparency is preserved across Chromium builds. */\n hasAlpha: boolean;\n}\n\n/** Server-root-relative URL pathname -> that asset's codec facts. */\nexport type MediaCodecMap = Record<string, AssetCodecFacts>;\n\nexport interface BrowserHostileCodec {\n /** Coarse `canPlayType()` input; `null` when no representative mime exists\n * (ProRes: browsers never decode it, so the runtime always proxies rather\n * than probing `canPlayType`). */\n representativeMime: string | null;\n /**\n * Whether the server may transcode before any browser asks. True only where\n * no cross-platform decode exists, so some client is sure to need the\n * substitute; false where the first `?hf-proxy=` request can do it lazily.\n */\n prewarm: boolean;\n}\n\n/**\n * Browser-hostile codec table v1. One exported constant so extending it is a\n * one-line change. `ffprobe` cannot emit exact RFC 6381 codec strings, so\n * these `representativeMime` values are deliberately coarse (a false\n * positive costs one proxy transcode, never correctness; a false negative is\n * rescued by the runtime's reactive zero-videoWidth swap).\n */\nexport const BROWSER_HOSTILE_CODECS: Record<string, BrowserHostileCodec> = {\n // HEVC decode is platform-bound, not absent: macOS Chrome answers\n // canPlayType with \"probably\" and keeps the source, while Chrome on\n // Windows/Linux and Firefox everywhere need the substitute.\n hevc: { representativeMime: 'video/mp4; codecs=\"hvc1.1.6.L120.B0\"', prewarm: true },\n prores: { representativeMime: null, prewarm: true },\n // Every mainstream engine decodes AV1 and VP9, so canPlayType keeps the\n // original and only the rare browser that cannot pays for a transcode.\n av1: { representativeMime: 'video/mp4; codecs=\"av01.0.08M.08\"', prewarm: false },\n vp9: { representativeMime: 'video/webm; codecs=\"vp09.00.10.08\"', prewarm: false },\n};\n\n/** `Object.hasOwn` rather than a bare index: a codec named `constructor` or\n * `toString` would otherwise resolve against `Object.prototype`. */\nfunction hostileCodecEntry(codecName: string): BrowserHostileCodec | undefined {\n return Object.hasOwn(BROWSER_HOSTILE_CODECS, codecName)\n ? BROWSER_HOSTILE_CODECS[codecName]\n : undefined;\n}\n\n/** The pre-warm gate: true only for codecs with no cross-platform browser\n * decode, so some client will ask. See `BrowserHostileCodec.prewarm`. */\nexport function shouldPrewarmProxy(facts: AssetCodecFacts): boolean {\n return hostileCodecEntry(facts.codecName)?.prewarm === true;\n}\n\n// Per process, and deliberately in one unit — a call to `resolveProxy` — so\n// the pair reads as a ratio. Summarised at exit on stderr, in the same\n// `[hyperframes:<area>] {json}` shape as `writeUrlDownloadTelemetry`.\nconst proxyDemand = { prewarmsRequested: 0, proxyRequests: 0 };\n\n/** Snapshot of this process's pre-warm demand counters. */\nexport function mediaProxyDemand(): { prewarmsRequested: number; proxyRequests: number } {\n return { ...proxyDemand };\n}\n\nfunction writeDemandLine(event: \"prewarm_requested\" | \"summary\"): void {\n try {\n process.stderr.write(\n `[hyperframes:media-proxy] ${JSON.stringify({ event, ...proxyDemand })}\\n`,\n );\n } catch {\n // Observability must never change proxy correctness.\n }\n}\n\n/** Mirrors `isGpuProbeDebugEnabled` in packages/engine/src/utils/gpuEncoder.ts. */\nfunction isMediaProxyDebugEnabled(): boolean {\n const value = process.env.HYPERFRAMES_DEBUG_MEDIA_PROXY;\n return value === \"1\" || value === \"true\";\n}\n\n// Registered on the first pre-warm rather than at import, so a process that\n// never pre-warms adds no handler and prints nothing. Sync-only, mirroring the\n// `process.on(\"exit\")` shutdown hooks in packages/cli/src/cli.ts:331 and\n// packages/cli/src/commands/preview.ts:1329.\nlet summaryHookInstalled = false;\n\n/**\n * One proxy asked for before any browser wanted it. \"Requested\", not\n * \"started\": a warm cache makes `resolveProxy` a no-op and this counter cannot\n * see that, so it is an upper bound on transcodes, not a measure of CPU. The\n * number it does answer exactly is the one that decides policy — a nonzero\n * count beside `proxyRequests: 0` means nothing ever redeemed the pre-warm.\n *\n * The per-asset line is debug-only: a composition with fifty hostile clips\n * would otherwise print fifty JSON lines into a clack-formatted terminal on\n * every re-render. The exit summary carries the same numbers unconditionally.\n */\nexport function recordProxyPrewarm(): void {\n proxyDemand.prewarmsRequested++;\n if (!summaryHookInstalled) {\n summaryHookInstalled = true;\n process.once(\"exit\", () => writeDemandLine(\"summary\"));\n }\n if (isMediaProxyDebugEnabled()) writeDemandLine(\"prewarm_requested\");\n}\n\n/** One proxy resolved for a browser that asked, counted on the path that calls\n * `resolveProxy` so it shares a unit with `prewarmsRequested`. A 304 does not\n * count; an unconditional Range refill still does, so read it as zero versus\n * nonzero. Never logged per event, only in the exit summary. */\nexport function recordProxyRequest(): void {\n proxyDemand.proxyRequests++;\n}\n\nexport type ProxyVariant = \"h264\" | \"vp8\";\nexport type ProxyVariantRequest = ProxyVariant | \"auto\";\n\nexport const PROXY_VARIANT_CONFIG: Record<\n ProxyVariant,\n { extension: \".mp4\" | \".webm\"; contentType: \"video/mp4\" | \"video/webm\" }\n> = {\n h264: { extension: \".mp4\", contentType: \"video/mp4\" },\n vp8: { extension: \".webm\", contentType: \"video/webm\" },\n};\n\nexport function isProxyVariant(value: string): value is ProxyVariant {\n return Object.hasOwn(PROXY_VARIANT_CONFIG, value);\n}\n\nexport function isProxyVariantRequest(value: string): value is ProxyVariantRequest {\n return value === \"auto\" || isProxyVariant(value);\n}\n\nexport function proxyVariantFor(facts: AssetCodecFacts): ProxyVariant {\n return facts.hasAlpha ? \"vp8\" : \"h264\";\n}\n\nexport function resolveProxyVariantRequest(\n request: ProxyVariantRequest,\n facts: AssetCodecFacts,\n): ProxyVariant | null {\n const expected = proxyVariantFor(facts);\n return request === \"auto\" || request === expected ? expected : null;\n}\n\nexport type MediaProxyIneligibilityReason = \"browser_safe_codec\" | \"unknown_codec\";\n\nexport type MediaProxyEligibility =\n | { eligible: true }\n | { eligible: false; reason: MediaProxyIneligibilityReason };\n\n/** Single policy gate shared by proactive scans and on-demand proxy routes. */\nexport function decideMediaProxyEligibility(facts: AssetCodecFacts | null): MediaProxyEligibility {\n if (!facts) return { eligible: false, reason: \"unknown_codec\" };\n if (!facts.browserHostile) return { eligible: false, reason: \"browser_safe_codec\" };\n return { eligible: true };\n}\n\nfunction codecFactsFor(codecName: string, hasAlpha: boolean): AssetCodecFacts {\n const hostile = hostileCodecEntry(codecName);\n return {\n codecName,\n browserHostile: hostile !== undefined,\n representativeMime: hostile?.representativeMime ?? null,\n hasAlpha,\n };\n}\n\n/** `undefined` when ffprobe itself failed: no answer about the file, so nothing to cache. */\nasync function probeCodecFacts(\n filePath: string,\n runner?: FfprobeRunner,\n): Promise<AssetCodecFacts | null | undefined> {\n const metadata = runner\n ? await probeMediaMetadata(filePath, runner)\n : await probeMediaMetadata(filePath);\n if (metadata.probeError) return undefined;\n if (metadata.kind !== \"video\") return null;\n const codecName = metadata.color.codecName;\n if (!codecName) return null;\n return codecFactsFor(codecName, pixelFormatHasAlpha(metadata.color.pixelFormat));\n}\n\ninterface CachedAssetProbe {\n /** The file a symlinked path pointed at, so retargeting the link is a miss. */\n target: string;\n mtimeMs: number;\n size: number;\n facts: AssetCodecFacts | null;\n}\n\n/** Per (path, mtime) probe cache. Construct one per project/server lifetime\n * and reuse it across scans; a fresh instance defeats the caching benefit. */\nexport type MediaCodecProbeCache = Map<string, CachedAssetProbe>;\n\nexport function createMediaCodecProbeCache(): MediaCodecProbeCache {\n return new Map();\n}\n\n// Shared by every route and scan in this process that does not pass its own cache.\nconst defaultProbeCache: MediaCodecProbeCache = new Map();\nconst MAX_PROBE_CACHE_ENTRIES = 512;\n\nfunction rememberProbeResult(\n cache: MediaCodecProbeCache,\n filePath: string,\n result: CachedAssetProbe,\n): void {\n if (!cache.has(filePath) && cache.size >= MAX_PROBE_CACHE_ENTRIES) {\n const oldest = cache.keys().next().value;\n if (oldest) cache.delete(oldest);\n }\n // Refresh insertion order so frequently used assets remain resident.\n cache.delete(filePath);\n cache.set(filePath, result);\n}\n\n/**\n * Codec facts for one video, cached per path until its target, mtime or size changes; a hit skips\n * `runner`. Never throws: no video stream or a failed probe gives `null`, and a failed probe is not cached.\n */\nexport async function probeAssetCodec(\n filePath: string,\n runner?: FfprobeRunner,\n cache: MediaCodecProbeCache = defaultProbeCache,\n): Promise<AssetCodecFacts | null> {\n let stat: ReturnType<typeof statSync>;\n let target: string;\n try {\n stat = statSync(filePath);\n target = realpath(filePath);\n } catch {\n return null;\n }\n const cached = cache.get(filePath);\n if (\n cached &&\n cached.target === target &&\n cached.mtimeMs === stat.mtimeMs &&\n cached.size === stat.size\n ) {\n rememberProbeResult(cache, filePath, cached);\n return cached.facts;\n }\n const facts = await probeCodecFacts(filePath, runner);\n if (facts === undefined) return null;\n rememberProbeResult(cache, filePath, { target, mtimeMs: stat.mtimeMs, size: stat.size, facts });\n return facts;\n}\n\n/** Structurally compatible with `packages/lint/src/hevcPreviewLint.ts`'s\n * (unexported) `HtmlSourceLike`. */\nexport interface HtmlSourceLike {\n html: string;\n compSrcPath?: string;\n}\n\n/**\n * Resolve a `<video src>` reference to an existing local file.\n *\n * `rootRelativePathname` is the map key format PINNED by this plan's Key\n * Technical Decisions: project-root-relative URL pathname, percent-decoded,\n * query-string-stripped, forward-slash separated, leading-slash prefixed\n * (e.g. \"/assets/videos/clip.mp4\"). This must match what the runtime derives\n * via `new URL(el.currentSrc || el.src, document.baseURI).pathname`, because\n * server-side scanning resolves filesystem paths while the DOM sees served\n * URLs — a documented prior source of this exact class of bug.\n */\nfunction resolveExistingLocalAsset(\n projectDir: string,\n url: string,\n): { resolvedPath: string; rootRelativePathname: string } | null {\n const asset = resolveAsset(projectDir, url);\n if (!asset) return null;\n return {\n resolvedPath: asset.resolved,\n rootRelativePathname: `/${asset.rootRelativePath.split(sep).join(\"/\")}`,\n };\n}\n\n/**\n * Collects local `<video src>` references, resolved to their absolute path\n * and deduped by that path, keyed by the pinned root-relative URL pathname.\n */\n// fallow-ignore-next-line complexity\nfunction collectLocalVideoAssets(\n projectDir: string,\n htmlSources: HtmlSourceLike[],\n): Map<string, string> {\n const candidates = new Map<string, string>();\n\n for (const { html, compSrcPath } of htmlSources) {\n for (const tag of scanHtmlOpeningTags(html)) {\n if (tag.name !== \"video\" || !tag.closed) continue;\n const attribute = tag.attributes.find((attr) => attr.name === \"src\");\n if (attribute?.kind !== \"value\") continue;\n const rawSrc = decodeAuthoredAttribute(attribute.value);\n // Placeholder check runs on the RAW value: cleanAssetUrl() splits on ?/# and would chop inside a ${...} token.\n if (isUnresolvedAssetPlaceholder(rawSrc)) continue;\n const src = cleanAssetUrl(rawSrc);\n if (!src || isRemoteOrInlineUrl(src)) continue;\n const rootRelativeSrc = compSrcPath ? rewriteAssetPath(compSrcPath, src) : src;\n const resolved = resolveExistingLocalAsset(projectDir, rootRelativeSrc);\n if (!resolved) continue;\n candidates.set(resolved.resolvedPath, resolved.rootRelativePathname);\n }\n }\n\n return candidates;\n}\n\n// Bounds concurrent ffprobe child processes for projects referencing many\n// videos, mirroring `PROBE_CONCURRENCY` in `hevcPreviewLint.ts`.\nconst PROBE_CONCURRENCY = 8;\n\nexport interface ScanProjectMediaCodecMapOptions {\n /** Persisted across calls by the caller for the mtime-cache benefit;\n * defaults to a shared module-level cache when omitted. */\n cache?: MediaCodecProbeCache;\n runner?: FfprobeRunner;\n}\n\n/**\n * Scans a project's composition HTML for local `<video src>` references and\n * returns the injection map: root-relative URL pathname -> codec facts.\n * Best-effort throughout — a video whose codec can't be determined (missing\n * ffprobe, probe error, no video stream) is simply omitted, never thrown.\n */\nexport async function scanProjectMediaCodecMap(\n projectDir: string,\n htmlSources: HtmlSourceLike[],\n options: ScanProjectMediaCodecMapOptions = {},\n): Promise<MediaCodecMap> {\n const candidates = collectLocalVideoAssets(projectDir, htmlSources);\n if (candidates.size === 0) return {};\n\n const cache = options.cache ?? defaultProbeCache;\n const entries = [...candidates.entries()]; // [resolvedPath, rootRelativePathname]\n const facts = new Array<AssetCodecFacts | null>(entries.length).fill(null);\n let nextIndex = 0;\n const workerCount = Math.min(PROBE_CONCURRENCY, entries.length);\n await Promise.all(\n Array.from({ length: workerCount }, async () => {\n while (nextIndex < entries.length) {\n const index = nextIndex++;\n const entry = entries[index];\n if (!entry) break;\n facts[index] = await probeAssetCodec(entry[0], options.runner, cache);\n }\n }),\n );\n\n const map: MediaCodecMap = {};\n entries.forEach(([, pathname], index) => {\n const entryFacts = facts[index];\n if (entryFacts?.browserHostile) map[pathname] = entryFacts;\n });\n return map;\n}\n","import { execFile } from \"node:child_process\";\nimport { extname } from \"node:path\";\nimport {\n firstFrameColourArgs,\n parseFirstFrameColour,\n type ToneMapSourceColour,\n} from \"@hyperframes/core\";\nimport { findFfBinary } from \"@hyperframes/parsers/ff-binaries\";\n\nexport interface FfprobeRunResult {\n status: number | null;\n stdout: string | Buffer;\n stderr: string | Buffer;\n /** Spawn-level failure (covers both `NodeJS.ErrnoException` and\n * `ExecFileException`); only `code === \"ENOENT\"` is ever inspected. */\n error?: { code?: string | number | null | undefined };\n}\n\n/** Injectable ffprobe runner. May be synchronous (tests) or async (the\n * default `execFile`-based runner below), so cold scans can run many probes\n * concurrently off the event loop. */\nexport type FfprobeRunner = (\n command: string,\n args: string[],\n options?: { timeout?: number; maxBuffer?: number },\n) => FfprobeRunResult | Promise<FfprobeRunResult>;\n\n/** Default runner: genuinely async (`execFile`), unlike the previous\n * `spawnSync`-based one — a pool of concurrent probes actually parallelizes\n * (mirrors `execFileAsync` in packages/lint/src/hevcPreviewLint.ts). */\nconst execFileRunner: FfprobeRunner = (command, args, options) =>\n new Promise<FfprobeRunResult>((resolvePromise) => {\n execFile(\n command,\n args,\n { timeout: options?.timeout, maxBuffer: options?.maxBuffer, windowsHide: true },\n (error, stdout, stderr) => {\n if (error && error.code === \"ENOENT\") {\n resolvePromise({ status: null, stdout: \"\", stderr: \"\", error });\n return;\n }\n if (error) {\n // Nonzero exit / timeout / kill: report a nonzero status; callers\n // only distinguish \"ok\" (0) from \"failed\" from \"ENOENT\".\n const status = typeof error.code === \"number\" ? error.code : 1;\n resolvePromise({ status, stdout: stdout ?? \"\", stderr: stderr ?? \"\" });\n return;\n }\n resolvePromise({ status: 0, stdout: stdout ?? \"\", stderr: stderr ?? \"\" });\n },\n );\n });\n\nexport type MediaDynamicRange = \"hdr\" | \"sdr\" | \"unknown\";\nexport type MediaHdrTransfer = \"pq\" | \"hlg\" | \"unknown\";\n\nexport interface MediaColorMetadata {\n dynamicRange: MediaDynamicRange;\n hdrTransfer: MediaHdrTransfer | null;\n label: string;\n isHdr: boolean;\n codecName?: string;\n profile?: string;\n pixelFormat?: string;\n colorSpace?: string;\n colorTransfer?: string;\n colorPrimaries?: string;\n bitsPerRawSample?: string;\n}\n\nexport interface MediaMetadata {\n kind: \"video\" | \"image\" | \"audio\" | \"unknown\";\n color: MediaColorMetadata;\n /** Video audio stream, when probed. Absent means the drop path stays muted. */\n hasAudio?: boolean;\n probeError?: string;\n}\n\ninterface FfprobeStream {\n codec_type?: string;\n codec_name?: string;\n profile?: string;\n pix_fmt?: string;\n color_space?: string;\n color_transfer?: string;\n color_primaries?: string;\n bits_per_raw_sample?: string;\n disposition?: { attached_pic?: number };\n}\n\nconst VIDEO_EXT = new Set([\n \".mp4\",\n \".mov\",\n \".webm\",\n \".mkv\",\n \".avi\",\n \".m4v\",\n \".mxf\",\n \".mts\",\n \".m2ts\",\n \".ts\",\n]);\nconst IMAGE_EXT = new Set([\".jpg\", \".jpeg\", \".png\", \".webp\", \".avif\"]);\nconst AUDIO_EXT = new Set([\".mp3\", \".wav\", \".ogg\", \".m4a\", \".aac\", \".flac\"]);\n\nfunction lower(value: string | undefined): string {\n return value?.toLowerCase() ?? \"\";\n}\n\nfunction inferKindFromPath(path: string): MediaMetadata[\"kind\"] {\n const ext = extname(path).toLowerCase();\n if (VIDEO_EXT.has(ext)) return \"video\";\n if (IMAGE_EXT.has(ext)) return \"image\";\n if (AUDIO_EXT.has(ext)) return \"audio\";\n return \"unknown\";\n}\n\nfunction colorLabel(input: {\n isHdr: boolean;\n hdrTransfer: MediaHdrTransfer | null;\n colorPrimaries: string;\n colorSpace: string;\n colorTransfer: string;\n}): string {\n if (input.isHdr) {\n if (input.hdrTransfer === \"pq\") return \"HDR PQ\";\n if (input.hdrTransfer === \"hlg\") return \"HDR HLG\";\n return \"HDR\";\n }\n if (\n input.colorPrimaries.includes(\"bt709\") ||\n input.colorSpace.includes(\"bt709\") ||\n input.colorTransfer.includes(\"bt709\")\n ) {\n return \"SDR Rec.709\";\n }\n return \"SDR/unknown\";\n}\n\n// Conservative alpha-bearing pix_fmt list: yuva* (yuva420p, yuva444p10le...),\n// rgba/argb/bgra/abgr (packed RGB+alpha), gbrap* (planar GBR+alpha, ProRes\n// 4444 decodes to these), ya* (gray+alpha). Prefix match keeps bit-depth /\n// endianness suffixes covered.\nconst ALPHA_PIX_FMT_RE = /^(?:yuva|rgba|argb|bgra|abgr|gbrap|ya)/;\n\n/** True when an ffprobe `pix_fmt` carries an alpha component. */\nexport function pixelFormatHasAlpha(pixFmt: string | undefined): boolean {\n return pixFmt !== undefined && ALPHA_PIX_FMT_RE.test(pixFmt.toLowerCase());\n}\n\nexport function classifyMediaColor(stream: FfprobeStream | null | undefined): MediaColorMetadata {\n const colorPrimaries = lower(stream?.color_primaries);\n const colorSpace = lower(stream?.color_space);\n const colorTransfer = lower(stream?.color_transfer);\n const isHdr =\n colorPrimaries.includes(\"bt2020\") ||\n colorSpace.includes(\"bt2020\") ||\n colorTransfer === \"smpte2084\" ||\n colorTransfer === \"arib-std-b67\";\n const hdrTransfer: MediaHdrTransfer | null = isHdr\n ? colorTransfer === \"smpte2084\"\n ? \"pq\"\n : colorTransfer === \"arib-std-b67\"\n ? \"hlg\"\n : \"unknown\"\n : null;\n\n return {\n dynamicRange: stream ? (isHdr ? \"hdr\" : \"sdr\") : \"unknown\",\n hdrTransfer,\n label: stream\n ? colorLabel({ isHdr, hdrTransfer, colorPrimaries, colorSpace, colorTransfer })\n : \"Unknown\",\n isHdr,\n codecName: stream?.codec_name,\n profile: stream?.profile,\n pixelFormat: stream?.pix_fmt,\n colorSpace: stream?.color_space,\n colorTransfer: stream?.color_transfer,\n colorPrimaries: stream?.color_primaries,\n bitsPerRawSample: stream?.bits_per_raw_sample,\n };\n}\n\n/** The first shown frame's colour tags, which zscale reads per frame. Empty when unread. */\nexport async function probeFirstFrameColour(\n filePath: string,\n runner: FfprobeRunner = execFileRunner,\n): Promise<ToneMapSourceColour> {\n const ffmpegPath =\n findFfBinary(\"ffmpeg\", { configuredMustExist: true }) ??\n (runner === execFileRunner ? undefined : \"ffmpeg\");\n if (!ffmpegPath) return {};\n const result = await runner(ffmpegPath, firstFrameColourArgs(filePath), {\n timeout: 15_000,\n maxBuffer: 1024 * 1024,\n });\n return result.status === 0 ? parseFirstFrameColour(String(result.stderr)) : {};\n}\n\nexport async function probeMediaMetadata(\n filePath: string,\n runner: FfprobeRunner = execFileRunner,\n): Promise<MediaMetadata> {\n const kind = inferKindFromPath(filePath);\n if (kind === \"audio\" || kind === \"unknown\") {\n return { kind, color: classifyMediaColor(null) };\n }\n\n // The default runner degrades a missing ffprobe to \"unavailable\" without\n // spawning; injected runners own execution and receive the normal command.\n const ffprobePath =\n findFfBinary(\"ffprobe\", { configuredMustExist: true }) ??\n (runner === execFileRunner ? undefined : \"ffprobe\");\n if (!ffprobePath) {\n return { kind, color: classifyMediaColor(null), probeError: \"ffprobe unavailable\" };\n }\n\n const result = await runner(\n ffprobePath,\n [\n \"-v\",\n \"error\",\n \"-show_entries\",\n \"stream=codec_type,codec_name,profile,pix_fmt,color_space,color_transfer,color_primaries,bits_per_raw_sample:stream_disposition=attached_pic\",\n \"-of\",\n \"json\",\n \"--\",\n filePath,\n ],\n { timeout: 15_000, maxBuffer: 1024 * 1024 },\n );\n\n if (result.error?.code === \"ENOENT\") {\n return { kind, color: classifyMediaColor(null), probeError: \"ffprobe unavailable\" };\n }\n if (result.status !== 0) {\n return { kind, color: classifyMediaColor(null), probeError: \"ffprobe failed\" };\n }\n\n try {\n const parsed = JSON.parse(String(result.stdout || \"{}\")) as { streams?: FfprobeStream[] };\n const stream = parsed.streams?.find((item) => {\n if (kind === \"image\") return item.codec_type === \"video\";\n return item.codec_type === kind && item.disposition?.attached_pic !== 1;\n });\n const metadata: MediaMetadata = { kind, color: classifyMediaColor(stream) };\n if (kind === \"video\") {\n metadata.hasAudio = (parsed.streams ?? []).some((item) => item.codec_type === \"audio\");\n }\n return metadata;\n } catch {\n return { kind, color: classifyMediaColor(null), probeError: \"ffprobe returned invalid json\" };\n }\n}\n"],"mappings":"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","names":[]}
|
package/dist/helpers/loudness.js
CHANGED
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
requiredFfmpeg,
|
|
12
12
|
resolveLocalAudioPath,
|
|
13
13
|
updateAudioVolume
|
|
14
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-32IH7WBY.js";
|
|
15
|
+
import "../chunk-DTKK7TKL.js";
|
|
16
|
+
import "../chunk-6XMC64FJ.js";
|
|
17
|
+
import "../chunk-4ETS2LXI.js";
|
|
15
18
|
export {
|
|
16
19
|
DEFAULT_TARGET_LUFS,
|
|
17
20
|
NORMALIZE_TRUE_PEAK_CEILING_DBFS,
|
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
resolveProxyVariantRequest,
|
|
14
14
|
scanProjectMediaCodecMap,
|
|
15
15
|
shouldPrewarmProxy
|
|
16
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-RN33UALN.js";
|
|
17
17
|
import "../chunk-DTKK7TKL.js";
|
|
18
18
|
import "../chunk-6XMC64FJ.js";
|
|
19
19
|
import "../chunk-4ETS2LXI.js";
|
|
@@ -4,9 +4,9 @@ import {
|
|
|
4
4
|
isAutoProxyEnabled,
|
|
5
5
|
proxyEtagSalt,
|
|
6
6
|
resolvePreviewMediaCodecProbeCache
|
|
7
|
-
} from "../chunk-
|
|
8
|
-
import "../chunk-
|
|
9
|
-
import "../chunk-
|
|
7
|
+
} from "../chunk-AYKLBPP2.js";
|
|
8
|
+
import "../chunk-6EFY2WPN.js";
|
|
9
|
+
import "../chunk-RN33UALN.js";
|
|
10
10
|
import "../chunk-DTKK7TKL.js";
|
|
11
11
|
import "../chunk-6XMC64FJ.js";
|
|
12
12
|
import "../chunk-4ETS2LXI.js";
|
|
@@ -14,8 +14,8 @@ import {
|
|
|
14
14
|
proxyActivityMark,
|
|
15
15
|
resolveProxy,
|
|
16
16
|
waitForProxy
|
|
17
|
-
} from "../chunk-
|
|
18
|
-
import "../chunk-
|
|
17
|
+
} from "../chunk-6EFY2WPN.js";
|
|
18
|
+
import "../chunk-RN33UALN.js";
|
|
19
19
|
import "../chunk-DTKK7TKL.js";
|
|
20
20
|
import "../chunk-6XMC64FJ.js";
|
|
21
21
|
import "../chunk-4ETS2LXI.js";
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ export { PatchOperation, dedupeClonedCompositionId, findTargetElement, parseSour
|
|
|
11
11
|
export { ScreenshotClip, clearElementScreenshotIsolation, getElementScreenshotClip } from './helpers/screenshotClip.js';
|
|
12
12
|
export { STUDIO_MANUAL_EDITS_PATH, StudioManualEditsRenderScriptOptions, createStudioManualEditsRenderBodyScript, createStudioPositionSeekReapplyScript } from './helpers/manualEditsRenderScript.js';
|
|
13
13
|
export { STUDIO_MOTION_PATH, StudioMotionRenderScriptOptions, createStudioMotionRenderBodyScript } from './helpers/studioMotionRenderScript.js';
|
|
14
|
+
export { decodeWellFormedEscapes } from '@hyperframes/parsers/asset-paths';
|
|
14
15
|
export { isSafePath } from '@hyperframes/core/safe-path';
|
|
15
16
|
import '@hyperframes/core';
|
|
16
17
|
import '@hyperframes/parsers';
|