@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.
- package/dist/distributed.js +396 -35
- package/dist/hyperframe.manifest.json +1 -1
- package/dist/hyperframe.runtime.iife.js +17 -17
- package/dist/index.js +196 -76
- package/dist/public-server.js +166 -53
- package/dist/services/render/observability.d.ts +23 -1
- package/dist/services/render/stages/captureStage.d.ts +3 -0
- package/dist/services/renderOrchestrator.d.ts +6 -1
- package/package.json +7 -7
package/dist/distributed.js
CHANGED
|
@@ -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,
|
|
2125
|
+
function resolvePreparedPlayback(metadata, loopOverride, windowSeconds) {
|
|
1802
2126
|
const gifDuration = metadata.durationSeconds > 0 ? metadata.durationSeconds : null;
|
|
1803
2127
|
let loopIterations = 1;
|
|
1804
|
-
if (
|
|
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
|
|
2297
|
+
const loopOverride = readLoopOverride(img);
|
|
2298
|
+
const loop = resolveLoop(metadata, loopOverride);
|
|
1974
2299
|
const windowSeconds = resolveClipWindowSeconds(img, compositionDuration);
|
|
1975
|
-
const { loopIterations, padSeconds } = resolvePreparedPlayback(
|
|
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
|
|
3481
|
-
const
|
|
3482
|
-
const
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
11095
|
-
|
|
11096
|
-
|
|
11097
|
-
|
|
11098
|
-
|
|
11099
|
-
|
|
11100
|
-
|
|
11101
|
-
|
|
11102
|
-
|
|
11103
|
-
|
|
11104
|
-
|
|
11105
|
-
|
|
11106
|
-
|
|
11107
|
-
|
|
11108
|
-
|
|
11109
|
-
|
|
11110
|
-
|
|
11111
|
-
|
|
11112
|
-
|
|
11113
|
-
|
|
11114
|
-
|
|
11115
|
-
|
|
11116
|
-
|
|
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": "
|
|
4
|
+
"sha256": "5eb70bc91857dda413466347acea0d0d51a916f954755c7294546bb4e0465065",
|
|
5
5
|
"artifacts": {
|
|
6
6
|
"iife": "hyperframe.runtime.iife.js",
|
|
7
7
|
"esm": "hyperframe.runtime.mjs"
|