@hyperframes/engine 0.8.199 → 0.8.201

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.
@@ -94,11 +94,11 @@ export function formatFfmpegError(exitCode, stderr, tailLines = DEFAULT_STDERR_T
94
94
  ? `FFmpeg exited with code ${exitCode}\nffmpeg stderr (tail):\n${tail}`
95
95
  : `FFmpeg exited with code ${exitCode}`;
96
96
  }
97
- /** formatFfmpegError, led by the timeout when this process was killed at its deadline. */
97
+ /** formatFfmpegError, led by the timeout when this process was killed for making no progress. */
98
98
  export function describeFfmpegFailure(result, timeoutMs) {
99
99
  const error = formatFfmpegError(result.exitCode, result.stderr);
100
- if (result.terminationReason === "deadline") {
101
- return (`FFmpeg timed out after ${timeoutMs} ms (ffmpegProcessTimeout; long renders can raise ` +
100
+ if (result.terminationReason === "inactivity") {
101
+ return (`FFmpeg timed out after ${timeoutMs} ms without progress (ffmpegProcessTimeout, ` +
102
102
  `FFMPEG_PROCESS_TIMEOUT_MS). ${error}`);
103
103
  }
104
104
  if (result.exitCode !== null)
@@ -147,25 +147,22 @@ const ABORTED_BEFORE_START = {
147
147
  export async function runFfmpeg(args, opts) {
148
148
  if (opts?.signal?.aborted)
149
149
  return { ...ABORTED_BEFORE_START };
150
- const stallTimeout = opts?.stallTimeout;
151
150
  let framesWritten = 0;
152
- const readStats = ffmpegStatsReader(({ frames }) => {
153
- if (frames === undefined || frames <= framesWritten)
151
+ let secondsWritten = 0;
152
+ const readStats = ffmpegStatsReader(({ frames = 0, seconds = 0 }) => {
153
+ if (frames <= framesWritten && seconds <= secondsWritten)
154
154
  return;
155
- framesWritten = frames;
155
+ framesWritten = Math.max(frames, framesWritten);
156
+ secondsWritten = Math.max(seconds, secondsWritten);
156
157
  managed.markActivity();
157
158
  });
158
159
  const managed = new ManagedChildProcess(spawnFfmpeg(args), {
159
160
  signal: opts?.signal,
160
- ...(stallTimeout === undefined
161
- ? { deadlineAtMs: Date.now() + (opts?.timeout ?? DEFAULT_TIMEOUT) }
162
- : { inactivityTimeoutMs: stallTimeout }),
163
- onStderr: stallTimeout === undefined
164
- ? opts?.onStderr
165
- : (chunk) => {
166
- readStats(chunk);
167
- opts?.onStderr?.(chunk);
168
- },
161
+ inactivityTimeoutMs: opts?.timeout ?? DEFAULT_TIMEOUT,
162
+ onStderr: (chunk) => {
163
+ readStats(chunk);
164
+ opts?.onStderr?.(chunk);
165
+ },
169
166
  });
170
167
  return toResult(await managed.wait());
171
168
  }
@@ -586,7 +586,7 @@ function checksumMismatchError(source, status, telemetry) {
586
586
  // Response protocol, streamed byte accounting, hashes, and payload validation
587
587
  // share one lifecycle so no validation can happen after publication.
588
588
  // fallow-ignore-next-line complexity
589
- async function fetchToPartial(url, partialPath, controller, options) {
589
+ async function fetchToPartial(url, partialPath, controller, options, onSizedBodyBytes) {
590
590
  const { response, finalUrl } = await fetchWithValidatedRedirects(url, controller);
591
591
  const finalIdentity = safeDownloadUrlIdentity(finalUrl);
592
592
  const rangeDisposition = classifyRangeDisposition(response);
@@ -643,6 +643,8 @@ async function fetchToPartial(url, partialPath, controller, options) {
643
643
  const inspector = new Transform({
644
644
  transform(chunk, _encoding, callback) {
645
645
  const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
646
+ if (declaredLength !== undefined)
647
+ onSizedBodyBytes();
646
648
  receivedBytes += bytes.length;
647
649
  sha256.update(bytes);
648
650
  md5.update(bytes);
@@ -771,16 +773,26 @@ async function runDownloadAttempt(url, localPath, timeoutMs, attempt, options, s
771
773
  controller.abort();
772
774
  };
773
775
  signal?.addEventListener("abort", onCallerAbort, { once: true });
774
- const timeoutId = setTimeout(() => {
775
- timedOut = true;
776
- controller.abort();
777
- }, timeoutMs);
776
+ let timeoutId;
777
+ const armTimeout = () => {
778
+ clearTimeout(timeoutId);
779
+ timeoutId = setTimeout(() => {
780
+ timedOut = true;
781
+ controller.abort();
782
+ }, timeoutMs);
783
+ };
784
+ armTimeout();
785
+ let sizedBodyStarted = false;
786
+ const extendTimeout = () => {
787
+ sizedBodyStarted = true;
788
+ armTimeout();
789
+ };
778
790
  const identity = safeDownloadUrlIdentity(url);
779
791
  try {
780
792
  if (callerAborted) {
781
793
  throw new UrlDownloadError("cancelled", false, "Download cancelled");
782
794
  }
783
- const integrity = await fetchToPartial(url, partialPath, controller, options);
795
+ const integrity = await fetchToPartial(url, partialPath, controller, options, extendTimeout);
784
796
  let outcome = syncAndPublishPartial(partialPath, localPath);
785
797
  let publishedIntegrity = integrity;
786
798
  if (outcome === "race_reused") {
@@ -827,7 +839,9 @@ async function runDownloadAttempt(url, localPath, timeoutMs, attempt, options, s
827
839
  throw classified;
828
840
  }
829
841
  if (timedOut) {
830
- const classified = new UrlDownloadError("timeout", true, `Download timeout after ${timeoutMs / 1000}s`);
842
+ const classified = new UrlDownloadError("timeout", true, sizedBodyStarted
843
+ ? `Download stalled: no data for ${timeoutMs / 1000}s`
844
+ : `Download timeout after ${timeoutMs / 1000}s`);
831
845
  emitDownloadTelemetry(options, {
832
846
  urlFingerprint: identity.urlFingerprint,
833
847
  initialHost: identity.host,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/engine",
3
- "version": "0.8.199",
3
+ "version": "0.8.201",
4
4
  "description": "Seekable web page to video rendering engine (Puppeteer + FFmpeg)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -50,8 +50,8 @@
50
50
  "linkedom": "^0.18.12",
51
51
  "puppeteer": "^25.8.0",
52
52
  "puppeteer-core": "^25.8.0",
53
- "@hyperframes/core": "^0.8.199",
54
- "@hyperframes/parsers": "^0.8.199"
53
+ "@hyperframes/core": "^0.8.201",
54
+ "@hyperframes/parsers": "^0.8.201"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/node": "^25.0.10",