@hyperframes/engine 0.8.183 → 0.8.185

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/index.d.ts CHANGED
@@ -53,7 +53,7 @@ export { MIXED_AUDIO_FILENAME, parseAudioElements, parseMasterFxChain, processCo
53
53
  export { audioFailureDiagnostics } from "./services/audioFailureDiagnostic.js";
54
54
  export { cloneCaptureWarning, cloneCaptureWarnings } from "./services/captureWarning.js";
55
55
  export type { AudioElement, AudioFailureReason, AudioFailureStage, AudioProcessingFailure, AudioFailureDiagnostic, AudioSubprocessEvidence, AudioTrack, AudioVolumeKeyframe, MixResult, } from "./services/audioMixer.types.js";
56
- export { calculateOptimalWorkers, computeWorkerSizing, selectVerifySampleIndicesForTask, verifyDiskDrawElementSamples, distributeFrames, distributeFramesInterleaved, executeParallelCapture, mergeWorkerFrames, getSystemResources, shouldDisableBrowserPoolForParallelWorker, type WorkerTask, type WorkerResult, type WorkerSizing, type WorkerSizingBound, type ParallelProgress, } from "./services/parallelCoordinator.js";
56
+ export { calculateOptimalWorkers, computeWorkerSizing, selectVerifySampleIndicesForTask, verifyDiskDrawElementSamples, distributeFrames, distributeFramesInterleaved, executeParallelCapture, mergeWorkerFrames, getSystemResources, shouldDisableBrowserPoolForParallelWorker, type WorkerTask, type WorkerResult, type WorkerSizing, type WorkerSizingBound, type LandedFrame, type ParallelProgress, } from "./services/parallelCoordinator.js";
57
57
  export { createFileServer, type FileServerOptions, type FileServerHandle, } from "./services/fileServer.js";
58
58
  export { quantizeTimeToFrame, MEDIA_VISUAL_STYLE_PROPERTIES } from "@hyperframes/core";
59
59
  export { frameFileExtension } from "./services/frameCapture.js";
@@ -364,11 +364,7 @@ async function probeBeginFrameSupport(browser, timeoutMs = BEGINFRAME_PROBE_TIME
364
364
  };
365
365
  }
366
366
  if (page && !(await settleWithin(page.close(), BEGINFRAME_PROBE_CLEANUP_TIMEOUT_MS))) {
367
- return {
368
- supported: false,
369
- detail: `${result.detail}; probe page cleanup timed out`,
370
- durationMs: Date.now() - started,
371
- };
367
+ return { ...result, detail: `${result.detail}; probe page cleanup timed out` };
372
368
  }
373
369
  return result;
374
370
  }
@@ -1498,8 +1498,8 @@ export function buildLiveMapWarning(libraries) {
1498
1498
  message: `Live map viewport(s) detected in the composition (${libraries.join(", ")}). ` +
1499
1499
  `Map tiles load over the network at render time, which the deterministic-render ` +
1500
1500
  `contract forbids: frames captured before tiles arrive ship a blank or partial map ` +
1501
- `with no error. Bake the map to a video first (see the motion-graphics maps skill / ` +
1502
- `bake-basemap.mjs) and use the baked file as the imagery layer.`,
1501
+ `with no error. Bake the map to a video first (the hyperframes skill's ` +
1502
+ `scripts/bake-basemap.mjs, see references/film.md) and use the baked file as the imagery layer.`,
1503
1503
  details: { sources: [...libraries] },
1504
1504
  };
1505
1505
  }
@@ -55,6 +55,13 @@ export interface ParallelProgress {
55
55
  workerProgress: Map<number, number>;
56
56
  /** Latest lifecycle transition; absent on ordinary completed-frame updates. */
57
57
  latestWorkerPhase?: ParallelWorkerPhaseDiagnostic;
58
+ /** Disk capture only: the frame file this update finished writing, readable from now on. */
59
+ landedFrame?: LandedFrame;
60
+ }
61
+ /** A captured frame file: `index` is its file index (`frame_<index>`), `path` where it was written. */
62
+ export interface LandedFrame {
63
+ index: number;
64
+ path: string;
58
65
  }
59
66
  export type ParallelWorkerPhase = "browser_launch" | "browser_probe" | "session_init" | "frame_capture" | "frame_encode";
60
67
  export interface ParallelWorkerPhaseDiagnostic {
@@ -400,16 +400,18 @@ async function captureFrameRange(session, task, captureOptions, signal, onFrameC
400
400
  throw new Error("Parallel worker cancelled");
401
401
  const time = (i * captureOptions.fps.den) / captureOptions.fps.num;
402
402
  const fileFrameIdx = i - outputOffset;
403
+ let landedFrame;
403
404
  if (onFrameBuffer) {
404
405
  const { buffer } = await runCaptureOperation("frame_capture", i, () => captureFrameToBuffer(session, fileFrameIdx, time));
405
406
  await onFrameBuffer(i, buffer, session);
406
407
  }
407
408
  else {
408
- await runCaptureOperation("frame_capture", i, () => captureFrame(session, fileFrameIdx, time));
409
+ const { path } = await runCaptureOperation("frame_capture", i, () => captureFrame(session, fileFrameIdx, time));
410
+ landedFrame = { index: fileFrameIdx, path };
409
411
  }
410
412
  framesCaptured++;
411
413
  if (onFrameCaptured)
412
- onFrameCaptured(task.workerId, i);
414
+ onFrameCaptured(task.workerId, i, landedFrame);
413
415
  }
414
416
  return framesCaptured;
415
417
  }
@@ -722,7 +724,7 @@ export async function executeParallelCapture(serverUrl, workDir, tasks, captureO
722
724
  const workerPhases = new Map();
723
725
  for (const task of tasks)
724
726
  workerProgress.set(task.workerId, 0);
725
- const onFrameCaptured = (workerId, _frameIndex) => {
727
+ const onFrameCaptured = (workerId, _frameIndex, landedFrame) => {
726
728
  const current = workerProgress.get(workerId) || 0;
727
729
  workerProgress.set(workerId, current + 1);
728
730
  if (onProgress) {
@@ -732,6 +734,7 @@ export async function executeParallelCapture(serverUrl, workDir, tasks, captureO
732
734
  capturedFrames,
733
735
  activeWorkers: tasks.length,
734
736
  workerProgress: new Map(workerProgress),
737
+ ...(landedFrame && { landedFrame }),
735
738
  });
736
739
  }
737
740
  };
@@ -13,6 +13,7 @@ import { createConcurrencyLimit } from "../utils/concurrencyLimit.js";
13
13
  import { parseHTML } from "linkedom";
14
14
  import { isPathInside } from "@hyperframes/parsers/asset-paths";
15
15
  import { resolveProjectRelativeSrc } from "@hyperframes/parsers/asset-resolution";
16
+ import { alphaDecoderFor } from "@hyperframes/parsers/media-alpha";
16
17
  import { MEDIA_RENDER_ID_ATTR, isAudibleVideoElement, fpsToFfmpegArg, fpsToNumber, MEDIA_DURATION_CLAMP_EPSILON_SECONDS, normalizeRateSpec, readElementRateSpec, shiftRateLane, sourceTimeAt, timeAtSourceTime, exportClipWindow, hasClipStarted, isInClipWindow, parseStrictFiniteTimingNumber, readMediaStart, toFps, firstFrameColourArgs, hdrToSdrToneMapFilter, parseFirstFrameColour, } from "@hyperframes/core";
17
18
  import { resolveReferencedStart } from "./referenceResolver.js";
18
19
  import { isKnownInactiveTimelineWindow } from "./mediaTimelineWindow.js";
@@ -376,7 +377,7 @@ export function parseVideoElements(html) {
376
377
  mediaStart: readMediaStart(el),
377
378
  playbackRate: readElementRateSpec(el),
378
379
  loop: el.hasAttribute("loop"),
379
- hasAudio: hasAudioAttr === "true",
380
+ hasAudio: hasAudioAttr === "true" && isAudibleVideoElement(el),
380
381
  ...(isSelfOrAncestorHidden(el) ||
381
382
  (isAudibleVideoElement(el) && isMemberGroupHidden(groupsById, el))
382
383
  ? { hidden: true }
@@ -958,12 +959,7 @@ export function codecMayHaveAlpha(codec) {
958
959
  return ALPHA_CAPABLE_CODECS.has((codec ?? "").toLowerCase());
959
960
  }
960
961
  export function decoderForCodec(codec) {
961
- const c = (codec ?? "").toLowerCase();
962
- if (c === "vp9")
963
- return "libvpx-vp9";
964
- if (c === "vp8")
965
- return "libvpx";
966
- return c;
962
+ return alphaDecoderFor(codec) ?? (codec ?? "").toLowerCase();
967
963
  }
968
964
  export function resolveFrameFormat(metadata, requested) {
969
965
  if (metadata.hasAlpha || codecMayHaveAlpha(metadata.videoCodec))
@@ -110,20 +110,6 @@ export declare function probeMediaProfile(filePath: string, options?: {
110
110
  signal?: AbortSignal;
111
111
  }): Promise<MediaProbeProfile>;
112
112
  export declare function extractPngMetadataFromBuffer(buf: Buffer): StillImageMetadata | null;
113
- /**
114
- * Does this pix_fmt carry an alpha channel?
115
- *
116
- * Exported so the test asserts the shipped predicate rather than a copy of
117
- * the pattern. Anchored at the start, matching studio-server's
118
- * mediaMetadata.ts: the previous inline pattern bound its `(^|[^a-z])` anchor
119
- * to the first alternative only — `|` is looser than concatenation — so the
120
- * guard was decorative for every other name. It also missed abgr, ya8/ya16
121
- * and ayuv64, and its `gray[a-z0-9]*a` branch matched only gray8a/gray16a,
122
- * names FFmpeg renamed to ya8/ya16 in 2013. A `ya8` grayscale-plus-alpha PNG
123
- * reported hasAlpha:false, resolveFrameFormat picked jpg, and the overlay
124
- * flattened to an opaque rectangle.
125
- */
126
- export declare function pixelFormatHasAlpha(pixelFormat: string): boolean;
127
113
  /**
128
114
  * Read an ffprobe tag case-insensitively. ffmpeg/libavformat versions disagree
129
115
  * on tag casing — VP9 alpha is `alpha_mode` in older builds and `ALPHA_MODE`
@@ -5,6 +5,7 @@ import { chunkCrc32 } from "./crc32.js";
5
5
  import { StringDecoder } from "node:string_decoder";
6
6
  import { basename } from "path";
7
7
  import { redactTelemetryString } from "@hyperframes/core";
8
+ import { videoStreamHasAlpha } from "@hyperframes/parsers/media-alpha";
8
9
  import { urlInputArgs } from "./urlInput.js";
9
10
  import { FFPROBE_PATH_ENV, getFfprobeBinary } from "./ffmpegBinaries.js";
10
11
  import { ManagedChildProcess } from "./managedChildProcess.js";
@@ -437,22 +438,6 @@ export function extractPngMetadataFromBuffer(buf) {
437
438
  }
438
439
  return width > 0 && height > 0 ? { width, height, colorSpace: colorSpaceFromCicp } : null;
439
440
  }
440
- /**
441
- * Does this pix_fmt carry an alpha channel?
442
- *
443
- * Exported so the test asserts the shipped predicate rather than a copy of
444
- * the pattern. Anchored at the start, matching studio-server's
445
- * mediaMetadata.ts: the previous inline pattern bound its `(^|[^a-z])` anchor
446
- * to the first alternative only — `|` is looser than concatenation — so the
447
- * guard was decorative for every other name. It also missed abgr, ya8/ya16
448
- * and ayuv64, and its `gray[a-z0-9]*a` branch matched only gray8a/gray16a,
449
- * names FFmpeg renamed to ya8/ya16 in 2013. A `ya8` grayscale-plus-alpha PNG
450
- * reported hasAlpha:false, resolveFrameFormat picked jpg, and the overlay
451
- * flattened to an opaque rectangle.
452
- */
453
- export function pixelFormatHasAlpha(pixelFormat) {
454
- return /^(?:yuva|rgba|argb|bgra|abgr|gbrap|ya|ayuv)/i.test(pixelFormat);
455
- }
456
441
  function extractStillImageMetadata(filePath) {
457
442
  try {
458
443
  return extractPngMetadataFromBuffer(readFileSync(filePath));
@@ -613,16 +598,7 @@ export async function extractMediaMetadata(filePath) {
613
598
  };
614
599
  const colorSpace = merged.colorTransfer || merged.colorPrimaries || merged.colorSpace ? merged : null;
615
600
  const pixelFormat = videoStream.pix_fmt || "";
616
- const alphaMode = readTagCI(videoStream.tags, "alpha_mode");
617
- const hasAlpha = pixelFormatHasAlpha(pixelFormat) || alphaMode === "1";
618
- // Anchored at the start, matching studio-server's mediaMetadata.ts. The
619
- // previous pattern bound its `(^|[^a-z])` anchor to the FIRST alternative
620
- // only — `|` is looser than concatenation — so the guard was decorative
621
- // for every other name. It also missed abgr, ya8/ya16 and ayuv64, and its
622
- // `gray[a-z0-9]*a` branch matched only gray8a/gray16a, names FFmpeg
623
- // renamed to ya8/ya16 in 2013. A `ya8` grayscale-plus-alpha PNG reported
624
- // hasAlpha:false, resolveFrameFormat picked jpg, and the overlay
625
- // flattened to an opaque rectangle.
601
+ const hasAlpha = videoStreamHasAlpha(videoStream);
626
602
  const containerDuration = output?.format.duration ? parseFloat(output.format.duration) : 0;
627
603
  const streamDuration = videoStream.duration ? parseFloat(videoStream.duration) : 0;
628
604
  const parsedStreamStart = videoStream.start_time ? parseFloat(videoStream.start_time) : 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/engine",
3
- "version": "0.8.183",
3
+ "version": "0.8.185",
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/parsers": "^0.8.183",
54
- "@hyperframes/core": "^0.8.183"
53
+ "@hyperframes/core": "^0.8.185",
54
+ "@hyperframes/parsers": "^0.8.185"
55
55
  },
56
56
  "devDependencies": {
57
57
  "@types/node": "^25.0.10",