@hyperframes/studio-server 0.8.200 → 0.8.202

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.
@@ -1,14 +1,14 @@
1
1
  import {
2
2
  PROXY_PARAMS_VERSION,
3
3
  resolveProxy
4
- } from "./chunk-URMIRXBS.js";
4
+ } from "./chunk-RX6VALW3.js";
5
5
  import {
6
6
  createMediaCodecProbeCache,
7
7
  proxyVariantFor,
8
8
  recordProxyPrewarm,
9
- scanProjectMediaCodecMap,
9
+ scanProjectMedia,
10
10
  shouldPrewarmProxy
11
- } from "./chunk-XMNHDWQI.js";
11
+ } from "./chunk-C2RFW2FX.js";
12
12
 
13
13
  // src/helpers/mediaProxyPreview.ts
14
14
  import { resolve } from "path";
@@ -25,17 +25,15 @@ function proxyEtagSalt(raw, box) {
25
25
  return `:proxy:${raw}:${PROXY_PARAMS_VERSION}${box ? `:${formatPreviewProxyBox(box)}` : ""}`;
26
26
  }
27
27
  async function injectMediaCodecMapIntoHtml(html, projectDir, htmlSources, probeCache, prewarm = true, signal, box) {
28
- let map;
28
+ let scan;
29
29
  try {
30
- map = await scanProjectMediaCodecMap(
31
- projectDir,
32
- htmlSources,
33
- probeCache ? { cache: probeCache } : {}
34
- );
30
+ scan = await scanProjectMedia(projectDir, htmlSources, probeCache ? { cache: probeCache } : {});
35
31
  } catch {
36
32
  return html;
37
33
  }
38
- if (Object.keys(map).length === 0) return html;
34
+ const { codecMap: map, sourceFacts } = scan;
35
+ const hasMap = Object.keys(map).length > 0;
36
+ if (!hasMap && Object.keys(sourceFacts).length === 0) return html;
39
37
  for (const [rootRelativePathname, facts] of Object.entries(map)) {
40
38
  if (!prewarm || !shouldPrewarmProxy(facts)) continue;
41
39
  recordProxyPrewarm();
@@ -48,8 +46,12 @@ async function injectMediaCodecMapIntoHtml(html, projectDir, htmlSources, probeC
48
46
  ).catch(() => {
49
47
  });
50
48
  }
51
- const json = JSON.stringify(map).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
52
- const tag = `<script data-hf-media-codec-map>window.__HF_MEDIA_CODEC_MAP__=${json};</script>`;
49
+ const safeJson = (value) => JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
50
+ const assignments = [
51
+ ...hasMap ? [`window.__HF_MEDIA_CODEC_MAP__=${safeJson(map)};`] : [],
52
+ `window.__HF_MEDIA_SOURCE_FACTS__=${safeJson(sourceFacts)};`
53
+ ];
54
+ const tag = `<script data-hf-media-codec-map>${assignments.join("")}</script>`;
53
55
  return insertBeforeCloseTag(html, "head", `${tag}
54
56
  `) ?? `${tag}
55
57
  ${html}`;
@@ -13,6 +13,9 @@ import {
13
13
  isUnresolvedAssetPlaceholder,
14
14
  resolveExistingLocalAsset as resolveAsset
15
15
  } from "@hyperframes/parsers/asset-resolution";
16
+ import {
17
+ isHeavyForPreview
18
+ } from "@hyperframes/core";
16
19
 
17
20
  // src/helpers/mediaMetadata.ts
18
21
  import { execFile } from "child_process";
@@ -106,6 +109,22 @@ async function probeFirstFrameColour(filePath, runner = execFileRunner) {
106
109
  });
107
110
  return result.status === 0 ? parseFirstFrameColour(String(result.stderr)) : {};
108
111
  }
112
+ function bitDepthOf(stream) {
113
+ const raw = Number(stream.bits_per_raw_sample);
114
+ if (Number.isFinite(raw) && raw > 0) return raw;
115
+ const suffixed = /(\d{1,3})(?:le|be)$/.exec(stream.pix_fmt ?? "");
116
+ return suffixed ? Number(suffixed[1]) : 8;
117
+ }
118
+ function sourceFactsOf(stream, formatBitrate) {
119
+ if (!stream?.width || !stream.height) return void 0;
120
+ const bitrate = Number(stream.bit_rate ?? formatBitrate);
121
+ return {
122
+ width: stream.width,
123
+ height: stream.height,
124
+ bitDepth: bitDepthOf(stream),
125
+ bitrate: Number.isFinite(bitrate) ? bitrate : 0
126
+ };
127
+ }
109
128
  async function probeMediaMetadata(filePath, runner = execFileRunner) {
110
129
  const kind = inferKindFromPath(filePath);
111
130
  if (kind === "audio" || kind === "unknown") {
@@ -121,7 +140,7 @@ async function probeMediaMetadata(filePath, runner = execFileRunner) {
121
140
  "-v",
122
141
  "error",
123
142
  "-show_entries",
124
- "stream=codec_type,codec_name,profile,pix_fmt,color_space,color_transfer,color_primaries,bits_per_raw_sample:stream_disposition=attached_pic:stream_tags=alpha_mode:format=duration",
143
+ "stream=codec_type,codec_name,profile,pix_fmt,color_space,color_transfer,color_primaries,bits_per_raw_sample,width,height,bit_rate:stream_disposition=attached_pic:stream_tags=alpha_mode:format=duration,bit_rate",
125
144
  "-of",
126
145
  "json",
127
146
  "--",
@@ -147,6 +166,7 @@ async function probeMediaMetadata(filePath, runner = execFileRunner) {
147
166
  metadata.hasAlpha = stream !== void 0 && videoStreamHasAlpha(stream);
148
167
  const duration = Number(parsed.format?.duration);
149
168
  if (Number.isFinite(duration) && duration > 0) metadata.durationSeconds = duration;
169
+ metadata.source = sourceFactsOf(stream, parsed.format?.bit_rate);
150
170
  }
151
171
  return metadata;
152
172
  } catch {
@@ -219,18 +239,22 @@ function resolveProxyVariantRequest(request, facts) {
219
239
  const expected = proxyVariantFor(facts);
220
240
  return request === "auto" || request === expected ? expected : null;
221
241
  }
222
- function decideMediaProxyEligibility(facts) {
242
+ function decideMediaProxyEligibility(facts, lightBox) {
223
243
  if (!facts) return { eligible: false, reason: "unknown_codec" };
224
- if (!facts.browserHostile) return { eligible: false, reason: "browser_safe_codec" };
225
- return { eligible: true };
244
+ if (facts.browserHostile) return { eligible: true };
245
+ if (lightBox && facts.source && isHeavyForPreview(facts.source, lightBox)) {
246
+ return { eligible: true };
247
+ }
248
+ return { eligible: false, reason: "browser_safe_codec" };
226
249
  }
227
- function codecFactsFor(codecName, hasAlpha) {
250
+ function codecFactsFor(codecName, hasAlpha, source) {
228
251
  const hostile = hostileCodecEntry(codecName);
229
252
  return {
230
253
  codecName,
231
254
  browserHostile: hostile !== void 0,
232
255
  representativeMime: hostile?.representativeMime ?? null,
233
- hasAlpha
256
+ hasAlpha,
257
+ ...source && { source }
234
258
  };
235
259
  }
236
260
  async function probeCodecFacts(filePath, runner) {
@@ -239,7 +263,7 @@ async function probeCodecFacts(filePath, runner) {
239
263
  if (metadata.kind !== "video") return null;
240
264
  const codecName = metadata.color.codecName;
241
265
  if (!codecName) return null;
242
- return codecFactsFor(codecName, metadata.hasAlpha === true);
266
+ return codecFactsFor(codecName, metadata.hasAlpha === true, metadata.source);
243
267
  }
244
268
  function createMediaCodecProbeCache() {
245
269
  return /* @__PURE__ */ new Map();
@@ -301,9 +325,8 @@ function collectLocalVideoAssets(projectDir, htmlSources) {
301
325
  return candidates;
302
326
  }
303
327
  var PROBE_CONCURRENCY = 8;
304
- async function scanProjectMediaCodecMap(projectDir, htmlSources, options = {}) {
328
+ async function probeProjectAssets(projectDir, htmlSources, options) {
305
329
  const candidates = collectLocalVideoAssets(projectDir, htmlSources);
306
- if (candidates.size === 0) return {};
307
330
  const cache = options.cache ?? defaultProbeCache;
308
331
  const entries = [...candidates.entries()];
309
332
  const facts = new Array(entries.length).fill(null);
@@ -319,12 +342,18 @@ async function scanProjectMediaCodecMap(projectDir, htmlSources, options = {}) {
319
342
  }
320
343
  })
321
344
  );
322
- const map = {};
323
- entries.forEach(([, pathname], index) => {
324
- const entryFacts = facts[index];
325
- if (entryFacts?.browserHostile) map[pathname] = entryFacts;
326
- });
327
- return map;
345
+ return entries.map(([, pathname], index) => [pathname, facts[index] ?? null]);
346
+ }
347
+ async function scanProjectMedia(projectDir, htmlSources, options = {}) {
348
+ const scan = { codecMap: {}, sourceFacts: {} };
349
+ for (const [pathname, facts] of await probeProjectAssets(projectDir, htmlSources, options)) {
350
+ if (facts?.browserHostile) scan.codecMap[pathname] = facts;
351
+ if (facts?.source) scan.sourceFacts[pathname] = facts.source;
352
+ }
353
+ return scan;
354
+ }
355
+ async function scanProjectMediaCodecMap(projectDir, htmlSources, options = {}) {
356
+ return (await scanProjectMedia(projectDir, htmlSources, options)).codecMap;
328
357
  }
329
358
 
330
359
  export {
@@ -343,5 +372,6 @@ export {
343
372
  decideMediaProxyEligibility,
344
373
  createMediaCodecProbeCache,
345
374
  probeAssetCodec,
375
+ scanProjectMedia,
346
376
  scanProjectMediaCodecMap
347
377
  };
@@ -106,6 +106,7 @@ import { isAbsolute, relative, resolve, sep } from "path";
106
106
  import { replaceFileAtomically } from "@hyperframes/core/atomic-file";
107
107
 
108
108
  // src/history/ownerLock.ts
109
+ import { spawnSync } from "child_process";
109
110
  import { randomUUID as randomUUID2 } from "crypto";
110
111
  import {
111
112
  linkSync,
@@ -116,6 +117,7 @@ import {
116
117
  statSync,
117
118
  writeFileSync
118
119
  } from "fs";
120
+ import { uptime } from "os";
119
121
  import { join } from "path";
120
122
  var HistoryBusyError = class extends Error {
121
123
  constructor(pid, url) {
@@ -130,6 +132,7 @@ var HistoryBusyError = class extends Error {
130
132
  var WINDOWS_BUSY = /* @__PURE__ */ new Set(["EPERM", "EBUSY", "EACCES"]);
131
133
  var NO_HARD_LINKS = /* @__PURE__ */ new Set(["ENOTSUP", "EOPNOTSUPP", "ENOSYS", "EPERM"]);
132
134
  var TAKING_MS = 2e3;
135
+ var BOOT_SLACK_MS = 6e4;
133
136
  function busyOnWindows(error) {
134
137
  return process.platform === "win32" && WINDOWS_BUSY.has(error.code ?? "");
135
138
  }
@@ -142,6 +145,48 @@ function alive(pid) {
142
145
  return error.code === "EPERM";
143
146
  }
144
147
  }
148
+ function processStart(pid) {
149
+ try {
150
+ if (process.platform === "linux") {
151
+ const stat4 = readFileSync(`/proc/${pid}/stat`, "utf-8");
152
+ return stat4.slice(stat4.lastIndexOf(")") + 2).split(" ")[19] ?? null;
153
+ }
154
+ if (process.platform === "win32") return null;
155
+ const ps = spawnSync("ps", ["-o", "lstart=", "-p", String(pid)], {
156
+ encoding: "utf-8",
157
+ env: { ...process.env, LC_ALL: "C" }
158
+ });
159
+ return ps.status === 0 ? ps.stdout.trim() || null : null;
160
+ } catch {
161
+ return null;
162
+ }
163
+ }
164
+ function recordedStart(file, pid) {
165
+ try {
166
+ const [owner, ...start] = readFileSync(`${file}.start`, "utf-8").split("\n");
167
+ return Number(owner) === pid && start.length > 0 ? start.join("\n") : null;
168
+ } catch {
169
+ return null;
170
+ }
171
+ }
172
+ function recordStart(file) {
173
+ const start = processStart(process.pid);
174
+ if (start) writeFileSync(`${file}.start`, `${process.pid}
175
+ ${start}`);
176
+ }
177
+ function holdsLock(file, pid) {
178
+ if (!alive(pid)) return false;
179
+ const recorded = recordedStart(file, pid);
180
+ if (recorded !== null) {
181
+ const now = processStart(pid);
182
+ return now === null || now === recorded;
183
+ }
184
+ try {
185
+ return statSync(file).mtimeMs > Date.now() - uptime() * 1e3 - BOOT_SLACK_MS;
186
+ } catch {
187
+ return true;
188
+ }
189
+ }
145
190
  function ownerOf(file) {
146
191
  try {
147
192
  const text = readFileSync(file, "utf-8");
@@ -160,13 +205,14 @@ function beingTaken(file) {
160
205
  }
161
206
  }
162
207
  function deadHolder(file, pid) {
163
- return !alive(pid) && !beingTaken(file);
208
+ return !holdsLock(file, pid) && !beingTaken(file);
164
209
  }
165
210
  function claim(file) {
166
211
  const draft = `${file}-${process.pid}-${randomUUID2()}.tmp`;
167
212
  writeFileSync(draft, String(process.pid));
168
213
  try {
169
214
  linkSync(draft, file);
215
+ recordStart(file);
170
216
  return true;
171
217
  } catch (error) {
172
218
  const code = error.code ?? "";
@@ -180,6 +226,7 @@ function claim(file) {
180
226
  function createExclusively(file) {
181
227
  try {
182
228
  writeFileSync(file, String(process.pid), { flag: "wx" });
229
+ recordStart(file);
183
230
  return true;
184
231
  } catch (error) {
185
232
  if (error.code !== "EEXIST") throw error;
@@ -187,7 +234,9 @@ function createExclusively(file) {
187
234
  }
188
235
  }
189
236
  function releaseOwn(file) {
190
- if (ownerOf(file) === process.pid) rmSync(file, { force: true });
237
+ if (ownerOf(file) !== process.pid) return;
238
+ rmSync(file, { force: true });
239
+ rmSync(`${file}.start`, { force: true });
191
240
  }
192
241
  function evictDeadOwner(file) {
193
242
  const evictor = `${file}.evict`;
@@ -198,7 +247,10 @@ function evictDeadOwner(file) {
198
247
  }
199
248
  try {
200
249
  const pid = ownerOf(file);
201
- if (pid !== null && deadHolder(file, pid)) rmSync(file, { force: true });
250
+ if (pid !== null && deadHolder(file, pid)) {
251
+ rmSync(file, { force: true });
252
+ rmSync(`${file}.start`, { force: true });
253
+ }
202
254
  return true;
203
255
  } finally {
204
256
  releaseOwn(evictor);
@@ -213,7 +265,7 @@ function advertise(urlFile, url) {
213
265
  function servedBy(file, urlFile, pid) {
214
266
  try {
215
267
  const said = JSON.parse(readFileSync(urlFile, "utf-8"));
216
- const live = said.pid === pid && alive(pid) && ownerOf(file) === pid;
268
+ const live = said.pid === pid && holdsLock(file, pid) && ownerOf(file) === pid;
217
269
  return live && typeof said.url === "string" ? said.url : void 0;
218
270
  } catch {
219
271
  return void 0;
@@ -2,7 +2,7 @@ import {
2
2
  PROXY_VARIANT_CONFIG,
3
3
  probeFirstFrameColour,
4
4
  probeMediaMetadata
5
- } from "./chunk-XMNHDWQI.js";
5
+ } from "./chunk-C2RFW2FX.js";
6
6
  import {
7
7
  mkdirWithinProject,
8
8
  realProjectRoot,
@@ -11,23 +11,19 @@ import {
11
11
 
12
12
  // src/helpers/proxyTranscoder.ts
13
13
  import { spawn } from "child_process";
14
- import { createHash, randomUUID } from "crypto";
14
+ import { randomUUID } from "crypto";
15
15
  import {
16
- existsSync as existsSync2,
16
+ existsSync as existsSync4,
17
17
  mkdtempSync,
18
18
  renameSync,
19
- rmSync,
20
- statSync as statSync2,
19
+ rmSync as rmSync2,
21
20
  unlinkSync as unlinkSync2,
22
21
  utimesSync,
23
- writeFileSync
22
+ writeFileSync as writeFileSync2
24
23
  } from "fs";
25
- import { availableParallelism, tmpdir } from "os";
26
- import { basename, dirname, extname as extname2, isAbsolute, join as join2, relative, resolve, sep } from "path";
27
- import {
28
- formatPreviewProxyBox,
29
- hdrToSdrToneMapFilter
30
- } from "@hyperframes/core";
24
+ import { availableParallelism, tmpdir, totalmem } from "os";
25
+ import { basename as basename3, dirname as dirname3, extname as extname3, isAbsolute, join as join4, relative, resolve, sep } from "path";
26
+ import { hdrToSdrToneMapFilter } from "@hyperframes/core";
31
27
  import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
32
28
  import { alphaDecoderFor } from "@hyperframes/parsers/media-alpha";
33
29
 
@@ -143,8 +139,138 @@ function cleanupProxyCache(cacheDir, options = {}) {
143
139
  };
144
140
  }
145
141
 
142
+ // src/helpers/proxyCacheKey.ts
143
+ import { createHash } from "crypto";
144
+ import {
145
+ closeSync,
146
+ createReadStream,
147
+ existsSync as existsSync2,
148
+ fstatSync,
149
+ openSync,
150
+ readFileSync,
151
+ statSync as statSync2,
152
+ writeFileSync
153
+ } from "fs";
154
+ import { basename, dirname, extname as extname2, join as join2 } from "path";
155
+ import { formatPreviewProxyBox } from "@hyperframes/core";
156
+ var SMALL_FILE_BYTES = 16 * 1024 * 1024;
157
+ var COPY_NAME = /^[0-9a-f]{64}\.(?:mp4|webm)$/;
158
+ function digest(...parts) {
159
+ return createHash("sha256").update(parts.join("\0")).digest("hex");
160
+ }
161
+ function boxPart(box) {
162
+ return box ? formatPreviewProxyBox(box) : "";
163
+ }
164
+ function fastCopyKey(sourcePath, version, variant, box) {
165
+ const stat = statSync2(sourcePath);
166
+ return digest(
167
+ sourcePath,
168
+ String(stat.mtimeMs),
169
+ String(stat.size),
170
+ version,
171
+ variant,
172
+ boxPart(box)
173
+ );
174
+ }
175
+ async function contentCopyKey(sourcePath, version, variant, box, signal) {
176
+ const hash = createHash("sha256");
177
+ const fd = openSync(sourcePath, "r");
178
+ try {
179
+ if (fstatSync(fd).size <= SMALL_FILE_BYTES) hash.update(readFileSync(fd));
180
+ else
181
+ for await (const chunk of createReadStream(sourcePath, { fd, autoClose: false, signal }))
182
+ hash.update(chunk);
183
+ } finally {
184
+ closeSync(fd);
185
+ }
186
+ return digest(hash.digest("hex"), version, variant, boxPart(box));
187
+ }
188
+ function fastIndexPath(cachePath) {
189
+ return join2(dirname(cachePath), `.fast-${basename(cachePath, extname2(cachePath))}`);
190
+ }
191
+ function contentIndexPath(cacheDir, contentKey) {
192
+ return join2(cacheDir, `.content-${contentKey}`);
193
+ }
194
+ function readCopyIndex(indexPath) {
195
+ try {
196
+ const name = readFileSync(indexPath, "utf8").trim();
197
+ const target = join2(dirname(indexPath), name);
198
+ return COPY_NAME.test(name) && existsSync2(target) ? target : null;
199
+ } catch {
200
+ return null;
201
+ }
202
+ }
203
+ function writeCopyIndex(indexPath, copyPath) {
204
+ writeFileSync(indexPath, basename(copyPath));
205
+ }
206
+
207
+ // src/helpers/proxyLock.ts
208
+ import {
209
+ closeSync as closeSync2,
210
+ existsSync as existsSync3,
211
+ openSync as openSync2,
212
+ readFileSync as readFileSync2,
213
+ rmSync,
214
+ statSync as statSync3,
215
+ writeSync
216
+ } from "fs";
217
+ import { basename as basename2, dirname as dirname2, join as join3 } from "path";
218
+ var POLL_MS = 500;
219
+ var UNREADABLE_LOCK_MS = 1e4;
220
+ function lockPathFor(cachePath) {
221
+ return join3(dirname2(cachePath), `.lock-${basename2(cachePath)}`);
222
+ }
223
+ function ownerIsAlive(lockPath) {
224
+ try {
225
+ const pid = Number(readFileSync2(lockPath, "utf8"));
226
+ if (!Number.isInteger(pid) || pid <= 0) {
227
+ return Date.now() - statSync3(lockPath).mtimeMs < UNREADABLE_LOCK_MS;
228
+ }
229
+ try {
230
+ process.kill(pid, 0);
231
+ return true;
232
+ } catch (err) {
233
+ return err.code === "EPERM";
234
+ }
235
+ } catch {
236
+ return false;
237
+ }
238
+ }
239
+ function pause(ms, signal) {
240
+ return new Promise((resolve2, reject) => {
241
+ const timer = setTimeout(() => {
242
+ signal.removeEventListener("abort", onAbort);
243
+ resolve2();
244
+ }, ms);
245
+ const onAbort = () => {
246
+ clearTimeout(timer);
247
+ reject(signal.reason);
248
+ };
249
+ signal.addEventListener("abort", onAbort, { once: true });
250
+ });
251
+ }
252
+ async function acquireCopyLock(cachePath, signal) {
253
+ const lockPath = lockPathFor(cachePath);
254
+ for (; ; ) {
255
+ signal.throwIfAborted();
256
+ if (existsSync3(cachePath)) return null;
257
+ try {
258
+ const fd = openSync2(lockPath, "wx");
259
+ writeSync(fd, String(process.pid));
260
+ closeSync2(fd);
261
+ if (!existsSync3(cachePath)) return () => rmSync(lockPath, { force: true });
262
+ rmSync(lockPath, { force: true });
263
+ return null;
264
+ } catch (err) {
265
+ if (err.code !== "EEXIST") throw err;
266
+ }
267
+ if (ownerIsAlive(lockPath)) await pause(POLL_MS, signal);
268
+ else rmSync(lockPath, { force: true });
269
+ }
270
+ }
271
+
146
272
  // src/helpers/proxyTranscoder.ts
147
- var PROXY_PARAMS_VERSION = "v6";
273
+ var PROXY_PARAMS_VERSION = "v7";
148
274
  var CACHE_DIR_NAME = ".transcode-cache";
149
275
  function boundedEnvInteger(name, fallback, min, max) {
150
276
  const raw = process.env[name]?.trim();
@@ -235,33 +361,16 @@ function canonicalizeProxySource(projectDir, absoluteSourcePath) {
235
361
  if (requestedRelativePath === ".." || requestedRelativePath.startsWith(`..${sep}`) || isAbsolute(requestedRelativePath)) {
236
362
  throw new ProxySourceOutsideProjectError();
237
363
  }
238
- const canonicalProjectDir = realProjectRoot(projectDir);
239
- const canonicalSourcePath = realpath(absoluteSourcePath);
240
- const canonicalRelativePath = relative(canonicalProjectDir, canonicalSourcePath);
241
- const sourceIsInsideCanonicalProject = canonicalRelativePath !== ".." && !canonicalRelativePath.startsWith(`..${sep}`) && !isAbsolute(canonicalRelativePath);
242
- return {
243
- projectDir: canonicalProjectDir,
244
- sourcePath: canonicalSourcePath,
245
- relativePath: requestedRelativePath.normalize("NFC"),
246
- // An external target needs a stable identity in addition to its project-local
247
- // symlink path, otherwise retargeting the link can reuse an unrelated proxy.
248
- cacheIdentity: (sourceIsInsideCanonicalProject ? canonicalRelativePath : canonicalSourcePath).normalize("NFC")
249
- };
250
- }
251
- function buildProxyCacheKey(source, variant, box) {
252
- const stat = statSync2(source.sourcePath);
253
- const size = box ? `\0${formatPreviewProxyBox(box)}` : "";
254
- return createHash("sha256").update(
255
- `${source.relativePath}\0${source.cacheIdentity}\0${stat.mtimeMs}\0${stat.size}\0${PROXY_PARAMS_VERSION}\0${variant}${size}`
256
- ).digest("hex");
364
+ return { projectDir: realProjectRoot(projectDir), sourcePath: realpath(absoluteSourcePath) };
257
365
  }
258
366
  function getCanonicalProxyCachePath(source, variant, box) {
259
- const key = buildProxyCacheKey(source, variant, box);
260
- return join2(
367
+ const key = fastCopyKey(source.sourcePath, PROXY_PARAMS_VERSION, variant, box);
368
+ const made = join4(
261
369
  source.projectDir,
262
370
  CACHE_DIR_NAME,
263
371
  `${key}${PROXY_VARIANT_CONFIG[variant].extension}`
264
372
  );
373
+ return existsSync4(made) ? made : readCopyIndex(fastIndexPath(made)) ?? made;
265
374
  }
266
375
  function getProxyCachePath(projectDir, absoluteSourcePath, variant = "h264", box) {
267
376
  return getCanonicalProxyCachePath(
@@ -420,42 +529,106 @@ function chunkRanges(durationSeconds) {
420
529
  lengthSeconds: index === count - 1 ? durationSeconds - index * PROXY_CHUNK_SECONDS : PROXY_CHUNK_SECONDS
421
530
  }));
422
531
  }
423
- async function encodeInChunks(ranges, chunkPrefix, tempPath, encode, concat, onDone = () => {
532
+ var UNGATED = { size: 1, acquire: async () => {
533
+ }, release: () => {
534
+ } };
535
+ function createPieceGate(size) {
536
+ let free = size;
537
+ const waiters = [];
538
+ return {
539
+ size,
540
+ acquire: (signal) => {
541
+ signal.throwIfAborted();
542
+ if (free > 0) {
543
+ free--;
544
+ return Promise.resolve();
545
+ }
546
+ return new Promise((resolve2, reject) => {
547
+ const grant = () => {
548
+ signal.removeEventListener("abort", onAbort);
549
+ resolve2();
550
+ };
551
+ const onAbort = () => {
552
+ waiters.splice(waiters.indexOf(grant), 1);
553
+ reject(signal.reason);
554
+ };
555
+ signal.addEventListener("abort", onAbort, { once: true });
556
+ waiters.push(grant);
557
+ });
558
+ },
559
+ release: () => {
560
+ const next = waiters.shift();
561
+ if (next) next();
562
+ else free++;
563
+ }
564
+ };
565
+ }
566
+ async function encodeInChunks(ranges, chunkPrefix, tempPath, encode, concat, signal, gate, onDone = () => {
424
567
  }) {
425
- const extension = extname2(tempPath);
568
+ const extension = extname3(tempPath);
426
569
  const pieces = ranges.map((_, index) => `${chunkPrefix}${index}${extension}`);
570
+ const pending = ranges.flatMap((_, index) => existsSync4(pieces[index]) ? [] : [index]);
427
571
  let doneSeconds = 0;
428
572
  for (const [index, range] of ranges.entries()) {
429
- if (existsSync2(pieces[index])) doneSeconds += range.lengthSeconds;
573
+ if (!pending.includes(index)) doneSeconds += range.lengthSeconds;
430
574
  }
431
575
  if (doneSeconds) onDone(doneSeconds);
432
- for (const [index, range] of ranges.entries()) {
433
- const piece = pieces[index];
434
- if (existsSync2(piece)) continue;
435
- const pieceTemp = join2(dirname(piece), `.tmp-${randomUUID()}-${basename(piece)}`);
436
- try {
437
- await encode(pieceTemp, range);
438
- renameSync(pieceTemp, piece);
439
- doneSeconds += range.lengthSeconds;
440
- onDone(doneSeconds);
441
- } finally {
442
- if (existsSync2(pieceTemp)) unlinkSync2(pieceTemp);
576
+ const inFlight2 = /* @__PURE__ */ new Map();
577
+ const stop = new AbortController();
578
+ const pieceSignal = AbortSignal.any([signal, stop.signal]);
579
+ let failure;
580
+ let cursor = 0;
581
+ const work = async () => {
582
+ for (; ; ) {
583
+ await gate.acquire(pieceSignal);
584
+ try {
585
+ if (cursor >= pending.length) return;
586
+ const index = pending[cursor++];
587
+ const range = ranges[index];
588
+ const piece = pieces[index];
589
+ const pieceTemp = join4(dirname3(piece), `.tmp-${randomUUID()}-${basename3(piece)}`);
590
+ try {
591
+ await encode(pieceTemp, range, pieceSignal, (encodedSeconds) => {
592
+ inFlight2.set(index, encodedSeconds);
593
+ onDone(doneSeconds + [...inFlight2.values()].reduce((sum, seconds) => sum + seconds, 0));
594
+ });
595
+ renameSync(pieceTemp, piece);
596
+ inFlight2.delete(index);
597
+ doneSeconds += range.lengthSeconds;
598
+ onDone(doneSeconds);
599
+ } finally {
600
+ if (existsSync4(pieceTemp)) unlinkSync2(pieceTemp);
601
+ }
602
+ } finally {
603
+ gate.release();
604
+ }
443
605
  }
444
- }
445
- const listDir = mkdtempSync(join2(tmpdir(), "hf-proxy-join-"));
606
+ };
607
+ await Promise.all(
608
+ Array.from(
609
+ { length: Math.min(gate.size, pending.length) },
610
+ () => work().catch((error) => {
611
+ failure ??= error;
612
+ stop.abort();
613
+ })
614
+ )
615
+ );
616
+ if (failure !== void 0) throw failure;
617
+ const listDir = mkdtempSync(join4(tmpdir(), "hf-proxy-join-"));
446
618
  try {
447
- const listPath = join2(listDir, "pieces.txt");
448
- writeFileSync(
619
+ const listPath = join4(listDir, "pieces.txt");
620
+ writeFileSync2(
449
621
  listPath,
450
622
  pieces.map((piece) => `file '${piece.replace(/\\/g, "/").replace(/'/g, "'\\''")}'
451
623
  `).join("")
452
624
  );
453
625
  await concat(listPath, tempPath);
454
626
  } finally {
455
- rmSync(listDir, { recursive: true, force: true });
627
+ rmSync2(listDir, { recursive: true, force: true });
456
628
  }
457
- for (const piece of pieces) if (existsSync2(piece)) unlinkSync2(piece);
629
+ for (const piece of pieces) if (existsSync4(piece)) unlinkSync2(piece);
458
630
  }
631
+ var KEYFRAME_EVERY_SECOND_ARGS = ["-force_key_frames", "expr:gte(t,n_forced*1)"];
459
632
  var H264_ARGS = [
460
633
  "-c:v",
461
634
  "libx264",
@@ -478,7 +651,11 @@ var H264_ARGS = [
478
651
  "-movflags",
479
652
  "+faststart"
480
653
  ];
481
- var VP8_THREADS = String(Math.min(4, availableParallelism()));
654
+ var CORES = availableParallelism();
655
+ var PIECE_MEMORY_BYTES = 3 * 1024 ** 3;
656
+ var ALPHA_PIECE_GATE = createPieceGate(
657
+ Math.max(1, Math.min(4, CORES - 1, Math.floor(totalmem() / PIECE_MEMORY_BYTES)))
658
+ );
482
659
  var VP8_ARGS = [
483
660
  "-c:v",
484
661
  "libvpx",
@@ -498,8 +675,6 @@ var VP8_ARGS = [
498
675
  "bt709",
499
676
  "-cpu-used",
500
677
  "4",
501
- "-threads",
502
- VP8_THREADS,
503
678
  "-auto-alt-ref",
504
679
  "0",
505
680
  "-metadata:s:v:0",
@@ -533,7 +708,12 @@ async function runFfmpeg(sourcePath, outputPath, chunkPrefix, variant, box, sign
533
708
  const alphaDecoder = keepsAlpha ? alphaDecoderFor(metadata.color.codecName) : null;
534
709
  const decoderArgs = alphaDecoder ? ["-c:v", alphaDecoder] : [];
535
710
  const totalSeconds = metadata.durationSeconds;
536
- const encode = (target, range) => spawnFfmpeg(
711
+ const gate = keepsAlpha ? ALPHA_PIECE_GATE : UNGATED;
712
+ const vp8Threads = (pieces) => [
713
+ "-threads",
714
+ String(Math.min(4, Math.floor(CORES / pieces)))
715
+ ];
716
+ const encode = (target, range, pieceSignal, onEncoded) => spawnFfmpeg(
537
717
  ffmpegPath,
538
718
  [
539
719
  "-y",
@@ -547,21 +727,31 @@ async function runFfmpeg(sourcePath, outputPath, chunkPrefix, variant, box, sign
547
727
  ...range ? ["-t", String(range.lengthSeconds)] : [],
548
728
  "-vf",
549
729
  videoFilter,
550
- ...variant === "vp8" ? VP8_ARGS : H264_ARGS,
730
+ ...KEYFRAME_EVERY_SECOND_ARGS,
731
+ ...keepsAlpha ? [...VP8_ARGS, ...vp8Threads(range ? gate.size : 1)] : H264_ARGS,
551
732
  target
552
733
  ],
553
734
  TRANSCODE_STALL_MS,
554
- signal,
555
- (encodedSeconds) => onProgress({ doneSeconds: (range?.startSeconds ?? 0) + encodedSeconds, totalSeconds })
735
+ pieceSignal,
736
+ onEncoded
556
737
  );
557
738
  const ranges = chunkRanges(totalSeconds);
558
- if (!ranges) return encode(outputPath);
739
+ if (!ranges) {
740
+ return encode(
741
+ outputPath,
742
+ void 0,
743
+ signal,
744
+ (encodedSeconds) => onProgress({ doneSeconds: encodedSeconds, totalSeconds })
745
+ );
746
+ }
559
747
  await encodeInChunks(
560
748
  ranges,
561
749
  chunkPrefix,
562
750
  outputPath,
563
751
  encode,
564
752
  (listPath, target) => spawnFfmpeg(ffmpegPath, concatArgs(listPath, target, variant), CONCAT_TIMEOUT_MS, signal),
753
+ signal,
754
+ gate,
565
755
  (doneSeconds) => onProgress({ doneSeconds, totalSeconds })
566
756
  );
567
757
  }
@@ -656,14 +846,31 @@ function spawnFfmpeg(ffmpegPath, args, stallMs, signal, onProgress) {
656
846
  async function transcodeToCache(job, projectDir, absoluteSourcePath, cachePath, variant, box) {
657
847
  await acquireSlot(job);
658
848
  try {
659
- if (existsSync2(cachePath)) return cachePath;
660
- const cacheDir = dirname(cachePath);
849
+ if (existsSync4(cachePath)) return cachePath;
850
+ const cacheDir = dirname3(cachePath);
661
851
  mkdirWithinProject(projectDir, cacheDir);
662
- const tempPath = join2(cacheDir, `.tmp-${randomUUID()}-${basename(cachePath)}`);
852
+ const contentIndex = contentIndexPath(
853
+ cacheDir,
854
+ await contentCopyKey(
855
+ absoluteSourcePath,
856
+ PROXY_PARAMS_VERSION,
857
+ variant,
858
+ box,
859
+ job.abandoned.signal
860
+ )
861
+ );
862
+ const sameContent = readCopyIndex(contentIndex);
863
+ if (sameContent) {
864
+ writeCopyIndex(fastIndexPath(cachePath), sameContent);
865
+ return sameContent;
866
+ }
867
+ const release = await acquireCopyLock(cachePath, job.abandoned.signal);
868
+ if (!release) return cachePath;
869
+ const tempPath = join4(cacheDir, `.tmp-${randomUUID()}-${basename3(cachePath)}`);
663
870
  try {
664
- const chunkPrefix = join2(
871
+ const chunkPrefix = join4(
665
872
  cacheDir,
666
- `${CHUNK_PREFIX}${basename(cachePath, extname2(cachePath))}-`
873
+ `${CHUNK_PREFIX}${basename3(cachePath, extname3(cachePath))}-`
667
874
  );
668
875
  await runFfmpeg(
669
876
  absoluteSourcePath,
@@ -675,10 +882,12 @@ async function transcodeToCache(job, projectDir, absoluteSourcePath, cachePath,
675
882
  (progress) => reportProgress(job, progress)
676
883
  );
677
884
  renameSync(tempPath, cachePath);
885
+ writeCopyIndex(contentIndex, cachePath);
678
886
  maintainProxyCache(cacheDir);
679
887
  return cachePath;
680
888
  } finally {
681
- if (existsSync2(tempPath)) unlinkSync2(tempPath);
889
+ if (existsSync4(tempPath)) unlinkSync2(tempPath);
890
+ release();
682
891
  }
683
892
  } finally {
684
893
  releaseSlot();
@@ -686,8 +895,8 @@ async function transcodeToCache(job, projectDir, absoluteSourcePath, cachePath,
686
895
  }
687
896
  var proxyActivity = 0;
688
897
  function proxyActivityMark(projectDir) {
689
- if (!existsSync2(projectDir)) return String(proxyActivity);
690
- const cacheDir = join2(realpath(projectDir), CACHE_DIR_NAME) + sep;
898
+ if (!existsSync4(projectDir)) return String(proxyActivity);
899
+ const cacheDir = join4(realpath(projectDir), CACHE_DIR_NAME) + sep;
691
900
  for (const cachePath of inFlight.keys()) if (cachePath.startsWith(cacheDir)) return null;
692
901
  return String(proxyActivity);
693
902
  }
@@ -695,9 +904,9 @@ async function resolveProxy(projectDir, absoluteSourcePath, variant = "h264", bo
695
904
  options.signal?.throwIfAborted();
696
905
  const source = canonicalizeProxySource(projectDir, absoluteSourcePath);
697
906
  const cachePath = getCanonicalProxyCachePath(source, variant, box);
698
- if (existsSync2(cachePath)) {
907
+ if (existsSync4(cachePath)) {
699
908
  markCacheEntryUsed(cachePath);
700
- maintainProxyCache(dirname(cachePath));
909
+ maintainProxyCache(dirname3(cachePath));
701
910
  return cachePath;
702
911
  }
703
912
  const rememberedFailure = failedTranscodes.get(cachePath);
@@ -754,7 +963,7 @@ async function resolveProxies(projectDir, sources, onSettled = () => {
754
963
  const first = sources[0];
755
964
  if (first) {
756
965
  maintainProxyCache(
757
- dirname(getProxyCachePath(projectDir, first.sourcePath, first.variant)),
966
+ dirname3(getProxyCachePath(projectDir, first.sourcePath, first.variant)),
758
967
  Math.min(
759
968
  Number(process.env.HYPERFRAMES_PROXY_CACHE_STALE_TEMP_MS) || UNTOUCHED_TEMP_IS_DEAD_MS,
760
969
  UNTOUCHED_TEMP_IS_DEAD_MS
@@ -1,3 +1,5 @@
1
+ import { PreviewSourceFacts, PreviewProxyBox } from '@hyperframes/core';
2
+
1
3
  interface FfprobeRunResult {
2
4
  status: number | null;
3
5
  stdout: string | Buffer;
@@ -31,6 +33,8 @@ interface AssetCodecFacts {
31
33
  /** Source carries an alpha channel (`MediaMetadata.hasAlpha`). Alpha sources use a
32
34
  * VP8/WebM proxy so their transparency is preserved across Chromium builds. */
33
35
  hasAlpha: boolean;
36
+ /** Size, bit depth and bitrate, for deciding whether a playable file deserves a lighter copy. */
37
+ source?: PreviewSourceFacts;
34
38
  }
35
39
  /** Server-root-relative URL pathname -> that asset's codec facts. */
36
40
  type MediaCodecMap = Record<string, AssetCodecFacts>;
@@ -96,8 +100,11 @@ type MediaProxyEligibility = {
96
100
  eligible: false;
97
101
  reason: MediaProxyIneligibilityReason;
98
102
  };
99
- /** Single policy gate shared by proactive scans and on-demand proxy routes. */
100
- declare function decideMediaProxyEligibility(facts: AssetCodecFacts | null): MediaProxyEligibility;
103
+ /**
104
+ * Single policy gate shared by proactive scans and on-demand proxy routes. A file Chrome can play is
105
+ * eligible only for a lighter copy (`lightBox`, the box that request asks for) and only when it is heavy there.
106
+ */
107
+ declare function decideMediaProxyEligibility(facts: AssetCodecFacts | null, lightBox?: PreviewProxyBox): MediaProxyEligibility;
101
108
  interface CachedAssetProbe {
102
109
  /** The file a symlinked path pointed at, so retargeting the link is a miss. */
103
110
  target: string;
@@ -126,12 +133,15 @@ interface ScanProjectMediaCodecMapOptions {
126
133
  cache?: MediaCodecProbeCache;
127
134
  runner?: FfprobeRunner;
128
135
  }
129
- /**
130
- * Scans a project's composition HTML for local `<video src>` references and
131
- * returns the injection map: root-relative URL pathname -> codec facts.
132
- * Best-effort throughout — a video whose codec can't be determined (missing
133
- * ffprobe, probe error, no video stream) is simply omitted, never thrown.
134
- */
136
+ /** What a scan of a project's local videos finds: the hostile ones for the proxy map, and every readable one's size. */
137
+ interface ProjectMediaScan {
138
+ codecMap: MediaCodecMap;
139
+ /** Root-relative URL pathname -> size, bit depth and bitrate. */
140
+ sourceFacts: Record<string, PreviewSourceFacts>;
141
+ }
142
+ /** Scans composition HTML for local `<video src>` references; a video that can't be probed is omitted. */
143
+ declare function scanProjectMedia(projectDir: string, htmlSources: HtmlSourceLike[], options?: ScanProjectMediaCodecMapOptions): Promise<ProjectMediaScan>;
144
+ /** The injection map alone: root-relative URL pathname -> codec facts of the hostile videos. */
135
145
  declare function scanProjectMediaCodecMap(projectDir: string, htmlSources: HtmlSourceLike[], options?: ScanProjectMediaCodecMapOptions): Promise<MediaCodecMap>;
136
146
 
137
- export { type AssetCodecFacts, BROWSER_HOSTILE_CODECS, type BrowserHostileCodec, type HtmlSourceLike, type MediaCodecMap, type MediaCodecProbeCache, type MediaProxyEligibility, type MediaProxyIneligibilityReason, PROXY_VARIANT_CONFIG, type ProxyVariant, type ProxyVariantRequest, type ScanProjectMediaCodecMapOptions, createMediaCodecProbeCache, decideMediaProxyEligibility, isProxyVariant, isProxyVariantRequest, mediaProxyDemand, probeAssetCodec, proxyVariantFor, recordProxyPrewarm, recordProxyRequest, resolveProxyVariantRequest, scanProjectMediaCodecMap, shouldPrewarmProxy };
147
+ export { type AssetCodecFacts, BROWSER_HOSTILE_CODECS, type BrowserHostileCodec, type HtmlSourceLike, type MediaCodecMap, type MediaCodecProbeCache, type MediaProxyEligibility, type MediaProxyIneligibilityReason, PROXY_VARIANT_CONFIG, type ProjectMediaScan, type ProxyVariant, type ProxyVariantRequest, type ScanProjectMediaCodecMapOptions, createMediaCodecProbeCache, decideMediaProxyEligibility, isProxyVariant, isProxyVariantRequest, mediaProxyDemand, probeAssetCodec, proxyVariantFor, recordProxyPrewarm, recordProxyRequest, resolveProxyVariantRequest, scanProjectMedia, scanProjectMediaCodecMap, shouldPrewarmProxy };
@@ -11,9 +11,10 @@ import {
11
11
  recordProxyPrewarm,
12
12
  recordProxyRequest,
13
13
  resolveProxyVariantRequest,
14
+ scanProjectMedia,
14
15
  scanProjectMediaCodecMap,
15
16
  shouldPrewarmProxy
16
- } from "../chunk-XMNHDWQI.js";
17
+ } from "../chunk-C2RFW2FX.js";
17
18
  import "../chunk-QF7MFJJU.js";
18
19
  import "../chunk-SVQFE44C.js";
19
20
  import "../chunk-L3HJVTQE.js";
@@ -30,6 +31,7 @@ export {
30
31
  recordProxyPrewarm,
31
32
  recordProxyRequest,
32
33
  resolveProxyVariantRequest,
34
+ scanProjectMedia,
33
35
  scanProjectMediaCodecMap,
34
36
  shouldPrewarmProxy
35
37
  };
@@ -4,9 +4,9 @@ import {
4
4
  isAutoProxyEnabled,
5
5
  proxyEtagSalt,
6
6
  resolvePreviewMediaCodecProbeCache
7
- } from "../chunk-ZCCJNXS7.js";
8
- import "../chunk-URMIRXBS.js";
9
- import "../chunk-XMNHDWQI.js";
7
+ } from "../chunk-2MJKVMZU.js";
8
+ import "../chunk-RX6VALW3.js";
9
+ import "../chunk-C2RFW2FX.js";
10
10
  import "../chunk-QF7MFJJU.js";
11
11
  import "../chunk-SVQFE44C.js";
12
12
  import "../chunk-L3HJVTQE.js";
@@ -11,7 +11,7 @@ import { ProxyVariant } from './mediaCodecMap.js';
11
11
  * caller's `signal` detaches that caller; once no caller is left, the copy is
12
12
  * dropped from the queue or its ffmpeg is killed.
13
13
  */
14
- declare const PROXY_PARAMS_VERSION = "v6";
14
+ declare const PROXY_PARAMS_VERSION = "v7";
15
15
  declare const CACHE_DIR_NAME = ".transcode-cache";
16
16
  /** A copy that makes no progress this long is ended: a hung encode, not a slow one. */
17
17
  declare const TRANSCODE_STALL_MS: number;
@@ -17,8 +17,8 @@ import {
17
17
  resolveProxies,
18
18
  resolveProxy,
19
19
  waitForProxy
20
- } from "../chunk-URMIRXBS.js";
21
- import "../chunk-XMNHDWQI.js";
20
+ } from "../chunk-RX6VALW3.js";
21
+ import "../chunk-C2RFW2FX.js";
22
22
  import "../chunk-QF7MFJJU.js";
23
23
  import "../chunk-SVQFE44C.js";
24
24
  import "../chunk-L3HJVTQE.js";
@@ -22,7 +22,7 @@ import {
22
22
  recordRenderSuccess,
23
23
  registerRenderOutput,
24
24
  retainSnapshotOutput
25
- } from "../chunk-VKQTZKGA.js";
25
+ } from "../chunk-OSZE5WT3.js";
26
26
  import "../chunk-QF7MFJJU.js";
27
27
  import "../chunk-SVQFE44C.js";
28
28
  import "../chunk-L3HJVTQE.js";
package/dist/index.d.ts CHANGED
@@ -93,7 +93,7 @@ interface DuplicateElementResult {
93
93
  matched: boolean;
94
94
  newId: string | null;
95
95
  }
96
- declare function duplicateElementInHtml(source: string, target: SourceMutationTarget, newId: string, at: number): DuplicateElementResult;
96
+ declare function duplicateElementInHtml(source: string, target: SourceMutationTarget, newId: string, at: number, fallbackDuration?: number): DuplicateElementResult;
97
97
 
98
98
  interface FileMutationInput {
99
99
  sourceFile: string;
package/dist/index.js CHANGED
@@ -62,7 +62,7 @@ import {
62
62
  resolveProjectSignature,
63
63
  retainSnapshotOutput,
64
64
  settledFileTag
65
- } from "./chunk-VKQTZKGA.js";
65
+ } from "./chunk-OSZE5WT3.js";
66
66
  import {
67
67
  clearElementScreenshotIsolation,
68
68
  getElementScreenshotClip
@@ -82,7 +82,7 @@ import {
82
82
  isAutoProxyEnabled,
83
83
  proxyEtagSalt,
84
84
  resolvePreviewMediaCodecProbeCache
85
- } from "./chunk-ZCCJNXS7.js";
85
+ } from "./chunk-2MJKVMZU.js";
86
86
  import {
87
87
  CACHE_DIR_NAME,
88
88
  PROXY_PENDING_RETRY_AFTER_SECONDS,
@@ -93,7 +93,7 @@ import {
93
93
  proxyActivityMark,
94
94
  resolveProxy,
95
95
  waitForProxy
96
- } from "./chunk-URMIRXBS.js";
96
+ } from "./chunk-RX6VALW3.js";
97
97
  import {
98
98
  PROXY_VARIANT_CONFIG,
99
99
  decideMediaProxyEligibility,
@@ -102,7 +102,7 @@ import {
102
102
  probeMediaMetadata,
103
103
  recordProxyRequest,
104
104
  resolveProxyVariantRequest
105
- } from "./chunk-XMNHDWQI.js";
105
+ } from "./chunk-C2RFW2FX.js";
106
106
  import {
107
107
  folderGone,
108
108
  isInHiddenOrVendorDir,
@@ -3885,7 +3885,11 @@ import {
3885
3885
  STUDIO_PREVIEW_MARK_META,
3886
3886
  STUDIO_PREVIEW_ERRORS
3887
3887
  } from "@hyperframes/core/studio-preview-mark";
3888
- import { PREVIEW_PROXY_BOX_PARAM, parsePreviewProxyBox } from "@hyperframes/core";
3888
+ import {
3889
+ PREVIEW_PROXY_BOX_PARAM,
3890
+ PREVIEW_PROXY_LIGHT_PARAM,
3891
+ parsePreviewProxyBox
3892
+ } from "@hyperframes/core";
3889
3893
  import {
3890
3894
  GSAP_BASE_URL_META,
3891
3895
  gsapCdnDist as gsapCdnDist2,
@@ -4502,7 +4506,8 @@ ${etag}`;
4502
4506
  const proxyParam = c.req.query("hf-proxy");
4503
4507
  const boxParam = c.req.query(PREVIEW_PROXY_BOX_PARAM);
4504
4508
  const proxyBox = boxParam === void 0 ? void 0 : parsePreviewProxyBox(boxParam);
4505
- if (proxyBox === null || proxyBox && proxyParam === void 0) {
4509
+ const lightRequested = c.req.query(PREVIEW_PROXY_LIGHT_PARAM) !== void 0;
4510
+ if (proxyBox === null || proxyBox && proxyParam === void 0 || lightRequested && !proxyBox) {
4506
4511
  return c.text("not found", 404);
4507
4512
  }
4508
4513
  let proxyVariant;
@@ -4511,7 +4516,7 @@ ${etag}`;
4511
4516
  return c.text("not found", 404);
4512
4517
  }
4513
4518
  const facts = await probeAssetCodec(file, void 0, mediaCodecProbeCache);
4514
- const eligibility = decideMediaProxyEligibility(facts);
4519
+ const eligibility = decideMediaProxyEligibility(facts, lightRequested ? proxyBox : void 0);
4515
4520
  if (!eligibility.eligible) {
4516
4521
  return c.text(`media proxy unavailable: ${eligibility.reason}`, 422);
4517
4522
  }
@@ -6648,9 +6653,29 @@ function registerHistoryRoutes(api, adapter) {
6648
6653
  );
6649
6654
  }
6650
6655
 
6656
+ // src/helpers/previewErrorPage.ts
6657
+ import { STUDIO_PREVIEW_ERRORS as STUDIO_PREVIEW_ERRORS2 } from "@hyperframes/core/studio-preview-mark";
6658
+ function previewErrorPage(status) {
6659
+ const message = `The preview page could not be loaded (HTTP ${status}).`;
6660
+ return `<!doctype html><meta charset="utf-8"><script>window.${STUDIO_PREVIEW_ERRORS2}=[${JSON.stringify(message)}]</script><p>${message}</p>`;
6661
+ }
6662
+
6651
6663
  // src/createStudioApi.ts
6664
+ async function answerRefusedFrameWithPage(c, next) {
6665
+ await next();
6666
+ if (c.req.header("sec-fetch-dest") !== "iframe" || c.res.status < 400) return;
6667
+ const headers = new Headers(c.res.headers);
6668
+ headers.set("content-type", "text/html; charset=utf-8");
6669
+ headers.delete("content-length");
6670
+ c.res = new Response(previewErrorPage(c.res.status), {
6671
+ status: c.res.status,
6672
+ headers
6673
+ });
6674
+ }
6652
6675
  function createStudioApi(adapter) {
6653
6676
  const api = new Hono();
6677
+ api.use("/projects/:id/preview/*", answerRefusedFrameWithPage);
6678
+ api.use("/projects/:id/preview", answerRefusedFrameWithPage);
6654
6679
  api.use(async function answerProjectDirMissingAfterErrorHandlers(c, next) {
6655
6680
  const hostHeaders = new Headers(c.res.headers);
6656
6681
  await next();
@@ -6701,13 +6726,14 @@ function createStudioApi(adapter) {
6701
6726
  // src/helpers/duplicateElement.ts
6702
6727
  import { sameInstant } from "@hyperframes/core/clip-facts";
6703
6728
  import { ensureHfIds as ensureHfIds4 } from "@hyperframes/parsers/hf-ids";
6704
- function duplicateElementInHtml(source, target, newId, at) {
6729
+ function duplicateElementInHtml(source, target, newId, at, fallbackDuration = 0) {
6705
6730
  const { document, wrappedFragment } = parseSourceDocument(source);
6706
6731
  const element = findTargetElement(document, target);
6707
6732
  if (!element || !element.parentElement) return { html: source, matched: false, newId: null };
6708
- const duration = numericAttribute(element, "data-duration");
6733
+ const duration = element.hasAttribute("data-duration") ? numericAttribute(element, "data-duration") : fallbackDuration;
6734
+ const start = numericAttribute(element, "data-start");
6709
6735
  const track = numericAttribute(element, "data-track-index") ?? 0;
6710
- if (duration === null || numericAttribute(element, "data-start") === null) {
6736
+ if (duration === null || start === null) {
6711
6737
  return { html: source, matched: false, newId: null };
6712
6738
  }
6713
6739
  const uniqueId = nextUniqueId(document, newId);
@@ -6717,13 +6743,13 @@ function duplicateElementInHtml(source, target, newId, at) {
6717
6743
  clone.setAttribute("id", uniqueId);
6718
6744
  dedupeClonedCompositionId(document, clone);
6719
6745
  stripStableIds(clone);
6720
- clone.setAttribute("data-start", String(at));
6746
+ moveElementStart(clone, start, at);
6721
6747
  element.parentElement.insertBefore(clone, element.nextSibling);
6722
6748
  const html = wrappedFragment ? document.body.innerHTML || "" : document.toString();
6723
6749
  return { html: ensureHfIds4(html), matched: true, newId: uniqueId };
6724
6750
  }
6725
6751
  function rippleElements(document, element, at, duration, track) {
6726
- for (const candidate of Array.from(document.querySelectorAll("[data-start][data-duration]"))) {
6752
+ for (const candidate of Array.from(document.querySelectorAll("[data-start]"))) {
6727
6753
  if (candidate === element || candidate.getAttribute("data-track-index") !== String(track)) {
6728
6754
  continue;
6729
6755
  }
@@ -6731,9 +6757,15 @@ function rippleElements(document, element, at, duration, track) {
6731
6757
  if (start === null) continue;
6732
6758
  const metInsertionPoint = sameInstant(start, at);
6733
6759
  if (start < at && !metInsertionPoint) continue;
6734
- candidate.setAttribute("data-start", String((metInsertionPoint ? at : start) + duration));
6760
+ moveElementStart(candidate, start, (metInsertionPoint ? at : start) + duration);
6735
6761
  }
6736
6762
  }
6763
+ function moveElementStart(element, start, nextStart) {
6764
+ element.setAttribute("data-start", String(nextStart));
6765
+ if (!element.hasAttribute("data-end")) return;
6766
+ const end = numericAttribute(element, "data-end");
6767
+ if (end !== null) element.setAttribute("data-end", String(end + (nextStart - start)));
6768
+ }
6737
6769
  function stripStableIds(element) {
6738
6770
  element.removeAttribute("data-hf-id");
6739
6771
  for (const child of Array.from(element.querySelectorAll("[data-hf-id]"))) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/studio-server",
3
- "version": "0.8.200",
3
+ "version": "0.8.202",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/heygen-com/hyperframes",
@@ -84,8 +84,8 @@
84
84
  "linkedom": "^0.18.12",
85
85
  "postcss": "^8.5.8",
86
86
  "postcss-selector-parser": "^7.1.2",
87
- "@hyperframes/core": "0.8.200",
88
- "@hyperframes/parsers": "0.8.200"
87
+ "@hyperframes/core": "0.8.202",
88
+ "@hyperframes/parsers": "0.8.202"
89
89
  },
90
90
  "devDependencies": {
91
91
  "@types/node": "^25.0.10",