hyperframes 0.7.89 → 0.7.90

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/cli.js CHANGED
@@ -50,7 +50,7 @@ var VERSION;
50
50
  var init_version = __esm({
51
51
  "src/version.ts"() {
52
52
  "use strict";
53
- VERSION = true ? "0.7.89" : "0.0.0-dev";
53
+ VERSION = true ? "0.7.90" : "0.0.0-dev";
54
54
  }
55
55
  });
56
56
 
@@ -62735,7 +62735,7 @@ function parsePositiveByteLimitMb(content, noLimitCutoffBytes) {
62735
62735
  }
62736
62736
  return Number(bytes / BYTES_PER_MIB_BIGINT);
62737
62737
  }
62738
- function getCgroupLimitMb() {
62738
+ function getCgroupMemoryLimitMb() {
62739
62739
  if (_cachedCgroupLimitMb !== void 0) return _cachedCgroupLimitMb;
62740
62740
  if (process.platform !== "linux") {
62741
62741
  _cachedCgroupLimitMb = null;
@@ -62782,7 +62782,7 @@ function warnCgroupReadFailure(path2, error) {
62782
62782
  }
62783
62783
  function getSystemTotalMb() {
62784
62784
  const hostTotalMb = Math.floor(totalmem() / BYTES_PER_MIB);
62785
- const cgroupLimitMb = getCgroupLimitMb();
62785
+ const cgroupLimitMb = getCgroupMemoryLimitMb();
62786
62786
  return cgroupLimitMb === null ? hostTotalMb : Math.min(hostTotalMb, cgroupLimitMb);
62787
62787
  }
62788
62788
  function isLowMemorySystem(totalMb = getSystemTotalMb()) {
@@ -68812,7 +68812,7 @@ function sanitizeFfprobeDiagnostic(stderr, filePath) {
68812
68812
  if (redacted.length <= FFPROBE_ERROR_MAX_CHARS) return redacted;
68813
68813
  return `\u2026${redacted.slice(-(FFPROBE_ERROR_MAX_CHARS - 1))}`;
68814
68814
  }
68815
- async function runFfprobe(filePath, argsWithoutInput, signal) {
68815
+ async function runFfprobe(filePath, argsWithoutInput, signal, stdoutOptions) {
68816
68816
  if (filePath === "-") {
68817
68817
  throw new Error('[FFmpeg] Refusing to probe "-": stdin is not a supported input path.');
68818
68818
  }
@@ -68826,12 +68826,17 @@ async function runFfprobe(filePath, argsWithoutInput, signal) {
68826
68826
  const decoder = new StringDecoder("utf8");
68827
68827
  let stdout2 = "";
68828
68828
  let stdoutTruncated = false;
68829
+ const stdoutMaxChars = stdoutOptions?.maxChars ?? FFPROBE_STDOUT_MAX_CHARS;
68829
68830
  proc.stdout.on("data", (data2) => {
68830
68831
  if (stdoutTruncated) return;
68831
68832
  stdout2 += decoder.write(data2);
68832
- if (stdout2.length > FFPROBE_STDOUT_MAX_CHARS) {
68833
- stdoutTruncated = true;
68834
- stdout2 = "";
68833
+ if (stdout2.length > stdoutMaxChars) {
68834
+ if (stdoutOptions?.retainTail) {
68835
+ stdout2 = stdout2.slice(-stdoutMaxChars);
68836
+ } else {
68837
+ stdoutTruncated = true;
68838
+ stdout2 = "";
68839
+ }
68835
68840
  }
68836
68841
  });
68837
68842
  const managed = new ManagedChildProcess(proc, {
@@ -68843,7 +68848,7 @@ async function runFfprobe(filePath, argsWithoutInput, signal) {
68843
68848
  stdout2 += decoder.end();
68844
68849
  if (stdoutTruncated) {
68845
68850
  throw new Error(
68846
- `[FFmpeg] ffprobe output exceeded ${FFPROBE_STDOUT_MAX_CHARS} characters; refusing to parse a truncated result.`
68851
+ `[FFmpeg] ffprobe output exceeded ${stdoutMaxChars} characters; refusing to parse a truncated result.`
68847
68852
  );
68848
68853
  }
68849
68854
  if (outcome.reason === "spawn_error") {
@@ -68990,6 +68995,7 @@ async function extractMediaMetadata(filePath) {
68990
68995
  return {
68991
68996
  durationSeconds: 0,
68992
68997
  videoStreamDurationSeconds: 0,
68998
+ videoStreamStartSeconds: 0,
68993
68999
  width: stillImageMeta.width,
68994
69000
  height: stillImageMeta.height,
68995
69001
  fps: 0,
@@ -69021,9 +69027,12 @@ async function extractMediaMetadata(filePath) {
69021
69027
  const hasAlpha = pixelFormatHasAlpha(pixelFormat) || alphaMode === "1";
69022
69028
  const containerDuration = output?.format.duration ? parseFloat(output.format.duration) : 0;
69023
69029
  const streamDuration = videoStream.duration ? parseFloat(videoStream.duration) : 0;
69030
+ const parsedStreamStart = videoStream.start_time ? parseFloat(videoStream.start_time) : 0;
69031
+ const streamStart = Number.isFinite(parsedStreamStart) ? parsedStreamStart : 0;
69024
69032
  return {
69025
69033
  durationSeconds: containerDuration,
69026
69034
  videoStreamDurationSeconds: streamDuration > 0 ? streamDuration : containerDuration,
69035
+ videoStreamStartSeconds: streamStart,
69027
69036
  width: videoStream.width || stillImage()?.width || 0,
69028
69037
  height: videoStream.height || stillImage()?.height || 0,
69029
69038
  fps,
@@ -69042,6 +69051,59 @@ async function extractMediaMetadata(filePath) {
69042
69051
  });
69043
69052
  return probePromise;
69044
69053
  }
69054
+ async function extractFinalVideoFrameTimestamp(filePath, metadata, signal) {
69055
+ const videoDurationSeconds = metadata.videoStreamDurationSeconds;
69056
+ const candidateStreamStart = metadata.videoStreamStartSeconds ?? 0;
69057
+ const videoStreamStartSeconds = Number.isFinite(candidateStreamStart) ? candidateStreamStart : 0;
69058
+ const cacheKey = `${filePath}\0${String(videoStreamStartSeconds)}\0${String(videoDurationSeconds)}`;
69059
+ let probeCache = finalVideoFrameTimestampCache;
69060
+ if (signal) {
69061
+ probeCache = finalVideoFrameTimestampSignalCaches.get(signal) ?? /* @__PURE__ */ new Map();
69062
+ finalVideoFrameTimestampSignalCaches.set(signal, probeCache);
69063
+ }
69064
+ const cached2 = probeCache.get(cacheKey);
69065
+ if (cached2) return cached2;
69066
+ const probePromise = (async () => {
69067
+ if (!(videoDurationSeconds > 0) || !Number.isFinite(videoDurationSeconds)) {
69068
+ throw new Error(
69069
+ `[FFmpeg] Cannot locate final video frame for invalid duration ${String(videoDurationSeconds)}`
69070
+ );
69071
+ }
69072
+ const streamEnd = videoStreamStartSeconds + videoDurationSeconds;
69073
+ const intervalStart = Math.max(videoStreamStartSeconds, streamEnd - 1);
69074
+ const parseFinalTimestamp = (stdout2) => stdout2.split("\n").map((line2) => line2.trim().split(",")[0]?.trim() ?? "").filter((value) => value.length > 0).map((value) => Number(value)).filter((timestamp2) => Number.isFinite(timestamp2)).at(-1);
69075
+ const probe = async (readInterval) => {
69076
+ const args = [
69077
+ "-select_streams",
69078
+ "v:0",
69079
+ "-show_entries",
69080
+ "frame=best_effort_timestamp_time",
69081
+ "-of",
69082
+ "csv=p=0"
69083
+ ];
69084
+ if (readInterval) args.splice(2, 0, "-read_intervals", readInterval);
69085
+ const stdout2 = await runFfprobe(filePath, args, signal, {
69086
+ retainTail: true,
69087
+ maxChars: 64 * 1024
69088
+ });
69089
+ return parseFinalTimestamp(stdout2);
69090
+ };
69091
+ const timestamp = await probe(`${intervalStart}%${streamEnd}`) ?? await probe(
69092
+ /* full scan */
69093
+ );
69094
+ if (timestamp === void 0) {
69095
+ throw new Error("[FFmpeg] ffprobe found no decodable final video frame");
69096
+ }
69097
+ return Math.min(Math.max(timestamp - videoStreamStartSeconds, 0), videoDurationSeconds);
69098
+ })();
69099
+ probeCache.set(cacheKey, probePromise);
69100
+ probePromise.catch(() => {
69101
+ if (probeCache.get(cacheKey) === probePromise) {
69102
+ probeCache.delete(cacheKey);
69103
+ }
69104
+ });
69105
+ return probePromise;
69106
+ }
69045
69107
  async function extractAudioMetadata(filePath, options) {
69046
69108
  const cached2 = options?.signal ? void 0 : audioMetadataCache.get(filePath);
69047
69109
  if (cached2) return cached2;
@@ -69148,7 +69210,7 @@ async function analyzeKeyframeIntervalsUncached(filePath) {
69148
69210
  isProblematic: maxInterval > 2
69149
69211
  };
69150
69212
  }
69151
- var FFPROBE_STDERR_MAX_BYTES, FFPROBE_STDOUT_MAX_CHARS, FFPROBE_ERROR_MAX_CHARS, videoMetadataCache, audioMetadataCache, AAC_LC_SAMPLES_PER_PACKET, nativeCrc32, extractVideoMetadata, keyframeCache;
69213
+ var FFPROBE_STDERR_MAX_BYTES, FFPROBE_STDOUT_MAX_CHARS, FFPROBE_ERROR_MAX_CHARS, videoMetadataCache, finalVideoFrameTimestampCache, finalVideoFrameTimestampSignalCaches, audioMetadataCache, AAC_LC_SAMPLES_PER_PACKET, nativeCrc32, extractVideoMetadata, keyframeCache;
69152
69214
  var init_ffprobe = __esm({
69153
69215
  "../engine/src/utils/ffprobe.ts"() {
69154
69216
  "use strict";
@@ -69160,6 +69222,8 @@ var init_ffprobe = __esm({
69160
69222
  FFPROBE_STDOUT_MAX_CHARS = 8e6;
69161
69223
  FFPROBE_ERROR_MAX_CHARS = 4 * 1024;
69162
69224
  videoMetadataCache = /* @__PURE__ */ new Map();
69225
+ finalVideoFrameTimestampCache = /* @__PURE__ */ new Map();
69226
+ finalVideoFrameTimestampSignalCaches = /* @__PURE__ */ new WeakMap();
69163
69227
  audioMetadataCache = /* @__PURE__ */ new Map();
69164
69228
  AAC_LC_SAMPLES_PER_PACKET = 1024;
69165
69229
  nativeCrc32 = typeof zlib.crc32 === "function" ? zlib.crc32 : void 0;
@@ -83919,20 +83983,21 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
83919
83983
  } catch (error) {
83920
83984
  throw classifyVideoExtractionError(error);
83921
83985
  }
83922
- if (!(metadata.durationSeconds > 0)) {
83986
+ const playableDuration = resolvePlayableVideoDuration(metadata);
83987
+ if (!(playableDuration > 0)) {
83923
83988
  throw new VideoSourceExtractionError(
83924
83989
  "invalid_media",
83925
83990
  false,
83926
83991
  "Video source has no positive duration",
83927
- `Video source duration is ${metadata.durationSeconds}s`
83992
+ `Playable video stream duration is ${playableDuration}s`
83928
83993
  );
83929
83994
  }
83930
- if (startTime >= metadata.durationSeconds) {
83995
+ if (startTime >= playableDuration) {
83931
83996
  throw new VideoSourceExtractionError(
83932
83997
  "media_start_out_of_range",
83933
83998
  false,
83934
83999
  "Video media start is outside the source duration",
83935
- `Video media start ${startTime}s is outside source duration ${metadata.durationSeconds}s`
84000
+ `Video media start ${startTime}s is outside playable video duration ${playableDuration}s`
83936
84001
  );
83937
84002
  }
83938
84003
  const format = resolveFrameFormat(metadata, options.format);
@@ -83947,19 +84012,25 @@ async function extractVideoFramesRange(videoPath, videoId, startTime, duration,
83947
84012
  if (codecMayHaveAlpha(metadata.videoCodec)) {
83948
84013
  args.push("-c:v", decoderForCodec(metadata.videoCodec));
83949
84014
  }
83950
- args.push("-ss", String(startTime), "-i", videoPath, "-t", String(duration));
84015
+ if (options.finalFrameOnly) {
84016
+ args.push("-i", videoPath, "-ss", String(startTime), "-frames:v", "1");
84017
+ } else {
84018
+ args.push("-ss", String(startTime), "-i", videoPath, "-t", String(duration));
84019
+ }
83951
84020
  const vfFilters = [];
83952
84021
  if (isHdr && isMacOS) {
83953
84022
  vfFilters.push("format=nv12");
83954
84023
  }
83955
- if (!metadata.isVFR) {
84024
+ if (!options.finalFrameOnly && !metadata.isVFR) {
83956
84025
  vfFilters.push(`fps=${fps}`);
83957
84026
  }
83958
84027
  if (options.sdrToHdrTransfer) {
83959
84028
  vfFilters.push(SDR_TO_HDR_COLORSPACE_FILTER);
83960
84029
  }
83961
84030
  if (vfFilters.length > 0) args.push("-vf", vfFilters.join(","));
83962
- if (metadata.isVFR) args.push("-fps_mode", "cfr", "-r", String(fps));
84031
+ if (!options.finalFrameOnly && metadata.isVFR) {
84032
+ args.push("-fps_mode", "cfr", "-r", String(fps));
84033
+ }
83963
84034
  args.push("-q:v", format === "jpg" ? String(Math.ceil((100 - quality) / 3)) : "0");
83964
84035
  if (format === "png") args.push("-compression_level", "1");
83965
84036
  args.push("-y", outputPattern);
@@ -84036,10 +84107,124 @@ function classifyFfmpegSpawnError(error, stderr = "") {
84036
84107
  diagnostic
84037
84108
  );
84038
84109
  }
84039
- function resolveSegmentDuration(requested, mediaStart, metadata) {
84110
+ function resolveSegmentDuration(requested, mediaStart, sourceDuration) {
84040
84111
  if (Number.isFinite(requested) && requested > 0) return requested;
84041
- const sourceRemaining = metadata.durationSeconds - mediaStart;
84042
- return sourceRemaining > 0 ? sourceRemaining : metadata.durationSeconds;
84112
+ const sourceRemaining = sourceDuration - mediaStart;
84113
+ return sourceRemaining > 0 ? sourceRemaining : sourceDuration;
84114
+ }
84115
+ function resolvePlayableVideoDuration(metadata) {
84116
+ return Number.isFinite(metadata.videoStreamDurationSeconds) && metadata.videoStreamDurationSeconds > 0 ? metadata.videoStreamDurationSeconds : metadata.durationSeconds;
84117
+ }
84118
+ function resolveTimelineExtractionWindow(video, resolvedDuration, timelineEnd, sourceDuration) {
84119
+ if (timelineEnd === void 0) {
84120
+ return {
84121
+ compositionStart: video.start,
84122
+ mediaStart: video.mediaStart,
84123
+ durationSeconds: resolvedDuration
84124
+ };
84125
+ }
84126
+ if (!Number.isFinite(timelineEnd)) {
84127
+ throw new Error(`Video extraction timelineEnd must be finite; got ${String(timelineEnd)}`);
84128
+ }
84129
+ const compositionStart = Math.max(0, video.start);
84130
+ const trimmedPreroll = compositionStart - video.start;
84131
+ const timelineDuration2 = Math.max(0, timelineEnd - compositionStart);
84132
+ const resolvedVisibleDuration = resolvedDuration - trimmedPreroll;
84133
+ const visibleDuration = Math.max(0, Math.min(resolvedVisibleDuration, timelineDuration2));
84134
+ let mediaStart = video.mediaStart + trimmedPreroll;
84135
+ if (visibleDuration > 0 && sourceDuration !== void 0) {
84136
+ const sourceRemaining = Math.max(0, sourceDuration - video.mediaStart);
84137
+ if (sourceRemaining > 0 && video.loop && Number.isFinite(video.end)) {
84138
+ const phaseOffset = trimmedPreroll % sourceRemaining;
84139
+ const phaseRemaining = sourceRemaining - phaseOffset;
84140
+ if (visibleDuration >= phaseRemaining) {
84141
+ return {
84142
+ compositionStart: video.start,
84143
+ mediaStart: video.mediaStart,
84144
+ durationSeconds: sourceRemaining,
84145
+ preserveTimelinePhase: true
84146
+ };
84147
+ }
84148
+ mediaStart = video.mediaStart + phaseOffset;
84149
+ } else if (sourceRemaining > 0) {
84150
+ const sourceVisibleAfterPreroll = Math.max(0, sourceRemaining - trimmedPreroll);
84151
+ if (visibleDuration <= sourceVisibleAfterPreroll) {
84152
+ return {
84153
+ compositionStart,
84154
+ mediaStart,
84155
+ durationSeconds: visibleDuration
84156
+ };
84157
+ }
84158
+ const extractionDuration = Math.min(
84159
+ sourceRemaining,
84160
+ Math.max(sourceVisibleAfterPreroll, FINAL_FRAME_LOGICAL_DURATION_SECONDS)
84161
+ );
84162
+ const extractionOffset = sourceRemaining - extractionDuration;
84163
+ return {
84164
+ compositionStart: video.start + extractionOffset,
84165
+ mediaStart: video.mediaStart + extractionOffset,
84166
+ durationSeconds: extractionDuration,
84167
+ preserveTimelineEnd: true,
84168
+ ensureFinalFrame: true
84169
+ };
84170
+ }
84171
+ }
84172
+ return {
84173
+ compositionStart,
84174
+ mediaStart,
84175
+ durationSeconds: visibleDuration
84176
+ };
84177
+ }
84178
+ async function resolveFinalFrameExtractionWindow(videoPath, video, metadata, window3, signal) {
84179
+ if (!window3.ensureFinalFrame) return window3;
84180
+ const playableDuration = resolvePlayableVideoDuration(metadata);
84181
+ const finalFrameTimestamp = await extractFinalVideoFrameTimestamp(
84182
+ videoPath,
84183
+ {
84184
+ videoStreamDurationSeconds: playableDuration,
84185
+ videoStreamStartSeconds: metadata.videoStreamStartSeconds
84186
+ },
84187
+ signal
84188
+ );
84189
+ if (window3.mediaStart < finalFrameTimestamp - 1e-9) return window3;
84190
+ const sourceRemaining = playableDuration - video.mediaStart;
84191
+ const logicalDuration = Math.min(sourceRemaining, FINAL_FRAME_LOGICAL_DURATION_SECONDS);
84192
+ return {
84193
+ compositionStart: Math.max(0, video.start),
84194
+ mediaStart: playableDuration - logicalDuration,
84195
+ extractionMediaStart: finalFrameTimestamp,
84196
+ durationSeconds: logicalDuration,
84197
+ preserveTimelineEnd: true,
84198
+ finalFrameOnly: true
84199
+ };
84200
+ }
84201
+ function resolveVideoExtractionWindow(video, metadata, timelineEnd) {
84202
+ const playableDuration = resolvePlayableVideoDuration(metadata);
84203
+ if (!(playableDuration > 0)) {
84204
+ throw new VideoSourceExtractionError(
84205
+ "invalid_media",
84206
+ false,
84207
+ "Video source has no positive duration",
84208
+ `Playable video stream duration is ${playableDuration}s`
84209
+ );
84210
+ }
84211
+ if (video.mediaStart >= playableDuration) {
84212
+ throw new VideoSourceExtractionError(
84213
+ "media_start_out_of_range",
84214
+ false,
84215
+ "Video media start is outside the source duration",
84216
+ `Video media start ${video.mediaStart}s is outside playable video duration ${playableDuration}s`
84217
+ );
84218
+ }
84219
+ const resolvedDuration = resolveSegmentDuration(
84220
+ video.end - video.start,
84221
+ video.mediaStart,
84222
+ playableDuration
84223
+ );
84224
+ return resolveTimelineExtractionWindow(video, resolvedDuration, timelineEnd, playableDuration);
84225
+ }
84226
+ function resolveVideoExtractionDuration(video, metadata, timelineEnd) {
84227
+ return resolveVideoExtractionWindow(video, metadata, timelineEnd).durationSeconds;
84043
84228
  }
84044
84229
  function codecMayHaveAlpha(codec) {
84045
84230
  return ALPHA_CAPABLE_CODECS.has((codec ?? "").toLowerCase());
@@ -84087,7 +84272,13 @@ function linkOrCopyFrame(src, dest) {
84087
84272
  }
84088
84273
  }
84089
84274
  function supersetGroupingKey(work, fps) {
84090
- return [work.videoPath, String(fps), work.format, work.sdrToHdrTransfer ?? ""].join("\0");
84275
+ return [
84276
+ work.videoPath,
84277
+ String(fps),
84278
+ work.format,
84279
+ work.sdrToHdrTransfer ?? "",
84280
+ work.finalFrameOnly ? "final" : "range"
84281
+ ].join("\0");
84091
84282
  }
84092
84283
  function isIntegralFrameOffset(offsetSeconds, fps) {
84093
84284
  const frames = offsetSeconds * fps;
@@ -84103,6 +84294,7 @@ function windowsOverlapOrTouch(misses, baseStart) {
84103
84294
  }
84104
84295
  function buildSupersetGroup(groupId, misses, fps) {
84105
84296
  if (misses.length < 2) return null;
84297
+ if (misses.some(({ work }) => work.finalFrameOnly)) return null;
84106
84298
  const baseStart = Math.min(...misses.map(({ work }) => work.video.mediaStart));
84107
84299
  if (!misses.every(({ work }) => isIntegralFrameOffset(work.video.mediaStart - baseStart, fps))) {
84108
84300
  return null;
@@ -84203,6 +84395,11 @@ function resolveProjectRelativeSrc(src, baseDir, compiledDir) {
84203
84395
  return candidates.find(existsSync14) ?? join12(baseDir, cleanSrc);
84204
84396
  }
84205
84397
  async function extractAllVideoFrames(videos, baseDir, options, signal, config, compiledDir) {
84398
+ if (options.timelineEnd !== void 0 && !Number.isFinite(options.timelineEnd)) {
84399
+ throw new Error(
84400
+ `Video extraction timelineEnd must be finite; got ${String(options.timelineEnd)}`
84401
+ );
84402
+ }
84206
84403
  const startTime = Date.now();
84207
84404
  const extracted = [];
84208
84405
  const errors = [];
@@ -84232,6 +84429,7 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config, c
84232
84429
  const warnedSrcs = /* @__PURE__ */ new Set();
84233
84430
  for (const video of videos) {
84234
84431
  if (signal?.aborted) break;
84432
+ if (options.timelineEnd !== void 0 && video.start >= options.timelineEnd) continue;
84235
84433
  try {
84236
84434
  let videoPath = video.src;
84237
84435
  if (!isHttpUrl(videoPath)) {
@@ -84283,9 +84481,7 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config, c
84283
84481
  videoPath,
84284
84482
  mtimeMs: stat3.mtimeMs,
84285
84483
  size: stat3.size,
84286
- mediaStart: video.mediaStart,
84287
- start: video.start,
84288
- end: video.end
84484
+ mediaStart: video.mediaStart
84289
84485
  };
84290
84486
  });
84291
84487
  const phase2ProbeStart = Date.now();
@@ -84337,12 +84533,13 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config, c
84337
84533
  const entry = resolvedVideos[i2];
84338
84534
  const metadata = videoMetadata[i2];
84339
84535
  if (!entry || !metadata) continue;
84340
- if (entry.video.mediaStart >= metadata.durationSeconds) {
84536
+ const playableDuration = resolvePlayableVideoDuration(metadata);
84537
+ if (entry.video.mediaStart >= playableDuration) {
84341
84538
  errors.push({
84342
84539
  videoId: entry.video.id,
84343
84540
  kind: "media_start_out_of_range",
84344
84541
  retryable: false,
84345
- error: `SDR\u2192HDR conversion skipped: mediaStart (${entry.video.mediaStart}s) \u2265 source duration (${metadata.durationSeconds}s)`
84542
+ error: `SDR\u2192HDR conversion skipped: mediaStart (${entry.video.mediaStart}s) \u2265 playable video duration (${playableDuration}s)`
84346
84543
  });
84347
84544
  hdrSkippedIndices.add(i2);
84348
84545
  continue;
@@ -84397,7 +84594,12 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config, c
84397
84594
  };
84398
84595
  }
84399
84596
  function scopedExtractionOptions(work) {
84400
- return { ...options, format: work.format, sdrToHdrTransfer: work.sdrToHdrTransfer };
84597
+ return {
84598
+ ...options,
84599
+ format: work.format,
84600
+ sdrToHdrTransfer: work.sdrToHdrTransfer,
84601
+ finalFrameOnly: work.finalFrameOnly
84602
+ };
84401
84603
  }
84402
84604
  function rehydratePublishedCache(work, target) {
84403
84605
  const rehydrated = rehydrateCacheEntry(target.entry, {
@@ -84413,18 +84615,17 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config, c
84413
84615
  if (!cacheRootDir) return { work };
84414
84616
  const keyInput = cacheKeyInputs[work.index];
84415
84617
  if (!keyInput) return { work };
84416
- const transform = work.sdrToHdrTransfer ? sdrToHdrTransformKey(work.sdrToHdrTransfer) : void 0;
84417
- const keyDuration = resolveSegmentDuration(
84418
- keyInput.end - keyInput.start,
84419
- keyInput.mediaStart,
84420
- work.metadata
84421
- );
84618
+ const transformParts = [
84619
+ work.sdrToHdrTransfer ? sdrToHdrTransformKey(work.sdrToHdrTransfer) : void 0,
84620
+ work.finalFrameOnly ? "final-frame" : void 0
84621
+ ].filter((part) => part !== void 0);
84622
+ const transform = transformParts.length > 0 ? transformParts.join("+") : void 0;
84422
84623
  const lookup = lookupCacheEntry(cacheRootDir, {
84423
84624
  videoPath: keyInput.videoPath,
84424
84625
  mtimeMs: keyInput.mtimeMs,
84425
84626
  size: keyInput.size,
84426
84627
  mediaStart: keyInput.mediaStart,
84427
- duration: keyDuration,
84628
+ duration: work.videoDuration,
84428
84629
  fps: options.fps,
84429
84630
  format: work.format,
84430
84631
  transform
@@ -84447,7 +84648,7 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config, c
84447
84648
  () => extractVideoFramesRange(
84448
84649
  work.videoPath,
84449
84650
  work.video.id,
84450
- work.video.mediaStart,
84651
+ work.extractionMediaStart,
84451
84652
  work.videoDuration,
84452
84653
  scopedExtractionOptions(work),
84453
84654
  signal,
@@ -84471,7 +84672,7 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config, c
84471
84672
  () => extractVideoFramesRange(
84472
84673
  work.videoPath,
84473
84674
  work.video.id,
84474
- work.video.mediaStart,
84675
+ work.extractionMediaStart,
84475
84676
  work.videoDuration,
84476
84677
  scopedExtractionOptions(work),
84477
84678
  signal,
@@ -84571,17 +84772,32 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config, c
84571
84772
  }
84572
84773
  try {
84573
84774
  const metadata = videoMetadata[index] ?? await extractMediaMetadata(videoPath);
84574
- const videoDuration = resolveSegmentDuration(
84575
- video.end - video.start,
84576
- video.mediaStart,
84577
- metadata
84775
+ const initialWindow = resolveVideoExtractionWindow(video, metadata, options.timelineEnd);
84776
+ const window3 = await resolveFinalFrameExtractionWindow(
84777
+ videoPath,
84778
+ video,
84779
+ metadata,
84780
+ initialWindow,
84781
+ signal
84578
84782
  );
84579
- if (video.end - video.start !== videoDuration) {
84580
- video.end = video.start + videoDuration;
84783
+ const videoDuration = window3.durationSeconds;
84784
+ if (videoDuration <= 0) {
84785
+ return { skipped: true };
84786
+ }
84787
+ if (!window3.preserveTimelinePhase) {
84788
+ video.start = window3.compositionStart;
84789
+ if (!window3.preserveTimelineEnd) {
84790
+ video.end = window3.compositionStart + videoDuration;
84791
+ }
84792
+ video.mediaStart = window3.mediaStart;
84581
84793
  }
84794
+ const keyInput = cacheKeyInputs[index];
84795
+ const extractionMediaStart = window3.extractionMediaStart ?? window3.mediaStart;
84796
+ if (keyInput) keyInput.mediaStart = extractionMediaStart;
84582
84797
  const format = resolveFrameFormat(metadata, options.format);
84583
84798
  const sdrToHdrTransfer = sdrToHdrTransfers[index];
84584
- const dedupeKey = `${videoPath}\0${video.mediaStart}\0${videoDuration}\0${options.fps}\0${format}\0${sdrToHdrTransfer ?? ""}`;
84799
+ const finalFrameOnly = window3.finalFrameOnly === true;
84800
+ const dedupeKey = `${videoPath}\0${extractionMediaStart}\0${videoDuration}\0${options.fps}\0${format}\0${sdrToHdrTransfer ?? ""}\0${finalFrameOnly ? "final" : "range"}`;
84585
84801
  return {
84586
84802
  work: {
84587
84803
  video,
@@ -84589,6 +84805,8 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config, c
84589
84805
  index,
84590
84806
  metadata,
84591
84807
  videoDuration,
84808
+ extractionMediaStart,
84809
+ finalFrameOnly,
84592
84810
  format,
84593
84811
  sdrToHdrTransfer,
84594
84812
  dedupeKey
@@ -84628,25 +84846,35 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config, c
84628
84846
  for (const groupOutcomes of supersetOutcomes) {
84629
84847
  for (const [key2, outcome] of groupOutcomes) uniqueOutcomes.set(key2, outcome);
84630
84848
  }
84631
- const results = preparedExtractions.map((prepared) => {
84632
- if ("error" in prepared) return prepared;
84849
+ const results = [];
84850
+ for (const prepared of preparedExtractions) {
84851
+ if ("skipped" in prepared) continue;
84852
+ if ("error" in prepared) {
84853
+ results.push(prepared);
84854
+ continue;
84855
+ }
84633
84856
  const outcome = uniqueOutcomes.get(prepared.work.dedupeKey);
84634
- if (!outcome)
84635
- return { error: extractionError(prepared.work.video.id, "missing extraction result") };
84857
+ if (!outcome) {
84858
+ results.push({
84859
+ error: extractionError(prepared.work.video.id, "missing extraction result")
84860
+ });
84861
+ continue;
84862
+ }
84636
84863
  if ("error" in outcome) {
84637
84864
  const isFollower = outcome.error.videoId !== prepared.work.video.id;
84638
84865
  const message = isFollower ? `[shared extraction, leader ${outcome.error.videoId}] ${outcome.error.error}` : outcome.error.error;
84639
- return {
84866
+ results.push({
84640
84867
  error: {
84641
84868
  videoId: prepared.work.video.id,
84642
84869
  kind: outcome.error.kind,
84643
84870
  retryable: outcome.error.retryable,
84644
84871
  error: message
84645
84872
  }
84646
- };
84873
+ });
84874
+ continue;
84647
84875
  }
84648
- return { result: { ...outcome.result, videoId: prepared.work.video.id } };
84649
- });
84876
+ results.push({ result: { ...outcome.result, videoId: prepared.work.video.id } });
84877
+ }
84650
84878
  breakdown.extractMs = Date.now() - phase3Start;
84651
84879
  for (const item of results) {
84652
84880
  if ("error" in item && item.error) {
@@ -84678,7 +84906,7 @@ async function extractAllVideoFrames(videos, baseDir, options, signal, config, c
84678
84906
  function getFrameIndexAtTime(extracted, globalTime, videoStart, loop = false, mediaStart = 0, holdLastFrame = false) {
84679
84907
  let localTime = globalTime - videoStart;
84680
84908
  if (localTime < 0) return null;
84681
- const loopDuration = Math.max(0, extracted.metadata.durationSeconds - mediaStart);
84909
+ const loopDuration = Math.max(0, resolvePlayableVideoDuration(extracted.metadata) - mediaStart);
84682
84910
  if (loop && loopDuration > 0 && localTime >= loopDuration) {
84683
84911
  localTime %= loopDuration;
84684
84912
  }
@@ -84712,7 +84940,7 @@ function createFrameLookupTable(videos, extracted) {
84712
84940
  }
84713
84941
  return table;
84714
84942
  }
84715
- var VIDEO_FRAME_FORMATS, EXTRACT_CACHE_MIN_AGE_MS, GC_STALENESS_MS, SDR_TO_HDR_COLORSPACE_FILTER, VideoSourceExtractionError, TRANSIENT_FFMPEG_SPAWN_CODES, ALPHA_CAPABLE_CODECS, FrameLookupTable;
84943
+ var VIDEO_FRAME_FORMATS, EXTRACT_CACHE_MIN_AGE_MS, GC_STALENESS_MS, SDR_TO_HDR_COLORSPACE_FILTER, VideoSourceExtractionError, TRANSIENT_FFMPEG_SPAWN_CODES, FINAL_FRAME_LOGICAL_DURATION_SECONDS, ALPHA_CAPABLE_CODECS, FrameLookupTable;
84716
84944
  var init_videoFrameExtractor = __esm({
84717
84945
  "../engine/src/services/videoFrameExtractor.ts"() {
84718
84946
  "use strict";
@@ -84744,6 +84972,7 @@ var init_videoFrameExtractor = __esm({
84744
84972
  hyperframesVideoSourceExtractionError = true;
84745
84973
  };
84746
84974
  TRANSIENT_FFMPEG_SPAWN_CODES = /* @__PURE__ */ new Set(["EAGAIN", "EMFILE", "ENFILE"]);
84975
+ FINAL_FRAME_LOGICAL_DURATION_SECONDS = 1e-6;
84747
84976
  ALPHA_CAPABLE_CODECS = /* @__PURE__ */ new Set(["vp9", "vp8", "prores"]);
84748
84977
  FrameLookupTable = class {
84749
84978
  videos = /* @__PURE__ */ new Map();
@@ -88349,6 +88578,7 @@ __export(src_exports, {
88349
88578
  executeParallelCapture: () => executeParallelCapture,
88350
88579
  extractAllVideoFrames: () => extractAllVideoFrames,
88351
88580
  extractAudioMetadata: () => extractAudioMetadata,
88581
+ extractFinalVideoFrameTimestamp: () => extractFinalVideoFrameTimestamp,
88352
88582
  extractMediaMetadata: () => extractMediaMetadata,
88353
88583
  extractVideoFramesRange: () => extractVideoFramesRange,
88354
88584
  extractVideoMetadata: () => extractVideoMetadata,
@@ -88356,6 +88586,7 @@ __export(src_exports, {
88356
88586
  formatFfmpegError: () => formatFfmpegError,
88357
88587
  getCapturePerfSummary: () => getCapturePerfSummary,
88358
88588
  getCdpSession: () => getCdpSession,
88589
+ getCgroupMemoryLimitMb: () => getCgroupMemoryLimitMb,
88359
88590
  getCompositionDuration: () => getCompositionDuration,
88360
88591
  getDrawElementVerificationDetails: () => getDrawElementVerificationDetails,
88361
88592
  getEncoderPreset: () => getEncoderPreset,
@@ -88413,8 +88644,13 @@ __export(src_exports, {
88413
88644
  resolveConfig: () => resolveConfig,
88414
88645
  resolveDeVerifyMinDb: () => resolveDeVerifyMinDb,
88415
88646
  resolveExtractCacheDir: () => resolveExtractCacheDir,
88647
+ resolveFinalFrameExtractionWindow: () => resolveFinalFrameExtractionWindow,
88416
88648
  resolveHeadlessShellPath: () => resolveHeadlessShellPath,
88649
+ resolvePlayableVideoDuration: () => resolvePlayableVideoDuration,
88417
88650
  resolveProjectRelativeSrc: () => resolveProjectRelativeSrc,
88651
+ resolveTimelineExtractionWindow: () => resolveTimelineExtractionWindow,
88652
+ resolveVideoExtractionDuration: () => resolveVideoExtractionDuration,
88653
+ resolveVideoExtractionWindow: () => resolveVideoExtractionWindow,
88418
88654
  roundedRectAlpha: () => roundedRectAlpha,
88419
88655
  runFfmpeg: () => runFfmpeg,
88420
88656
  runVideoExtractionWithRetry: () => runVideoExtractionWithRetry,
@@ -112460,7 +112696,7 @@ function expectedFramesForVideo(video, entry, fps) {
112460
112696
  const rounding = entry && !entry.metadata.isVFR ? "nearest" : "ceil";
112461
112697
  const slotFrames = expectedFramesForClip(video.start, video.end, fps, rounding);
112462
112698
  if (!entry) return slotFrames;
112463
- const sourceDuration = entry.metadata.durationSeconds - video.mediaStart;
112699
+ const sourceDuration = resolvePlayableVideoDuration(entry.metadata) - video.mediaStart;
112464
112700
  if (!Number.isFinite(sourceDuration) || sourceDuration <= 0) return slotFrames;
112465
112701
  const sourceFrames = expectedFramesForClip(0, sourceDuration, fps, rounding);
112466
112702
  return Math.min(slotFrames, sourceFrames);
@@ -112508,6 +112744,7 @@ var init_videoFrameCoverage = __esm({
112508
112744
  "../producer/src/services/render/videoFrameCoverage.ts"() {
112509
112745
  "use strict";
112510
112746
  init_esm10();
112747
+ init_src();
112511
112748
  VideoFrameCoverageError = class extends Error {
112512
112749
  // Read structurally by isVideoFrameCoverageError and cross-module callers.
112513
112750
  // fallow-ignore-next-line unused-class-member
@@ -119211,6 +119448,7 @@ async function runExtractVideosStage(input2) {
119211
119448
  fps: fpsToNumber(job.config.fps),
119212
119449
  outputDir: join47(compiledDir, "__hyperframes_video_frames"),
119213
119450
  format: job.config.videoFrameFormat ?? "auto",
119451
+ timelineEnd: composition.duration,
119214
119452
  maxTransientRetries: extractionPolicy.maxTransientRetries,
119215
119453
  collectProbeFailures: extractionPolicy.failureMode === "enforce"
119216
119454
  },
@@ -119559,17 +119797,26 @@ function closeHdrVideoFrameSource(source, log2) {
119559
119797
  });
119560
119798
  }
119561
119799
  }
119800
+ function resolveHdrVideoFrameIndex(time, startTime, fps, frameCount, loop = false) {
119801
+ const frameIndex = Math.round((time - startTime) * fps);
119802
+ if (frameIndex < 0 || frameCount < 1) return null;
119803
+ return loop ? frameIndex % frameCount : Math.min(frameIndex, frameCount - 1);
119804
+ }
119562
119805
  function blitHdrVideoLayer(canvas, el, time, fps, hdrVideoFrameSources, hdrStartTimes, width, height, log2, sourceTransfer, targetTransfer, hdrPerf) {
119563
119806
  const frameSource = hdrVideoFrameSources.get(el.id);
119564
119807
  const startTime = hdrStartTimes.get(el.id);
119565
119808
  if (!frameSource || startTime === void 0 || el.opacity <= 0) {
119566
119809
  return;
119567
119810
  }
119568
- const videoFrameIndex = Math.round((time - startTime) * fps) + 1;
119569
- if (videoFrameIndex < 1) return;
119570
- const effectiveIndex = Math.min(videoFrameIndex, frameSource.frameCount);
119571
- if (effectiveIndex < 1) return;
119572
- const frameOffset = (effectiveIndex - 1) * frameSource.frameSize;
119811
+ const effectiveIndex = resolveHdrVideoFrameIndex(
119812
+ time,
119813
+ startTime,
119814
+ fps,
119815
+ frameSource.frameCount,
119816
+ frameSource.loop
119817
+ );
119818
+ if (effectiveIndex === null) return;
119819
+ const frameOffset = effectiveIndex * frameSource.frameSize;
119573
119820
  try {
119574
119821
  if (hdrPerf) hdrPerf.hdrVideoLayerBlits += 1;
119575
119822
  const bytesRead = timeHdrPhase(
@@ -119955,8 +120202,402 @@ var init_hdrCompositor = __esm({
119955
120202
  }
119956
120203
  });
119957
120204
 
120205
+ // ../producer/src/services/render/stages/captureHdrResources.ts
120206
+ import {
120207
+ closeSync as closeSync7,
120208
+ constants as constants3,
120209
+ fstatSync as fstatSync4,
120210
+ mkdirSync as mkdirSync24,
120211
+ mkdtempSync as mkdtempSync6,
120212
+ openSync as openSync6,
120213
+ readFileSync as readFileSync30,
120214
+ rmSync as rmSync15,
120215
+ statfsSync as statfsSync2
120216
+ } from "fs";
120217
+ import { join as join50 } from "path";
120218
+ function tempDirSafePrefix(id) {
120219
+ const safe = id.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 80);
120220
+ return safe || "video";
120221
+ }
120222
+ function planHdrResources(args) {
120223
+ const { composition, nativeHdrVideoIds, nativeHdrImageIds, projectDir, compiledDir } = args;
120224
+ const hdrVideoIds = composition.videos.filter((v2) => nativeHdrVideoIds.has(v2.id)).map((v2) => v2.id);
120225
+ const hdrVideoSrcPaths = /* @__PURE__ */ new Map();
120226
+ for (const v2 of composition.videos) {
120227
+ if (!hdrVideoIds.includes(v2.id)) continue;
120228
+ let srcPath = v2.src;
120229
+ if (!srcPath.startsWith("/")) {
120230
+ const fromCompiled = join50(compiledDir, srcPath);
120231
+ srcPath = args.existsSync(fromCompiled) ? fromCompiled : join50(projectDir, srcPath);
120232
+ }
120233
+ hdrVideoSrcPaths.set(v2.id, srcPath);
120234
+ }
120235
+ const hdrVideoStartTimes = /* @__PURE__ */ new Map();
120236
+ for (const v2 of composition.videos) {
120237
+ if (hdrVideoIds.includes(v2.id)) hdrVideoStartTimes.set(v2.id, v2.start);
120238
+ }
120239
+ const hdrImageStartTimes = /* @__PURE__ */ new Map();
120240
+ for (const img of composition.images) {
120241
+ if (nativeHdrImageIds.has(img.id)) hdrImageStartTimes.set(img.id, img.start);
120242
+ }
120243
+ return {
120244
+ hdrVideoIds,
120245
+ hdrVideoSrcPaths,
120246
+ hdrVideoStartTimes,
120247
+ hdrImageStartTimes,
120248
+ hdrExtractionDims: /* @__PURE__ */ new Map(),
120249
+ hdrImageFitInfo: /* @__PURE__ */ new Map()
120250
+ };
120251
+ }
120252
+ async function probeHdrExtractionDims(args) {
120253
+ const { domSession, nativeHdrIds, nativeHdrImageIds, composition, prep } = args;
120254
+ const uniqueStartTimes = [
120255
+ .../* @__PURE__ */ new Set([...prep.hdrVideoStartTimes.values(), ...prep.hdrImageStartTimes.values()])
120256
+ ].sort((a, b2) => a - b2);
120257
+ for (const seekTime of uniqueStartTimes) {
120258
+ await domSession.page.evaluate((t2) => {
120259
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t2);
120260
+ }, seekTime);
120261
+ if (domSession.onBeforeCapture) {
120262
+ await domSession.onBeforeCapture(domSession.page, seekTime);
120263
+ }
120264
+ const stacking = await queryElementStacking(domSession.page, nativeHdrIds);
120265
+ for (const el of stacking) {
120266
+ if (el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0 && !prep.hdrExtractionDims.has(el.id)) {
120267
+ prep.hdrExtractionDims.set(el.id, { width: el.layoutWidth, height: el.layoutHeight });
120268
+ }
120269
+ if (el.isHdr && nativeHdrImageIds.has(el.id) && !prep.hdrImageFitInfo.has(el.id)) {
120270
+ prep.hdrImageFitInfo.set(el.id, { fit: el.objectFit, position: el.objectPosition });
120271
+ }
120272
+ }
120273
+ }
120274
+ for (const [imageId, startTime] of prep.hdrImageStartTimes) {
120275
+ if (prep.hdrExtractionDims.has(imageId)) continue;
120276
+ const img = composition.images.find((i2) => i2.id === imageId);
120277
+ if (!img) continue;
120278
+ const duration = img.end - img.start;
120279
+ const retryTime = startTime + Math.min(0.5, duration * 0.1);
120280
+ await domSession.page.evaluate((t2) => {
120281
+ if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t2);
120282
+ }, retryTime);
120283
+ if (domSession.onBeforeCapture) {
120284
+ await domSession.onBeforeCapture(domSession.page, retryTime);
120285
+ }
120286
+ const retryStacking = await queryElementStacking(domSession.page, nativeHdrIds);
120287
+ for (const el of retryStacking) {
120288
+ if (el.id === imageId && el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0) {
120289
+ prep.hdrExtractionDims.set(el.id, { width: el.layoutWidth, height: el.layoutHeight });
120290
+ if (!prep.hdrImageFitInfo.has(el.id)) {
120291
+ prep.hdrImageFitInfo.set(el.id, { fit: el.objectFit, position: el.objectPosition });
120292
+ }
120293
+ break;
120294
+ }
120295
+ }
120296
+ }
120297
+ }
120298
+ function estimateHdrExtractionBytes(videos, fps) {
120299
+ let total = 0;
120300
+ for (const v2 of videos) {
120301
+ const frames = Math.ceil(Math.max(0, v2.durationSeconds) * fps);
120302
+ total += frames * v2.width * v2.height * 6;
120303
+ }
120304
+ return total;
120305
+ }
120306
+ function resolveHdrExtractionBudgetBytes(raw, cgroupLimitMb = getCgroupMemoryLimitMb()) {
120307
+ let configuredBudget;
120308
+ if (raw !== void 0 && raw.trim() !== "") {
120309
+ const value = Number(raw);
120310
+ if (!Number.isFinite(value) || value <= 0) {
120311
+ throw new Error(`${HDR_EXTRACTION_MAX_BYTES_ENV} must be a positive finite byte count`);
120312
+ }
120313
+ configuredBudget = Math.floor(value);
120314
+ }
120315
+ const cgroupBudget = cgroupLimitMb !== null && Number.isFinite(cgroupLimitMb) && cgroupLimitMb > 0 ? Math.floor(cgroupLimitMb * BYTES_PER_MIB2 * HDR_EXTRACTION_CGROUP_BUDGET_FRACTION) : void 0;
120316
+ if (configuredBudget === void 0) return cgroupBudget;
120317
+ if (cgroupBudget === void 0) return configuredBudget;
120318
+ return Math.min(configuredBudget, cgroupBudget);
120319
+ }
120320
+ function resolveHdrExtractionActiveBudgetBytes(configuredBudgetBytes, freeBytes) {
120321
+ const diskBudgetBytes = Math.floor(freeBytes * HDR_EXTRACTION_HEADROOM_FRACTION);
120322
+ return configuredBudgetBytes === void 0 ? diskBudgetBytes : Math.min(configuredBudgetBytes, diskBudgetBytes);
120323
+ }
120324
+ function reserveHdrExtractionBytes(estimatedBytes, budgetBytes) {
120325
+ if (!Number.isFinite(estimatedBytes) || estimatedBytes < 0) {
120326
+ throw new Error(`HDR extraction reservation must be a finite non-negative byte count`);
120327
+ }
120328
+ const aggregateBytes = aggregateHdrExtractionReservedBytes + estimatedBytes;
120329
+ if (budgetBytes !== void 0 && aggregateBytes > budgetBytes) {
120330
+ throw new Error(
120331
+ `Concurrent HDR pre-extractions need ~${(aggregateBytes / 1e9).toFixed(1)} GB of raw 16-bit frame scratch, exceeding the active ${(budgetBytes / 1e9).toFixed(1)} GB budget.`
120332
+ );
120333
+ }
120334
+ aggregateHdrExtractionReservedBytes = aggregateBytes;
120335
+ let released = false;
120336
+ return () => {
120337
+ if (released) return;
120338
+ released = true;
120339
+ aggregateHdrExtractionReservedBytes = Math.max(
120340
+ 0,
120341
+ aggregateHdrExtractionReservedBytes - estimatedBytes
120342
+ );
120343
+ };
120344
+ }
120345
+ function resolveHdrExtractionWindow(video, compositionDuration, metadata) {
120346
+ if (!Number.isFinite(compositionDuration) || compositionDuration <= 0) {
120347
+ throw new Error(
120348
+ `Cannot extract HDR video "${video.id}" with invalid composition duration ${String(compositionDuration)}`
120349
+ );
120350
+ }
120351
+ const window3 = resolveVideoExtractionWindow(video, metadata, compositionDuration);
120352
+ if (!Number.isFinite(window3.durationSeconds)) {
120353
+ throw new Error(
120354
+ `HDR video "${video.id}" has no finite interval inside the ${compositionDuration}s composition`
120355
+ );
120356
+ }
120357
+ if (window3.durationSeconds <= 0) return null;
120358
+ if (!Number.isFinite(window3.mediaStart) || window3.mediaStart < 0) {
120359
+ throw new Error(`HDR video "${video.id}" has invalid mediaStart ${String(window3.mediaStart)}`);
120360
+ }
120361
+ return window3;
120362
+ }
120363
+ function cleanupHdrFrameDirectory(frameDir, rawPath, log2) {
120364
+ if (process.env.KEEP_TEMP === "1") return;
120365
+ try {
120366
+ rmSync15(frameDir, { recursive: true, force: true });
120367
+ } catch (err) {
120368
+ log2?.warn("Failed to clean up HDR raw frame directory", {
120369
+ frameDir,
120370
+ rawPath,
120371
+ error: err instanceof Error ? err.message : String(err)
120372
+ });
120373
+ }
120374
+ }
120375
+ function cleanupHdrVideoFrameSource(source, log2) {
120376
+ closeHdrVideoFrameSource(source, log2);
120377
+ cleanupHdrFrameDirectory(source.dir, source.rawPath, log2);
120378
+ }
120379
+ function assertHdrExtractionDiskHeadroom(framesDir, plannedVideos, fps, log2) {
120380
+ const estimatedBytes = estimateHdrExtractionBytes(plannedVideos, fps);
120381
+ const configuredBudget = resolveHdrExtractionBudgetBytes(
120382
+ process.env[HDR_EXTRACTION_MAX_BYTES_ENV]
120383
+ );
120384
+ const estimatedGb = (estimatedBytes / 1e9).toFixed(1);
120385
+ if (configuredBudget !== void 0 && estimatedBytes > configuredBudget) {
120386
+ throw new Error(
120387
+ `HDR pre-extraction needs ~${estimatedGb} GB of raw 16-bit frames, exceeding the active scratch budget of ${(configuredBudget / 1e9).toFixed(1)} GB. If the composition doesn't need HDR output, re-run with --sdr; otherwise reduce its HDR duration/resolution or use a render container with a larger memory limit.`
120388
+ );
120389
+ }
120390
+ let freeBytes;
120391
+ try {
120392
+ const stat3 = statfsSync2(framesDir);
120393
+ freeBytes = stat3.bavail * stat3.bsize;
120394
+ } catch {
120395
+ return { estimatedBytes, budgetBytes: configuredBudget };
120396
+ }
120397
+ const diskBudgetBytes = Math.floor(freeBytes * HDR_EXTRACTION_HEADROOM_FRACTION);
120398
+ if (estimatedBytes > diskBudgetBytes) {
120399
+ throw new Error(
120400
+ `HDR pre-extraction needs ~${estimatedGb} GB of raw 16-bit frames but only ${(freeBytes / 1e9).toFixed(1)} GB is free at ${framesDir}. If the composition doesn't need HDR output, re-run with --sdr; otherwise free up disk space and retry.`
120401
+ );
120402
+ }
120403
+ if (estimatedBytes > HDR_EXTRACTION_WARN_BYTES) {
120404
+ log2.warn(
120405
+ `HDR pre-extraction will write ~${estimatedGb} GB of raw 16-bit frames (pass --sdr to skip if HDR output isn't needed)`,
120406
+ { estimatedBytes, freeBytes }
120407
+ );
120408
+ }
120409
+ return {
120410
+ estimatedBytes,
120411
+ budgetBytes: resolveHdrExtractionActiveBudgetBytes(configuredBudget, freeBytes)
120412
+ };
120413
+ }
120414
+ async function extractHdrVideoFrames(args) {
120415
+ const { job, log: log2, framesDir, composition, prep, width, height, abortSignal, hdrDiagnostics } = args;
120416
+ const runFfmpegImpl = args.runFfmpegImpl ?? runFfmpeg;
120417
+ const extractMediaMetadataImpl = args.extractMediaMetadataImpl ?? extractMediaMetadata;
120418
+ const resolveFinalFrameExtractionWindowImpl = args.resolveFinalFrameExtractionWindowImpl ?? resolveFinalFrameExtractionWindow;
120419
+ const out = /* @__PURE__ */ new Map();
120420
+ mkdirSync24(framesDir, { recursive: true });
120421
+ const plannedVideos = [];
120422
+ const extractionWindows = /* @__PURE__ */ new Map();
120423
+ for (const [videoId, srcPath] of prep.hdrVideoSrcPaths) {
120424
+ const video = composition.videos.find((v2) => v2.id === videoId);
120425
+ if (!video) continue;
120426
+ const dims = prep.hdrExtractionDims.get(videoId) ?? { width, height };
120427
+ const metadata = await extractMediaMetadataImpl(srcPath);
120428
+ const initialWindow = resolveHdrExtractionWindow(video, composition.duration, metadata);
120429
+ if (!initialWindow) continue;
120430
+ const window3 = await resolveFinalFrameExtractionWindowImpl(
120431
+ srcPath,
120432
+ video,
120433
+ metadata,
120434
+ initialWindow,
120435
+ abortSignal
120436
+ );
120437
+ extractionWindows.set(videoId, window3);
120438
+ prep.hdrVideoStartTimes.set(videoId, window3.compositionStart);
120439
+ plannedVideos.push({
120440
+ durationSeconds: window3.durationSeconds,
120441
+ width: dims.width,
120442
+ height: dims.height
120443
+ });
120444
+ }
120445
+ const { estimatedBytes, budgetBytes } = assertHdrExtractionDiskHeadroom(
120446
+ framesDir,
120447
+ plannedVideos,
120448
+ fpsToNumber(job.config.fps),
120449
+ log2
120450
+ );
120451
+ const releaseReservation = reserveHdrExtractionBytes(estimatedBytes, budgetBytes);
120452
+ const createdFrameDirs = /* @__PURE__ */ new Set();
120453
+ try {
120454
+ for (const [videoId, srcPath] of prep.hdrVideoSrcPaths) {
120455
+ const video = composition.videos.find((v2) => v2.id === videoId);
120456
+ const window3 = extractionWindows.get(videoId);
120457
+ if (!video || !window3) continue;
120458
+ mkdirSync24(framesDir, { recursive: true });
120459
+ const frameDir = mkdtempSync6(join50(framesDir, `hdr_${tempDirSafePrefix(videoId)}-`));
120460
+ createdFrameDirs.add(frameDir);
120461
+ const dims = prep.hdrExtractionDims.get(videoId) ?? { width, height };
120462
+ const rawPath = join50(frameDir, "frames.rgb48le");
120463
+ const extractionStart = String(window3.extractionMediaStart ?? window3.mediaStart);
120464
+ const ffmpegArgs = [];
120465
+ if (window3.finalFrameOnly) {
120466
+ ffmpegArgs.push("-i", srcPath, "-ss", extractionStart, "-frames:v", "1");
120467
+ } else {
120468
+ ffmpegArgs.push(
120469
+ "-ss",
120470
+ extractionStart,
120471
+ "-i",
120472
+ srcPath,
120473
+ "-t",
120474
+ String(window3.durationSeconds)
120475
+ );
120476
+ }
120477
+ if (!window3.finalFrameOnly) {
120478
+ ffmpegArgs.push("-r", fpsToFfmpegArg(job.config.fps));
120479
+ }
120480
+ ffmpegArgs.push(
120481
+ "-vf",
120482
+ `scale=${dims.width}:${dims.height}:force_original_aspect_ratio=increase,crop=${dims.width}:${dims.height}`,
120483
+ "-pix_fmt",
120484
+ "rgb48le",
120485
+ "-f",
120486
+ "rawvideo",
120487
+ "-y",
120488
+ rawPath
120489
+ );
120490
+ const result = await runFfmpegImpl(ffmpegArgs, { signal: abortSignal });
120491
+ if (!result.success) {
120492
+ hdrDiagnostics.videoExtractionFailures += 1;
120493
+ log2.error("HDR frame pre-extraction failed; aborting render", {
120494
+ videoId,
120495
+ srcPath,
120496
+ stderr: result.stderr.slice(-400)
120497
+ });
120498
+ throw new Error(
120499
+ `HDR frame extraction failed for video "${videoId}". Aborting render to avoid shipping black HDR layers.`
120500
+ );
120501
+ }
120502
+ const frameSize = dims.width * dims.height * 6;
120503
+ const fd = openSync6(rawPath, constants3.O_RDONLY | NO_FOLLOW_FLAG);
120504
+ let handedOff = false;
120505
+ try {
120506
+ const frameCount = Math.floor(fstatSync4(fd).size / frameSize);
120507
+ if (frameCount < 1) {
120508
+ hdrDiagnostics.videoExtractionFailures += 1;
120509
+ throw new Error(
120510
+ `HDR frame extraction produced no frames for video "${videoId}". Aborting render to avoid shipping black HDR layers.`
120511
+ );
120512
+ }
120513
+ out.set(videoId, {
120514
+ dir: frameDir,
120515
+ rawPath,
120516
+ fd,
120517
+ width: dims.width,
120518
+ height: dims.height,
120519
+ frameSize,
120520
+ frameCount,
120521
+ scratch: Buffer.allocUnsafe(frameSize),
120522
+ loop: video.loop
120523
+ });
120524
+ handedOff = true;
120525
+ } finally {
120526
+ if (!handedOff) closeSync7(fd);
120527
+ }
120528
+ }
120529
+ return { sources: out, estimatedBytes, releaseReservation };
120530
+ } catch (error) {
120531
+ for (const source of out.values()) {
120532
+ cleanupHdrVideoFrameSource(source, log2);
120533
+ createdFrameDirs.delete(source.dir);
120534
+ }
120535
+ for (const frameDir of createdFrameDirs) {
120536
+ cleanupHdrFrameDirectory(frameDir, void 0, log2);
120537
+ }
120538
+ releaseReservation();
120539
+ throw error;
120540
+ }
120541
+ }
120542
+ function decodeHdrImageBuffers(args) {
120543
+ const { log: log2, hdrImageSrcPaths, prep, hdrDiagnostics } = args;
120544
+ const out = /* @__PURE__ */ new Map();
120545
+ for (const [imageId, srcPath] of hdrImageSrcPaths) {
120546
+ try {
120547
+ const decoded = decodePngToRgb48le(readFileSync30(srcPath));
120548
+ const layout2 = prep.hdrExtractionDims.get(imageId);
120549
+ const fitInfo = prep.hdrImageFitInfo.get(imageId);
120550
+ if (layout2 && (layout2.width !== decoded.width || layout2.height !== decoded.height)) {
120551
+ const fit = normalizeObjectFit(fitInfo?.fit);
120552
+ const resampled = resampleRgb48leObjectFit(
120553
+ decoded.data,
120554
+ decoded.width,
120555
+ decoded.height,
120556
+ layout2.width,
120557
+ layout2.height,
120558
+ fit,
120559
+ fitInfo?.position
120560
+ );
120561
+ out.set(imageId, { data: resampled, width: layout2.width, height: layout2.height });
120562
+ } else {
120563
+ out.set(imageId, {
120564
+ data: Buffer.from(decoded.data),
120565
+ width: decoded.width,
120566
+ height: decoded.height
120567
+ });
120568
+ }
120569
+ } catch (err) {
120570
+ hdrDiagnostics.imageDecodeFailures += 1;
120571
+ log2.error("HDR image decode failed; aborting render", {
120572
+ imageId,
120573
+ srcPath,
120574
+ error: err instanceof Error ? err.message : String(err)
120575
+ });
120576
+ throw new Error(
120577
+ `HDR image decode failed for image "${imageId}". Aborting render to avoid shipping missing HDR image layers.`
120578
+ );
120579
+ }
120580
+ }
120581
+ return out;
120582
+ }
120583
+ var NO_FOLLOW_FLAG, HDR_EXTRACTION_HEADROOM_FRACTION, HDR_EXTRACTION_CGROUP_BUDGET_FRACTION, HDR_EXTRACTION_WARN_BYTES, HDR_EXTRACTION_MAX_BYTES_ENV, BYTES_PER_MIB2, aggregateHdrExtractionReservedBytes;
120584
+ var init_captureHdrResources = __esm({
120585
+ "../producer/src/services/render/stages/captureHdrResources.ts"() {
120586
+ "use strict";
120587
+ init_src();
120588
+ init_dist3();
120589
+ init_hdrCompositor();
120590
+ NO_FOLLOW_FLAG = constants3.O_NOFOLLOW ?? 0;
120591
+ HDR_EXTRACTION_HEADROOM_FRACTION = 0.9;
120592
+ HDR_EXTRACTION_CGROUP_BUDGET_FRACTION = 0.5;
120593
+ HDR_EXTRACTION_WARN_BYTES = 1e10;
120594
+ HDR_EXTRACTION_MAX_BYTES_ENV = "HDR_EXTRACTION_MAX_BYTES";
120595
+ BYTES_PER_MIB2 = 1024 * 1024;
120596
+ aggregateHdrExtractionReservedBytes = 0;
120597
+ }
120598
+ });
120599
+
119958
120600
  // ../producer/src/services/render/stages/captureHdrFrameShared.ts
119959
- import { rmSync as rmSync15 } from "fs";
119960
120601
  function shouldUseHybridLayeredPath(args) {
119961
120602
  if (args.hasHdrContent) return false;
119962
120603
  if (args.workerCount <= 1) return false;
@@ -120180,17 +120821,7 @@ function cleanupEndedHdrVideos(args) {
120180
120821
  if (!stillNeeded) {
120181
120822
  const frameSource = hdrVideoFrameSources.get(videoId);
120182
120823
  if (frameSource) {
120183
- closeHdrVideoFrameSource(frameSource, log2);
120184
- try {
120185
- rmSync15(frameSource.dir, { recursive: true, force: true });
120186
- } catch (err) {
120187
- log2.warn("Failed to clean up HDR raw frame directory", {
120188
- videoId,
120189
- frameDir: frameSource.dir,
120190
- rawPath: frameSource.rawPath,
120191
- error: err instanceof Error ? err.message : String(err)
120192
- });
120193
- }
120824
+ cleanupHdrVideoFrameSource(frameSource, log2);
120194
120825
  hdrVideoFrameSources.delete(videoId);
120195
120826
  }
120196
120827
  cleanedUpVideos.add(videoId);
@@ -120203,6 +120834,7 @@ var init_captureHdrFrameShared = __esm({
120203
120834
  "use strict";
120204
120835
  init_src();
120205
120836
  init_hdrCompositor();
120837
+ init_captureHdrResources();
120206
120838
  init_hdrPerf();
120207
120839
  }
120208
120840
  });
@@ -120210,7 +120842,7 @@ var init_captureHdrFrameShared = __esm({
120210
120842
  // ../producer/src/services/render/stages/captureStreamingStage.ts
120211
120843
  import { mkdtemp as mkdtemp2, writeFile as writeFile2 } from "fs/promises";
120212
120844
  import { tmpdir as tmpdir7 } from "os";
120213
- import { join as join50 } from "path";
120845
+ import { join as join51 } from "path";
120214
120846
  function resolveDeStallTimeoutMs() {
120215
120847
  const raw = process.env.HF_DE_STALL_MS ?? process.env.HF_DE_PARALLEL_STALL_MS;
120216
120848
  const parsed = raw ? Number(raw) : Number.NaN;
@@ -120314,11 +120946,11 @@ function createDrainFrameGuard(args) {
120314
120946
  return buf;
120315
120947
  }
120316
120948
  if (db < verifyMinDb) {
120317
- const dumpDir = await mkdtemp2(join50(tmpdir7(), "hf-de-verify-fail-")).catch(() => null);
120949
+ const dumpDir = await mkdtemp2(join51(tmpdir7(), "hf-de-verify-fail-")).catch(() => null);
120318
120950
  if (dumpDir) {
120319
120951
  await Promise.all([
120320
- writeFile2(join50(dumpDir, `frame-${idx}-de.jpg`), buf),
120321
- writeFile2(join50(dumpDir, `frame-${idx}-truth.jpg`), truth)
120952
+ writeFile2(join51(dumpDir, `frame-${idx}-de.jpg`), buf),
120953
+ writeFile2(join51(dumpDir, `frame-${idx}-truth.jpg`), truth)
120322
120954
  ]).catch(() => {
120323
120955
  });
120324
120956
  }
@@ -120788,263 +121420,6 @@ var init_hdrImageTransferCache = __esm({
120788
121420
  }
120789
121421
  });
120790
121422
 
120791
- // ../producer/src/services/render/stages/captureHdrResources.ts
120792
- import {
120793
- closeSync as closeSync7,
120794
- constants as constants3,
120795
- fstatSync as fstatSync4,
120796
- mkdirSync as mkdirSync24,
120797
- mkdtempSync as mkdtempSync6,
120798
- openSync as openSync6,
120799
- readFileSync as readFileSync30,
120800
- statfsSync as statfsSync2
120801
- } from "fs";
120802
- import { join as join51 } from "path";
120803
- function tempDirSafePrefix(id) {
120804
- const safe = id.replace(/[^A-Za-z0-9_.-]/g, "_").slice(0, 80);
120805
- return safe || "video";
120806
- }
120807
- function planHdrResources(args) {
120808
- const { composition, nativeHdrVideoIds, nativeHdrImageIds, projectDir, compiledDir } = args;
120809
- const hdrVideoIds = composition.videos.filter((v2) => nativeHdrVideoIds.has(v2.id)).map((v2) => v2.id);
120810
- const hdrVideoSrcPaths = /* @__PURE__ */ new Map();
120811
- for (const v2 of composition.videos) {
120812
- if (!hdrVideoIds.includes(v2.id)) continue;
120813
- let srcPath = v2.src;
120814
- if (!srcPath.startsWith("/")) {
120815
- const fromCompiled = join51(compiledDir, srcPath);
120816
- srcPath = args.existsSync(fromCompiled) ? fromCompiled : join51(projectDir, srcPath);
120817
- }
120818
- hdrVideoSrcPaths.set(v2.id, srcPath);
120819
- }
120820
- const hdrVideoStartTimes = /* @__PURE__ */ new Map();
120821
- for (const v2 of composition.videos) {
120822
- if (hdrVideoIds.includes(v2.id)) hdrVideoStartTimes.set(v2.id, v2.start);
120823
- }
120824
- const hdrImageStartTimes = /* @__PURE__ */ new Map();
120825
- for (const img of composition.images) {
120826
- if (nativeHdrImageIds.has(img.id)) hdrImageStartTimes.set(img.id, img.start);
120827
- }
120828
- return {
120829
- hdrVideoIds,
120830
- hdrVideoSrcPaths,
120831
- hdrVideoStartTimes,
120832
- hdrImageStartTimes,
120833
- hdrExtractionDims: /* @__PURE__ */ new Map(),
120834
- hdrImageFitInfo: /* @__PURE__ */ new Map()
120835
- };
120836
- }
120837
- async function probeHdrExtractionDims(args) {
120838
- const { domSession, nativeHdrIds, nativeHdrImageIds, composition, prep } = args;
120839
- const uniqueStartTimes = [
120840
- .../* @__PURE__ */ new Set([...prep.hdrVideoStartTimes.values(), ...prep.hdrImageStartTimes.values()])
120841
- ].sort((a, b2) => a - b2);
120842
- for (const seekTime of uniqueStartTimes) {
120843
- await domSession.page.evaluate((t2) => {
120844
- if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t2);
120845
- }, seekTime);
120846
- if (domSession.onBeforeCapture) {
120847
- await domSession.onBeforeCapture(domSession.page, seekTime);
120848
- }
120849
- const stacking = await queryElementStacking(domSession.page, nativeHdrIds);
120850
- for (const el of stacking) {
120851
- if (el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0 && !prep.hdrExtractionDims.has(el.id)) {
120852
- prep.hdrExtractionDims.set(el.id, { width: el.layoutWidth, height: el.layoutHeight });
120853
- }
120854
- if (el.isHdr && nativeHdrImageIds.has(el.id) && !prep.hdrImageFitInfo.has(el.id)) {
120855
- prep.hdrImageFitInfo.set(el.id, { fit: el.objectFit, position: el.objectPosition });
120856
- }
120857
- }
120858
- }
120859
- for (const [imageId, startTime] of prep.hdrImageStartTimes) {
120860
- if (prep.hdrExtractionDims.has(imageId)) continue;
120861
- const img = composition.images.find((i2) => i2.id === imageId);
120862
- if (!img) continue;
120863
- const duration = img.end - img.start;
120864
- const retryTime = startTime + Math.min(0.5, duration * 0.1);
120865
- await domSession.page.evaluate((t2) => {
120866
- if (window.__hf && typeof window.__hf.seek === "function") window.__hf.seek(t2);
120867
- }, retryTime);
120868
- if (domSession.onBeforeCapture) {
120869
- await domSession.onBeforeCapture(domSession.page, retryTime);
120870
- }
120871
- const retryStacking = await queryElementStacking(domSession.page, nativeHdrIds);
120872
- for (const el of retryStacking) {
120873
- if (el.id === imageId && el.isHdr && el.layoutWidth > 0 && el.layoutHeight > 0) {
120874
- prep.hdrExtractionDims.set(el.id, { width: el.layoutWidth, height: el.layoutHeight });
120875
- if (!prep.hdrImageFitInfo.has(el.id)) {
120876
- prep.hdrImageFitInfo.set(el.id, { fit: el.objectFit, position: el.objectPosition });
120877
- }
120878
- break;
120879
- }
120880
- }
120881
- }
120882
- }
120883
- function estimateHdrExtractionBytes(videos, fps) {
120884
- let total = 0;
120885
- for (const v2 of videos) {
120886
- const frames = Math.ceil(Math.max(0, v2.durationSeconds) * fps);
120887
- total += frames * v2.width * v2.height * 6;
120888
- }
120889
- return total;
120890
- }
120891
- function assertHdrExtractionDiskHeadroom(framesDir, plannedVideos, fps, log2) {
120892
- const estimatedBytes = estimateHdrExtractionBytes(plannedVideos, fps);
120893
- let freeBytes;
120894
- try {
120895
- const stat3 = statfsSync2(framesDir);
120896
- freeBytes = stat3.bavail * stat3.bsize;
120897
- } catch {
120898
- return;
120899
- }
120900
- const estimatedGb = (estimatedBytes / 1e9).toFixed(1);
120901
- if (estimatedBytes > freeBytes * HDR_EXTRACTION_HEADROOM_FRACTION) {
120902
- throw new Error(
120903
- `HDR pre-extraction needs ~${estimatedGb} GB of raw 16-bit frames but only ${(freeBytes / 1e9).toFixed(1)} GB is free at ${framesDir}. If the composition doesn't need HDR output, re-run with --sdr; otherwise free up disk space and retry.`
120904
- );
120905
- }
120906
- if (estimatedBytes > HDR_EXTRACTION_WARN_BYTES) {
120907
- log2.warn(
120908
- `HDR pre-extraction will write ~${estimatedGb} GB of raw 16-bit frames (pass --sdr to skip if HDR output isn't needed)`,
120909
- { estimatedBytes, freeBytes }
120910
- );
120911
- }
120912
- }
120913
- async function extractHdrVideoFrames(args) {
120914
- const { job, log: log2, framesDir, composition, prep, width, height, abortSignal, hdrDiagnostics } = args;
120915
- const out = /* @__PURE__ */ new Map();
120916
- mkdirSync24(framesDir, { recursive: true });
120917
- const plannedVideos = [];
120918
- for (const [videoId] of prep.hdrVideoSrcPaths) {
120919
- const video = composition.videos.find((v2) => v2.id === videoId);
120920
- if (!video) continue;
120921
- const dims = prep.hdrExtractionDims.get(videoId) ?? { width, height };
120922
- plannedVideos.push({
120923
- durationSeconds: video.end - video.start,
120924
- width: dims.width,
120925
- height: dims.height
120926
- });
120927
- }
120928
- assertHdrExtractionDiskHeadroom(framesDir, plannedVideos, fpsToNumber(job.config.fps), log2);
120929
- for (const [videoId, srcPath] of prep.hdrVideoSrcPaths) {
120930
- const video = composition.videos.find((v2) => v2.id === videoId);
120931
- if (!video) continue;
120932
- mkdirSync24(framesDir, { recursive: true });
120933
- const frameDir = mkdtempSync6(join51(framesDir, `hdr_${tempDirSafePrefix(videoId)}-`));
120934
- const duration = video.end - video.start;
120935
- const dims = prep.hdrExtractionDims.get(videoId) ?? { width, height };
120936
- const rawPath = join51(frameDir, "frames.rgb48le");
120937
- const ffmpegArgs = [
120938
- "-ss",
120939
- String(video.mediaStart),
120940
- "-i",
120941
- srcPath,
120942
- "-t",
120943
- String(duration),
120944
- "-r",
120945
- fpsToFfmpegArg(job.config.fps),
120946
- "-vf",
120947
- `scale=${dims.width}:${dims.height}:force_original_aspect_ratio=increase,crop=${dims.width}:${dims.height}`,
120948
- "-pix_fmt",
120949
- "rgb48le",
120950
- "-f",
120951
- "rawvideo",
120952
- "-y",
120953
- rawPath
120954
- ];
120955
- const result = await runFfmpeg(ffmpegArgs, { signal: abortSignal });
120956
- if (!result.success) {
120957
- hdrDiagnostics.videoExtractionFailures += 1;
120958
- log2.error("HDR frame pre-extraction failed; aborting render", {
120959
- videoId,
120960
- srcPath,
120961
- stderr: result.stderr.slice(-400)
120962
- });
120963
- throw new Error(
120964
- `HDR frame extraction failed for video "${videoId}". Aborting render to avoid shipping black HDR layers.`
120965
- );
120966
- }
120967
- const frameSize = dims.width * dims.height * 6;
120968
- const fd = openSync6(rawPath, constants3.O_RDONLY | NO_FOLLOW_FLAG);
120969
- let handedOff = false;
120970
- try {
120971
- const frameCount = Math.floor(fstatSync4(fd).size / frameSize);
120972
- if (frameCount < 1) {
120973
- hdrDiagnostics.videoExtractionFailures += 1;
120974
- throw new Error(
120975
- `HDR frame extraction produced no frames for video "${videoId}". Aborting render to avoid shipping black HDR layers.`
120976
- );
120977
- }
120978
- out.set(videoId, {
120979
- dir: frameDir,
120980
- rawPath,
120981
- fd,
120982
- width: dims.width,
120983
- height: dims.height,
120984
- frameSize,
120985
- frameCount,
120986
- scratch: Buffer.allocUnsafe(frameSize)
120987
- });
120988
- handedOff = true;
120989
- } finally {
120990
- if (!handedOff) closeSync7(fd);
120991
- }
120992
- }
120993
- return out;
120994
- }
120995
- function decodeHdrImageBuffers(args) {
120996
- const { log: log2, hdrImageSrcPaths, prep, hdrDiagnostics } = args;
120997
- const out = /* @__PURE__ */ new Map();
120998
- for (const [imageId, srcPath] of hdrImageSrcPaths) {
120999
- try {
121000
- const decoded = decodePngToRgb48le(readFileSync30(srcPath));
121001
- const layout2 = prep.hdrExtractionDims.get(imageId);
121002
- const fitInfo = prep.hdrImageFitInfo.get(imageId);
121003
- if (layout2 && (layout2.width !== decoded.width || layout2.height !== decoded.height)) {
121004
- const fit = normalizeObjectFit(fitInfo?.fit);
121005
- const resampled = resampleRgb48leObjectFit(
121006
- decoded.data,
121007
- decoded.width,
121008
- decoded.height,
121009
- layout2.width,
121010
- layout2.height,
121011
- fit,
121012
- fitInfo?.position
121013
- );
121014
- out.set(imageId, { data: resampled, width: layout2.width, height: layout2.height });
121015
- } else {
121016
- out.set(imageId, {
121017
- data: Buffer.from(decoded.data),
121018
- width: decoded.width,
121019
- height: decoded.height
121020
- });
121021
- }
121022
- } catch (err) {
121023
- hdrDiagnostics.imageDecodeFailures += 1;
121024
- log2.error("HDR image decode failed; aborting render", {
121025
- imageId,
121026
- srcPath,
121027
- error: err instanceof Error ? err.message : String(err)
121028
- });
121029
- throw new Error(
121030
- `HDR image decode failed for image "${imageId}". Aborting render to avoid shipping missing HDR image layers.`
121031
- );
121032
- }
121033
- }
121034
- return out;
121035
- }
121036
- var NO_FOLLOW_FLAG, HDR_EXTRACTION_HEADROOM_FRACTION, HDR_EXTRACTION_WARN_BYTES;
121037
- var init_captureHdrResources = __esm({
121038
- "../producer/src/services/render/stages/captureHdrResources.ts"() {
121039
- "use strict";
121040
- init_src();
121041
- init_dist3();
121042
- NO_FOLLOW_FLAG = constants3.O_NOFOLLOW ?? 0;
121043
- HDR_EXTRACTION_HEADROOM_FRACTION = 0.9;
121044
- HDR_EXTRACTION_WARN_BYTES = 1e10;
121045
- }
121046
- });
121047
-
121048
121423
  // ../producer/src/services/render/stages/captureHdrSequentialLoop.ts
121049
121424
  import { join as join53 } from "path";
121050
121425
  async function runSequentialLayeredFrameLoop(input2) {
@@ -121714,6 +122089,7 @@ async function runCaptureHdrStage(input2) {
121714
122089
  let hdrEncoder = null;
121715
122090
  let hdrEncoderClosed = false;
121716
122091
  let domSessionClosed = false;
122092
+ let releaseHdrExtractionReservation = null;
121717
122093
  const hdrVideoFrameSources = /* @__PURE__ */ new Map();
121718
122094
  try {
121719
122095
  await initializeSession(domSession);
@@ -121790,7 +122166,8 @@ async function runCaptureHdrStage(input2) {
121790
122166
  abortSignal,
121791
122167
  hdrDiagnostics
121792
122168
  });
121793
- for (const [id, source] of extracted) hdrVideoFrameSources.set(id, source);
122169
+ releaseHdrExtractionReservation = extracted.releaseReservation;
122170
+ for (const [id, source] of extracted.sources) hdrVideoFrameSources.set(id, source);
121794
122171
  const hdrImageBuffers = decodeHdrImageBuffers({
121795
122172
  log: log2,
121796
122173
  hdrImageSrcPaths,
@@ -121939,9 +122316,11 @@ async function runCaptureHdrStage(input2) {
121939
122316
  });
121940
122317
  }
121941
122318
  for (const frameSource of hdrVideoFrameSources.values()) {
121942
- closeHdrVideoFrameSource(frameSource, log2);
122319
+ cleanupHdrVideoFrameSource(frameSource, log2);
121943
122320
  }
121944
122321
  hdrVideoFrameSources.clear();
122322
+ releaseHdrExtractionReservation?.();
122323
+ releaseHdrExtractionReservation = null;
121945
122324
  }
121946
122325
  return {
121947
122326
  lastBrowserConsole,
@@ -125376,6 +125755,22 @@ function parseServerQuality(value) {
125376
125755
  function parseServerFormat(value) {
125377
125756
  return value === "mp4" || value === "webm" || value === "mov" ? value : void 0;
125378
125757
  }
125758
+ function parseServerOutputDynamicRange(value) {
125759
+ return value === "auto" || value === "hdr" || value === "sdr" ? value : void 0;
125760
+ }
125761
+ function parseLegacyServerHdrMode(value) {
125762
+ return value === "auto" || value === "force-hdr" || value === "force-sdr" ? value : void 0;
125763
+ }
125764
+ function fromRenderHdrMode(hdrMode) {
125765
+ if (hdrMode === "force-hdr") return "hdr";
125766
+ if (hdrMode === "force-sdr") return "sdr";
125767
+ return hdrMode;
125768
+ }
125769
+ function toRenderHdrMode(outputDynamicRange) {
125770
+ if (outputDynamicRange === "hdr") return "force-hdr";
125771
+ if (outputDynamicRange === "sdr") return "force-sdr";
125772
+ return outputDynamicRange;
125773
+ }
125379
125774
  function parseRenderOptions(body) {
125380
125775
  const fps = parseServerFps(body.fps);
125381
125776
  const quality = parseServerQuality(body.quality);
@@ -125386,6 +125781,7 @@ function parseRenderOptions(body) {
125386
125781
  const outputPath = parseOutputCandidate(body);
125387
125782
  const entryFile = nonEmptyString(body.entryFile);
125388
125783
  const format = parseServerFormat(body.format);
125784
+ const outputDynamicRange = parseServerOutputDynamicRange(body.outputDynamicRange) ?? fromRenderHdrMode(parseLegacyServerHdrMode(body.hdrMode));
125389
125785
  const videoFrameFormat = isVideoFrameFormat(body.videoFrameFormat) ? body.videoFrameFormat : void 0;
125390
125786
  const { variables, outputResolution, outputResolutionAspectAgnostic } = parseRenderOverrides(body);
125391
125787
  return {
@@ -125398,6 +125794,7 @@ function parseRenderOptions(body) {
125398
125794
  strictness,
125399
125795
  entryFile,
125400
125796
  format,
125797
+ outputDynamicRange,
125401
125798
  variables,
125402
125799
  outputResolution,
125403
125800
  outputResolutionAspectAgnostic,
@@ -125430,7 +125827,8 @@ function buildRenderJobConfig(input2, outputPath, log2) {
125430
125827
  variables: input2.variables,
125431
125828
  outputResolution: input2.outputResolution,
125432
125829
  outputResolutionAspectAgnostic: input2.outputResolutionAspectAgnostic,
125433
- videoFrameFormat: input2.videoFrameFormat
125830
+ videoFrameFormat: input2.videoFrameFormat,
125831
+ hdrMode: toRenderHdrMode(input2.outputDynamicRange)
125434
125832
  }
125435
125833
  });
125436
125834
  return renderConfigFromRequest(request, { logger: log2 });
@@ -125446,6 +125844,17 @@ function validateRenderOverrides(body) {
125446
125844
  if (body.variables !== void 0 && !isPlainObject4(body.variables)) {
125447
125845
  return 'variables must be a JSON object keyed by variable id (e.g. {"title":"Hello"})';
125448
125846
  }
125847
+ if (body.outputDynamicRange !== void 0 && parseServerOutputDynamicRange(body.outputDynamicRange) === void 0) {
125848
+ return 'outputDynamicRange must be one of: "auto", "hdr", "sdr"';
125849
+ }
125850
+ const legacyHdrMode = parseLegacyServerHdrMode(body.hdrMode);
125851
+ if (body.hdrMode !== void 0 && legacyHdrMode === void 0) {
125852
+ return 'legacy hdrMode must be one of: "auto", "force-hdr", "force-sdr"';
125853
+ }
125854
+ const outputDynamicRange = parseServerOutputDynamicRange(body.outputDynamicRange);
125855
+ if (outputDynamicRange !== void 0 && legacyHdrMode !== void 0 && outputDynamicRange !== fromRenderHdrMode(legacyHdrMode)) {
125856
+ return "outputDynamicRange and legacy hdrMode must describe the same output policy";
125857
+ }
125449
125858
  return validateOutputResolutionOverride(body);
125450
125859
  }
125451
125860
  function validateOutputResolutionOverride(body) {
@@ -126386,6 +126795,10 @@ function readVideoMetadata(value, field) {
126386
126795
  record.videoStreamDurationSeconds,
126387
126796
  `${field}.videoStreamDurationSeconds`
126388
126797
  ),
126798
+ // Plans written before stream-start metadata existed implicitly used the
126799
+ // overwhelmingly common start-at-zero domain. Preserve that compatibility
126800
+ // while carrying non-zero edit-list/transport timestamps in new plans.
126801
+ videoStreamStartSeconds: record.videoStreamStartSeconds === void 0 ? 0 : readFiniteNumber(record.videoStreamStartSeconds, `${field}.videoStreamStartSeconds`),
126389
126802
  width: readPositiveInteger(record.width, `${field}.width`),
126390
126803
  height: readPositiveInteger(record.height, `${field}.height`),
126391
126804
  fps: readFiniteNumber(record.fps, `${field}.fps`),