@hyperframes/producer 0.8.203 → 0.8.205

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.
@@ -1335,6 +1335,12 @@ import {
1335
1335
  prepareCaptureSessionForReuse,
1336
1336
  spawnStreamingEncoder
1337
1337
  } from "@hyperframes/engine";
1338
+ var DEFAULT_CAPTURE_STALL_MS = 6e4;
1339
+ function resolveCaptureStallTimeoutMs() {
1340
+ const raw = process.env.HF_DE_STALL_MS ?? process.env.HF_DE_PARALLEL_STALL_MS;
1341
+ const parsed = raw ? Number(raw) : Number.NaN;
1342
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_CAPTURE_STALL_MS;
1343
+ }
1338
1344
 
1339
1345
  // src/services/render/segmentManifest.ts
1340
1346
  import { renameWithRetry as renameWithRetry2 } from "@hyperframes/engine";
@@ -1442,9 +1448,325 @@ async function measureCaptureCostFromSession(session, totalFrames, fps, log) {
1442
1448
 
1443
1449
  // src/services/render/observability.ts
1444
1450
  import { redactTelemetryString } from "@hyperframes/core";
1451
+ import { CaptureFailure as CaptureFailure2 } from "@hyperframes/engine";
1452
+ var MAX_EVENTS = 160;
1453
+ var ALLOWED_STRING_DATA_KEYS = /* @__PURE__ */ new Set([
1454
+ "browserGpuMode",
1455
+ "captureMode",
1456
+ "captureOperation",
1457
+ "compositionHash",
1458
+ "effectiveHdr",
1459
+ "format",
1460
+ "quality",
1461
+ "renderJobId",
1462
+ "requestedHdrMode",
1463
+ "requestedWorkers",
1464
+ // "calibrating" during pre-capture browser warm-up / calibration stages;
1465
+ // "capturing" during capture_disk / capture_streaming / capture_hdr_*
1466
+ // stages. Distinguishes healthy 0-frame heartbeats (browser starting up)
1467
+ // from actual zero-frame stalls once capture is meant to be underway.
1468
+ // Field signal ts=1784019503.
1469
+ "stagePhase",
1470
+ // Full-fidelity fast-capture fallback trigger — the specific reason
1471
+ // drawElement gated off ("filter:blur", "filter:drop-shadow",
1472
+ // "backdrop-filter", "clip-path", "at_risk_timeline", "swiftshader", …).
1473
+ // Populated on the `capture_fallback_profile` checkpoint emitted by
1474
+ // `fallbackCaptureProfile.ts` when the operator opts into
1475
+ // `HF_PROFILE_FALLBACK_CAPTURE=true` so downstream metric consumers can
1476
+ // characterize per-frame perf by the exact trigger. Complementary to
1477
+ // `deGateReason`, which is the low-cardinality bucket for aggregation —
1478
+ // this string preserves the fine-grained "which CSS property" for
1479
+ // diagnostic reads. Sanitized like any observation message; low-cardinality
1480
+ // by construction (bounded by the fallback-trigger enumeration).
1481
+ "triggerReason"
1482
+ ]);
1483
+ var RESERVED_LOG_KEYS = /* @__PURE__ */ new Set([
1484
+ "data",
1485
+ "durationMs",
1486
+ "elapsedMs",
1487
+ "message",
1488
+ "phase",
1489
+ "renderJobId",
1490
+ "status"
1491
+ ]);
1492
+ function sanitizeObservationMessage(value) {
1493
+ return redactTelemetryString(value);
1494
+ }
1495
+ function sanitizeObservationData(data) {
1496
+ if (!data) return void 0;
1497
+ const sanitized = {};
1498
+ for (const [key, value] of Object.entries(data)) {
1499
+ if (RESERVED_LOG_KEYS.has(key)) continue;
1500
+ if (typeof value === "string") {
1501
+ if (!ALLOWED_STRING_DATA_KEYS.has(key)) continue;
1502
+ sanitized[key] = sanitizeObservationMessage(value);
1503
+ } else {
1504
+ sanitized[key] = value;
1505
+ }
1506
+ }
1507
+ return sanitized;
1508
+ }
1509
+ function isHttpErrorDiagnostic(line) {
1510
+ return /\[Browser:HTTP\d{3}\]/.test(line);
1511
+ }
1512
+ function readUnsignedIntAfter(line, prefix) {
1513
+ const start = line.indexOf(prefix);
1514
+ if (start < 0) return void 0;
1515
+ let value = 0;
1516
+ let digits = 0;
1517
+ for (let i = start + prefix.length; i < line.length; i++) {
1518
+ const code = line.charCodeAt(i);
1519
+ if (code < 48 || code > 57) break;
1520
+ value = value * 10 + code - 48;
1521
+ digits += 1;
1522
+ if (value > Number.MAX_SAFE_INTEGER) return void 0;
1523
+ }
1524
+ return digits > 0 ? value : void 0;
1525
+ }
1526
+ function maxReading(current, next) {
1527
+ if (next === void 0) return current;
1528
+ return current === void 0 ? next : Math.max(current, next);
1529
+ }
1530
+ function summarizeInitObservability(lines, fallback) {
1531
+ let initDurationMs = fallback?.initDurationMs;
1532
+ let tweenCount = fallback?.tweenCount;
1533
+ let elementCount = fallback?.elementCount;
1534
+ for (const line of lines) {
1535
+ if (!line.includes("[FrameCapture:INIT]")) continue;
1536
+ initDurationMs = maxReading(initDurationMs, readUnsignedIntAfter(line, "initDurationMs="));
1537
+ tweenCount = maxReading(tweenCount, readUnsignedIntAfter(line, "tweenCount="));
1538
+ elementCount = maxReading(elementCount, readUnsignedIntAfter(line, "elementCount="));
1539
+ }
1540
+ if (initDurationMs === void 0 && tweenCount === void 0 && elementCount === void 0) {
1541
+ return void 0;
1542
+ }
1543
+ return { initDurationMs, tweenCount, elementCount };
1544
+ }
1545
+ function summarizeBrowserDiagnostics(lines) {
1546
+ let errors = 0;
1547
+ let pageErrors = 0;
1548
+ let requestFailed = 0;
1549
+ let httpErrors = 0;
1550
+ let navigationStarts = 0;
1551
+ let navigationFailures = 0;
1552
+ let consoleErrors = 0;
1553
+ let consoleWarnings = 0;
1554
+ for (const line of lines) {
1555
+ const isPageError = line.includes("PAGEERROR");
1556
+ const isRequestFailed = line.includes("REQUESTFAILED");
1557
+ const isHttpError = isHttpErrorDiagnostic(line);
1558
+ const isNavigationFailure = line.includes("[FrameCapture:ERROR] page.goto failed");
1559
+ const isConsoleError = line.includes("[error]");
1560
+ if (isPageError) pageErrors++;
1561
+ if (isRequestFailed) requestFailed++;
1562
+ if (isHttpError) httpErrors++;
1563
+ if (line.includes("[FrameCapture:NAV] page.goto start")) navigationStarts++;
1564
+ if (isNavigationFailure) navigationFailures++;
1565
+ if (isConsoleError) consoleErrors++;
1566
+ if (line.includes("[warn]")) consoleWarnings++;
1567
+ if (line.includes("ERROR") && !isPageError && !isRequestFailed && !isHttpError && !isNavigationFailure && !isConsoleError) {
1568
+ errors++;
1569
+ }
1570
+ }
1571
+ return {
1572
+ total: lines.length,
1573
+ errors,
1574
+ pageErrors,
1575
+ requestFailed,
1576
+ httpErrors,
1577
+ navigationStarts,
1578
+ navigationFailures,
1579
+ consoleErrors,
1580
+ consoleWarnings
1581
+ };
1582
+ }
1583
+ var RenderObservabilityRecorder = class {
1584
+ constructor(input) {
1585
+ this.input = input;
1586
+ }
1587
+ input;
1588
+ events = [];
1589
+ eventCount = 0;
1590
+ failedPhase;
1591
+ checkpoint(phase, message, data) {
1592
+ return this.record({
1593
+ phase,
1594
+ status: "checkpoint",
1595
+ elapsedMs: Date.now() - this.input.pipelineStartMs,
1596
+ message: sanitizeObservationMessage(message),
1597
+ data: sanitizeObservationData(data)
1598
+ });
1599
+ }
1600
+ stageStart(phase, data) {
1601
+ this.record({
1602
+ phase,
1603
+ status: "start",
1604
+ elapsedMs: Date.now() - this.input.pipelineStartMs,
1605
+ data: sanitizeObservationData(data)
1606
+ });
1607
+ return Date.now();
1608
+ }
1609
+ stageEnd(phase, startedAtMs, data) {
1610
+ this.record({
1611
+ phase,
1612
+ status: "end",
1613
+ elapsedMs: Date.now() - this.input.pipelineStartMs,
1614
+ durationMs: Date.now() - startedAtMs,
1615
+ data: sanitizeObservationData(data)
1616
+ });
1617
+ }
1618
+ stageError(phase, startedAtMs, error, data) {
1619
+ this.failedPhase = phase;
1620
+ this.record({
1621
+ phase,
1622
+ status: "error",
1623
+ elapsedMs: Date.now() - this.input.pipelineStartMs,
1624
+ durationMs: Date.now() - startedAtMs,
1625
+ message: sanitizeObservationMessage(normalizeErrorMessage(error)),
1626
+ data: sanitizeObservationData(data)
1627
+ });
1628
+ }
1629
+ summary(input) {
1630
+ const lastEvent = this.events[this.events.length - 1];
1631
+ return {
1632
+ renderJobId: this.input.renderJobId,
1633
+ compositionHash: input.compositionHash,
1634
+ events: this.events.slice(),
1635
+ eventCount: this.eventCount,
1636
+ lastEvent,
1637
+ failedPhase: this.failedPhase,
1638
+ browserDiagnostics: summarizeBrowserDiagnostics(input.lastBrowserConsole),
1639
+ capture: { ...input.capture },
1640
+ extraction: input.extraction ? { ...input.extraction } : void 0,
1641
+ init: summarizeInitObservability(input.lastBrowserConsole, input.initFallback)
1642
+ };
1643
+ }
1644
+ hasFailure() {
1645
+ return this.failedPhase !== void 0;
1646
+ }
1647
+ /** A phase failure that was subsequently recovered (e.g. the drawElement
1648
+ * self-verify fallback re-rendering via screenshot) should not brand the
1649
+ * whole render as failed in the summary. */
1650
+ clearFailure(phase) {
1651
+ if (this.failedPhase === phase) this.failedPhase = void 0;
1652
+ }
1653
+ record(event) {
1654
+ this.eventCount++;
1655
+ const eventWithJob = { ...event, renderJobId: this.input.renderJobId };
1656
+ this.events.push(eventWithJob);
1657
+ if (this.events.length > MAX_EVENTS) {
1658
+ this.events.shift();
1659
+ }
1660
+ this.input.log.info("[Render:trace]", {
1661
+ renderJobId: eventWithJob.renderJobId,
1662
+ phase: eventWithJob.phase,
1663
+ status: eventWithJob.status,
1664
+ elapsedMs: eventWithJob.elapsedMs,
1665
+ durationMs: eventWithJob.durationMs,
1666
+ message: eventWithJob.message,
1667
+ ...eventWithJob.data
1668
+ });
1669
+ return eventWithJob;
1670
+ }
1671
+ };
1445
1672
  var HEARTBEAT_RAMP_MS = [3e4, 6e4, 12e4];
1446
1673
  var HEARTBEAT_REPEAT_MS = 12e4;
1447
1674
  var HEARTBEAT_RAMP_END_MS = HEARTBEAT_RAMP_MS[HEARTBEAT_RAMP_MS.length - 1] ?? HEARTBEAT_REPEAT_MS;
1675
+ function heartbeatTargetMs(index) {
1676
+ const rampTarget = HEARTBEAT_RAMP_MS[index];
1677
+ if (rampTarget !== void 0) return rampTarget;
1678
+ const overflow = index - HEARTBEAT_RAMP_MS.length + 1;
1679
+ return HEARTBEAT_RAMP_END_MS + overflow * HEARTBEAT_REPEAT_MS;
1680
+ }
1681
+ var RenderStageStalledError = class extends CaptureFailure2 {
1682
+ code = "RENDER_STAGE_STALLED";
1683
+ owner = "system";
1684
+ retryable = false;
1685
+ constructor(phase, silentMs, progress) {
1686
+ super({
1687
+ kind: "protocol_timeout",
1688
+ message: `[Render] ${phase} stalled: no progress for ${silentMs}ms (stuck at ${progress}).`
1689
+ });
1690
+ this.name = "RenderStageStalledError";
1691
+ }
1692
+ };
1693
+ var STALL_POLL_MS = 5e3;
1694
+ function watchStall(phase, rule) {
1695
+ let last = rule.progress();
1696
+ let budgetMs = rule.firstProgressMs;
1697
+ let lastChangeAt = Date.now();
1698
+ let attempt;
1699
+ let timer;
1700
+ const restartWindow = (ms) => {
1701
+ last = rule.progress();
1702
+ budgetMs = ms;
1703
+ lastChangeAt = Date.now();
1704
+ };
1705
+ const tripped = new Promise((_, reject) => {
1706
+ timer = setInterval(
1707
+ () => {
1708
+ const current = rule.progress();
1709
+ if (current !== last) return restartWindow(rule.noProgressMs);
1710
+ if (Date.now() - lastChangeAt < budgetMs) return;
1711
+ const error = new RenderStageStalledError(phase, budgetMs, current);
1712
+ if (attempt && !attempt.signal.aborted) {
1713
+ attempt.abort(error);
1714
+ return restartWindow(rule.noProgressMs);
1715
+ }
1716
+ clearInterval(timer);
1717
+ reject(error);
1718
+ },
1719
+ Math.min(STALL_POLL_MS, rule.noProgressMs, rule.firstProgressMs)
1720
+ );
1721
+ });
1722
+ const openAttempt = (parent) => {
1723
+ attempt = new AbortController();
1724
+ restartWindow(rule.firstProgressMs);
1725
+ return parent ? AbortSignal.any([parent, attempt.signal]) : attempt.signal;
1726
+ };
1727
+ const stalledBy = () => attempt?.signal.aborted ? attempt.signal.reason : void 0;
1728
+ return { tripped, openAttempt, stalledBy, stop: () => clearInterval(timer) };
1729
+ }
1730
+ var openUnwatchedAttempt = (parent) => parent ?? new AbortController().signal;
1731
+ async function observeRenderStage(recorder, phase, data, fn, options = {}) {
1732
+ const heartbeatMessage = options.heartbeatMessage ?? "stage still running";
1733
+ const startedAt = recorder.stageStart(phase, data);
1734
+ let heartbeatCount = 0;
1735
+ let lastFiredAtMs = 0;
1736
+ let heartbeatTimer;
1737
+ const scheduleNextHeartbeat = () => {
1738
+ const targetMs = heartbeatTargetMs(heartbeatCount);
1739
+ heartbeatTimer = setTimeout(() => {
1740
+ lastFiredAtMs = targetMs;
1741
+ heartbeatCount += 1;
1742
+ recorder.checkpoint(phase, heartbeatMessage, {
1743
+ ...data,
1744
+ heartbeatIndex: heartbeatCount,
1745
+ stageElapsedMs: Date.now() - startedAt
1746
+ });
1747
+ scheduleNextHeartbeat();
1748
+ }, targetMs - lastFiredAtMs);
1749
+ heartbeatTimer.unref?.();
1750
+ };
1751
+ scheduleNextHeartbeat();
1752
+ const stall = options.stall ? watchStall(phase, options.stall) : void 0;
1753
+ const clearHeartbeats = () => {
1754
+ clearTimeout(heartbeatTimer);
1755
+ stall?.stop();
1756
+ };
1757
+ try {
1758
+ const run = fn(stall?.openAttempt ?? openUnwatchedAttempt);
1759
+ const result = await (stall ? Promise.race([run, stall.tripped]) : run);
1760
+ clearHeartbeats();
1761
+ recorder.stageEnd(phase, startedAt, data);
1762
+ return result;
1763
+ } catch (caught) {
1764
+ const error = stall?.stalledBy() ?? caught;
1765
+ clearHeartbeats();
1766
+ recorder.stageError(phase, startedAt, error, data);
1767
+ throw error;
1768
+ }
1769
+ }
1448
1770
 
1449
1771
  // src/services/render/videoFrameCoverage.ts
1450
1772
  import { parseHTML } from "linkedom";
@@ -1506,6 +1828,8 @@ import {
1506
1828
  readDeclaredDefaults,
1507
1829
  parseHostVariableValues,
1508
1830
  headStyleRuns,
1831
+ isAlwaysAppliedStyle,
1832
+ dedupeFontFaceRules,
1509
1833
  inlineScriptRuns,
1510
1834
  parsesAsScript,
1511
1835
  styleElementsFor,
@@ -1798,12 +2122,12 @@ function resolveClipWindowSeconds(img, compositionDuration) {
1798
2122
  }
1799
2123
  return null;
1800
2124
  }
1801
- function resolvePreparedPlayback(metadata, loop, windowSeconds) {
2125
+ function resolvePreparedPlayback(metadata, loopOverride, windowSeconds) {
1802
2126
  const gifDuration = metadata.durationSeconds > 0 ? metadata.durationSeconds : null;
1803
2127
  let loopIterations = 1;
1804
- if (loop) {
2128
+ if (resolveLoop(metadata, loopOverride)) {
1805
2129
  loopIterations = windowSeconds != null && gifDuration != null ? Math.min(MAX_LOOP_ITERATIONS, Math.max(1, Math.ceil(windowSeconds / gifDuration))) : 1;
1806
- } else if (metadata.loopCount != null && metadata.loopCount > 1) {
2130
+ } else if (loopOverride == null && metadata.loopCount != null && metadata.loopCount > 1) {
1807
2131
  loopIterations = Math.min(MAX_LOOP_ITERATIONS, metadata.loopCount);
1808
2132
  }
1809
2133
  let padSeconds = 0;
@@ -1970,9 +2294,14 @@ async function prepareAnimatedGifInputs(html, options) {
1970
2294
  const bytes = readFileSync2(sourcePath);
1971
2295
  const metadata = parseAnimatedGifMetadata(bytes);
1972
2296
  if (!metadata?.animated) continue;
1973
- const loop = resolveLoop(metadata, readLoopOverride(img));
2297
+ const loopOverride = readLoopOverride(img);
2298
+ const loop = resolveLoop(metadata, loopOverride);
1974
2299
  const windowSeconds = resolveClipWindowSeconds(img, compositionDuration);
1975
- const { loopIterations, padSeconds } = resolvePreparedPlayback(metadata, loop, windowSeconds);
2300
+ const { loopIterations, padSeconds } = resolvePreparedPlayback(
2301
+ metadata,
2302
+ loopOverride,
2303
+ windowSeconds
2304
+ );
1976
2305
  const hash = computePreparedGifHash(bytes, loopIterations, padSeconds);
1977
2306
  const filename = `${CACHE_SCHEMA}-${hash.slice(0, 24)}.webm`;
1978
2307
  const cachePath = join4(cacheDir, filename);
@@ -2638,6 +2967,11 @@ function coalesceHeadStylesAndBodyScripts(html) {
2638
2967
  const head = document2.querySelector("head");
2639
2968
  const body = document2.querySelector("body");
2640
2969
  if (!head && !body) return html;
2970
+ const liveStyles = [...document2.querySelectorAll("style")].filter(isAlwaysAppliedStyle);
2971
+ const dedupedStyles = dedupeFontFaceRules(liveStyles.map((el) => el.textContent || ""));
2972
+ liveStyles.forEach((el, i) => {
2973
+ el.textContent = dedupedStyles[i] ?? "";
2974
+ });
2641
2975
  const styleEls = head ? Array.from(head.querySelectorAll("style")) : [];
2642
2976
  const importRe = /@import\s+url\([^)]*\)\s*;|@import\s+["'][^"']+["']\s*;/gi;
2643
2977
  for (const run of styleEls.length > 1 ? headStyleRuns(styleEls) : []) {
@@ -3477,9 +3811,10 @@ async function discoverMediaFromBrowser(page) {
3477
3811
  const tagName = isImage ? "image" : htmlEl.tagName.toLowerCase() === "video" ? "video" : "audio";
3478
3812
  const id = htmlEl.getAttribute("data-hf-render-id") || htmlEl.id || (isImage ? autoImageIds.get(htmlEl) : void 0);
3479
3813
  if (!id) return;
3480
- const ownSrc = htmlEl.getAttribute("src");
3481
- const blankSrc = ownSrc !== null && ownSrc.trim() === "" && !htmlEl.querySelector("source[src]");
3482
- const src = blankSrc ? "" : htmlEl.currentSrc || htmlEl.src || ownSrc || "";
3814
+ const resolvedSrc = htmlEl.currentSrc || htmlEl.src || htmlEl.getAttribute("src") || "";
3815
+ const withoutQueryAndHash = (url) => url.replace(/[?#][\s\S]*$/, "");
3816
+ const isPageItself = withoutQueryAndHash(resolvedSrc) === withoutQueryAndHash(location.href);
3817
+ const src = isPageItself ? "" : resolvedSrc;
3483
3818
  const start = parseFloat(htmlEl.getAttribute("data-start") || "0");
3484
3819
  const endRaw = htmlEl.getAttribute("data-end");
3485
3820
  const durationRaw = htmlEl.getAttribute("data-duration");
@@ -5793,6 +6128,7 @@ async function runCaptureStage(input) {
5793
6128
  interleaveFrames: input.interleaveFrames,
5794
6129
  dedupPerfs,
5795
6130
  pageErrorLines: workerPageErrors,
6131
+ openAttempt: input.openAttempt,
5796
6132
  onProgress: (progress) => {
5797
6133
  if (progress.latestWorkerPhase) {
5798
6134
  if (progress.capturedFrames === 0) reportWorkerStartup(job, progress, onProgress);
@@ -5864,6 +6200,11 @@ async function runSequentialDiskCapture(input, captureOptions, captureCfg, check
5864
6200
  let transientRetriesUsed = 0;
5865
6201
  let resumeFrom = 0;
5866
6202
  while (true) {
6203
+ const attempt = input.openAttempt?.();
6204
+ const assertAttemptActive = () => {
6205
+ assertNotAborted();
6206
+ attempt?.throwIfAborted();
6207
+ };
5867
6208
  const videoInjector = createRenderVideoFrameInjector();
5868
6209
  const session = probeSession ?? await createCaptureSession5(
5869
6210
  fileServer.url,
@@ -5879,10 +6220,10 @@ async function runSequentialDiskCapture(input, captureOptions, captureCfg, check
5879
6220
  probeSession = null;
5880
6221
  }
5881
6222
  await ensureSessionReady(session);
5882
- assertNotAborted();
6223
+ assertAttemptActive();
5883
6224
  lastBrowserConsole = session.browserConsoleBuffer;
5884
6225
  await captureSessionFrames(session, {
5885
- input,
6226
+ input: { ...input, assertNotAborted: assertAttemptActive },
5886
6227
  rangeStart,
5887
6228
  rangeEnd,
5888
6229
  resumeFrom,
@@ -6566,7 +6907,17 @@ function isTransientCaptureRetryEligible(failureKind, missing, retriesUsed) {
6566
6907
  function captureAttemptMadeProgress(attemptTargetFrameCount, remainingFrameCount) {
6567
6908
  return remainingFrameCount < attemptTargetFrameCount;
6568
6909
  }
6910
+ var FIRST_FRAME_STALL_MS = 3e5;
6911
+ function frameStallRule(job) {
6912
+ const noProgressMs = resolveCaptureStallTimeoutMs();
6913
+ return {
6914
+ progress: () => job.framesRendered ?? 0,
6915
+ noProgressMs,
6916
+ firstProgressMs: Math.max(FIRST_FRAME_STALL_MS, noProgressMs)
6917
+ };
6918
+ }
6569
6919
  function isRecoverableParallelCaptureError(error) {
6920
+ if (error instanceof RenderStageStalledError) return true;
6570
6921
  const message = normalizeErrorMessage(error);
6571
6922
  if (!message.includes("[Parallel] Capture failed")) return false;
6572
6923
  const kind = classifyCaptureFailure3(error).kind;
@@ -6618,8 +6969,10 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
6618
6969
  )
6619
6970
  ];
6620
6971
  options.dedupPerfs.length = 0;
6972
+ let attemptSignal = options.abortSignal;
6621
6973
  try {
6622
6974
  for (const tasks of batches) {
6975
+ attemptSignal = options.openAttempt?.(options.abortSignal) ?? options.abortSignal;
6623
6976
  const capturedBeforeBatch = countCapturedFrames(
6624
6977
  options.totalFrames,
6625
6978
  options.framesDir,
@@ -6632,7 +6985,7 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
6632
6985
  tasks,
6633
6986
  options.captureOptions,
6634
6987
  options.createBeforeCaptureHook,
6635
- options.abortSignal,
6988
+ attemptSignal,
6636
6989
  options.onProgress ? (progress) => {
6637
6990
  options.onProgress?.({
6638
6991
  ...progress,
@@ -6680,7 +7033,8 @@ async function executeDiskCaptureWithAdaptiveRetry(options) {
6680
7033
  currentWorkers = nextWorkers;
6681
7034
  missingRanges = remaining;
6682
7035
  attempt++;
6683
- } catch (error) {
7036
+ } catch (caught) {
7037
+ const error = attemptSignal?.aborted && !options.abortSignal?.aborted ? attemptSignal.reason : caught;
6684
7038
  const failure = classifyCaptureFailure3(error, { signal: options.abortSignal });
6685
7039
  if (failure.kind === "cancelled") {
6686
7040
  throw error;
@@ -11091,29 +11445,36 @@ async function renderChunk(planDir, chunkIndex, outputChunkPath, options) {
11091
11445
  const captureSession = session;
11092
11446
  session = null;
11093
11447
  overlap.current = canEncodeWhileCapturing(encodeInput, chunkWorkerCount, cpus().length) ? encodeWhileCapturing(encodeInput, framesInChunk) : null;
11094
- await runCaptureStage({
11095
- fileServer,
11096
- workDir,
11097
- framesDir,
11098
- job,
11099
- totalFrames: framesInChunk,
11100
- cfg: captureCfg,
11101
- plan: capturePlan,
11102
- log,
11103
- probeSession: captureSession,
11104
- captureAttempts,
11105
- // This sink records each worker's effective capture mode so a
11106
- // fallback remains observable to adapters and smoke tests.
11107
- dedupPerfs: capturePerfs,
11108
- buildCaptureOptions: () => captureOptions,
11109
- createRenderVideoFrameInjector: createChunkVideoFrameInjector,
11110
- abortSignal: void 0,
11111
- assertNotAborted: () => {
11112
- },
11113
- frameRange: range,
11114
- onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame),
11115
- interleaveFrames: interleavesCapture(overlap.current !== null, options)
11116
- }).catch(async (captureError) => {
11448
+ await observeRenderStage(
11449
+ new RenderObservabilityRecorder({ pipelineStartMs: start, log }),
11450
+ "capture_chunk",
11451
+ { chunkIndex },
11452
+ (openAttempt) => runCaptureStage({
11453
+ fileServer,
11454
+ workDir,
11455
+ framesDir,
11456
+ job,
11457
+ totalFrames: framesInChunk,
11458
+ cfg: captureCfg,
11459
+ plan: capturePlan,
11460
+ log,
11461
+ probeSession: captureSession,
11462
+ captureAttempts,
11463
+ // This sink records each worker's effective capture mode so a
11464
+ // fallback remains observable to adapters and smoke tests.
11465
+ dedupPerfs: capturePerfs,
11466
+ buildCaptureOptions: () => captureOptions,
11467
+ createRenderVideoFrameInjector: createChunkVideoFrameInjector,
11468
+ abortSignal: void 0,
11469
+ assertNotAborted: () => {
11470
+ },
11471
+ frameRange: range,
11472
+ onFrameLanded: (frame) => overlap.current?.onFrameLanded(frame),
11473
+ interleaveFrames: interleavesCapture(overlap.current !== null, options),
11474
+ openAttempt
11475
+ }),
11476
+ { stall: frameStallRule(job) }
11477
+ ).catch(async (captureError) => {
11117
11478
  await overlap.current?.abort();
11118
11479
  overlap.current = null;
11119
11480
  throw captureError;
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": "0.1.0",
3
3
  "buildId": "dev",
4
- "sha256": "93402c78fff7cdcf247d971840896d2b0d3380fa1d0901f31618ba6f62160667",
4
+ "sha256": "5eb70bc91857dda413466347acea0d0d51a916f954755c7294546bb4e0465065",
5
5
  "artifacts": {
6
6
  "iife": "hyperframe.runtime.iife.js",
7
7
  "esm": "hyperframe.runtime.mjs"