@hyperframes/engine 0.9.25 → 0.9.27

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.
@@ -3,11 +3,12 @@
3
3
  * so long audio is encoded as frame-aligned segments in parallel and their frames joined.
4
4
  */
5
5
  import { execFile } from "node:child_process";
6
- import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
6
+ import { readFileSync, writeFileSync } from "node:fs";
7
7
  import { removeScratch } from "@hyperframes/core/remove-scratch";
8
8
  import { dirname, join } from "node:path";
9
9
  import { getFfmpegBinary } from "../utils/ffmpegBinaries.js";
10
10
  import { runFfmpeg } from "../utils/runFfmpeg.js";
11
+ import { createStagingDir } from "@hyperframes/core/atomic-file";
11
12
  const SAMPLE_RATE = 48_000;
12
13
  const FRAME_SAMPLES = 1024;
13
14
  const MIN_SEGMENT_SECONDS = 60;
@@ -78,7 +79,7 @@ export async function encodeAac(input, outputPath, totalDuration, { signal, time
78
79
  if (!(await supportsSettsDuration())) {
79
80
  return encodeInOnePass(input, outputPath, { signal, timeout, filter });
80
81
  }
81
- const dir = mkdtempSync(join(dirname(outputPath), ".aac-segments-"));
82
+ const dir = createStagingDir(dirname(outputPath), ".aac-segments-");
82
83
  try {
83
84
  const segments = planSegments(totalDuration, count).map((segment, k) => ({
84
85
  ...segment,
@@ -1,6 +1,6 @@
1
1
  // fallow-ignore-file complexity code-duplication
2
2
  import { isSelfOrAncestorHidden, memberGroupKey, isMemberGroupHidden } from "./mediaHidden.js";
3
- import { closeSync, existsSync, mkdirSync, mkdtempSync, openSync, rmSync, writeFileSync } from "fs";
3
+ import { closeSync, existsSync, mkdirSync, openSync, rmSync, writeFileSync } from "fs";
4
4
  import { renameWithRetry } from "@hyperframes/core/rename-retry";
5
5
  import { removeScratch } from "@hyperframes/core/remove-scratch";
6
6
  import { join, dirname, isAbsolute, relative } from "path";
@@ -28,6 +28,7 @@ import { chainTailSeconds } from "@hyperframes/core/audio-fx-tail";
28
28
  import { isAudibleVideoElement, MEDIA_RENDER_ID_ATTR, normalizePlaybackRate, normalizeRateSpec, parseStrictFiniteTimingNumber, readElementRateSpec, readMediaStart, sourceTimeAt, timeAtSourceTime, } from "@hyperframes/core";
29
29
  import { readMasterFxChain, resolveAudioGroups } from "@hyperframes/core/audio-groups";
30
30
  import { applyAudioFxChain, AudioFxRenderError } from "./audioFxRender.js";
31
+ import { createStagingDir } from "@hyperframes/core/atomic-file";
31
32
  /** An MP4-family container records AAC priming as an edit list; a raw .aac plays ~21 ms late.
32
33
  * The video mux, the distributed plan and the PNG-sequence sidecar all read this one name. */
33
34
  export const MIXED_AUDIO_FILENAME = "audio.m4a";
@@ -539,7 +540,7 @@ async function extractAudioSegment(srcPath, outputPath, stage, mediaStart, durat
539
540
  });
540
541
  const loopDuration = (metadata?.durationSeconds ?? 0) - mediaStart;
541
542
  if (options?.loop && loopDuration > 0 && sourceDuration > loopDuration) {
542
- const cycleDir = mkdtempSync(join(outputDir, ".audio-loop-"));
543
+ const cycleDir = createStagingDir(outputDir, ".audio-loop-");
543
544
  const cyclePath = join(cycleDir, "cycle.wav");
544
545
  try {
545
546
  const cycle = await extractAudioSegment(srcPath, cyclePath, stage, mediaStart, loopDuration, 1, signal, config, audioSidecarUrl, { padToDuration: true });
@@ -746,7 +747,7 @@ async function mixAudioTracksFlat(tracks, outputPath, totalDuration, signal, con
746
747
  const runMix = async (ignoreAutomation, useNormalize) => {
747
748
  const inputs = [];
748
749
  tracks.forEach((track) => inputs.push("-i", track.srcPath));
749
- const scriptDir = mkdtempSync(join(outputDir, ".filter-complex-"));
750
+ const scriptDir = createStagingDir(outputDir, ".filter-complex-");
750
751
  const scriptPath = join(scriptDir, "graph.txt");
751
752
  const fd = openSync(scriptPath, "wx", 0o600);
752
753
  try {
@@ -898,7 +899,7 @@ async function mixGroupMembersFlat(memberTracks, outputPath, totalDuration, sign
898
899
  // global track count.
899
900
  `${mixInputs}amix=inputs=${memberTracks.length}:duration=longest:dropout_transition=0[mixed];[mixed]volume=${formatFilterNumber(memberTracks.length)}[out]`;
900
901
  const filterComplex = [...inputFilters, mixFilter].join(";");
901
- const scriptDir = mkdtempSync(join(outputDir, ".group-filter-complex-"));
902
+ const scriptDir = createStagingDir(outputDir, ".group-filter-complex-");
902
903
  const scriptPath = join(scriptDir, "graph.txt");
903
904
  const fd = openSync(scriptPath, "wx", 0o600);
904
905
  try {
@@ -1013,7 +1014,7 @@ async function mixGroupMembers(memberTracks, outputPath, totalDuration, signal,
1013
1014
  return mixGroupMembersFlat(memberTracks, outputPath, totalDuration, signal, config);
1014
1015
  }
1015
1016
  mkdirSync(dirname(outputPath), { recursive: true });
1016
- const scratchDir = mkdtempSync(join(dirname(outputPath), ".mix-batches-"));
1017
+ const scratchDir = createStagingDir(dirname(outputPath), ".mix-batches-");
1017
1018
  try {
1018
1019
  const summed = await sumInSlices(memberTracks, scratchDir, totalDuration, signal, config);
1019
1020
  if (!summed.success)
@@ -1030,7 +1031,7 @@ async function mixAudioTracks(tracks, outputPath, totalDuration, signal, config)
1030
1031
  return mixAudioTracksFlat(tracks, outputPath, totalDuration, signal, config);
1031
1032
  }
1032
1033
  mkdirSync(dirname(outputPath), { recursive: true });
1033
- const scratchDir = mkdtempSync(join(dirname(outputPath), ".mix-batches-"));
1034
+ const scratchDir = createStagingDir(dirname(outputPath), ".mix-batches-");
1034
1035
  try {
1035
1036
  const summed = await sumInSlices(tracks, scratchDir, totalDuration, signal, config);
1036
1037
  if (!summed.success) {
@@ -14,11 +14,12 @@
14
14
  * `extractAudioSegment`). Anything else is rejected so
15
15
  * the caller can fall back to the expression path rather than corrupting audio.
16
16
  */
17
- import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "fs";
17
+ import { readFileSync, rmSync, writeFileSync } from "fs";
18
18
  import { renameWithRetry } from "@hyperframes/core/rename-retry";
19
19
  import { dirname, join } from "path";
20
20
  import { normaliseEnvelope } from "@hyperframes/core/media-volume-envelope";
21
21
  import { riffChunks, wavFormatTag } from "./wavChunks.js";
22
+ import { createStagingDir } from "@hyperframes/core/atomic-file";
22
23
  const PCM_FORMAT = 1; // WAVE_FORMAT_PCM
23
24
  const FLOAT_FORMAT = 3; // WAVE_FORMAT_IEEE_FLOAT
24
25
  /** The `fmt ` chunk, or null for a format this cannot safely edit in place. */
@@ -129,7 +130,7 @@ export function applyVolumeEnvelopeToWav(wavPath, keyframes, trackStart, baseVol
129
130
  scaleSamples(buffer, layout, gainAt);
130
131
  // A private sibling directory owns the staging file; the same-filesystem
131
132
  // rename keeps a failed write from truncating the original WAV.
132
- stagingDir = mkdtempSync(join(dirname(wavPath), ".hf-volume-"));
133
+ stagingDir = createStagingDir(dirname(wavPath), ".hf-volume-");
133
134
  const tempPath = join(stagingDir, "audio.wav");
134
135
  writeFileSync(tempPath, buffer, { flag: "wx" });
135
136
  renameWithRetry(tempPath, wavPath);
@@ -6,7 +6,7 @@
6
6
  * Supports CPU (libx264) and GPU encoding.
7
7
  */
8
8
  import { frameSequencePath } from "./extractedFrameIndex.js";
9
- import { closeSync, copyFileSync, existsSync, ftruncateSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, statSync, writeFileSync, writeSync, } from "fs";
9
+ import { closeSync, copyFileSync, existsSync, ftruncateSync, mkdirSync, openSync, readFileSync, readdirSync, statSync, writeFileSync, writeSync, } from "fs";
10
10
  import { join, dirname, extname, resolve } from "path";
11
11
  import { DEFAULT_CONFIG } from "../config.js";
12
12
  import { getCachedGpuEncoder, getCachedNvencPresets, getGpuEncoderName, buildVideoToolboxRateControlArgs, mapPresetForGpuEncoder, } from "../utils/gpuEncoder.js";
@@ -19,6 +19,7 @@ import { fpsToFfmpegArg, fpsToNumber } from "@hyperframes/core";
19
19
  import { appendVp9CpuUsedArg } from "./vp9Options.js";
20
20
  import { appendSoftwareEncoderThreadsArg } from "./softwareEncoderThreads.js";
21
21
  import { appendRenderProvenanceArgs } from "../utils/renderProvenance.js";
22
+ import { createStagingDir } from "@hyperframes/core/atomic-file";
22
23
  export const ENCODER_PRESETS = {
23
24
  draft: { preset: "ultrafast", quality: 28, codec: "h264" },
24
25
  standard: { preset: "medium", quality: 18, codec: "h264" },
@@ -553,7 +554,7 @@ export async function encodeFramesChunkedConcat(framesDir, framePattern, outputP
553
554
  mkdirSync(dirname(outputPath), { recursive: true });
554
555
  // Keep intermediates under the caller-owned output directory for its existing
555
556
  // cleanup/debug policy, but never reuse another invocation's chunk files.
556
- const chunkDir = mkdtempSync(join(dirname(outputPath), "chunk-encode-"));
557
+ const chunkDir = createStagingDir(dirname(outputPath), "chunk-encode-");
557
558
  const chunkPaths = [];
558
559
  for (let i = 0; i < chunkCount; i++) {
559
560
  if (signal?.aborted) {
@@ -1,4 +1,4 @@
1
- import { closeSync, createReadStream, createWriteStream, existsSync, fsyncSync, linkSync, mkdtempSync, mkdirSync, lstatSync, openSync, readdirSync, rmdirSync, rmSync, statSync, unlinkSync, } from "fs";
1
+ import { closeSync, createReadStream, createWriteStream, existsSync, fsyncSync, linkSync, mkdirSync, lstatSync, openSync, readdirSync, rmdirSync, rmSync, statSync, unlinkSync, } from "fs";
2
2
  import { createHash, randomUUID } from "crypto";
3
3
  import dns from "node:dns";
4
4
  import { fetchGoesThroughEnvProxy } from "@hyperframes/core/env-proxy";
@@ -6,6 +6,7 @@ import { isBlockedNetworkHost } from "./networkPolicy.js";
6
6
  import { dirname, extname, join } from "path";
7
7
  import { Readable, Transform } from "stream";
8
8
  import { pipeline } from "stream/promises";
9
+ import { createStagingDir } from "@hyperframes/core/atomic-file";
9
10
  const inFlightDownloads = new Map();
10
11
  const signalScopes = new WeakMap();
11
12
  let nextSignalScope = 1;
@@ -763,7 +764,7 @@ function emitDownloadTelemetry(options, event) {
763
764
  async function runDownloadAttempt(url, localPath, timeoutMs, attempt, options, signal) {
764
765
  // A private, unguessable directory prevents symlink planting and keeps the
765
766
  // partial on the destination filesystem so the final rename stays atomic.
766
- const attemptDir = mkdtempSync(join(dirname(localPath), ".hf-download-"));
767
+ const attemptDir = createStagingDir(dirname(localPath), ".hf-download-");
767
768
  const partialPath = join(attemptDir, "payload");
768
769
  const controller = new AbortController();
769
770
  let timedOut = false;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hyperframes/engine",
3
- "version": "0.9.25",
3
+ "version": "0.9.27",
4
4
  "description": "Seekable web page to video rendering engine (Puppeteer + FFmpeg)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -61,8 +61,8 @@
61
61
  "puppeteer": "~25.11.0",
62
62
  "puppeteer-core": "^25.11.0",
63
63
  "undici": "7.25.0",
64
- "@hyperframes/core": "^0.9.25",
65
- "@hyperframes/parsers": "^0.9.25"
64
+ "@hyperframes/core": "^0.9.27",
65
+ "@hyperframes/parsers": "^0.9.27"
66
66
  },
67
67
  "devDependencies": {
68
68
  "@types/node": "^25.0.10",