@hyperframes/engine 0.8.205 → 0.8.207
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 +5 -4
- package/dist/index.js +4 -3
- package/dist/services/aacEncode.js +3 -2
- package/dist/services/audioFxRender.js +3 -2
- package/dist/services/audioMixer.js +10 -9
- package/dist/services/browserManager.d.ts +2 -1
- package/dist/services/browserManager.js +5 -3
- package/dist/services/chunkEncoder.d.ts +8 -0
- package/dist/services/chunkEncoder.js +33 -0
- package/dist/services/chunkEncoder.types.d.ts +2 -0
- package/dist/services/drawElementCaptureError.d.ts +1 -0
- package/dist/services/drawElementCaptureError.js +13 -0
- package/dist/services/frameCapture.d.ts +4 -1
- package/dist/services/frameCapture.js +109 -13
- package/dist/services/hdrCapture.js +2 -2
- package/dist/services/parallelCoordinator.d.ts +2 -0
- package/dist/services/parallelCoordinator.js +7 -5
- package/dist/services/streamingEncoder.d.ts +2 -0
- package/dist/services/streamingEncoder.js +2 -1
- package/dist/services/videoFrameExtractor.d.ts +1 -0
- package/dist/services/videoFrameExtractor.js +22 -12
- package/dist/utils/ffprobe.d.ts +5 -0
- package/dist/utils/ffprobe.js +1 -1
- package/dist/utils/removeScratch.d.ts +5 -0
- package/dist/utils/removeScratch.js +23 -0
- package/dist/utils/renameWithRetry.d.ts +4 -0
- package/dist/utils/renameWithRetry.js +41 -6
- package/package.json +3 -3
package/dist/index.d.ts
CHANGED
|
@@ -43,10 +43,10 @@ export { CaptureFailure, classifyCaptureFailure, isFatalCaptureFailure, type Cap
|
|
|
43
43
|
export { createChromeMemorySampler, type ChromeMemoryStats, } from "./services/chromeMemorySampler.js";
|
|
44
44
|
export { installMediaRenderIdBridge } from "./services/mediaRenderIdBridge.js";
|
|
45
45
|
export { beginFrameCapture, pageScreenshotCapture, getCdpSession, injectVideoFramesBatch, syncVideoFrameVisibility, cdpSessionCache, probeBeginFrameLiveness, initTransparentBackground, capturesAlpha, captureAlphaPng, applyDomLayerMask, removeDomLayerMask, DOM_LAYER_MASK_STYLE_ID, type BeginFrameResult, } from "./services/screenshotService.js";
|
|
46
|
-
export { buildEncoderArgs, encodeFramesFromDir, buildConcatArgs, concatVideoFiles, encodeFramesChunkedConcat, muxVideoWithAudio, applyFaststart, packageHls, detectGpuEncoder, ENCODER_PRESETS, getEncoderPreset, HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST, HLS_AUDIO_PLAYLIST, type GpuEncoder, type PackageHlsOptions, } from "./services/chunkEncoder.js";
|
|
46
|
+
export { buildEncoderArgs, encodeFramesFromDir, buildConcatArgs, concatVideoFiles, encodeFramesChunkedConcat, muxVideoWithAudio, applyFaststart, packageHls, detectGpuEncoder, ENCODER_PRESETS, getEncoderPreset, appendRateCapArgs, keyframeIntervalToGopSize, HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST, HLS_AUDIO_PLAYLIST, type GpuEncoder, type PackageHlsOptions, } from "./services/chunkEncoder.js";
|
|
47
47
|
export type { EncoderOptions, EncodeResult, MuxResult } from "./services/chunkEncoder.types.js";
|
|
48
48
|
export { spawnStreamingEncoder, createFrameReorderBuffer, type StreamingEncoder, type StreamingEncoderOptions, type StreamingEncoderResult, type FrameReorderBuffer, } from "./services/streamingEncoder.js";
|
|
49
|
-
export { parseVideoElements, parseImageElements, extractVideoFramesRange, extractAllVideoFrames, isVideoPastTimelineEnd, resolveTimelineExtractionWindow, resolveVideoExtractionWindow, rebaseVideoToWindow, resolveFinalFrameExtractionWindow, resolveFrameFormat, resolveVideoExtractionDuration, resolvePlayableVideoDuration, extractionFrameCountForDuration, resolveProjectRelativeSrc, getFrameAtTime, isVideoHiddenBeforeStreamStart, createFrameLookupTable, FrameLookupTable, analyzeClipMediaFit, classifyVideoExtractionError, isVideoSourceExtractionError, runVideoExtractionWithRetry, safeVideoExtractionSourceIdentity, VideoSourceExtractionError, type VideoElement, type ImageElement, type ExtractedFrames, type DeferredFrameRange, type FrameRange, type ExtractionOptions, type ExtractionResult, type ExtractionPhaseBreakdown, type TimelineExtractionWindow, type VideoExtractionFailure, type VideoExtractionFailureKind, type VideoExtractionFailureGroupDetails, type VideoExtractionFailureRetry, type VideoExtractionFailureStatusClass, type SafeVideoExtractionSourceIdentity, type VideoFrameFormat, VIDEO_FRAME_FORMATS, isVideoFrameFormat, EXTRACT_CACHE_MIN_AGE_MS, } from "./services/videoFrameExtractor.js";
|
|
49
|
+
export { parseVideoElements, parseImageElements, extractVideoFramesRange, extractAllVideoFrames, isVideoPastTimelineEnd, resolveTimelineExtractionWindow, resolveVideoExtractionWindow, rebaseVideoToWindow, resolveFinalFrameExtractionWindow, resolveFrameFormat, resolveVideoExtractionDuration, resolvePlayableVideoDuration, extractionFrameCountForDuration, resolveProjectRelativeSrc, getFrameAtTime, isVideoHiddenBeforeStreamStart, createFrameLookupTable, FrameLookupTable, analyzeClipMediaFit, classifyVideoExtractionError, isVideoSourceExtractionError, runVideoExtractionWithRetry, safeVideoExtractionSourceIdentity, VideoSourceExtractionError, type VideoElement, type ImageElement, type ExtractedFrames, type DeferredFrameRange, type FrameRange, type ExtractionOptions, type ExtractionResult, type ExtractionPhaseBreakdown, type TimelineExtractionWindow, type VideoExtractionFailure, type VideoExtractionFailureKind, type VideoExtractionFailureGroupDetails, type VideoExtractionFailureRetry, type VideoExtractionFailureStatusClass, type SafeVideoExtractionSourceIdentity, type VideoFrameFormat, VIDEO_FRAME_FORMATS, isVideoFrameFormat, EXTRACT_CACHE_MIN_AGE_MS, sdrCanvasColourFilters, } from "./services/videoFrameExtractor.js";
|
|
50
50
|
export { isSelfOrAncestorHidden } from "./services/mediaHidden.js";
|
|
51
51
|
export { directorySizeBytes, gcExtractionCache } from "./services/extractionCache.js";
|
|
52
52
|
export { resolveReferencedStart, resolveReferencedDuration, type RefResolverEl, type RefResolverDoc, } from "./services/referenceResolver.js";
|
|
@@ -55,7 +55,7 @@ export { MIXED_AUDIO_FILENAME, parseAudioElements, parseMasterFxChain, processCo
|
|
|
55
55
|
export { audioFailureDiagnostics } from "./services/audioFailureDiagnostic.js";
|
|
56
56
|
export { cloneCaptureWarning, cloneCaptureWarnings } from "./services/captureWarning.js";
|
|
57
57
|
export type { AudioElement, AudioFailureReason, AudioFailureStage, AudioProcessingFailure, AudioFailureDiagnostic, AudioSubprocessEvidence, AudioTrack, AudioVolumeKeyframe, MixResult, } from "./services/audioMixer.types.js";
|
|
58
|
-
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";
|
|
58
|
+
export { calculateOptimalWorkers, computeWorkerSizing, selectVerifySampleIndicesForTask, verifyDiskDrawElementSamples, distributeFrames, distributeFramesInterleaved, executeParallelCapture, mergeWorkerFrames, getSystemResources, shouldDisableBrowserPoolForParallelWorker, type WorkerTask, type WorkerResult, type WorkerSizing, type WorkerSizingBound, type LandedFrame, type ParallelProgress, type ParallelWorkerPhaseDiagnostic, } from "./services/parallelCoordinator.js";
|
|
59
59
|
export { createFileServer, type FileServerOptions, type FileServerHandle, } from "./services/fileServer.js";
|
|
60
60
|
export { quantizeTimeToFrame, MEDIA_VISUAL_STYLE_PROPERTIES } from "@hyperframes/core";
|
|
61
61
|
export { frameFileExtension } from "./services/frameCapture.js";
|
|
@@ -65,7 +65,7 @@ export type { MotionBlurOptions, MotionBlurBlendSpace } from "./services/motionB
|
|
|
65
65
|
export type { PageMotionBlurOff } from "./types.js";
|
|
66
66
|
export { assertSwiftShader, readWebGlVendorInfo, SwiftShaderAssertionError, BROWSER_GPU_NOT_SOFTWARE, } from "./utils/assertSwiftShader.js";
|
|
67
67
|
export { readWebGlVendorInfoFromCanvas } from "./utils/readWebGlVendorInfoFromCanvas.js";
|
|
68
|
-
export { extractMediaMetadata, extractVideoMetadata, extractFinalVideoFrameTimestamp, extractAudioMetadata, declareContentAddressedUrl, declareProbeFactsUrl, PROBE_FACTS_VERSION, buildProbeFactsText, probeMediaProfile, analyzeKeyframeIntervals, type VideoMetadata, type AudioMetadata, type MediaProbeProfile, type KeyframeAnalysis, } from "./utils/ffprobe.js";
|
|
68
|
+
export { extractMediaMetadata, extractVideoMetadata, extractFinalVideoFrameTimestamp, extractAudioMetadata, declareContentAddressedUrl, declareProbeFactsUrl, PROBE_FACTS_VERSION, buildProbeFactsText, probeMediaProfile, runFfprobe, analyzeKeyframeIntervals, type VideoMetadata, type AudioMetadata, type MediaProbeProfile, type KeyframeAnalysis, } from "./utils/ffprobe.js";
|
|
69
69
|
export { NOT_MEDIA_PAYLOAD, NotMediaPayloadError, assertMediaPayload, fingerprintElementId, isNotMediaPayload, } from "./utils/notMediaPayload.js";
|
|
70
70
|
export { assertPublicHttpsUrl, isBlockedNetworkHost, downloadToTemp, fetchPublicHttpsText, isHttpUrl, safeDownloadUrlIdentity, writeUrlDownloadTelemetry, type SafeDownloadUrlIdentity, type UrlDownloadOptions, type UrlDownloadTelemetry, type PublicHttpsTextOptions, } from "./utils/urlDownloader.js";
|
|
71
71
|
export { runFfmpeg, formatFfmpegError, describeFfmpegFailure, isExternalFfmpegInterruption, type RunFfmpegOptions, type RunFfmpegResult, } from "./utils/runFfmpeg.js";
|
|
@@ -74,6 +74,7 @@ export { ManagedChildProcess, type ManagedChildProcessOptions, type ManagedChild
|
|
|
74
74
|
export { assertConfiguredFfmpegBinariesExist, getFfmpegBinary, getFfprobeBinary, FFMPEG_PATH_ENV, FFPROBE_PATH_ENV, } from "./utils/ffmpegBinaries.js";
|
|
75
75
|
export { trackChildProcess, killTrackedProcesses } from "./utils/processTracker.js";
|
|
76
76
|
export { renameWithRetry, renameWithRetryAsync } from "./utils/renameWithRetry.js";
|
|
77
|
+
export { removeScratch, collectScratchLeftovers } from "./utils/removeScratch.js";
|
|
77
78
|
export { psnrDb, resolveDeVerifyMinDb } from "./utils/psnr.js";
|
|
78
79
|
export { decodePng, decodePngToRgb48le, blitRgba8OverRgb48le, blitRgb48leRegion, blitRgb48leAffine, parseTransformMatrix, roundedRectAlpha, resampleRgb48leObjectFit, normalizeObjectFit, type ObjectFit, } from "./utils/alphaBlit.js";
|
|
79
80
|
export { groupIntoLayers, type CompositeLayer } from "./utils/layerCompositor.js";
|
package/dist/index.js
CHANGED
|
@@ -47,10 +47,10 @@ export { createChromeMemorySampler, } from "./services/chromeMemorySampler.js";
|
|
|
47
47
|
export { installMediaRenderIdBridge } from "./services/mediaRenderIdBridge.js";
|
|
48
48
|
export { beginFrameCapture, pageScreenshotCapture, getCdpSession, injectVideoFramesBatch, syncVideoFrameVisibility, cdpSessionCache, probeBeginFrameLiveness, initTransparentBackground, capturesAlpha, captureAlphaPng, applyDomLayerMask, removeDomLayerMask, DOM_LAYER_MASK_STYLE_ID, } from "./services/screenshotService.js";
|
|
49
49
|
// ── Encoding ───────────────────────────────────────────────────────────────────
|
|
50
|
-
export { buildEncoderArgs, encodeFramesFromDir, buildConcatArgs, concatVideoFiles, encodeFramesChunkedConcat, muxVideoWithAudio, applyFaststart, packageHls, detectGpuEncoder, ENCODER_PRESETS, getEncoderPreset, HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST, HLS_AUDIO_PLAYLIST, } from "./services/chunkEncoder.js";
|
|
50
|
+
export { buildEncoderArgs, encodeFramesFromDir, buildConcatArgs, concatVideoFiles, encodeFramesChunkedConcat, muxVideoWithAudio, applyFaststart, packageHls, detectGpuEncoder, ENCODER_PRESETS, getEncoderPreset, appendRateCapArgs, keyframeIntervalToGopSize, HLS_MASTER_PLAYLIST, HLS_VIDEO_PLAYLIST, HLS_AUDIO_PLAYLIST, } from "./services/chunkEncoder.js";
|
|
51
51
|
export { spawnStreamingEncoder, createFrameReorderBuffer, } from "./services/streamingEncoder.js";
|
|
52
52
|
// ── Media processing ───────────────────────────────────────────────────────────
|
|
53
|
-
export { parseVideoElements, parseImageElements, extractVideoFramesRange, extractAllVideoFrames, isVideoPastTimelineEnd, resolveTimelineExtractionWindow, resolveVideoExtractionWindow, rebaseVideoToWindow, resolveFinalFrameExtractionWindow, resolveFrameFormat, resolveVideoExtractionDuration, resolvePlayableVideoDuration, extractionFrameCountForDuration, resolveProjectRelativeSrc, getFrameAtTime, isVideoHiddenBeforeStreamStart, createFrameLookupTable, FrameLookupTable, analyzeClipMediaFit, classifyVideoExtractionError, isVideoSourceExtractionError, runVideoExtractionWithRetry, safeVideoExtractionSourceIdentity, VideoSourceExtractionError, VIDEO_FRAME_FORMATS, isVideoFrameFormat, EXTRACT_CACHE_MIN_AGE_MS, } from "./services/videoFrameExtractor.js";
|
|
53
|
+
export { parseVideoElements, parseImageElements, extractVideoFramesRange, extractAllVideoFrames, isVideoPastTimelineEnd, resolveTimelineExtractionWindow, resolveVideoExtractionWindow, rebaseVideoToWindow, resolveFinalFrameExtractionWindow, resolveFrameFormat, resolveVideoExtractionDuration, resolvePlayableVideoDuration, extractionFrameCountForDuration, resolveProjectRelativeSrc, getFrameAtTime, isVideoHiddenBeforeStreamStart, createFrameLookupTable, FrameLookupTable, analyzeClipMediaFit, classifyVideoExtractionError, isVideoSourceExtractionError, runVideoExtractionWithRetry, safeVideoExtractionSourceIdentity, VideoSourceExtractionError, VIDEO_FRAME_FORMATS, isVideoFrameFormat, EXTRACT_CACHE_MIN_AGE_MS, sdrCanvasColourFilters, } from "./services/videoFrameExtractor.js";
|
|
54
54
|
export { isSelfOrAncestorHidden } from "./services/mediaHidden.js";
|
|
55
55
|
export { directorySizeBytes, gcExtractionCache } from "./services/extractionCache.js";
|
|
56
56
|
export { resolveReferencedStart, resolveReferencedDuration, } from "./services/referenceResolver.js";
|
|
@@ -69,7 +69,7 @@ export { appendSoftwareEncoderThreadsArg } from "./services/softwareEncoderThrea
|
|
|
69
69
|
export { frameSequencePath } from "./services/extractedFrameIndex.js";
|
|
70
70
|
export { assertSwiftShader, readWebGlVendorInfo, SwiftShaderAssertionError, BROWSER_GPU_NOT_SOFTWARE, } from "./utils/assertSwiftShader.js";
|
|
71
71
|
export { readWebGlVendorInfoFromCanvas } from "./utils/readWebGlVendorInfoFromCanvas.js";
|
|
72
|
-
export { extractMediaMetadata, extractVideoMetadata, extractFinalVideoFrameTimestamp, extractAudioMetadata, declareContentAddressedUrl, declareProbeFactsUrl, PROBE_FACTS_VERSION, buildProbeFactsText, probeMediaProfile, analyzeKeyframeIntervals, } from "./utils/ffprobe.js";
|
|
72
|
+
export { extractMediaMetadata, extractVideoMetadata, extractFinalVideoFrameTimestamp, extractAudioMetadata, declareContentAddressedUrl, declareProbeFactsUrl, PROBE_FACTS_VERSION, buildProbeFactsText, probeMediaProfile, runFfprobe, analyzeKeyframeIntervals, } from "./utils/ffprobe.js";
|
|
73
73
|
export { NOT_MEDIA_PAYLOAD, NotMediaPayloadError, assertMediaPayload, fingerprintElementId, isNotMediaPayload, } from "./utils/notMediaPayload.js";
|
|
74
74
|
export { assertPublicHttpsUrl, isBlockedNetworkHost, downloadToTemp, fetchPublicHttpsText, isHttpUrl, safeDownloadUrlIdentity, writeUrlDownloadTelemetry, } from "./utils/urlDownloader.js";
|
|
75
75
|
export { runFfmpeg, formatFfmpegError, describeFfmpegFailure, isExternalFfmpegInterruption, } from "./utils/runFfmpeg.js";
|
|
@@ -78,6 +78,7 @@ export { ManagedChildProcess, } from "./utils/managedChildProcess.js";
|
|
|
78
78
|
export { assertConfiguredFfmpegBinariesExist, getFfmpegBinary, getFfprobeBinary, FFMPEG_PATH_ENV, FFPROBE_PATH_ENV, } from "./utils/ffmpegBinaries.js";
|
|
79
79
|
export { trackChildProcess, killTrackedProcesses } from "./utils/processTracker.js";
|
|
80
80
|
export { renameWithRetry, renameWithRetryAsync } from "./utils/renameWithRetry.js";
|
|
81
|
+
export { removeScratch, collectScratchLeftovers } from "./utils/removeScratch.js";
|
|
81
82
|
// drawElement self-verify comparison — shared by the streaming drain
|
|
82
83
|
// (producer) and the parallel disk-path verify (parallelCoordinator).
|
|
83
84
|
export { psnrDb, resolveDeVerifyMinDb } from "./utils/psnr.js";
|
|
@@ -3,7 +3,8 @@
|
|
|
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,
|
|
6
|
+
import { mkdtempSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { removeScratch } from "../utils/removeScratch.js";
|
|
7
8
|
import { dirname, join } from "node:path";
|
|
8
9
|
import { getFfmpegBinary } from "../utils/ffmpegBinaries.js";
|
|
9
10
|
import { runFfmpeg } from "../utils/runFfmpeg.js";
|
|
@@ -133,6 +134,6 @@ export async function encodeAac(input, outputPath, totalDuration, { signal, time
|
|
|
133
134
|
return { ...muxed, durationMs: slowest + muxed.durationMs };
|
|
134
135
|
}
|
|
135
136
|
finally {
|
|
136
|
-
|
|
137
|
+
removeScratch(dir);
|
|
137
138
|
}
|
|
138
139
|
}
|
|
@@ -11,13 +11,14 @@
|
|
|
11
11
|
* dynamics processors and the modulated delays) have no filter that behaves the
|
|
12
12
|
* same way, so preview would quietly stop predicting the render.
|
|
13
13
|
*/
|
|
14
|
-
import { closeSync, existsSync, mkdtempSync, openSync, readFileSync, readSync,
|
|
14
|
+
import { closeSync, existsSync, mkdtempSync, openSync, readFileSync, readSync, statSync, writeFileSync, writeSync, } from "node:fs";
|
|
15
15
|
import { tmpdir } from "node:os";
|
|
16
16
|
import { join } from "node:path";
|
|
17
17
|
import { pathToFileURL } from "node:url";
|
|
18
18
|
import { getAudioFxRuntimeScript } from "@hyperframes/core/audio-fx-runtime";
|
|
19
19
|
import { enabledAudioFxNodes } from "@hyperframes/core/audio-fx";
|
|
20
20
|
import { serializeAutomation } from "@hyperframes/core/audio-automation";
|
|
21
|
+
import { removeScratch } from "../utils/removeScratch.js";
|
|
21
22
|
import { acquireBrowser } from "./browserManager.js";
|
|
22
23
|
import { createEnvelopeWalker } from "./audioVolumeEnvelope.js";
|
|
23
24
|
import { riffChunks, wavFormatTag } from "./wavChunks.js";
|
|
@@ -460,7 +461,7 @@ async function withFxBrowser(options, body) {
|
|
|
460
461
|
throw new AudioFxRenderError(`Audio FX failed for track ${options.trackId}: ${err.message}`);
|
|
461
462
|
}
|
|
462
463
|
finally {
|
|
463
|
-
|
|
464
|
+
removeScratch(hostDir);
|
|
464
465
|
await lease?.release().catch(() => undefined);
|
|
465
466
|
}
|
|
466
467
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { isSelfOrAncestorHidden, memberGroupKey, isMemberGroupHidden } from "./mediaHidden.js";
|
|
3
3
|
import { closeSync, existsSync, mkdirSync, mkdtempSync, openSync, rmSync, writeFileSync } from "fs";
|
|
4
4
|
import { renameWithRetry } from "../utils/renameWithRetry.js";
|
|
5
|
+
import { removeScratch } from "../utils/removeScratch.js";
|
|
5
6
|
import { join, dirname, isAbsolute, relative } from "path";
|
|
6
7
|
import { parseHTML } from "linkedom";
|
|
7
8
|
import { extractAudioMetadata } from "../utils/ffprobe.js";
|
|
@@ -575,7 +576,7 @@ async function extractAudioSegment(srcPath, outputPath, stage, mediaStart, durat
|
|
|
575
576
|
};
|
|
576
577
|
}
|
|
577
578
|
finally {
|
|
578
|
-
|
|
579
|
+
removeScratch(cycleDir);
|
|
579
580
|
}
|
|
580
581
|
}
|
|
581
582
|
const inputSeek = mediaStart >= (metadata?.latestStreamLeadSeconds ?? 0);
|
|
@@ -773,7 +774,7 @@ async function mixAudioTracksFlat(tracks, outputPath, totalDuration, signal, con
|
|
|
773
774
|
return await runFfmpeg(currentArgs, { signal, timeout: ffmpegProcessTimeout });
|
|
774
775
|
}
|
|
775
776
|
finally {
|
|
776
|
-
|
|
777
|
+
removeScratch(scriptDir);
|
|
777
778
|
}
|
|
778
779
|
};
|
|
779
780
|
let useNormalize = true;
|
|
@@ -934,7 +935,7 @@ async function mixGroupMembersFlat(memberTracks, outputPath, totalDuration, sign
|
|
|
934
935
|
return await runFfmpeg(currentArgs, { signal, timeout: ffmpegProcessTimeout });
|
|
935
936
|
}
|
|
936
937
|
finally {
|
|
937
|
-
|
|
938
|
+
removeScratch(scriptDir);
|
|
938
939
|
}
|
|
939
940
|
};
|
|
940
941
|
let useNormalize = true;
|
|
@@ -991,8 +992,8 @@ async function sumInSlices(tracks, scratchDir, totalDuration, signal, config) {
|
|
|
991
992
|
}
|
|
992
993
|
const foldedPath = join(scratchDir, `${id}.wav`);
|
|
993
994
|
const folded = await mixGroupMembersFlat([running, wavTrack(`${id}-slice`, slicePath)], foldedPath, totalDuration, signal, config);
|
|
994
|
-
|
|
995
|
-
|
|
995
|
+
removeScratch(running.srcPath);
|
|
996
|
+
removeScratch(slicePath);
|
|
996
997
|
if (!folded.success)
|
|
997
998
|
return folded;
|
|
998
999
|
running = wavTrack(id, foldedPath);
|
|
@@ -1011,11 +1012,11 @@ async function mixGroupMembers(memberTracks, outputPath, totalDuration, signal,
|
|
|
1011
1012
|
const summed = await sumInSlices(memberTracks, scratchDir, totalDuration, signal, config);
|
|
1012
1013
|
if (!summed.success)
|
|
1013
1014
|
return summed;
|
|
1014
|
-
renameWithRetry(summed.track.srcPath, outputPath);
|
|
1015
|
+
renameWithRetry(summed.track.srcPath, outputPath, { copyFallback: true });
|
|
1015
1016
|
return { success: true, degradedAutomation: summed.degradedAutomation };
|
|
1016
1017
|
}
|
|
1017
1018
|
finally {
|
|
1018
|
-
|
|
1019
|
+
removeScratch(scratchDir);
|
|
1019
1020
|
}
|
|
1020
1021
|
}
|
|
1021
1022
|
async function mixAudioTracks(tracks, outputPath, totalDuration, signal, config) {
|
|
@@ -1048,7 +1049,7 @@ async function mixAudioTracks(tracks, outputPath, totalDuration, signal, config)
|
|
|
1048
1049
|
: mixed;
|
|
1049
1050
|
}
|
|
1050
1051
|
finally {
|
|
1051
|
-
|
|
1052
|
+
removeScratch(scratchDir);
|
|
1052
1053
|
}
|
|
1053
1054
|
}
|
|
1054
1055
|
/**
|
|
@@ -1085,7 +1086,7 @@ async function mixThroughFloatSum(tracks, masterChain, outputPath, workDir, tota
|
|
|
1085
1086
|
};
|
|
1086
1087
|
}
|
|
1087
1088
|
if (config?.keepEncoderInputAt)
|
|
1088
|
-
renameWithRetry(encoderInput, config.keepEncoderInputAt);
|
|
1089
|
+
renameWithRetry(encoderInput, config.keepEncoderInputAt, { copyFallback: true });
|
|
1089
1090
|
return {
|
|
1090
1091
|
...sum,
|
|
1091
1092
|
outputPath,
|
|
@@ -12,8 +12,9 @@ export { BrowserLeasePool } from "./browserLeasePool.js";
|
|
|
12
12
|
export type { BrowserLaunchFingerprint, BrowserLease, BrowserPoolState, CaptureMode, } from "./browserLeasePool.js";
|
|
13
13
|
/** Set while the host cancels renders on Ctrl+C; Puppeteer's own handler would exit before any cleanup ran. */
|
|
14
14
|
export declare function setHostHandlesSigint(owned: boolean): void;
|
|
15
|
-
export declare const
|
|
15
|
+
export declare const ownedLaunchOptions: () => {
|
|
16
16
|
handleSIGINT: boolean;
|
|
17
|
+
pipe: boolean;
|
|
17
18
|
};
|
|
18
19
|
export interface GpuProbeLifecycle {
|
|
19
20
|
launch(puppeteer: PuppeteerNode, options: LaunchOptions): Promise<Browser>;
|
|
@@ -23,7 +23,7 @@ let hostHandlesSigint = false;
|
|
|
23
23
|
export function setHostHandlesSigint(owned) {
|
|
24
24
|
hostHandlesSigint = owned;
|
|
25
25
|
}
|
|
26
|
-
export const
|
|
26
|
+
export const ownedLaunchOptions = () => ({ handleSIGINT: !hostHandlesSigint, pipe: true });
|
|
27
27
|
function isSoftwareWebGlRenderer(rendererInfo) {
|
|
28
28
|
const renderer = rendererInfo.trim().toLowerCase();
|
|
29
29
|
return (renderer.includes("swiftshader") ||
|
|
@@ -54,7 +54,7 @@ async function probeHardwareWebGlInfo(ppt, options) {
|
|
|
54
54
|
let probeBrowser;
|
|
55
55
|
try {
|
|
56
56
|
const launchOptions = {
|
|
57
|
-
...
|
|
57
|
+
...ownedLaunchOptions(),
|
|
58
58
|
headless: true,
|
|
59
59
|
args: withHyperframesBrowserMarker(options.args),
|
|
60
60
|
defaultViewport: { width: 64, height: 64 },
|
|
@@ -659,7 +659,7 @@ async function launchBrowser(fingerprint) {
|
|
|
659
659
|
let captureMode = fingerprint.requestedCaptureMode;
|
|
660
660
|
let browser;
|
|
661
661
|
const launch = (args) => ppt.launch({
|
|
662
|
-
...
|
|
662
|
+
...ownedLaunchOptions(),
|
|
663
663
|
headless: true,
|
|
664
664
|
args: withHyperframesBrowserMarker(args),
|
|
665
665
|
defaultViewport: null,
|
|
@@ -706,6 +706,8 @@ async function launchBrowser(fingerprint) {
|
|
|
706
706
|
}
|
|
707
707
|
function forceCloseBrowserProcess(browser) {
|
|
708
708
|
const proc = browser.process?.();
|
|
709
|
+
for (const stream of proc?.stdio ?? [])
|
|
710
|
+
stream?.on("error", () => { });
|
|
709
711
|
if (proc && !proc.killed) {
|
|
710
712
|
try {
|
|
711
713
|
proc.kill("SIGKILL");
|
|
@@ -87,6 +87,14 @@ export declare function appendLockedGopArgs(args: string[], gopSize: number): vo
|
|
|
87
87
|
* open-GOP references. `repeat-headers=1` keeps each boundary self-contained.
|
|
88
88
|
*/
|
|
89
89
|
export declare function lockedGopCodecParams(codec: "h264" | "h265", gopSize: number): string;
|
|
90
|
+
/** Frames per keyframe interval: the nearest whole frame, never below one. */
|
|
91
|
+
export declare function keyframeIntervalToGopSize(fps: Fps, seconds: number): number;
|
|
92
|
+
/** `-maxrate` / `-bufsize` for the VBV cap, written once for both arg builders. Buffer defaults to 2x. */
|
|
93
|
+
export declare function appendRateCapArgs(args: string[], options: {
|
|
94
|
+
codec?: string;
|
|
95
|
+
maxBitrate?: string;
|
|
96
|
+
bufferSize?: string;
|
|
97
|
+
}): void;
|
|
90
98
|
export declare function buildEncoderArgs(options: EncoderOptions, inputArgs: string[], outputPath: string, gpuEncoder?: GpuEncoder): string[];
|
|
91
99
|
export declare function encodeFramesFromDir(framesDir: string, framePattern: string, outputPath: string, options: EncoderOptions, signal?: AbortSignal, config?: Partial<Pick<EngineConfig, "ffmpegEncodeTimeout">>, onFramesEncoded?: (frames: number) => void): Promise<EncodeResult>;
|
|
92
100
|
export declare function buildConcatArgs(concatListPath: string, outputPath: string): string[];
|
|
@@ -128,6 +128,38 @@ export function lockedGopCodecParams(codec, gopSize) {
|
|
|
128
128
|
const shared = "scenecut=0:open-gop=0:repeat-headers=1";
|
|
129
129
|
return codec === "h264" ? shared : `keyint=${gopSize}:min-keyint=${gopSize}:${shared}`;
|
|
130
130
|
}
|
|
131
|
+
/** Frames per keyframe interval: the nearest whole frame, never below one. */
|
|
132
|
+
export function keyframeIntervalToGopSize(fps, seconds) {
|
|
133
|
+
return Math.max(1, Math.round(fpsToNumber(fps) * seconds));
|
|
134
|
+
}
|
|
135
|
+
const BITRATE_PATTERN = /^(\d+(?:\.\d+)?)([kKmMgG]?)$/;
|
|
136
|
+
function bitrateToBitsPerSecond(value) {
|
|
137
|
+
const match = BITRATE_PATTERN.exec(value);
|
|
138
|
+
if (!match)
|
|
139
|
+
throw new Error(`[chunkEncoder] invalid bitrate ${JSON.stringify(value)}`);
|
|
140
|
+
const unit = match[2].toLowerCase();
|
|
141
|
+
const scale = { g: 1_000_000_000, m: 1_000_000, k: 1_000 }[unit] ?? 1;
|
|
142
|
+
return Number(match[1]) * scale;
|
|
143
|
+
}
|
|
144
|
+
/** `-maxrate` / `-bufsize` for the VBV cap, written once for both arg builders. Buffer defaults to 2x. */
|
|
145
|
+
export function appendRateCapArgs(args, options) {
|
|
146
|
+
const { maxBitrate, bufferSize } = options;
|
|
147
|
+
if (maxBitrate === undefined) {
|
|
148
|
+
if (bufferSize !== undefined)
|
|
149
|
+
throw new Error("[chunkEncoder] bufferSize requires maxBitrate");
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
const codec = options.codec ?? "h264";
|
|
153
|
+
if (codec !== "h264" && codec !== "h265") {
|
|
154
|
+
throw new Error(`[chunkEncoder] maxBitrate is not supported for codec ${codec}`);
|
|
155
|
+
}
|
|
156
|
+
const cap = bitrateToBitsPerSecond(maxBitrate);
|
|
157
|
+
const buffer = bufferSize !== undefined ? bitrateToBitsPerSecond(bufferSize) : cap * 2;
|
|
158
|
+
if (cap <= 0 || buffer <= 0) {
|
|
159
|
+
throw new Error("[chunkEncoder] maxBitrate and bufferSize must be positive");
|
|
160
|
+
}
|
|
161
|
+
args.push("-maxrate", String(Math.round(cap)), "-bufsize", String(Math.round(buffer)));
|
|
162
|
+
}
|
|
131
163
|
export function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = null) {
|
|
132
164
|
const { fps, codec = "h264", preset = "medium", quality = 23, bitrate, pixelFormat = "yuv420p", vp9CpuUsed, useGpu = false, } = options;
|
|
133
165
|
// libx264 cannot encode HDR. If a caller passes hdr with codec=h264 we'd
|
|
@@ -142,6 +174,7 @@ export function buildEncoderArgs(options, inputArgs, outputPath, gpuEncoder = nu
|
|
|
142
174
|
// Without -xerror, ffmpeg ends the video at the first frame file it cannot read and still exits 0.
|
|
143
175
|
const args = ["-xerror", ...inputArgs, "-r", fpsToFfmpegArg(fps)];
|
|
144
176
|
const shouldUseGpu = useGpu && gpuEncoder !== null;
|
|
177
|
+
appendRateCapArgs(args, options);
|
|
145
178
|
if (codec === "h264" || codec === "h265") {
|
|
146
179
|
if (shouldUseGpu) {
|
|
147
180
|
const encoderName = getGpuEncoderName(gpuEncoder, codec);
|
|
@@ -9,6 +9,8 @@ export interface EncoderOptions {
|
|
|
9
9
|
preset?: string;
|
|
10
10
|
quality?: number;
|
|
11
11
|
bitrate?: string;
|
|
12
|
+
maxBitrate?: string;
|
|
13
|
+
bufferSize?: string;
|
|
12
14
|
pixelFormat?: string;
|
|
13
15
|
/** libvpx-vp9 -cpu-used value. Defaults to the engine VP9 setting. */
|
|
14
16
|
vp9CpuUsed?: number;
|
|
@@ -3,5 +3,6 @@ export declare class DrawElementCaptureError extends Error {
|
|
|
3
3
|
readonly frameIndex: number;
|
|
4
4
|
constructor(frameIndex: number, reason: string, cause?: unknown);
|
|
5
5
|
}
|
|
6
|
+
export declare function asDrawElementCaptureFailure(frameIndex: number, encodeResult: Promise<Buffer>): Promise<Buffer>;
|
|
6
7
|
/** Structural matching survives engine/producer copies and worker error wrappers. */
|
|
7
8
|
export declare function isDrawElementCaptureError(error: unknown): boolean;
|
|
@@ -9,6 +9,19 @@ export class DrawElementCaptureError extends Error {
|
|
|
9
9
|
this.frameIndex = frameIndex;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
+
function markRejectionHandled(promise) {
|
|
13
|
+
promise.catch(() => { });
|
|
14
|
+
}
|
|
15
|
+
export function asDrawElementCaptureFailure(frameIndex, encodeResult) {
|
|
16
|
+
const wrapped = encodeResult.catch((cause) => {
|
|
17
|
+
if (isDrawElementCaptureError(cause))
|
|
18
|
+
throw cause;
|
|
19
|
+
const reason = cause instanceof Error ? cause.message : String(cause);
|
|
20
|
+
throw new DrawElementCaptureError(frameIndex, reason, cause);
|
|
21
|
+
});
|
|
22
|
+
markRejectionHandled(wrapped);
|
|
23
|
+
return wrapped;
|
|
24
|
+
}
|
|
12
25
|
/** Structural matching survives engine/producer copies and worker error wrappers. */
|
|
13
26
|
export function isDrawElementCaptureError(error) {
|
|
14
27
|
let current = error;
|
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* the page to expose window.__hf, then captures frames deterministically
|
|
7
7
|
* via Chrome's BeginFrame API or Page.captureScreenshot fallback.
|
|
8
8
|
*/
|
|
9
|
-
import { type Browser, type Page, type Protocol } from "puppeteer-core";
|
|
9
|
+
import { type Browser, type CDPSession, type Page, type Protocol } from "puppeteer-core";
|
|
10
10
|
import { type RawAuthoredTiming } from "@hyperframes/core";
|
|
11
11
|
import { type MotionBlurPlan } from "./motionBlur.js";
|
|
12
12
|
import { type BrowserLease, type CaptureMode } from "./browserManager.js";
|
|
@@ -429,6 +429,7 @@ export interface IsolatedComposition {
|
|
|
429
429
|
compositionId: string;
|
|
430
430
|
elementId: string | null;
|
|
431
431
|
}
|
|
432
|
+
export declare function authorScriptsMayRegisterTimelines(runtimeAttr: string, authoredBegin: string, authoredEnd: string): Promise<boolean>;
|
|
432
433
|
export interface SubTimelinePollOptions {
|
|
433
434
|
intervalMs?: number;
|
|
434
435
|
getScriptLoadFailures?: () => readonly string[];
|
|
@@ -467,6 +468,8 @@ export declare function classifyConsoleScriptError(type: string, text: string):
|
|
|
467
468
|
export declare function classifyConsoleScriptFailure(type: string, text: string): string | null;
|
|
468
469
|
/** `runtime-error:<first line>` when a script served with the composition threw; null if not attributable to one. */
|
|
469
470
|
export declare function classifyPageError(details: Protocol.Runtime.ExceptionDetails, projectScripts: ReadonlySet<string>): string | null;
|
|
471
|
+
/** ` [thrown at: <url>:<line>:<col>, <lookup>]` for a throw's authored frame, so the author can find the element. */
|
|
472
|
+
export declare function describeThrowSite(client: CDPSession, details: Protocol.Runtime.ExceptionDetails, projectScripts: ReadonlySet<string>): Promise<string>;
|
|
470
473
|
export declare function initializeSession(session: CaptureSession): Promise<void>;
|
|
471
474
|
/**
|
|
472
475
|
* Internal helper: seek timeline and inject video frames.
|
|
@@ -9,9 +9,9 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import { existsSync, mkdirSync, writeFileSync } from "fs";
|
|
11
11
|
import { join } from "path";
|
|
12
|
-
import { COMPOSITION_SOURCE_URL, MEDIA_RENDER_ID_ATTR, quantizeSeekTime, quantizeTimeToFrame, fpsToNumber, resolveAuthoredTimingWindow, SCENE_CALLBACK_ERROR_PREFIX, } from "@hyperframes/core";
|
|
12
|
+
import { COMPOSITION_SOURCE_URL, MEDIA_RENDER_ID_ATTR, quantizeSeekTime, quantizeTimeToFrame, fpsToNumber, resolveAuthoredTimingWindow, AUTHORED_SOURCE_BEGIN, AUTHORED_SOURCE_END, RUNTIME_BOOTSTRAP_ATTR, SCENE_CALLBACK_ERROR_PREFIX, } from "@hyperframes/core";
|
|
13
13
|
import { holdRenderFrameVideos } from "./renderFrameVideoHold.js";
|
|
14
|
-
import { DrawElementCaptureError } from "./drawElementCaptureError.js";
|
|
14
|
+
import { asDrawElementCaptureFailure, DrawElementCaptureError } from "./drawElementCaptureError.js";
|
|
15
15
|
import { encodePng } from "../utils/alphaBlit.js";
|
|
16
16
|
import { DEFAULT_SAMPLES_PER_FRAME, MotionBlurAccumulator, SPATIAL_TWEEN_PROPERTIES, adaptiveSampleCount, motionBlurProbeTimes, motionBlurSampleTimes, motionBlurWindowIsStatic, probeDiffMagnitude, resolveMotionBlurPlan, } from "./motionBlur.js";
|
|
17
17
|
import { describePageMotionBlur, describePageMotionBlurAtEnd, installPageMotionBlur, } from "./pageMotionBlur.js";
|
|
@@ -1361,6 +1361,50 @@ export async function waitForSubCompositionTimelines(session, page, timeoutMs, o
|
|
|
1361
1361
|
memo.unregisteredIds = session.pendingTimelineIds ?? [];
|
|
1362
1362
|
}
|
|
1363
1363
|
}
|
|
1364
|
+
// Runs in the page. False once every inline, template and same-origin script is read and none
|
|
1365
|
+
// mentions __timelines or imports code; an unreadable or module script, or a page still loading, answers true.
|
|
1366
|
+
export async function authorScriptsMayRegisterTimelines(runtimeAttr, authoredBegin, authoredEnd) {
|
|
1367
|
+
if (document.readyState !== "complete")
|
|
1368
|
+
return true;
|
|
1369
|
+
const memo = globalThis;
|
|
1370
|
+
memo.__hfAuthorScan ??= await (async () => {
|
|
1371
|
+
const scripts = [
|
|
1372
|
+
...Array.from(document.querySelectorAll("script")),
|
|
1373
|
+
...Array.from(document.querySelectorAll("template")).flatMap((t) => Array.from(t.content.querySelectorAll("script"))),
|
|
1374
|
+
].filter((script) => !script.hasAttribute(runtimeAttr));
|
|
1375
|
+
// Code that imports another file may register through it, which is not read here.
|
|
1376
|
+
const mayRegister = (code) => code.includes("__timelines") || /\bimport\s*\(|^\s*import\s*[\w{*"']/m.test(code);
|
|
1377
|
+
for (const script of scripts) {
|
|
1378
|
+
if (script.type === "module")
|
|
1379
|
+
return true;
|
|
1380
|
+
if (!script.src) {
|
|
1381
|
+
const text = script.textContent ?? "";
|
|
1382
|
+
const authored = text.includes(authoredBegin)
|
|
1383
|
+
? text
|
|
1384
|
+
.split(authoredBegin)
|
|
1385
|
+
.slice(1)
|
|
1386
|
+
.map((region) => region.split(authoredEnd)[0])
|
|
1387
|
+
.join("\n")
|
|
1388
|
+
: text;
|
|
1389
|
+
if (mayRegister(authored))
|
|
1390
|
+
return true;
|
|
1391
|
+
continue;
|
|
1392
|
+
}
|
|
1393
|
+
const url = new URL(script.src, location.href);
|
|
1394
|
+
if (url.origin !== location.origin)
|
|
1395
|
+
return true;
|
|
1396
|
+
try {
|
|
1397
|
+
if (mayRegister(await (await fetch(url)).text()))
|
|
1398
|
+
return true;
|
|
1399
|
+
}
|
|
1400
|
+
catch {
|
|
1401
|
+
return true;
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
return false;
|
|
1405
|
+
})();
|
|
1406
|
+
return memo.__hfAuthorScan;
|
|
1407
|
+
}
|
|
1364
1408
|
export async function pollSubCompositionTimelines(page, timeoutMs, { intervalMs = 150, getScriptLoadFailures, scriptFailureGraceMs = 2_000, getPageErrors, scriptErrorGraceMs = 5_000, onPending, shouldStop, knownUnregisteredIds = [], } = {}) {
|
|
1365
1409
|
// Hosts may opt out of the timeline wait with `data-no-timeline` —
|
|
1366
1410
|
// compositions driven purely by CSS animations / rAF (the render-compat
|
|
@@ -1386,15 +1430,19 @@ export async function pollSubCompositionTimelines(page, timeoutMs, { intervalMs
|
|
|
1386
1430
|
// A page error is given its grace from when it first appeared, not from when the poll began.
|
|
1387
1431
|
let errorSeenAt;
|
|
1388
1432
|
for (;;) {
|
|
1389
|
-
|
|
1433
|
+
const failures = getScriptLoadFailures?.() ?? [];
|
|
1434
|
+
const errors = getPageErrors?.() ?? [];
|
|
1435
|
+
const failed = failures.length > 0 ? failures : errors;
|
|
1436
|
+
// A failed script keeps the wait, so the failure is still named; only a quiet page is ready early.
|
|
1437
|
+
ready =
|
|
1438
|
+
Boolean(await page.evaluate(expression)) ||
|
|
1439
|
+
(failed.length === 0 &&
|
|
1440
|
+
!(await page.evaluate(authorScriptsMayRegisterTimelines, RUNTIME_BOOTSTRAP_ATTR, AUTHORED_SOURCE_BEGIN, AUTHORED_SOURCE_END)));
|
|
1390
1441
|
if (ready || shouldStop?.())
|
|
1391
1442
|
break;
|
|
1392
1443
|
const now = Date.now();
|
|
1393
1444
|
if (now >= deadline)
|
|
1394
1445
|
break;
|
|
1395
|
-
const failures = getScriptLoadFailures?.() ?? [];
|
|
1396
|
-
const errors = getPageErrors?.() ?? [];
|
|
1397
|
-
const failed = failures.length > 0 ? failures : errors;
|
|
1398
1446
|
const graceMs = failures.length > 0 ? scriptFailureGraceMs : scriptErrorGraceMs;
|
|
1399
1447
|
if (failed.length > 0)
|
|
1400
1448
|
errorSeenAt ??= now;
|
|
@@ -1839,10 +1887,41 @@ export function classifyPageError(details, projectScripts) {
|
|
|
1839
1887
|
if (frames.length === 0 && details.text.startsWith("Uncaught (in promise)"))
|
|
1840
1888
|
return null;
|
|
1841
1889
|
const urls = frames.length > 0 ? frames : [details.url];
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1890
|
+
return urls.some((url) => isOwnedScriptUrl(url, projectScripts))
|
|
1891
|
+
? `runtime-error:${firstLine}`
|
|
1892
|
+
: null;
|
|
1893
|
+
}
|
|
1894
|
+
// The render compiler names the composition's inline code.
|
|
1895
|
+
// Inline handlers report the document, which a widget can move.
|
|
1896
|
+
function isOwnedScriptUrl(url, projectScripts) {
|
|
1897
|
+
return url !== undefined && (projectScripts.has(url) || url.startsWith(COMPOSITION_SOURCE_URL));
|
|
1898
|
+
}
|
|
1899
|
+
const LOOKUP_CALL = /\b(?:getElementById|querySelectorAll|querySelector|getElementsByClassName|getElementsByTagName)\(\s*(["'`])((?:(?!\1).)*)\1/g;
|
|
1900
|
+
/** ` [thrown at: <url>:<line>:<col>, <lookup>]` for a throw's authored frame, so the author can find the element. */
|
|
1901
|
+
export async function describeThrowSite(client, details, projectScripts) {
|
|
1902
|
+
const frame = (details.stackTrace?.callFrames ?? []).find((f) => isOwnedScriptUrl(f.url, projectScripts));
|
|
1903
|
+
if (!frame)
|
|
1904
|
+
return "";
|
|
1905
|
+
let where = `${frame.url}:${frame.lineNumber + 1}:${frame.columnNumber + 1}`;
|
|
1906
|
+
try {
|
|
1907
|
+
// Source is only fetchable with the debugger on; enabling after the fact keeps script ids.
|
|
1908
|
+
await client.send("Debugger.enable");
|
|
1909
|
+
const { scriptSource } = await client.send("Debugger.getScriptSource", {
|
|
1910
|
+
scriptId: frame.scriptId,
|
|
1911
|
+
});
|
|
1912
|
+
const line = scriptSource.split("\n")[frame.lineNumber] ?? "";
|
|
1913
|
+
const lookups = [...line.matchAll(LOOKUP_CALL)];
|
|
1914
|
+
// Only a lone lookup is certain to be the one that returned null; with several the position alone is given.
|
|
1915
|
+
if (lookups.length === 1)
|
|
1916
|
+
where += `, ${lookups[0]?.[0]})`;
|
|
1917
|
+
}
|
|
1918
|
+
catch {
|
|
1919
|
+
// Source unavailable: line:col alone still locates the throw.
|
|
1920
|
+
}
|
|
1921
|
+
finally {
|
|
1922
|
+
await client.send("Debugger.disable").catch(() => undefined);
|
|
1923
|
+
}
|
|
1924
|
+
return ` [thrown at: ${where}]`;
|
|
1846
1925
|
}
|
|
1847
1926
|
export async function initializeSession(session) {
|
|
1848
1927
|
const { page, serverUrl } = session;
|
|
@@ -1913,7 +1992,20 @@ export async function initializeSession(session) {
|
|
|
1913
1992
|
// Unlike the pageerror Error, this keeps the script URL of syntax errors and thrown non-errors.
|
|
1914
1993
|
const runtimeClient = await getCdpSession(page);
|
|
1915
1994
|
runtimeClient.on("Runtime.exceptionThrown", ({ exceptionDetails }) => {
|
|
1916
|
-
|
|
1995
|
+
const error = classifyPageError(exceptionDetails, projectScripts);
|
|
1996
|
+
recordPageError(session, error);
|
|
1997
|
+
if (!error)
|
|
1998
|
+
return;
|
|
1999
|
+
// Recorded first so fail-fast sees it; the throw site is appended once the source is fetched.
|
|
2000
|
+
void describeThrowSite(runtimeClient, exceptionDetails, projectScripts).then((site) => {
|
|
2001
|
+
const i = session.pageErrors.indexOf(error);
|
|
2002
|
+
if (!site || i < 0)
|
|
2003
|
+
return;
|
|
2004
|
+
if (session.pageErrors.includes(`${error}${site}`))
|
|
2005
|
+
session.pageErrors.splice(i, 1);
|
|
2006
|
+
else
|
|
2007
|
+
session.pageErrors[i] = `${error}${site}`;
|
|
2008
|
+
});
|
|
1917
2009
|
});
|
|
1918
2010
|
await runtimeClient.send("Runtime.enable");
|
|
1919
2011
|
const pageNavigationTimeout = session.config?.pageNavigationTimeout ?? DEFAULT_CONFIG.pageNavigationTimeout;
|
|
@@ -3556,7 +3648,8 @@ export async function captureFrameToBufferPipelined(session, frameIndex, time) {
|
|
|
3556
3648
|
// syncToPaintEvent = true); see initDrawElementOrTransparentBackground. The
|
|
3557
3649
|
// BeginFrame branch present in the synchronous captureFrameCore is therefore
|
|
3558
3650
|
// unreachable here and intentionally omitted.
|
|
3559
|
-
const
|
|
3651
|
+
const produced = await produceDrawElementFrame(page, options.width, options.height, options.quality ?? 80, true);
|
|
3652
|
+
const encodeResult = asDrawElementCaptureFailure(frameIndex, produced.encodeResult);
|
|
3560
3653
|
const captureTimeMs = Date.now() - startTime;
|
|
3561
3654
|
session.capturePerf.frames += 1;
|
|
3562
3655
|
session.capturePerf.seekMs += seekMs;
|
|
@@ -3671,7 +3764,10 @@ export async function captureFramesBatchPipelined(session, frameIndices, times)
|
|
|
3671
3764
|
const encodeResult = encodeResults[i];
|
|
3672
3765
|
if (frameIndex === undefined || !encodeResult)
|
|
3673
3766
|
break;
|
|
3674
|
-
results.push({
|
|
3767
|
+
results.push({
|
|
3768
|
+
frameIndex,
|
|
3769
|
+
encodeResult: asDrawElementCaptureFailure(frameIndex, encodeResult),
|
|
3770
|
+
});
|
|
3675
3771
|
}
|
|
3676
3772
|
if (failedAt !== null) {
|
|
3677
3773
|
// `error` is a plain string here (produceDrawElementFrameBatch returns it
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
* Performance: ~6 fps at 1080x1920 via base64 transfer.
|
|
20
20
|
*/
|
|
21
21
|
import { withHyperframesBrowserMarker } from "@hyperframes/core/browser-marker";
|
|
22
|
-
import { resolveFullChromePath,
|
|
22
|
+
import { resolveFullChromePath, ownedLaunchOptions } from "./browserManager.js";
|
|
23
23
|
// ── PQ (SMPTE 2084) OETF ─────────────────────────────────────────────────────
|
|
24
24
|
const PQ_M1 = 0.1593017578125;
|
|
25
25
|
const PQ_M2 = 78.84375;
|
|
@@ -181,7 +181,7 @@ export async function launchHdrBrowser(width, height) {
|
|
|
181
181
|
throw new Error("[HDR] No Chrome binary found. Install: npx @puppeteer/browsers install chrome@stable");
|
|
182
182
|
}
|
|
183
183
|
const browser = await ppt.launch({
|
|
184
|
-
...
|
|
184
|
+
...ownedLaunchOptions(),
|
|
185
185
|
headless: false,
|
|
186
186
|
executablePath: chromePath,
|
|
187
187
|
args: withHyperframesBrowserMarker(buildHdrChromeArgs(width, height)),
|
|
@@ -74,6 +74,7 @@ export interface ParallelWorkerPhaseDiagnostic {
|
|
|
74
74
|
browserVersion: string;
|
|
75
75
|
canvasDrawElement: boolean | "unknown";
|
|
76
76
|
gpuBackend: string;
|
|
77
|
+
readStep?: () => string | undefined;
|
|
77
78
|
}
|
|
78
79
|
export interface WorkerSizingConfig extends Partial<Pick<EngineConfig, "concurrency" | "coresPerWorker" | "minParallelFrames" | "largeRenderThreshold">> {
|
|
79
80
|
/**
|
|
@@ -237,6 +238,7 @@ export interface ParallelCaptureHooks {
|
|
|
237
238
|
* frame (the ordered writer) with the original error, not a stall.
|
|
238
239
|
*/
|
|
239
240
|
onWorkerFailure?: (failure: CaptureFailure) => void;
|
|
241
|
+
onWorkerOperation?: (diagnostic: ParallelWorkerPhaseDiagnostic) => void;
|
|
240
242
|
}
|
|
241
243
|
/**
|
|
242
244
|
* The pool's single failure gate. Both halves of the contract the streaming
|
|
@@ -320,20 +320,22 @@ function resolveParallelWorkerTimeoutMs(enabled) {
|
|
|
320
320
|
return Number.isFinite(parsed) && parsed > 0 ? parsed : 30_000;
|
|
321
321
|
}
|
|
322
322
|
// fallow-ignore-next-line complexity
|
|
323
|
-
async function captureFrameRange(session, task, captureOptions, signal, onFrameCaptured, onFrameBuffer, phaseTimeoutMs, diagnosticFor, onWorkerPhase) {
|
|
323
|
+
async function captureFrameRange(session, task, captureOptions, signal, onFrameCaptured, onFrameBuffer, phaseTimeoutMs, diagnosticFor, onWorkerPhase, onWorkerOperation) {
|
|
324
324
|
let framesCaptured = 0;
|
|
325
325
|
const outputOffset = task.outputFrameOffset ?? 0;
|
|
326
326
|
const stride = task.frameStride ?? 1;
|
|
327
327
|
const runCaptureOperation = (phase, frameIndex, operation) => {
|
|
328
|
-
const diagnostic = diagnosticFor(phase, frameIndex);
|
|
328
|
+
const diagnostic = { ...diagnosticFor(phase, frameIndex), readStep: () => session.captureStep };
|
|
329
329
|
if (framesCaptured === 0)
|
|
330
330
|
onWorkerPhase?.(diagnostic);
|
|
331
|
+
onWorkerOperation?.(diagnostic);
|
|
331
332
|
return withParallelWorkerDeadline(operation(), diagnostic, phaseTimeoutMs, signal);
|
|
332
333
|
};
|
|
333
334
|
const awaitEncode = (frameIndex, encodeResult) => {
|
|
334
335
|
const diagnostic = diagnosticFor("frame_encode", frameIndex);
|
|
335
336
|
if (framesCaptured === 0)
|
|
336
337
|
onWorkerPhase?.(diagnostic);
|
|
338
|
+
onWorkerOperation?.(diagnostic);
|
|
337
339
|
return withParallelWorkerDeadline(encodeResult, diagnostic, phaseTimeoutMs, signal);
|
|
338
340
|
};
|
|
339
341
|
// Depth-2 pipelined drawElement produce (HF_DE_PARALLEL_STREAM spike): frame
|
|
@@ -550,7 +552,7 @@ export async function verifyDiskDrawElementSamples(session, task, streaming) {
|
|
|
550
552
|
// flagged only because the disk self-verify call shifted its line range into
|
|
551
553
|
// the changed-code audit. Not restructured by this PR.
|
|
552
554
|
// fallow-ignore-next-line complexity
|
|
553
|
-
async function executeWorkerTask(task, serverUrl, captureOptions, createBeforeCaptureHook, signal, onFrameCaptured, onFrameBuffer, config, parallel, onFailure, onWorkerPhase) {
|
|
555
|
+
async function executeWorkerTask(task, serverUrl, captureOptions, createBeforeCaptureHook, signal, onFrameCaptured, onFrameBuffer, config, parallel, onFailure, onWorkerPhase, onWorkerOperation) {
|
|
554
556
|
const startTime = Date.now();
|
|
555
557
|
let framesCaptured = 0;
|
|
556
558
|
if (!existsSync(task.outputDir))
|
|
@@ -605,7 +607,7 @@ async function executeWorkerTask(task, serverUrl, captureOptions, createBeforeCa
|
|
|
605
607
|
canvasDrawElement = activeSession.captureMode === "drawelement";
|
|
606
608
|
gpuBackend = activeSession.gpuRenderer ?? "unknown";
|
|
607
609
|
logParDebug(() => `[par:w${task.workerId}] init done (mode=${session?.captureMode} workerEncode=${session?.workerEncodeEnabled === true})`);
|
|
608
|
-
framesCaptured = await captureFrameRange(activeSession, task, captureOptions, signal, onFrameCaptured, onFrameBuffer, phaseTimeoutMs, diagnosticFor, onWorkerPhase);
|
|
610
|
+
framesCaptured = await captureFrameRange(activeSession, task, captureOptions, signal, onFrameCaptured, onFrameBuffer, phaseTimeoutMs, diagnosticFor, onWorkerPhase, onWorkerOperation);
|
|
609
611
|
await verifyDiskDrawElementSamples(activeSession, task, Boolean(onFrameBuffer));
|
|
610
612
|
perf = getCapturePerfSummary(activeSession);
|
|
611
613
|
return {
|
|
@@ -775,7 +777,7 @@ export async function executeParallelCapture(serverUrl, workDir, tasks, captureO
|
|
|
775
777
|
hooks,
|
|
776
778
|
});
|
|
777
779
|
const onFailure = failureHandler.onFailure;
|
|
778
|
-
const results = await Promise.all(tasks.map((task) => executeWorkerTask(task, serverUrl, workerCaptureOptions, createBeforeCaptureHook, workerSignal, onFrameCaptured, onFrameBuffer, config, parallel, onFailure, onWorkerPhase)));
|
|
780
|
+
const results = await Promise.all(tasks.map((task) => executeWorkerTask(task, serverUrl, workerCaptureOptions, createBeforeCaptureHook, workerSignal, onFrameCaptured, onFrameBuffer, config, parallel, onFailure, onWorkerPhase, hooks?.onWorkerOperation)));
|
|
779
781
|
flagSilentWorkerExits(results);
|
|
780
782
|
const errors = results.filter((r) => r.failure || r.error);
|
|
781
783
|
if (errors.length > 0) {
|
|
@@ -38,6 +38,8 @@ export interface StreamingEncoderOptions {
|
|
|
38
38
|
preset?: string;
|
|
39
39
|
quality?: number;
|
|
40
40
|
bitrate?: string;
|
|
41
|
+
maxBitrate?: string;
|
|
42
|
+
bufferSize?: string;
|
|
41
43
|
pixelFormat?: string;
|
|
42
44
|
/** libvpx-vp9 -cpu-used value. Defaults to the engine VP9 setting. */
|
|
43
45
|
vp9CpuUsed?: number;
|
|
@@ -30,7 +30,7 @@ import { fpsToFfmpegArg, fpsToNumber } from "@hyperframes/core";
|
|
|
30
30
|
import { appendVp9CpuUsedArg } from "./vp9Options.js";
|
|
31
31
|
import { appendSoftwareEncoderThreadsArg } from "./softwareEncoderThreads.js";
|
|
32
32
|
import { appendRenderProvenanceArgs } from "../utils/renderProvenance.js";
|
|
33
|
-
import { appendLockedGopArgs, lockedGopCodecParams, resolveLockedGopSize } from "./chunkEncoder.js";
|
|
33
|
+
import { appendLockedGopArgs, appendRateCapArgs, lockedGopCodecParams, resolveLockedGopSize, } from "./chunkEncoder.js";
|
|
34
34
|
export function createFrameReorderBuffer(startFrame, endFrame) {
|
|
35
35
|
let cursor = startFrame;
|
|
36
36
|
let aborted = null;
|
|
@@ -121,6 +121,7 @@ export function buildStreamingArgs(options, outputPath, gpuEncoder = null) {
|
|
|
121
121
|
}
|
|
122
122
|
args.push("-r", fpsToFfmpegArg(fps));
|
|
123
123
|
const shouldUseGpu = useGpu && gpuEncoder !== null;
|
|
124
|
+
appendRateCapArgs(args, options);
|
|
124
125
|
if (codec === "h264" || codec === "h265") {
|
|
125
126
|
if (shouldUseGpu) {
|
|
126
127
|
const encoderName = getGpuEncoderName(gpuEncoder, codec);
|
|
@@ -258,6 +258,7 @@ export interface ImageElement {
|
|
|
258
258
|
end: number;
|
|
259
259
|
}
|
|
260
260
|
export declare function parseImageElements(html: string): ImageElement[];
|
|
261
|
+
export declare function sdrCanvasColourFilters(metadata: VideoMetadata, toneMappedToSdr?: boolean): string[];
|
|
261
262
|
export declare function extractVideoFramesRange(...args: Parameters<typeof extractVideoFramesRangeUnredacted>): Promise<ExtractedFrames>;
|
|
262
263
|
declare function extractVideoFramesRangeUnredacted(videoPath: string, videoId: string, startTime: number, duration: number, options: ExtractionOptions, signal?: AbortSignal, config?: Partial<Pick<EngineConfig, "ffmpegProcessTimeout">>,
|
|
263
264
|
/** Frames go straight here, with no per-videoId subdir (the cache layer's keyed entry). */
|
|
@@ -14,7 +14,7 @@ import { parseHTML } from "linkedom";
|
|
|
14
14
|
import { isPathInside } from "@hyperframes/parsers/asset-paths";
|
|
15
15
|
import { resolveProjectRelativeSrc } from "@hyperframes/parsers/asset-resolution";
|
|
16
16
|
import { alphaDecoderFor } from "@hyperframes/parsers/media-alpha";
|
|
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
|
+
import { MEDIA_RENDER_ID_ATTR, isAudibleVideoElement, fpsToFfmpegArg, fpsToNumber, MEDIA_DURATION_CLAMP_EPSILON_SECONDS, normalizeRateSpec, readElementRateSpec, shiftRateLane, sourceTimeAt, timeAtSourceTime, exportClipWindow, hasClipStarted, isInClipWindow, mediaElementTime, parseStrictFiniteTimingNumber, readMediaStart, toFps, firstFrameColourArgs, hdrToSdrToneMapFilter, parseFirstFrameColour, } from "@hyperframes/core";
|
|
18
18
|
import { resolveReferencedStart } from "./referenceResolver.js";
|
|
19
19
|
import { isKnownInactiveTimelineWindow } from "./mediaTimelineWindow.js";
|
|
20
20
|
import { extractFinalVideoFrameTimestamp, extractMediaMetadata, mediaFileIdentity, readMetadataCache, readStoredFinalFrame, resolvePlayableVideoDuration, writeMetadataCache, } from "../utils/ffprobe.js";
|
|
@@ -477,6 +477,12 @@ function chromeGuessForUntaggedMatrix(metadata) {
|
|
|
477
477
|
const hd = metadata.height >= 720 && !CHROME_BT601_UNTAGGED_CODECS.has(metadata.videoCodec);
|
|
478
478
|
return [`setparams=colorspace=${hd ? "bt709" : "smpte170m"}`];
|
|
479
479
|
}
|
|
480
|
+
export function sdrCanvasColourFilters(metadata, toneMappedToSdr = false) {
|
|
481
|
+
if (!isHdrColorSpaceUtil(metadata.colorSpace)) {
|
|
482
|
+
return [...chromeGuessForUntaggedMatrix(metadata), SDR_CANVAS_PASSTHROUGH_FILTER];
|
|
483
|
+
}
|
|
484
|
+
return toneMappedToSdr ? [SDR_CANVAS_PASSTHROUGH_FILTER] : [];
|
|
485
|
+
}
|
|
480
486
|
/**
|
|
481
487
|
* Longest stretch of output one ffmpeg process extracts; longer ranges split into segments
|
|
482
488
|
* that run side by side. Segments sample the same absolute times as a single pass, so their
|
|
@@ -617,9 +623,6 @@ outputDirOverride) {
|
|
|
617
623
|
throw new Error("frameRanges extraction needs a constant-frame-rate source");
|
|
618
624
|
}
|
|
619
625
|
const args = urlInputArgs(videoPath);
|
|
620
|
-
if (decodeWithVideoToolbox) {
|
|
621
|
-
args.push("-hwaccel", "videotoolbox");
|
|
622
|
-
}
|
|
623
626
|
// Always force the alpha-aware decoder on codecs that can carry alpha. The
|
|
624
627
|
// alternative — gating on `metadata.hasAlpha` — relies on tag detection that
|
|
625
628
|
// has at least three known failure modes: case-sensitivity across ffmpeg
|
|
@@ -678,7 +681,7 @@ outputDirOverride) {
|
|
|
678
681
|
vfFilters.push(hdrToSdrToneMapFilter(metadata.colorSpace ?? {}, frameColour));
|
|
679
682
|
}
|
|
680
683
|
if (toneMappedToSdr || (!isHdr && !options.sdrToHdrTransfer)) {
|
|
681
|
-
vfFilters.push(...(
|
|
684
|
+
vfFilters.push(...sdrCanvasColourFilters(metadata, toneMappedToSdr));
|
|
682
685
|
if (format === "jpg") {
|
|
683
686
|
if (options.shownBox)
|
|
684
687
|
vfFilters.push(halveToCoverScaleFilter(options.shownBox));
|
|
@@ -702,9 +705,9 @@ outputDirOverride) {
|
|
|
702
705
|
// emits that last slot: the fps filter ends a `-t` window at the nearest slot. A
|
|
703
706
|
// decimal rate counts zero frames here and keeps the `-t` window.
|
|
704
707
|
const boundVfrFrameCount = metadata.isVFR && totalFrames > 0;
|
|
705
|
-
const
|
|
706
|
-
?
|
|
707
|
-
decodeArgs,
|
|
708
|
+
const extract = (hwaccel) => sampleAtOutputFps && (options.frameRanges || boundVfrFrameCount || totalFrames > segmentFrames)
|
|
709
|
+
? runSegmentedExtraction({
|
|
710
|
+
decodeArgs: [...hwaccel, ...decodeArgs],
|
|
708
711
|
filterAndEncodeArgs: (seek) => [...filterArgsFor(seek), ...encodeArgs],
|
|
709
712
|
videoPath,
|
|
710
713
|
startTime,
|
|
@@ -713,7 +716,14 @@ outputDirOverride) {
|
|
|
713
716
|
segments: splitFrameRanges(options.frameRanges ?? [{ firstFrame: 0, frames: totalFrames }], segmentFrames),
|
|
714
717
|
runOptions,
|
|
715
718
|
})
|
|
716
|
-
:
|
|
719
|
+
: inExtractionSlot(() => runFfmpeg([...hwaccel, ...args, ...filterArgsFor(startTime), ...encodeArgs], runOptions), signal);
|
|
720
|
+
let processResult = await extract(decodeWithVideoToolbox ? ["-hwaccel", "videotoolbox"] : []);
|
|
721
|
+
const ffmpegExitedFailing = processResult.terminationReason === "exit" &&
|
|
722
|
+
!processResult.success &&
|
|
723
|
+
!processResult.failureReason;
|
|
724
|
+
const videoToolboxWasOptional = decodeWithVideoToolbox && !keepsHdrForChrome;
|
|
725
|
+
if (ffmpegExitedFailing && videoToolboxWasOptional)
|
|
726
|
+
processResult = await extract([]);
|
|
717
727
|
if (processResult.failureReason === "external_interruption") {
|
|
718
728
|
throw new VideoSourceExtractionError("external_interruption", true, "Video frame extraction interrupted by host lifecycle", `FFmpeg exited with code ${processResult.exitCode}: ${processResult.stderr.slice(-500)}`);
|
|
719
729
|
}
|
|
@@ -1832,9 +1842,9 @@ function getFrameIndexAtTime(extracted, globalTime, videoStart, loop = false, me
|
|
|
1832
1842
|
if (cycle > 0 && sourceTime >= cycle)
|
|
1833
1843
|
sourceTime %= cycle;
|
|
1834
1844
|
}
|
|
1835
|
-
//
|
|
1836
|
-
|
|
1837
|
-
const frameIndex = Math.floor(
|
|
1845
|
+
// The frame the preview's <video> shows at this source time, so render and preview pick the same one.
|
|
1846
|
+
const shownTime = mediaElementTime(mediaStart + sourceTime) - mediaStart;
|
|
1847
|
+
const frameIndex = Math.floor(shownTime * extracted.fps);
|
|
1838
1848
|
if (frameIndex < 0 || extracted.totalFrames <= 0)
|
|
1839
1849
|
return null;
|
|
1840
1850
|
if (isVideoHiddenBeforeStreamStart(extracted.metadata.videoStreamLeadSeconds, mediaStart + sourceTime)) {
|
package/dist/utils/ffprobe.d.ts
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
/** Spawn ffprobe with given args, return stdout. Throws on non-zero exit or missing binary. */
|
|
2
|
+
export declare function runFfprobe(filePath: string, argsWithoutInput: string[], signal?: AbortSignal, stdoutOptions?: {
|
|
3
|
+
retainTail?: boolean;
|
|
4
|
+
maxChars?: number;
|
|
5
|
+
}): Promise<string>;
|
|
1
6
|
export interface MediaProbeCacheEntry<T = FFProbeOutput> {
|
|
2
7
|
identity: string | null;
|
|
3
8
|
promise: Promise<T>;
|
package/dist/utils/ffprobe.js
CHANGED
|
@@ -49,7 +49,7 @@ function sanitizeFfprobeDiagnostic(stderr, filePath) {
|
|
|
49
49
|
return `…${redacted.slice(-(FFPROBE_ERROR_MAX_CHARS - 1))}`;
|
|
50
50
|
}
|
|
51
51
|
/** Spawn ffprobe with given args, return stdout. Throws on non-zero exit or missing binary. */
|
|
52
|
-
async function runFfprobe(filePath, argsWithoutInput, signal, stdoutOptions) {
|
|
52
|
+
export async function runFfprobe(filePath, argsWithoutInput, signal, stdoutOptions) {
|
|
53
53
|
// `--` stops option parsing so a path like "-intro.mp4" is a filename, but
|
|
54
54
|
// it does NOT cover a path of exactly "-": ffprobe rewrites that to `fd:`
|
|
55
55
|
// AFTER option parsing and then reads stdin. Since stdin here is a pipe the
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Removes a scratch folder or file the mix is already done with. A refusal (Windows temp folders that deny
|
|
2
|
+
* delete) leaves it for the render's own cleanup and never fails the audio it served. */
|
|
3
|
+
export declare function removeScratch(path: string): void;
|
|
4
|
+
/** Runs `run`; whatever removeScratch left behind inside it (this run's only) goes to `report` once it ends. */
|
|
5
|
+
export declare function collectScratchLeftovers<T>(run: () => Promise<T>, report: (left: string[]) => void): Promise<T>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { rmSync } from "node:fs";
|
|
3
|
+
const collecting = new AsyncLocalStorage();
|
|
4
|
+
/** Removes a scratch folder or file the mix is already done with. A refusal (Windows temp folders that deny
|
|
5
|
+
* delete) leaves it for the render's own cleanup and never fails the audio it served. */
|
|
6
|
+
export function removeScratch(path) {
|
|
7
|
+
try {
|
|
8
|
+
rmSync(path, { recursive: true, force: true });
|
|
9
|
+
}
|
|
10
|
+
catch {
|
|
11
|
+
collecting.getStore()?.push(path);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** Runs `run`; whatever removeScratch left behind inside it (this run's only) goes to `report` once it ends. */
|
|
15
|
+
export async function collectScratchLeftovers(run, report) {
|
|
16
|
+
const left = [];
|
|
17
|
+
try {
|
|
18
|
+
return await collecting.run(left, run);
|
|
19
|
+
}
|
|
20
|
+
finally {
|
|
21
|
+
report(left);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
interface Options {
|
|
2
2
|
platform?: NodeJS.Platform;
|
|
3
|
+
/** Copy the bytes when Windows keeps refusing (a TEMP that denies delete). Not atomic: scratch files only. */
|
|
4
|
+
copyFallback?: boolean;
|
|
3
5
|
}
|
|
4
6
|
/** `renameSync` that rides out Windows' transient file locks; other platforms and errors throw at once. */
|
|
5
7
|
export declare function renameWithRetry(from: string, to: string, options?: Options & {
|
|
6
8
|
rename?: (from: string, to: string) => void;
|
|
9
|
+
copy?: (from: string, to: string) => void;
|
|
7
10
|
}): void;
|
|
8
11
|
/** Async twin of {@link renameWithRetry}. */
|
|
9
12
|
export declare function renameWithRetryAsync(from: string, to: string, options?: Options & {
|
|
10
13
|
rename?: (from: string, to: string) => Promise<void>;
|
|
14
|
+
copy?: (from: string, to: string) => Promise<void>;
|
|
11
15
|
}): Promise<void>;
|
|
12
16
|
export {};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { renameSync } from "node:fs";
|
|
2
|
-
import { rename } from "node:fs/promises";
|
|
1
|
+
import { copyFileSync, renameSync } from "node:fs";
|
|
2
|
+
import { copyFile, rename } from "node:fs/promises";
|
|
3
|
+
import { removeScratch } from "./removeScratch.js";
|
|
3
4
|
// Windows reports EPERM/EACCES/EBUSY while antivirus or a just-exited process still holds the
|
|
4
5
|
// file; the hold clears within milliseconds. Total wait before giving up: about 1.3 s.
|
|
5
6
|
const RETRYABLE = new Set(["EPERM", "EACCES", "EBUSY"]);
|
|
@@ -9,12 +10,21 @@ function isRetryable(error, platform) {
|
|
|
9
10
|
return false;
|
|
10
11
|
return RETRYABLE.has(error?.code ?? "");
|
|
11
12
|
}
|
|
13
|
+
/** What a refused rename does next: wait and try again, fall back to a copy, or give up with the error. */
|
|
14
|
+
function nextStep(error, attempt, options, platform) {
|
|
15
|
+
if (!isRetryable(error, platform))
|
|
16
|
+
return "throw";
|
|
17
|
+
if (attempt < BACKOFF_MS.length)
|
|
18
|
+
return "wait";
|
|
19
|
+
return options.copyFallback ? "copy" : "throw";
|
|
20
|
+
}
|
|
12
21
|
function sleepSync(ms) {
|
|
13
22
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
14
23
|
}
|
|
15
24
|
/** `renameSync` that rides out Windows' transient file locks; other platforms and errors throw at once. */
|
|
16
25
|
export function renameWithRetry(from, to, options = {}) {
|
|
17
26
|
const doRename = options.rename ?? renameSync;
|
|
27
|
+
const doCopy = options.copy ?? copyFileSync;
|
|
18
28
|
const platform = options.platform ?? process.platform;
|
|
19
29
|
for (let attempt = 0;; attempt++) {
|
|
20
30
|
try {
|
|
@@ -22,15 +32,28 @@ export function renameWithRetry(from, to, options = {}) {
|
|
|
22
32
|
return;
|
|
23
33
|
}
|
|
24
34
|
catch (error) {
|
|
25
|
-
|
|
35
|
+
const step = nextStep(error, attempt, options, platform);
|
|
36
|
+
if (step === "throw")
|
|
37
|
+
throw error;
|
|
38
|
+
if (step === "wait") {
|
|
39
|
+
sleepSync(BACKOFF_MS[attempt]);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
doCopy(from, to);
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
26
46
|
throw error;
|
|
27
|
-
|
|
47
|
+
}
|
|
48
|
+
removeScratch(from);
|
|
49
|
+
return;
|
|
28
50
|
}
|
|
29
51
|
}
|
|
30
52
|
}
|
|
31
53
|
/** Async twin of {@link renameWithRetry}. */
|
|
32
54
|
export async function renameWithRetryAsync(from, to, options = {}) {
|
|
33
55
|
const doRename = options.rename ?? rename;
|
|
56
|
+
const doCopy = options.copy ?? copyFile;
|
|
34
57
|
const platform = options.platform ?? process.platform;
|
|
35
58
|
for (let attempt = 0;; attempt++) {
|
|
36
59
|
try {
|
|
@@ -38,9 +61,21 @@ export async function renameWithRetryAsync(from, to, options = {}) {
|
|
|
38
61
|
return;
|
|
39
62
|
}
|
|
40
63
|
catch (error) {
|
|
41
|
-
|
|
64
|
+
const step = nextStep(error, attempt, options, platform);
|
|
65
|
+
if (step === "throw")
|
|
42
66
|
throw error;
|
|
43
|
-
|
|
67
|
+
if (step === "wait") {
|
|
68
|
+
await new Promise((resolve) => setTimeout(resolve, BACKOFF_MS[attempt]));
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
await doCopy(from, to);
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
throw error;
|
|
76
|
+
}
|
|
77
|
+
removeScratch(from);
|
|
78
|
+
return;
|
|
44
79
|
}
|
|
45
80
|
}
|
|
46
81
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/engine",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.207",
|
|
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/
|
|
54
|
-
"@hyperframes/
|
|
53
|
+
"@hyperframes/core": "^0.8.207",
|
|
54
|
+
"@hyperframes/parsers": "^0.8.207"
|
|
55
55
|
},
|
|
56
56
|
"devDependencies": {
|
|
57
57
|
"@types/node": "^25.0.10",
|