@ttt-productions/media-processing-core 0.8.13 → 0.8.15

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.
@@ -1,5 +1,5 @@
1
1
  // =============================================================================
2
- // Canonical media content inspection (canonical-upload-content-classification)
2
+ // Canonical media content inspection
3
3
  // =============================================================================
4
4
  //
5
5
  // The ONE server-side classification authority. Consumes a LOCAL file (the
@@ -11,11 +11,11 @@
11
11
  // filename, extension, container brand, and payload-byte heuristics are never
12
12
  // consulted.
13
13
  //
14
- // Design (reviewed 2026-07-31; see the consuming app's
15
- // CODE_CHANGE_canonical_upload_content_classification.md):
14
+ // Design:
16
15
  // - family selection by a bounded 64 KiB SIGNATURE read (header only —
17
- // semantic elements are NEVER searched for in arbitrary payload; that
18
- // flat-scan heuristic caused the WebM audio misclassification twice);
16
+ // semantic elements are NEVER searched for in arbitrary payload: payload
17
+ // bytes can mimic container elements, so a flat scan misclassifies WebM
18
+ // audio as video);
19
19
  // - shared A/V containers (EBML/ISO-BMFF/Ogg/AVI) classified by a hardened
20
20
  // `ffprobe` stream table (libavformat's structural parsers), with timeout,
21
21
  // kill-on-abort, output cap, and strict schema validation;
@@ -28,11 +28,11 @@
28
28
  import { open } from "node:fs/promises";
29
29
  import { z } from "zod";
30
30
  import { MediaInspectionResultSchema, } from "@ttt-productions/media-schemas";
31
- import { runCmd } from "../video/ffmpeg.js";
31
+ import { DEFAULT_PROBE_TIMEOUT_MS, runCmd } from "../video/ffmpeg.js";
32
+ import { measureDurationSec } from "../duration/media-duration.js";
32
33
  /** Bump on any classification-rule change (rides every decision event). */
33
- export const INSPECTOR_VERSION = "mpc-inspect-1";
34
+ export const INSPECTOR_VERSION = "mpc-inspect-2";
34
35
  const SIGNATURE_READ_BYTES = 64 * 1024;
35
- const DEFAULT_PROBE_TIMEOUT_MS = 20_000;
36
36
  const PROBE_SIZE_BYTES = 4 * 1024 * 1024; // -probesize: metadata, not playback
37
37
  const ANALYZE_DURATION_US = 10 * 1000 * 1000;
38
38
  // ---- ffprobe JSON boundary (strict; anything else is malformed) ----
@@ -44,8 +44,6 @@ const ProbeStreamSchema = z
44
44
  .object({ attached_pic: z.number().int().min(0).max(1).optional() })
45
45
  .loose()
46
46
  .optional(),
47
- nb_frames: z.string().max(20).optional(),
48
- duration: z.string().max(32).optional(),
49
47
  })
50
48
  .loose();
51
49
  const ProbeJsonSchema = z
@@ -213,6 +211,7 @@ function baseResult(partial, ffprobeVersion) {
213
211
  reasonCode: partial.reasonCode,
214
212
  ...(partial.canonicalKind ? { canonicalKind: partial.canonicalKind } : {}),
215
213
  ...(partial.formatId ? { formatId: partial.formatId } : {}),
214
+ ...(partial.durationSec !== undefined ? { durationSec: partial.durationSec } : {}),
216
215
  };
217
216
  return MediaInspectionResultSchema.parse(result);
218
217
  }
@@ -253,8 +252,8 @@ export async function inspectMedia(args) {
253
252
  return indeterminate("unknown", "unrecognized_signature", ffprobeVersion);
254
253
  case "svg":
255
254
  // Scriptable XML "image" — structurally recognized, deliberately NOT a
256
- // processable format until a sanitize/rasterize design exists (launch
257
- // policy 2026-07-31). Never classified by decoder capability alone.
255
+ // processable format: it can carry script and no sanitize/rasterize step
256
+ // exists. Never classified by decoder capability alone.
258
257
  return baseResult({
259
258
  status: "unsupported",
260
259
  container: "svg",
@@ -372,6 +371,9 @@ async function classifyAvContainer(args, container, formatId, probe, timeoutMs,
372
371
  }
373
372
  }
374
373
  const codecs = { audio: [...audioCodecs], video: [...videoCodecs] };
374
+ const measure = args.deps?.measureDuration ?? measureDurationSec;
375
+ const durationSec = await measure(args.localPath, { signal: args.signal });
376
+ const duration = durationSec !== undefined ? { durationSec } : {};
375
377
  // classification table (fail-closed)
376
378
  if (counts.timedVideo > 0) {
377
379
  return baseResult({
@@ -381,6 +383,7 @@ async function classifyAvContainer(args, container, formatId, probe, timeoutMs,
381
383
  ...(formatId ? { formatId } : {}),
382
384
  streams: counts,
383
385
  codecs,
386
+ ...duration,
384
387
  safetyPlan: "video-frames",
385
388
  reasonCode: "ok",
386
389
  }, ffprobeVersion);
@@ -393,6 +396,7 @@ async function classifyAvContainer(args, container, formatId, probe, timeoutMs,
393
396
  ...(formatId ? { formatId } : {}),
394
397
  streams: counts,
395
398
  codecs,
399
+ ...duration,
396
400
  safetyPlan: counts.attachedPictures > 0 ? "audio-plus-artwork" : "audio-only",
397
401
  reasonCode: "ok",
398
402
  }, ffprobeVersion);
@@ -405,6 +409,7 @@ async function classifyAvContainer(args, container, formatId, probe, timeoutMs,
405
409
  ...(formatId ? { formatId } : {}),
406
410
  streams: counts,
407
411
  codecs,
412
+ ...duration,
408
413
  safetyPlan: "strict-video-fallback",
409
414
  reasonCode: sawUnknownType ? "unknown_stream_type" : "auxiliary_only",
410
415
  }, ffprobeVersion);
@@ -1 +1 @@
1
- {"version":3,"file":"inspect-media.js","sourceRoot":"","sources":["../../src/inspection/inspect-media.ts"],"names":[],"mappings":"AAAA,gFAAgF;AAChF,+EAA+E;AAC/E,gFAAgF;AAChF,EAAE;AACF,2EAA2E;AAC3E,0EAA0E;AAC1E,6EAA6E;AAC7E,2EAA2E;AAC3E,2EAA2E;AAC3E,yEAAyE;AACzE,8EAA8E;AAC9E,aAAa;AACb,EAAE;AACF,uDAAuD;AACvD,2DAA2D;AAC3D,yEAAyE;AACzE,0EAA0E;AAC1E,0EAA0E;AAC1E,6EAA6E;AAC7E,+EAA+E;AAC/E,+DAA+D;AAC/D,+EAA+E;AAC/E,8EAA8E;AAC9E,2EAA2E;AAC3E,iDAAiD;AACjD,iEAAiE;AACjE,4EAA4E;AAE5E,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AACxC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AACxB,OAAO,EACL,2BAA2B,GAK5B,MAAM,gCAAgC,CAAC;AACxC,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAE5C,2EAA2E;AAC3E,MAAM,CAAC,MAAM,iBAAiB,GAAG,eAAe,CAAC;AAEjD,MAAM,oBAAoB,GAAG,EAAE,GAAG,IAAI,CAAC;AACvC,MAAM,wBAAwB,GAAG,MAAM,CAAC;AACxC,MAAM,gBAAgB,GAAG,CAAC,GAAG,IAAI,GAAG,IAAI,CAAC,CAAC,qCAAqC;AAC/E,MAAM,mBAAmB,GAAG,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;AAE7C,uEAAuE;AAEvE,MAAM,iBAAiB,GAAG,CAAC;KACxB,MAAM,CAAC;IACN,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;IACzC,UAAU,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;IACzC,WAAW,EAAE,CAAC;SACX,MAAM,CAAC,EAAE,YAAY,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC;SACnE,KAAK,EAAE;SACP,QAAQ,EAAE;IACb,SAAS,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;IACxC,QAAQ,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,QAAQ,EAAE;CACxC,CAAC;KACD,KAAK,EAAE,CAAC;AAEX,MAAM,eAAe,GAAG,CAAC;KACtB,MAAM,CAAC;IACN,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,EAAE,WAAW,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,QAAQ,EAAE;IACpF,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,iBAAiB,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE;CACxD,CAAC;KACD,KAAK,EAAE,CAAC;AAEX,qEAAqE;AAErE,MAAM,SAAS,GAAsC;IACnD,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,MAAM;IACZ,GAAG,EAAE,KAAK;IACV,GAAG,EAAE,KAAK;IACV,GAAG,EAAE,KAAK;IACV,MAAM,EAAE,QAAQ;IAChB,KAAK,EAAE,OAAO;IACd,KAAK,EAAE,OAAO;IACd,GAAG,EAAE,KAAK;IACV,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,MAAM;IACZ,MAAM,EAAE,QAAQ;IAChB,GAAG,EAAE,KAAK;IACV,IAAI,EAAE,MAAM;CACb,CAAC;AAEF,SAAS,cAAc,CAAC,IAAwB;IAC9C,IAAI,CAAC,IAAI;QAAE,OAAO,OAAO,CAAC;IAC1B,MAAM,CAAC,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IAC7B,IAAI,SAAS,CAAC,CAAC,CAAC;QAAE,OAAO,SAAS,CAAC,CAAC,CAAC,CAAC;IACtC,IAAI,CAAC,CAAC,UAAU,CAAC,MAAM,CAAC;QAAE,OAAO,KAAK,CAAC;IACvC,IAAI,CAAC,CAAC,UAAU,CAAC,KAAK,CAAC;QAAE,OAAO,KAAK,CAAC;IACtC,OAAO,OAAO,CAAC;AACjB,CAAC;AAgBD;2EAC2E;AAC3E,MAAM,oBAAoB,GAAG,IAAI,GAAG,CAAC,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AAEvH,SAAS,KAAK,CAAC,CAAa,EAAE,GAAW,EAAE,CAAS;IAClD,IAAI,GAAG,GAAG,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IAC5C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,IAAI,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;IACpF,OAAO,IAAI,CAAC;AACd,CAAC;AAED,MAAM,UAAU,qBAAqB,CAAC,CAAa;IACjD,IAAI,CAAC,CAAC,MAAM,GAAG,EAAE;QAAE,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IAChD,oCAAoC;IACpC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IACrH,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IACvG,IAAI,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IACvF,IAAI,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,IAAI,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAChH,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IAClG,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,EAAE,CAAC;QAC3G,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAClE,CAAC;IACD,8EAA8E;IAC9E,CAAC;QACC,MAAM,MAAM,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC,CAAC;aAC7D,QAAQ,CAAC,QAAQ,CAAC;aAClB,WAAW,EAAE,CAAC;QACjB,IAAI,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,MAAM,CAAC,SAAS,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,IAAI,MAAM,CAAC,QAAQ,CAAC,eAAe,CAAC,CAAC,EAAE,CAAC;YACrJ,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;QAC3B,CAAC;IACH,CAAC;IACD,aAAa;IACb,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IAChG,IAAI,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,EAAE,CAAC;QACxB,MAAM,KAAK,GAAG,MAAM,CAAC,YAAY,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;QACjF,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC;IACtC,CAAC;IACD,IAAI,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IAClD,IAAI,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,IAAI,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IACzE,IAAI,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC,IAAI,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IACzE,IAAI,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IACjD,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,IAAI,CAAC,KAAK,IAAI;QAAE,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC;IACtE,IAAI,KAAK,CAAC,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;QAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IACnD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;AAC/B,CAAC;AAiBD,IAAI,oBAA+C,CAAC;AAEpD,KAAK,UAAU,iBAAiB;IAC9B,IAAI,oBAAoB,KAAK,SAAS;QAAE,OAAO,oBAAoB,IAAI,SAAS,CAAC;IACjF,IAAI,CAAC;QACH,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,SAAS,EAAE,CAAC,UAAU,CAAC,EAAE,EAAE,SAAS,EAAE,KAAK,EAAE,CAAC,CAAC;QACtE,MAAM,KAAK,GAAG,CAAC,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;QACrE,oBAAoB,GAAG,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC5D,CAAC;IAAC,MAAM,CAAC;QACP,oBAAoB,GAAG,IAAI,CAAC;IAC9B,CAAC;IACD,OAAO,oBAAoB,IAAI,SAAS,CAAC;AAC3C,CAAC;AAED,KAAK,UAAU,YAAY,CACzB,SAAiB,EACjB,IAAiD;IAEjD,IAAI,CAAC,CAAC;IACN,IAAI,CAAC;QACH,CAAC,GAAG,MAAM,MAAM,CACd,SAAS,EACT;YACE,IAAI;YACJ,OAAO;YACP,eAAe;YACf,MAAM;YACN,cAAc;YACd,eAAe;YACf,YAAY;YACZ,MAAM,CAAC,gBAAgB,CAAC;YACxB,kBAAkB;YAClB,MAAM,CAAC,mBAAmB,CAAC;YAC3B,SAAS;SACV,EACD,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CACnD,CAAC;IACJ,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC,gDAAgD;IAC1F,CAAC;IACD,IAAI,CAAC,CAAC,QAAQ;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC;IACxD,IAAI,CAAC,CAAC,SAAS;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;IAC3D,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC;QAAE,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC;IACzD,IAAI,CAAC;QACH,OAAO,EAAE,EAAE,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,IAAI,IAAI,CAAC,EAAE,CAAC;IAC1D,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,EAAE,EAAE,KAAK,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC;IAC3C,CAAC;AACH,CAAC;AAED,KAAK,UAAU,kBAAkB,CAAC,SAAiB;IACjD,IAAI,CAAC;QACH,MAAM,KAAK,GAAG,CAAC,MAAM,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC;QAC9C,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,SAAS,EAAE,EAAE,gBAAgB,EAAE,SAAS,CAAC,8BAA8B,EAAE,CAAC,CAAC,QAAQ,EAAE,CAAC;QAC/G,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,CAAC,IAAI,CAAC,MAAM;YAAE,OAAO,IAAI,CAAC;QAC7C,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC,EAAE,CAAC;IAClD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC;IACd,CAAC;AACH,CAAC;AAED,4BAA4B;AAE5B,SAAS,UAAU,CACjB,OAC4D,EAC5D,cAAkC;IAElC,MAAM,MAAM,GAA0B;QACpC,gBAAgB,EAAE,iBAAiB;QACnC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC7C,OAAO,EAAE,OAAO,CAAC,OAAO,IAAI,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE;QAC1G,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,EAAE,KAAK,EAAE,EAAE,EAAE,KAAK,EAAE,EAAE,EAAE;QAClD,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,SAAS,EAAE,OAAO,CAAC,SAAS;QAC5B,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,UAAU,EAAE,OAAO,CAAC,UAAU;QAC9B,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC1E,GAAG,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,OAAO,CAAC,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAC5D,CAAC;IACF,OAAO,2BAA2B,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;AACnD,CAAC;AAED,SAAS,aAAa,CACpB,SAAiB,EACjB,UAAkB,EAClB,cAAkC,EAClC,QAAwB;IAExB,OAAO,UAAU,CACf;QACE,MAAM,EAAE,eAAe;QACvB,SAAS;QACT,UAAU,EAAE,uBAAuB;QACnC,UAAU;QACV,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KAClC,EACD,cAAc,CACf,CAAC;AACJ,CAAC;AAWD,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAsB;IACvD,MAAM,SAAS,GAAG,IAAI,CAAC,SAAS,IAAI,wBAAwB,CAAC;IAC7D,MAAM,KAAK,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,IAAI,YAAY,CAAC;IAC/C,MAAM,WAAW,GAAG,IAAI,CAAC,IAAI,EAAE,WAAW,IAAI,kBAAkB,CAAC;IACjE,MAAM,cAAc,GAAG,IAAI,CAAC,IAAI,EAAE,cAAc,IAAI,CAAC,MAAM,iBAAiB,EAAE,CAAC,CAAC;IAEhF,0CAA0C;IAC1C,IAAI,IAAgB,CAAC;IACrB,IAAI,CAAC;QACH,MAAM,EAAE,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,CAAC;QAC3C,IAAI,CAAC;YACH,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,CAAC,oBAAoB,CAAC,CAAC;YAC/C,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,EAAE,oBAAoB,EAAE,CAAC,CAAC,CAAC;YACrE,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,SAAS,CAAC,CAAC;QACpC,CAAC;gBAAS,CAAC;YACT,MAAM,EAAE,CAAC,KAAK,EAAE,CAAC;QACnB,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,aAAa,CAAC,SAAS,EAAE,aAAa,EAAE,cAAc,CAAC,CAAC;IACjE,CAAC;IAED,MAAM,GAAG,GAAG,qBAAqB,CAAC,IAAI,CAAC,CAAC;IAExC,oBAAoB;IACpB,QAAQ,GAAG,CAAC,MAAM,EAAE,CAAC;QACnB,KAAK,SAAS;YACZ,OAAO,aAAa,CAAC,SAAS,EAAE,wBAAwB,EAAE,cAAc,CAAC,CAAC;QAE5E,KAAK,KAAK;YACR,uEAAuE;YACvE,sEAAsE;YACtE,oEAAoE;YACpE,OAAO,UAAU,CACf;gBACE,MAAM,EAAE,aAAa;gBACrB,SAAS,EAAE,KAAK;gBAChB,QAAQ,EAAE,KAAK;gBACf,UAAU,EAAE,uBAAuB;gBACnC,UAAU,EAAE,wBAAwB;aACrC,EACD,cAAc,CACf,CAAC;QAEJ,KAAK,OAAO,CAAC,CAAC,CAAC;YACb,yEAAyE;YACzE,qCAAqC;YACrC,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;YAClD,IAAI,CAAC,OAAO;gBAAE,OAAO,UAAU,CAC7B,EAAE,MAAM,EAAE,WAAW,EAAE,SAAS,EAAE,GAAG,CAAC,SAAS,EAAE,QAAQ,EAAE,GAAG,CAAC,QAAQ,EAAE,UAAU,EAAE,uBAAuB,EAAE,UAAU,EAAE,qBAAqB,EAAE,EACjJ,cAAc,CACf,CAAC;YACF,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;YAC9B,OAAO,UAAU,CACf;gBACE,MAAM,EAAE,YAAY;gBACpB,aAAa,EAAE,OAAO;gBACtB,SAAS,EAAE,GAAG,CAAC,SAAS;gBACxB,QAAQ,EAAE,GAAG,CAAC,QAAQ;gBACtB,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,EAAE;gBAC5F,UAAU,EAAE,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,aAAa;gBAChE,UAAU,EAAE,IAAI;aACjB,EACD,cAAc,CACf,CAAC;QACJ,CAAC;QAED,KAAK,SAAS;YACZ,IAAI,oBAAoB,CAAC,GAAG,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;gBACxC,sEAAsE;gBACtE,uEAAuE;gBACvE,kEAAkE;gBAClE,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;gBAC/E,MAAM,MAAM,GAAG,MAAM,CAAC,EAAE,CAAC,CAAC,CAAC,eAAe,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;gBACzE,MAAM,OAAO,GAAG,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;gBACnE,MAAM,QAAQ,GAAG,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,yBAAyB;gBAC1G,MAAM,QAAQ,GAA8B,GAAG,CAAC,KAAK,CAAC,UAAU,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC;gBAC1F,MAAM,OAAO,GAAG,MAAM,WAAW,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;gBAClD,IAAI,CAAC,OAAO,IAAI,QAAQ,EAAE,CAAC;oBACzB,IAAI,QAAQ,KAAK,MAAM,IAAI,CAAC,OAAO,IAAI,CAAC,QAAQ,EAAE,CAAC;wBACjD,kEAAkE;wBAClE,6DAA6D;wBAC7D,OAAO,UAAU,CACf,EAAE,MAAM,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU,EAAE,uBAAuB,EAAE,UAAU,EAAE,yBAAyB,EAAE,EAClI,cAAc,CACf,CAAC;oBACJ,CAAC;oBACD,mEAAmE;oBACnE,mEAAmE;oBACnE,wBAAwB;oBACxB,OAAO,aAAa,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,wBAAwB,CAAC,CAAC,CAAC,qBAAqB,EAAE,cAAc,EAAE,QAAQ,CAAC,CAAC;gBACrH,CAAC;gBACD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC;gBAC9B,OAAO,UAAU,CACf;oBACE,MAAM,EAAE,YAAY;oBACpB,aAAa,EAAE,OAAO;oBACtB,SAAS,EAAE,MAAM;oBACjB,QAAQ;oBACR,OAAO,EAAE,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,WAAW,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,EAAE;oBAC5F,UAAU,EAAE,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,uBAAuB,CAAC,CAAC,CAAC,aAAa;oBAChE,UAAU,EAAE,IAAI;iBACjB,EACD,cAAc,CACf,CAAC;YACJ,CAAC;YACD,OAAO,mBAAmB,CAAC,IAAI,EAAE,SAAS,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC;QAE3F,KAAK,MAAM;YACT,OAAO,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC;QACrF,KAAK,KAAK;YACR,OAAO,mBAAmB,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC;QACnF,KAAK,KAAK;YACR,yEAAyE;YACzE,2EAA2E;YAC3E,OAAO,mBAAmB,CAAC,IAAI,EAAE,KAAK,EAAE,SAAS,EAAE,KAAK,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC;QACvF,KAAK,KAAK;YACR,OAAO,mBAAmB,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC;QACnF,KAAK,KAAK;YACR,OAAO,mBAAmB,CAAC,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC;QACnF,KAAK,MAAM;YACT,OAAO,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,SAAS,EAAE,cAAc,CAAC,CAAC;IACvF,CAAC;AACH,CAAC;AAED,KAAK,UAAU,mBAAmB,CAChC,IAAsB,EACtB,SAAiB,EACjB,QAAmC,EACnC,KAA6C,EAC7C,SAAiB,EACjB,cAAkC;IAElC,MAAM,MAAM,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,CAAC,CAAC;IAC/E,IAAI,CAAC,MAAM,CAAC,EAAE,EAAE,CAAC;QACf,OAAO,aAAa,CAAC,SAAS,EAAE,SAAS,MAAM,CAAC,MAAM,EAAE,EAAE,cAAc,EAAE,QAAQ,CAAC,CAAC;IACtF,CAAC;IACD,MAAM,MAAM,GAAG,eAAe,CAAC,SAAS,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IACtD,IAAI,CAAC,MAAM,CAAC,OAAO;QAAE,OAAO,aAAa,CAAC,SAAS,EAAE,sBAAsB,EAAE,cAAc,EAAE,QAAQ,CAAC,CAAC;IAEvG,MAAM,UAAU,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,IAAI,EAAE,CAAC;IAC7C,IAAI,UAAU,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,aAAa,CAAC,SAAS,EAAE,YAAY,EAAE,cAAc,EAAE,QAAQ,CAAC,CAAC;IAErG,MAAM,MAAM,GAA2B,EAAE,KAAK,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,gBAAgB,EAAE,CAAC,EAAE,WAAW,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC;IACtH,MAAM,WAAW,GAAG,IAAI,GAAG,EAAqB,CAAC;IACjD,MAAM,WAAW,GAAG,IAAI,GAAG,EAAqB,CAAC;IACjD,IAAI,cAAc,GAAG,KAAK,CAAC;IAE3B,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC3B,MAAM,IAAI,GAAG,CAAC,CAAC,UAAU,IAAI,EAAE,CAAC;QAChC,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;YACrB,MAAM,CAAC,KAAK,EAAE,CAAC;YACf,WAAW,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;QAChD,CAAC;aAAM,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;YAC5B,wEAAwE;YACxE,yEAAyE;YACzE,oEAAoE;YACpE,2CAA2C;YAC3C,IAAI,CAAC,CAAC,WAAW,EAAE,YAAY,KAAK,CAAC,EAAE,CAAC;gBACtC,MAAM,CAAC,gBAAgB,EAAE,CAAC;YAC5B,CAAC;iBAAM,CAAC;gBACN,MAAM,CAAC,UAAU,EAAE,CAAC;gBACpB,WAAW,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC;YAChD,CAAC;QACH,CAAC;aAAM,IAAI,IAAI,KAAK,UAAU,IAAI,IAAI,KAAK,MAAM,IAAI,IAAI,KAAK,YAAY,EAAE,CAAC;YAC3E,MAAM,CAAC,SAAS,EAAE,CAAC;QACrB,CAAC;aAAM,CAAC;YACN,MAAM,CAAC,SAAS,EAAE,CAAC;YACnB,cAAc,GAAG,IAAI,CAAC;QACxB,CAAC;IACH,CAAC;IAED,MAAM,MAAM,GAAG,EAAE,KAAK,EAAE,CAAC,GAAG,WAAW,CAAC,EAAE,KAAK,EAAE,CAAC,GAAG,WAAW,CAAC,EAAE,CAAC;IAEpE,qCAAqC;IACrC,IAAI,MAAM,CAAC,UAAU,GAAG,CAAC,EAAE,CAAC;QAC1B,OAAO,UAAU,CACf;YACE,MAAM,EAAE,YAAY;YACpB,aAAa,EAAE,OAAO;YACtB,SAAS;YACT,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACjC,OAAO,EAAE,MAAM;YACf,MAAM;YACN,UAAU,EAAE,cAAc;YAC1B,UAAU,EAAE,IAAI;SACjB,EACD,cAAc,CACf,CAAC;IACJ,CAAC;IACD,IAAI,MAAM,CAAC,KAAK,GAAG,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;QACxC,OAAO,UAAU,CACf;YACE,MAAM,EAAE,YAAY;YACpB,aAAa,EAAE,OAAO;YACtB,SAAS;YACT,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACjC,OAAO,EAAE,MAAM;YACf,MAAM;YACN,UAAU,EAAE,MAAM,CAAC,gBAAgB,GAAG,CAAC,CAAC,CAAC,CAAC,oBAAoB,CAAC,CAAC,CAAC,YAAY;YAC7E,UAAU,EAAE,IAAI;SACjB,EACD,cAAc,CACf,CAAC;IACJ,CAAC;IACD,2EAA2E;IAC3E,oDAAoD;IACpD,OAAO,UAAU,CACf;QACE,MAAM,EAAE,eAAe;QACvB,SAAS;QACT,GAAG,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACjC,OAAO,EAAE,MAAM;QACf,MAAM;QACN,UAAU,EAAE,uBAAuB;QACnC,UAAU,EAAE,cAAc,CAAC,CAAC,CAAC,qBAAqB,CAAC,CAAC,CAAC,gBAAgB;KACtE,EACD,cAAc,CACf,CAAC;AACJ,CAAC","sourcesContent":["// =============================================================================\n// Canonical media content inspection (canonical-upload-content-classification)\n// =============================================================================\n//\n// The ONE server-side classification authority. Consumes a LOCAL file (the\n// pipeline's generation-pinned temp download), establishes what the bytes\n// actually contain, and returns the bounded `MediaInspectionResult` contract\n// from @ttt-productions/media-schemas. Everything downstream — origin/kind\n// validation, processing-spec selection, the persisted asset kind, and the\n// safety (PhotoDNA) routing — derives from this one result. Client MIME,\n// filename, extension, container brand, and payload-byte heuristics are never\n// consulted.\n//\n// Design (reviewed 2026-07-31; see the consuming app's\n// CODE_CHANGE_canonical_upload_content_classification.md):\n// - family selection by a bounded 64 KiB SIGNATURE read (header only —\n// semantic elements are NEVER searched for in arbitrary payload; that\n// flat-scan heuristic caused the WebM audio misclassification twice);\n// - shared A/V containers (EBML/ISO-BMFF/Ogg/AVI) classified by a hardened\n// `ffprobe` stream table (libavformat's structural parsers), with timeout,\n// kill-on-abort, output cap, and strict schema validation;\n// - image-family proof by DECODER (sharp) — a signature or `ftyp` brand is a\n// candidate, not proof, and image-family ISO-BMFF (AVIF/HEIC) is resolved\n// BEFORE the ordinary \"any timed video stream wins\" rule (a valid AVIF\n// probes as a one-frame AV1 `video` stream);\n// - FAIL-CLOSED: anything unproven is `indeterminate` with the\n// `strict-video-fallback` safety plan — never audio, never publishable.\n\nimport { open } from \"node:fs/promises\";\nimport { z } from \"zod\";\nimport {\n MediaInspectionResultSchema,\n type MediaInspectionResult,\n type MediaInspectionStreams,\n type NormalizedCodecId,\n type MediaFormatId,\n} from \"@ttt-productions/media-schemas\";\nimport { runCmd } from \"../video/ffmpeg.js\";\n\n/** Bump on any classification-rule change (rides every decision event). */\nexport const INSPECTOR_VERSION = \"mpc-inspect-1\";\n\nconst SIGNATURE_READ_BYTES = 64 * 1024;\nconst DEFAULT_PROBE_TIMEOUT_MS = 20_000;\nconst PROBE_SIZE_BYTES = 4 * 1024 * 1024; // -probesize: metadata, not playback\nconst ANALYZE_DURATION_US = 10 * 1000 * 1000;\n\n// ---- ffprobe JSON boundary (strict; anything else is malformed) ----\n\nconst ProbeStreamSchema = z\n .object({\n codec_type: z.string().max(32).optional(),\n codec_name: z.string().max(64).optional(),\n disposition: z\n .object({ attached_pic: z.number().int().min(0).max(1).optional() })\n .loose()\n .optional(),\n nb_frames: z.string().max(20).optional(),\n duration: z.string().max(32).optional(),\n })\n .loose();\n\nconst ProbeJsonSchema = z\n .object({\n format: z.object({ format_name: z.string().max(200).optional() }).loose().optional(),\n streams: z.array(ProbeStreamSchema).max(256).optional(),\n })\n .loose();\n\n// ---- codec normalization (bounded — raw strings never escape) ----\n\nconst CODEC_MAP: Record<string, NormalizedCodecId> = {\n h264: \"h264\",\n hevc: \"hevc\",\n vp8: \"vp8\",\n vp9: \"vp9\",\n av1: \"av1\",\n theora: \"theora\",\n mpeg4: \"mpeg4\",\n mjpeg: \"mjpeg\",\n aac: \"aac\",\n alac: \"alac\",\n opus: \"opus\",\n vorbis: \"vorbis\",\n mp3: \"mp3\",\n flac: \"flac\",\n};\n\nfunction normalizeCodec(name: string | undefined): NormalizedCodecId {\n if (!name) return \"other\";\n const n = name.toLowerCase();\n if (CODEC_MAP[n]) return CODEC_MAP[n];\n if (n.startsWith(\"pcm_\")) return \"pcm\";\n if (n.startsWith(\"amr\")) return \"amr\";\n return \"other\";\n}\n\n// ---- signature families ----\n\ntype SignatureFamily =\n | { family: \"image\"; formatId: MediaFormatId; container: string }\n | { family: \"svg\" }\n | { family: \"ebml\" }\n | { family: \"isobmff\"; brand: string }\n | { family: \"ogg\" }\n | { family: \"avi\" }\n | { family: \"wav\" }\n | { family: \"mp3\" }\n | { family: \"flac\" }\n | { family: \"unknown\" };\n\n/** ISO-BMFF brands that declare an IMAGE-item file (HEIF family). Resolved via\n * decoder proof BEFORE the timed-video rule — see the AVIF note above. */\nconst ISOBMFF_IMAGE_BRANDS = new Set([\"avif\", \"avis\", \"heic\", \"heix\", \"heim\", \"heis\", \"hevc\", \"hevx\", \"mif1\", \"msf1\"]);\n\nfunction ascii(b: Uint8Array, off: number, s: string): boolean {\n if (off + s.length > b.length) return false;\n for (let i = 0; i < s.length; i++) if (b[off + i] !== s.charCodeAt(i)) return false;\n return true;\n}\n\nexport function detectSignatureFamily(b: Uint8Array): SignatureFamily {\n if (b.length < 12) return { family: \"unknown\" };\n // images (single-format signatures)\n if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) return { family: \"image\", formatId: \"jpeg\", container: \"jpeg\" };\n if (b[0] === 0x89 && ascii(b, 1, \"PNG\")) return { family: \"image\", formatId: \"png\", container: \"png\" };\n if (ascii(b, 0, \"GIF8\")) return { family: \"image\", formatId: \"gif\", container: \"gif\" };\n if (ascii(b, 0, \"RIFF\") && ascii(b, 8, \"WEBP\")) return { family: \"image\", formatId: \"webp\", container: \"webp\" };\n if (b[0] === 0x42 && b[1] === 0x4d) return { family: \"image\", formatId: \"bmp\", container: \"bmp\" };\n if ((b[0] === 0x49 && b[1] === 0x49 && b[2] === 0x2a) || (b[0] === 0x4d && b[1] === 0x4d && b[3] === 0x2a)) {\n return { family: \"image\", formatId: \"tiff\", container: \"tiff\" };\n }\n // SVG: XML text — scan a short prefix for '<svg' (bounded; case-insensitive).\n {\n const prefix = Buffer.from(b.slice(0, Math.min(b.length, 4096)))\n .toString(\"latin1\")\n .toLowerCase();\n if (prefix.includes(\"<svg\") && (prefix.trimStart().startsWith(\"<?xml\") || prefix.trimStart().startsWith(\"<svg\") || prefix.includes(\"<!doctype svg\"))) {\n return { family: \"svg\" };\n }\n }\n // containers\n if (b[0] === 0x1a && b[1] === 0x45 && b[2] === 0xdf && b[3] === 0xa3) return { family: \"ebml\" };\n if (ascii(b, 4, \"ftyp\")) {\n const brand = String.fromCharCode(b[8], b[9], b[10], b[11]).trim().toLowerCase();\n return { family: \"isobmff\", brand };\n }\n if (ascii(b, 0, \"OggS\")) return { family: \"ogg\" };\n if (ascii(b, 0, \"RIFF\") && ascii(b, 8, \"AVI \")) return { family: \"avi\" };\n if (ascii(b, 0, \"RIFF\") && ascii(b, 8, \"WAVE\")) return { family: \"wav\" };\n if (ascii(b, 0, \"ID3\")) return { family: \"mp3\" };\n if (b[0] === 0xff && (b[1] & 0xe0) === 0xe0) return { family: \"mp3\" };\n if (ascii(b, 0, \"fLaC\")) return { family: \"flac\" };\n return { family: \"unknown\" };\n}\n\n// ---- dependency seams (tests inject; production uses the real tools) ----\n\nexport interface InspectMediaDeps {\n /** Probe the local file; default shells to the hardened `ffprobe`. */\n probe?: (localPath: string, opts: { timeoutMs: number; signal?: AbortSignal }) => Promise<\n | { ok: true; json: unknown }\n | { ok: false; reason: \"timeout\" | \"truncated\" | \"failed\" | \"bad_json\" }\n >;\n /** Decode-prove an image; default uses sharp metadata. Returns frame count\n * (pages) on success, null on decode failure. */\n decodeImage?: (localPath: string) => Promise<{ frames: number } | null>;\n /** Injected ffprobe version (default: cached `ffprobe -version` first line). */\n ffprobeVersion?: string;\n}\n\nlet cachedFfprobeVersion: string | null | undefined;\n\nasync function getFfprobeVersion(): Promise<string | undefined> {\n if (cachedFfprobeVersion !== undefined) return cachedFfprobeVersion ?? undefined;\n try {\n const r = await runCmd(\"ffprobe\", [\"-version\"], { timeoutMs: 5_000 });\n const first = (r.stdout || r.stderr).split(/\\r?\\n/)[0]?.trim() ?? \"\";\n cachedFfprobeVersion = first ? first.slice(0, 120) : null;\n } catch {\n cachedFfprobeVersion = null;\n }\n return cachedFfprobeVersion ?? undefined;\n}\n\nasync function defaultProbe(\n localPath: string,\n opts: { timeoutMs: number; signal?: AbortSignal },\n): Promise<{ ok: true; json: unknown } | { ok: false; reason: \"timeout\" | \"truncated\" | \"failed\" | \"bad_json\" }> {\n let r;\n try {\n r = await runCmd(\n \"ffprobe\",\n [\n \"-v\",\n \"error\",\n \"-print_format\",\n \"json\",\n \"-show_format\",\n \"-show_streams\",\n \"-probesize\",\n String(PROBE_SIZE_BYTES),\n \"-analyzeduration\",\n String(ANALYZE_DURATION_US),\n localPath,\n ],\n { timeoutMs: opts.timeoutMs, signal: opts.signal },\n );\n } catch {\n return { ok: false, reason: \"failed\" }; // spawn failure (ffprobe missing) — fail closed\n }\n if (r.timedOut) return { ok: false, reason: \"timeout\" };\n if (r.truncated) return { ok: false, reason: \"truncated\" };\n if (r.code !== 0) return { ok: false, reason: \"failed\" };\n try {\n return { ok: true, json: JSON.parse(r.stdout || \"{}\") };\n } catch {\n return { ok: false, reason: \"bad_json\" };\n }\n}\n\nasync function defaultDecodeImage(localPath: string): Promise<{ frames: number } | null> {\n try {\n const sharp = (await import(\"sharp\")).default;\n const meta = await sharp(localPath, { limitInputPixels: 268402689 /* sharp default ~16k x 16k */ }).metadata();\n if (!meta.width || !meta.height) return null;\n return { frames: Math.max(1, meta.pages ?? 1) };\n } catch {\n return null;\n }\n}\n\n// ---- result builders ----\n\nfunction baseResult(\n partial: Omit<MediaInspectionResult, \"inspectorVersion\" | \"streams\" | \"codecs\"> &\n Partial<Pick<MediaInspectionResult, \"streams\" | \"codecs\">>,\n ffprobeVersion: string | undefined,\n): MediaInspectionResult {\n const result: MediaInspectionResult = {\n inspectorVersion: INSPECTOR_VERSION,\n ...(ffprobeVersion ? { ffprobeVersion } : {}),\n streams: partial.streams ?? { audio: 0, timedVideo: 0, attachedPictures: 0, imageFrames: 0, auxiliary: 0 },\n codecs: partial.codecs ?? { audio: [], video: [] },\n status: partial.status,\n container: partial.container,\n safetyPlan: partial.safetyPlan,\n reasonCode: partial.reasonCode,\n ...(partial.canonicalKind ? { canonicalKind: partial.canonicalKind } : {}),\n ...(partial.formatId ? { formatId: partial.formatId } : {}),\n };\n return MediaInspectionResultSchema.parse(result);\n}\n\nfunction indeterminate(\n container: string,\n reasonCode: string,\n ffprobeVersion: string | undefined,\n formatId?: MediaFormatId,\n): MediaInspectionResult {\n return baseResult(\n {\n status: \"indeterminate\",\n container,\n safetyPlan: \"strict-video-fallback\",\n reasonCode,\n ...(formatId ? { formatId } : {}),\n },\n ffprobeVersion,\n );\n}\n\n// ---- the inspector ----\n\nexport interface InspectMediaArgs {\n localPath: string;\n timeoutMs?: number;\n signal?: AbortSignal;\n deps?: InspectMediaDeps;\n}\n\nexport async function inspectMedia(args: InspectMediaArgs): Promise<MediaInspectionResult> {\n const timeoutMs = args.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;\n const probe = args.deps?.probe ?? defaultProbe;\n const decodeImage = args.deps?.decodeImage ?? defaultDecodeImage;\n const ffprobeVersion = args.deps?.ffprobeVersion ?? (await getFfprobeVersion());\n\n // 1) bounded signature read (header only)\n let head: Uint8Array;\n try {\n const fh = await open(args.localPath, \"r\");\n try {\n const buf = Buffer.alloc(SIGNATURE_READ_BYTES);\n const { bytesRead } = await fh.read(buf, 0, SIGNATURE_READ_BYTES, 0);\n head = buf.subarray(0, bytesRead);\n } finally {\n await fh.close();\n }\n } catch {\n return indeterminate(\"unknown\", \"read_failed\", ffprobeVersion);\n }\n\n const sig = detectSignatureFamily(head);\n\n // 2) family routing\n switch (sig.family) {\n case \"unknown\":\n return indeterminate(\"unknown\", \"unrecognized_signature\", ffprobeVersion);\n\n case \"svg\":\n // Scriptable XML \"image\" — structurally recognized, deliberately NOT a\n // processable format until a sanitize/rasterize design exists (launch\n // policy 2026-07-31). Never classified by decoder capability alone.\n return baseResult(\n {\n status: \"unsupported\",\n container: \"svg\",\n formatId: \"svg\",\n safetyPlan: \"strict-video-fallback\",\n reasonCode: \"unsupported_format:svg\",\n },\n ffprobeVersion,\n );\n\n case \"image\": {\n // Single-format image signature: decoder proof required — a signature is\n // a candidate, not a classification.\n const decoded = await decodeImage(args.localPath);\n if (!decoded) return baseResult(\n { status: \"malformed\", container: sig.container, formatId: sig.formatId, safetyPlan: \"strict-video-fallback\", reasonCode: \"image_decode_failed\" },\n ffprobeVersion,\n );\n const frames = decoded.frames;\n return baseResult(\n {\n status: \"definitive\",\n canonicalKind: \"image\",\n container: sig.container,\n formatId: sig.formatId,\n streams: { audio: 0, timedVideo: 0, attachedPictures: 0, imageFrames: frames, auxiliary: 0 },\n safetyPlan: frames > 1 ? \"animated-image-frames\" : \"still-image\",\n reasonCode: \"ok\",\n },\n ffprobeVersion,\n );\n }\n\n case \"isobmff\":\n if (ISOBMFF_IMAGE_BRANDS.has(sig.brand)) {\n // IMAGE-FAMILY ISO-BMFF resolves BEFORE the timed-video rule: a valid\n // AVIF probes as a one-frame AV1 `video` stream. Proof = image brand +\n // successful bounded decode + no ordinary timed A/V presentation.\n const probed = await probe(args.localPath, { timeoutMs, signal: args.signal });\n const parsed = probed.ok ? ProbeJsonSchema.safeParse(probed.json) : null;\n const streams = parsed?.success ? parsed.data.streams ?? [] : null;\n const hasAudio = streams ? streams.some((s) => s.codec_type === \"audio\") : true; // unknown ⇒ assume worst\n const formatId: MediaFormatId | undefined = sig.brand.startsWith(\"avi\") ? \"avif\" : \"heic\";\n const decoded = await decodeImage(args.localPath);\n if (!decoded || hasAudio) {\n if (formatId === \"heic\" && !decoded && !hasAudio) {\n // Recognized HEIC-family that the runtime cannot decode: explicit\n // unsupported-image (launch policy) — NEVER relabeled video.\n return baseResult(\n { status: \"unsupported\", container: \"heif\", formatId, safetyPlan: \"strict-video-fallback\", reasonCode: \"unsupported_format:heic\" },\n ffprobeVersion,\n );\n }\n // Undecodable/ambiguous image-brand file, or one carrying audio (a\n // real timed presentation wearing an image brand): fail toward the\n // stricter visual path.\n return indeterminate(\"heif\", decoded ? \"image_brand_with_audio\" : \"image_decode_failed\", ffprobeVersion, formatId);\n }\n const frames = decoded.frames;\n return baseResult(\n {\n status: \"definitive\",\n canonicalKind: \"image\",\n container: \"heif\",\n formatId,\n streams: { audio: 0, timedVideo: 0, attachedPictures: 0, imageFrames: frames, auxiliary: 0 },\n safetyPlan: frames > 1 ? \"animated-image-frames\" : \"still-image\",\n reasonCode: \"ok\",\n },\n ffprobeVersion,\n );\n }\n return classifyAvContainer(args, \"isobmff\", \"isobmff\", probe, timeoutMs, ffprobeVersion);\n\n case \"ebml\":\n return classifyAvContainer(args, \"webm\", \"webm\", probe, timeoutMs, ffprobeVersion);\n case \"ogg\":\n return classifyAvContainer(args, \"ogg\", \"ogg\", probe, timeoutMs, ffprobeVersion);\n case \"avi\":\n // Real container ffprobe parses; no registry entry (no origin enables it\n // today) — the stream truth still classifies it, policy rejects by format.\n return classifyAvContainer(args, \"avi\", undefined, probe, timeoutMs, ffprobeVersion);\n case \"wav\":\n return classifyAvContainer(args, \"wav\", \"wav\", probe, timeoutMs, ffprobeVersion);\n case \"mp3\":\n return classifyAvContainer(args, \"mp3\", \"mp3\", probe, timeoutMs, ffprobeVersion);\n case \"flac\":\n return classifyAvContainer(args, \"flac\", \"flac\", probe, timeoutMs, ffprobeVersion);\n }\n}\n\nasync function classifyAvContainer(\n args: InspectMediaArgs,\n container: string,\n formatId: MediaFormatId | undefined,\n probe: NonNullable<InspectMediaDeps[\"probe\"]>,\n timeoutMs: number,\n ffprobeVersion: string | undefined,\n): Promise<MediaInspectionResult> {\n const probed = await probe(args.localPath, { timeoutMs, signal: args.signal });\n if (!probed.ok) {\n return indeterminate(container, `probe_${probed.reason}`, ffprobeVersion, formatId);\n }\n const parsed = ProbeJsonSchema.safeParse(probed.json);\n if (!parsed.success) return indeterminate(container, \"probe_schema_invalid\", ffprobeVersion, formatId);\n\n const rawStreams = parsed.data.streams ?? [];\n if (rawStreams.length === 0) return indeterminate(container, \"no_streams\", ffprobeVersion, formatId);\n\n const counts: MediaInspectionStreams = { audio: 0, timedVideo: 0, attachedPictures: 0, imageFrames: 0, auxiliary: 0 };\n const audioCodecs = new Set<NormalizedCodecId>();\n const videoCodecs = new Set<NormalizedCodecId>();\n let sawUnknownType = false;\n\n for (const s of rawStreams) {\n const type = s.codec_type ?? \"\";\n if (type === \"audio\") {\n counts.audio++;\n audioCodecs.add(normalizeCodec(s.codec_name));\n } else if (type === \"video\") {\n // An attached picture (cover art) is NOT a timed presentation — but the\n // disposition flag is untrusted container data, so it is COUNTED and the\n // safety plan forces every such picture through the image hash path\n // before the audio guard may return clean.\n if (s.disposition?.attached_pic === 1) {\n counts.attachedPictures++;\n } else {\n counts.timedVideo++;\n videoCodecs.add(normalizeCodec(s.codec_name));\n }\n } else if (type === \"subtitle\" || type === \"data\" || type === \"attachment\") {\n counts.auxiliary++;\n } else {\n counts.auxiliary++;\n sawUnknownType = true;\n }\n }\n\n const codecs = { audio: [...audioCodecs], video: [...videoCodecs] };\n\n // classification table (fail-closed)\n if (counts.timedVideo > 0) {\n return baseResult(\n {\n status: \"definitive\",\n canonicalKind: \"video\",\n container,\n ...(formatId ? { formatId } : {}),\n streams: counts,\n codecs,\n safetyPlan: \"video-frames\",\n reasonCode: \"ok\",\n },\n ffprobeVersion,\n );\n }\n if (counts.audio > 0 && !sawUnknownType) {\n return baseResult(\n {\n status: \"definitive\",\n canonicalKind: \"audio\",\n container,\n ...(formatId ? { formatId } : {}),\n streams: counts,\n codecs,\n safetyPlan: counts.attachedPictures > 0 ? \"audio-plus-artwork\" : \"audio-only\",\n reasonCode: \"ok\",\n },\n ffprobeVersion,\n );\n }\n // auxiliary-only, unknown stream types alongside audio, or nothing usable:\n // never audio — the strict visual fallback owns it.\n return baseResult(\n {\n status: \"indeterminate\",\n container,\n ...(formatId ? { formatId } : {}),\n streams: counts,\n codecs,\n safetyPlan: \"strict-video-fallback\",\n reasonCode: sawUnknownType ? \"unknown_stream_type\" : \"auxiliary_only\",\n },\n ffprobeVersion,\n );\n}\n"]}
1
+ {"version":3,"file":"inspect-media.js","sourceRoot":"","sources":["../../src/inspection/inspect-media.ts"],"names":[],"mappings":"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","sourcesContent":["// =============================================================================\n// Canonical media content inspection\n// =============================================================================\n//\n// The ONE server-side classification authority. Consumes a LOCAL file (the\n// pipeline's generation-pinned temp download), establishes what the bytes\n// actually contain, and returns the bounded `MediaInspectionResult` contract\n// from @ttt-productions/media-schemas. Everything downstream — origin/kind\n// validation, processing-spec selection, the persisted asset kind, and the\n// safety (PhotoDNA) routing — derives from this one result. Client MIME,\n// filename, extension, container brand, and payload-byte heuristics are never\n// consulted.\n//\n// Design:\n// - family selection by a bounded 64 KiB SIGNATURE read (header only —\n// semantic elements are NEVER searched for in arbitrary payload: payload\n// bytes can mimic container elements, so a flat scan misclassifies WebM\n// audio as video);\n// - shared A/V containers (EBML/ISO-BMFF/Ogg/AVI) classified by a hardened\n// `ffprobe` stream table (libavformat's structural parsers), with timeout,\n// kill-on-abort, output cap, and strict schema validation;\n// - image-family proof by DECODER (sharp) — a signature or `ftyp` brand is a\n// candidate, not proof, and image-family ISO-BMFF (AVIF/HEIC) is resolved\n// BEFORE the ordinary \"any timed video stream wins\" rule (a valid AVIF\n// probes as a one-frame AV1 `video` stream);\n// - FAIL-CLOSED: anything unproven is `indeterminate` with the\n// `strict-video-fallback` safety plan — never audio, never publishable.\n\nimport { open } from \"node:fs/promises\";\nimport { z } from \"zod\";\nimport {\n MediaInspectionResultSchema,\n type MediaInspectionResult,\n type MediaInspectionStreams,\n type NormalizedCodecId,\n type MediaFormatId,\n} from \"@ttt-productions/media-schemas\";\nimport { DEFAULT_PROBE_TIMEOUT_MS, runCmd } from \"../video/ffmpeg.js\";\nimport { measureDurationSec, type MeasureDurationFn } from \"../duration/media-duration.js\";\n\n/** Bump on any classification-rule change (rides every decision event). */\nexport const INSPECTOR_VERSION = \"mpc-inspect-2\";\n\nconst SIGNATURE_READ_BYTES = 64 * 1024;\nconst PROBE_SIZE_BYTES = 4 * 1024 * 1024; // -probesize: metadata, not playback\nconst ANALYZE_DURATION_US = 10 * 1000 * 1000;\n\n// ---- ffprobe JSON boundary (strict; anything else is malformed) ----\n\nconst ProbeStreamSchema = z\n .object({\n codec_type: z.string().max(32).optional(),\n codec_name: z.string().max(64).optional(),\n disposition: z\n .object({ attached_pic: z.number().int().min(0).max(1).optional() })\n .loose()\n .optional(),\n })\n .loose();\n\nconst ProbeJsonSchema = z\n .object({\n format: z.object({ format_name: z.string().max(200).optional() }).loose().optional(),\n streams: z.array(ProbeStreamSchema).max(256).optional(),\n })\n .loose();\n\n// ---- codec normalization (bounded — raw strings never escape) ----\n\nconst CODEC_MAP: Record<string, NormalizedCodecId> = {\n h264: \"h264\",\n hevc: \"hevc\",\n vp8: \"vp8\",\n vp9: \"vp9\",\n av1: \"av1\",\n theora: \"theora\",\n mpeg4: \"mpeg4\",\n mjpeg: \"mjpeg\",\n aac: \"aac\",\n alac: \"alac\",\n opus: \"opus\",\n vorbis: \"vorbis\",\n mp3: \"mp3\",\n flac: \"flac\",\n};\n\nfunction normalizeCodec(name: string | undefined): NormalizedCodecId {\n if (!name) return \"other\";\n const n = name.toLowerCase();\n if (CODEC_MAP[n]) return CODEC_MAP[n];\n if (n.startsWith(\"pcm_\")) return \"pcm\";\n if (n.startsWith(\"amr\")) return \"amr\";\n return \"other\";\n}\n\n// ---- signature families ----\n\ntype SignatureFamily =\n | { family: \"image\"; formatId: MediaFormatId; container: string }\n | { family: \"svg\" }\n | { family: \"ebml\" }\n | { family: \"isobmff\"; brand: string }\n | { family: \"ogg\" }\n | { family: \"avi\" }\n | { family: \"wav\" }\n | { family: \"mp3\" }\n | { family: \"flac\" }\n | { family: \"unknown\" };\n\n/** ISO-BMFF brands that declare an IMAGE-item file (HEIF family). Resolved via\n * decoder proof BEFORE the timed-video rule — see the AVIF note above. */\nconst ISOBMFF_IMAGE_BRANDS = new Set([\"avif\", \"avis\", \"heic\", \"heix\", \"heim\", \"heis\", \"hevc\", \"hevx\", \"mif1\", \"msf1\"]);\n\nfunction ascii(b: Uint8Array, off: number, s: string): boolean {\n if (off + s.length > b.length) return false;\n for (let i = 0; i < s.length; i++) if (b[off + i] !== s.charCodeAt(i)) return false;\n return true;\n}\n\nexport function detectSignatureFamily(b: Uint8Array): SignatureFamily {\n if (b.length < 12) return { family: \"unknown\" };\n // images (single-format signatures)\n if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff) return { family: \"image\", formatId: \"jpeg\", container: \"jpeg\" };\n if (b[0] === 0x89 && ascii(b, 1, \"PNG\")) return { family: \"image\", formatId: \"png\", container: \"png\" };\n if (ascii(b, 0, \"GIF8\")) return { family: \"image\", formatId: \"gif\", container: \"gif\" };\n if (ascii(b, 0, \"RIFF\") && ascii(b, 8, \"WEBP\")) return { family: \"image\", formatId: \"webp\", container: \"webp\" };\n if (b[0] === 0x42 && b[1] === 0x4d) return { family: \"image\", formatId: \"bmp\", container: \"bmp\" };\n if ((b[0] === 0x49 && b[1] === 0x49 && b[2] === 0x2a) || (b[0] === 0x4d && b[1] === 0x4d && b[3] === 0x2a)) {\n return { family: \"image\", formatId: \"tiff\", container: \"tiff\" };\n }\n // SVG: XML text — scan a short prefix for '<svg' (bounded; case-insensitive).\n {\n const prefix = Buffer.from(b.slice(0, Math.min(b.length, 4096)))\n .toString(\"latin1\")\n .toLowerCase();\n if (prefix.includes(\"<svg\") && (prefix.trimStart().startsWith(\"<?xml\") || prefix.trimStart().startsWith(\"<svg\") || prefix.includes(\"<!doctype svg\"))) {\n return { family: \"svg\" };\n }\n }\n // containers\n if (b[0] === 0x1a && b[1] === 0x45 && b[2] === 0xdf && b[3] === 0xa3) return { family: \"ebml\" };\n if (ascii(b, 4, \"ftyp\")) {\n const brand = String.fromCharCode(b[8], b[9], b[10], b[11]).trim().toLowerCase();\n return { family: \"isobmff\", brand };\n }\n if (ascii(b, 0, \"OggS\")) return { family: \"ogg\" };\n if (ascii(b, 0, \"RIFF\") && ascii(b, 8, \"AVI \")) return { family: \"avi\" };\n if (ascii(b, 0, \"RIFF\") && ascii(b, 8, \"WAVE\")) return { family: \"wav\" };\n if (ascii(b, 0, \"ID3\")) return { family: \"mp3\" };\n if (b[0] === 0xff && (b[1] & 0xe0) === 0xe0) return { family: \"mp3\" };\n if (ascii(b, 0, \"fLaC\")) return { family: \"flac\" };\n return { family: \"unknown\" };\n}\n\n// ---- dependency seams (tests inject; production uses the real tools) ----\n\nexport interface InspectMediaDeps {\n /** Probe the local file; default shells to the hardened `ffprobe`. */\n probe?: (localPath: string, opts: { timeoutMs: number; signal?: AbortSignal }) => Promise<\n | { ok: true; json: unknown }\n | { ok: false; reason: \"timeout\" | \"truncated\" | \"failed\" | \"bad_json\" }\n >;\n /** Decode-prove an image; default uses sharp metadata. Returns frame count\n * (pages) on success, null on decode failure. */\n decodeImage?: (localPath: string) => Promise<{ frames: number } | null>;\n /** Measure the file's length; default reads its packet timestamps\n * (`measureDurationSec`). */\n measureDuration?: MeasureDurationFn;\n /** Injected ffprobe version (default: cached `ffprobe -version` first line). */\n ffprobeVersion?: string;\n}\n\nlet cachedFfprobeVersion: string | null | undefined;\n\nasync function getFfprobeVersion(): Promise<string | undefined> {\n if (cachedFfprobeVersion !== undefined) return cachedFfprobeVersion ?? undefined;\n try {\n const r = await runCmd(\"ffprobe\", [\"-version\"], { timeoutMs: 5_000 });\n const first = (r.stdout || r.stderr).split(/\\r?\\n/)[0]?.trim() ?? \"\";\n cachedFfprobeVersion = first ? first.slice(0, 120) : null;\n } catch {\n cachedFfprobeVersion = null;\n }\n return cachedFfprobeVersion ?? undefined;\n}\n\nasync function defaultProbe(\n localPath: string,\n opts: { timeoutMs: number; signal?: AbortSignal },\n): Promise<{ ok: true; json: unknown } | { ok: false; reason: \"timeout\" | \"truncated\" | \"failed\" | \"bad_json\" }> {\n let r;\n try {\n r = await runCmd(\n \"ffprobe\",\n [\n \"-v\",\n \"error\",\n \"-print_format\",\n \"json\",\n \"-show_format\",\n \"-show_streams\",\n \"-probesize\",\n String(PROBE_SIZE_BYTES),\n \"-analyzeduration\",\n String(ANALYZE_DURATION_US),\n localPath,\n ],\n { timeoutMs: opts.timeoutMs, signal: opts.signal },\n );\n } catch {\n return { ok: false, reason: \"failed\" }; // spawn failure (ffprobe missing) — fail closed\n }\n if (r.timedOut) return { ok: false, reason: \"timeout\" };\n if (r.truncated) return { ok: false, reason: \"truncated\" };\n if (r.code !== 0) return { ok: false, reason: \"failed\" };\n try {\n return { ok: true, json: JSON.parse(r.stdout || \"{}\") };\n } catch {\n return { ok: false, reason: \"bad_json\" };\n }\n}\n\nasync function defaultDecodeImage(localPath: string): Promise<{ frames: number } | null> {\n try {\n const sharp = (await import(\"sharp\")).default;\n const meta = await sharp(localPath, { limitInputPixels: 268402689 /* sharp default ~16k x 16k */ }).metadata();\n if (!meta.width || !meta.height) return null;\n return { frames: Math.max(1, meta.pages ?? 1) };\n } catch {\n return null;\n }\n}\n\n// ---- result builders ----\n\nfunction baseResult(\n partial: Omit<MediaInspectionResult, \"inspectorVersion\" | \"streams\" | \"codecs\"> &\n Partial<Pick<MediaInspectionResult, \"streams\" | \"codecs\">>,\n ffprobeVersion: string | undefined,\n): MediaInspectionResult {\n const result: MediaInspectionResult = {\n inspectorVersion: INSPECTOR_VERSION,\n ...(ffprobeVersion ? { ffprobeVersion } : {}),\n streams: partial.streams ?? { audio: 0, timedVideo: 0, attachedPictures: 0, imageFrames: 0, auxiliary: 0 },\n codecs: partial.codecs ?? { audio: [], video: [] },\n status: partial.status,\n container: partial.container,\n safetyPlan: partial.safetyPlan,\n reasonCode: partial.reasonCode,\n ...(partial.canonicalKind ? { canonicalKind: partial.canonicalKind } : {}),\n ...(partial.formatId ? { formatId: partial.formatId } : {}),\n ...(partial.durationSec !== undefined ? { durationSec: partial.durationSec } : {}),\n };\n return MediaInspectionResultSchema.parse(result);\n}\n\nfunction indeterminate(\n container: string,\n reasonCode: string,\n ffprobeVersion: string | undefined,\n formatId?: MediaFormatId,\n): MediaInspectionResult {\n return baseResult(\n {\n status: \"indeterminate\",\n container,\n safetyPlan: \"strict-video-fallback\",\n reasonCode,\n ...(formatId ? { formatId } : {}),\n },\n ffprobeVersion,\n );\n}\n\n// ---- the inspector ----\n\nexport interface InspectMediaArgs {\n localPath: string;\n timeoutMs?: number;\n signal?: AbortSignal;\n deps?: InspectMediaDeps;\n}\n\nexport async function inspectMedia(args: InspectMediaArgs): Promise<MediaInspectionResult> {\n const timeoutMs = args.timeoutMs ?? DEFAULT_PROBE_TIMEOUT_MS;\n const probe = args.deps?.probe ?? defaultProbe;\n const decodeImage = args.deps?.decodeImage ?? defaultDecodeImage;\n const ffprobeVersion = args.deps?.ffprobeVersion ?? (await getFfprobeVersion());\n\n // 1) bounded signature read (header only)\n let head: Uint8Array;\n try {\n const fh = await open(args.localPath, \"r\");\n try {\n const buf = Buffer.alloc(SIGNATURE_READ_BYTES);\n const { bytesRead } = await fh.read(buf, 0, SIGNATURE_READ_BYTES, 0);\n head = buf.subarray(0, bytesRead);\n } finally {\n await fh.close();\n }\n } catch {\n return indeterminate(\"unknown\", \"read_failed\", ffprobeVersion);\n }\n\n const sig = detectSignatureFamily(head);\n\n // 2) family routing\n switch (sig.family) {\n case \"unknown\":\n return indeterminate(\"unknown\", \"unrecognized_signature\", ffprobeVersion);\n\n case \"svg\":\n // Scriptable XML \"image\" — structurally recognized, deliberately NOT a\n // processable format: it can carry script and no sanitize/rasterize step\n // exists. Never classified by decoder capability alone.\n return baseResult(\n {\n status: \"unsupported\",\n container: \"svg\",\n formatId: \"svg\",\n safetyPlan: \"strict-video-fallback\",\n reasonCode: \"unsupported_format:svg\",\n },\n ffprobeVersion,\n );\n\n case \"image\": {\n // Single-format image signature: decoder proof required — a signature is\n // a candidate, not a classification.\n const decoded = await decodeImage(args.localPath);\n if (!decoded) return baseResult(\n { status: \"malformed\", container: sig.container, formatId: sig.formatId, safetyPlan: \"strict-video-fallback\", reasonCode: \"image_decode_failed\" },\n ffprobeVersion,\n );\n const frames = decoded.frames;\n return baseResult(\n {\n status: \"definitive\",\n canonicalKind: \"image\",\n container: sig.container,\n formatId: sig.formatId,\n streams: { audio: 0, timedVideo: 0, attachedPictures: 0, imageFrames: frames, auxiliary: 0 },\n safetyPlan: frames > 1 ? \"animated-image-frames\" : \"still-image\",\n reasonCode: \"ok\",\n },\n ffprobeVersion,\n );\n }\n\n case \"isobmff\":\n if (ISOBMFF_IMAGE_BRANDS.has(sig.brand)) {\n // IMAGE-FAMILY ISO-BMFF resolves BEFORE the timed-video rule: a valid\n // AVIF probes as a one-frame AV1 `video` stream. Proof = image brand +\n // successful bounded decode + no ordinary timed A/V presentation.\n const probed = await probe(args.localPath, { timeoutMs, signal: args.signal });\n const parsed = probed.ok ? ProbeJsonSchema.safeParse(probed.json) : null;\n const streams = parsed?.success ? parsed.data.streams ?? [] : null;\n const hasAudio = streams ? streams.some((s) => s.codec_type === \"audio\") : true; // unknown ⇒ assume worst\n const formatId: MediaFormatId | undefined = sig.brand.startsWith(\"avi\") ? \"avif\" : \"heic\";\n const decoded = await decodeImage(args.localPath);\n if (!decoded || hasAudio) {\n if (formatId === \"heic\" && !decoded && !hasAudio) {\n // Recognized HEIC-family that the runtime cannot decode: explicit\n // unsupported-image (launch policy) — NEVER relabeled video.\n return baseResult(\n { status: \"unsupported\", container: \"heif\", formatId, safetyPlan: \"strict-video-fallback\", reasonCode: \"unsupported_format:heic\" },\n ffprobeVersion,\n );\n }\n // Undecodable/ambiguous image-brand file, or one carrying audio (a\n // real timed presentation wearing an image brand): fail toward the\n // stricter visual path.\n return indeterminate(\"heif\", decoded ? \"image_brand_with_audio\" : \"image_decode_failed\", ffprobeVersion, formatId);\n }\n const frames = decoded.frames;\n return baseResult(\n {\n status: \"definitive\",\n canonicalKind: \"image\",\n container: \"heif\",\n formatId,\n streams: { audio: 0, timedVideo: 0, attachedPictures: 0, imageFrames: frames, auxiliary: 0 },\n safetyPlan: frames > 1 ? \"animated-image-frames\" : \"still-image\",\n reasonCode: \"ok\",\n },\n ffprobeVersion,\n );\n }\n return classifyAvContainer(args, \"isobmff\", \"isobmff\", probe, timeoutMs, ffprobeVersion);\n\n case \"ebml\":\n return classifyAvContainer(args, \"webm\", \"webm\", probe, timeoutMs, ffprobeVersion);\n case \"ogg\":\n return classifyAvContainer(args, \"ogg\", \"ogg\", probe, timeoutMs, ffprobeVersion);\n case \"avi\":\n // Real container ffprobe parses; no registry entry (no origin enables it\n // today) — the stream truth still classifies it, policy rejects by format.\n return classifyAvContainer(args, \"avi\", undefined, probe, timeoutMs, ffprobeVersion);\n case \"wav\":\n return classifyAvContainer(args, \"wav\", \"wav\", probe, timeoutMs, ffprobeVersion);\n case \"mp3\":\n return classifyAvContainer(args, \"mp3\", \"mp3\", probe, timeoutMs, ffprobeVersion);\n case \"flac\":\n return classifyAvContainer(args, \"flac\", \"flac\", probe, timeoutMs, ffprobeVersion);\n }\n}\n\nasync function classifyAvContainer(\n args: InspectMediaArgs,\n container: string,\n formatId: MediaFormatId | undefined,\n probe: NonNullable<InspectMediaDeps[\"probe\"]>,\n timeoutMs: number,\n ffprobeVersion: string | undefined,\n): Promise<MediaInspectionResult> {\n const probed = await probe(args.localPath, { timeoutMs, signal: args.signal });\n if (!probed.ok) {\n return indeterminate(container, `probe_${probed.reason}`, ffprobeVersion, formatId);\n }\n const parsed = ProbeJsonSchema.safeParse(probed.json);\n if (!parsed.success) return indeterminate(container, \"probe_schema_invalid\", ffprobeVersion, formatId);\n\n const rawStreams = parsed.data.streams ?? [];\n if (rawStreams.length === 0) return indeterminate(container, \"no_streams\", ffprobeVersion, formatId);\n\n const counts: MediaInspectionStreams = { audio: 0, timedVideo: 0, attachedPictures: 0, imageFrames: 0, auxiliary: 0 };\n const audioCodecs = new Set<NormalizedCodecId>();\n const videoCodecs = new Set<NormalizedCodecId>();\n let sawUnknownType = false;\n\n for (const s of rawStreams) {\n const type = s.codec_type ?? \"\";\n if (type === \"audio\") {\n counts.audio++;\n audioCodecs.add(normalizeCodec(s.codec_name));\n } else if (type === \"video\") {\n // An attached picture (cover art) is NOT a timed presentation — but the\n // disposition flag is untrusted container data, so it is COUNTED and the\n // safety plan forces every such picture through the image hash path\n // before the audio guard may return clean.\n if (s.disposition?.attached_pic === 1) {\n counts.attachedPictures++;\n } else {\n counts.timedVideo++;\n videoCodecs.add(normalizeCodec(s.codec_name));\n }\n } else if (type === \"subtitle\" || type === \"data\" || type === \"attachment\") {\n counts.auxiliary++;\n } else {\n counts.auxiliary++;\n sawUnknownType = true;\n }\n }\n\n const codecs = { audio: [...audioCodecs], video: [...videoCodecs] };\n const measure = args.deps?.measureDuration ?? measureDurationSec;\n const durationSec = await measure(args.localPath, { signal: args.signal });\n const duration = durationSec !== undefined ? { durationSec } : {};\n\n // classification table (fail-closed)\n if (counts.timedVideo > 0) {\n return baseResult(\n {\n status: \"definitive\",\n canonicalKind: \"video\",\n container,\n ...(formatId ? { formatId } : {}),\n streams: counts,\n codecs,\n ...duration,\n safetyPlan: \"video-frames\",\n reasonCode: \"ok\",\n },\n ffprobeVersion,\n );\n }\n if (counts.audio > 0 && !sawUnknownType) {\n return baseResult(\n {\n status: \"definitive\",\n canonicalKind: \"audio\",\n container,\n ...(formatId ? { formatId } : {}),\n streams: counts,\n codecs,\n ...duration,\n safetyPlan: counts.attachedPictures > 0 ? \"audio-plus-artwork\" : \"audio-only\",\n reasonCode: \"ok\",\n },\n ffprobeVersion,\n );\n }\n // auxiliary-only, unknown stream types alongside audio, or nothing usable:\n // never audio — the strict visual fallback owns it.\n return baseResult(\n {\n status: \"indeterminate\",\n container,\n ...(formatId ? { formatId } : {}),\n streams: counts,\n codecs,\n ...duration,\n safetyPlan: \"strict-video-fallback\",\n reasonCode: sawUnknownType ? \"unknown_stream_type\" : \"auxiliary_only\",\n },\n ffprobeVersion,\n );\n}\n"]}
@@ -1,4 +1,6 @@
1
1
  export interface RunCmdResult {
2
+ /** The exit code; `-1` when the process was ended by a signal, so a child
3
+ * killed partway (a crash, an outside kill) never reads as a clean exit. */
2
4
  code: number;
3
5
  stdout: string;
4
6
  stderr: string;
@@ -24,15 +26,21 @@ export interface RunFfmpegOptions {
24
26
  /** Which phase to report in callbacks. */
25
27
  phase?: FfmpegProgressPhase;
26
28
  }
29
+ /** Wall-clock ceiling for an ffprobe call that opens a file and reads its
30
+ * stream table (metadata only, never every packet). */
31
+ export declare const DEFAULT_PROBE_TIMEOUT_MS = 20000;
27
32
  export declare function runCmd(cmd: string, args: string[], opts?: {
28
33
  cwd?: string;
29
34
  /** Hard wall-clock ceiling; on expiry the child is SIGKILLed and the
30
35
  * result resolves with `timedOut: true`. A malformed/adversarial input
31
- * must never be able to wedge a warm instance (canonical-upload-content-
32
- * classification hardening, 2026-07-31). */
36
+ * must never be able to wedge a warm instance. */
33
37
  timeoutMs?: number;
34
38
  /** Abort kills the child immediately (same SIGKILL path as the timeout). */
35
39
  signal?: AbortSignal;
40
+ /** Receives stdout one line at a time instead of capturing it, so a
41
+ * listing of any length is read in bounded memory; `stdout` is then
42
+ * empty. A single line over the capture cap sets `truncated`. */
43
+ onStdoutLine?: (line: string) => void;
36
44
  }): Promise<RunCmdResult>;
37
45
  /**
38
46
  * Runs ffmpeg with optional AbortSignal + progress reporting.
@@ -1 +1 @@
1
- {"version":3,"file":"ffmpeg.d.ts","sourceRoot":"","sources":["../../src/video/ffmpeg.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf;;uDAEmD;IACnD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;2EACuE;IACvE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,QAAQ,CAAC;AAEzD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE;QAAE,KAAK,EAAE,mBAAmB,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC/F,0CAA0C;IAC1C,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC7B;AAOD,wBAAgB,MAAM,CACpB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,CAAC,EAAE;IACL,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;iDAG6C;IAC7C,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB,GACA,OAAO,CAAC,YAAY,CAAC,CAoDvB;AA4CD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAuFxF;AAED,wBAAsB,qBAAqB,IAAI,OAAO,CACpD;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAC/D,CAcA"}
1
+ {"version":3,"file":"ffmpeg.d.ts","sourceRoot":"","sources":["../../src/video/ffmpeg.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,YAAY;IAC3B;iFAC6E;IAC7E,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf;;uDAEmD;IACnD,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;2EACuE;IACvE,SAAS,CAAC,EAAE,OAAO,CAAC;CACrB;AAED,MAAM,MAAM,mBAAmB,GAAG,WAAW,GAAG,QAAQ,CAAC;AAEzD,MAAM,WAAW,gBAAgB;IAC/B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,CAAC,CAAC,EAAE;QAAE,KAAK,EAAE,mBAAmB,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC/F,0CAA0C;IAC1C,KAAK,CAAC,EAAE,mBAAmB,CAAC;CAC7B;AAID;wDACwD;AACxD,eAAO,MAAM,wBAAwB,QAAS,CAAC;AAK/C,wBAAgB,MAAM,CACpB,GAAG,EAAE,MAAM,EACX,IAAI,EAAE,MAAM,EAAE,EACd,IAAI,CAAC,EAAE;IACL,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;uDAEmD;IACnD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4EAA4E;IAC5E,MAAM,CAAC,EAAE,WAAW,CAAC;IACrB;;sEAEkE;IAClE,YAAY,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;CACvC,GACA,OAAO,CAAC,YAAY,CAAC,CAwEvB;AA4CD;;;GAGG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAuFxF;AAED,wBAAsB,qBAAqB,IAAI,OAAO,CACpD;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,OAAO,CAAA;CAAE,CAC/D,CAcA"}
@@ -1,5 +1,8 @@
1
1
  import { spawn } from "node:child_process";
2
2
  const MAX_CAPTURE_BYTES = 1024 * 1024; // 1MB per stream
3
+ /** Wall-clock ceiling for an ffprobe call that opens a file and reads its
4
+ * stream table (metadata only, never every packet). */
5
+ export const DEFAULT_PROBE_TIMEOUT_MS = 20_000;
3
6
  let ffmpegChecked = null;
4
7
  let ffmpegVersion = null;
5
8
  export function runCmd(cmd, args, opts) {
@@ -33,7 +36,29 @@ export function runCmd(cmd, args, opts) {
33
36
  else
34
37
  opts.signal.addEventListener("abort", onAbort, { once: true });
35
38
  }
39
+ const onLine = opts?.onStdoutLine;
40
+ let lineBuf = "";
41
+ // Decodes across chunk boundaries, so a multi-byte character split
42
+ // between two chunks reaches the line intact.
43
+ if (onLine)
44
+ p.stdout.setEncoding("utf8");
36
45
  p.stdout.on("data", (d) => {
46
+ if (onLine) {
47
+ if (truncated)
48
+ return;
49
+ const lines = (lineBuf + String(d)).split(/\r?\n/);
50
+ lineBuf = lines.pop() ?? "";
51
+ for (const line of [...lines, lineBuf]) {
52
+ if (line.length > MAX_CAPTURE_BYTES) {
53
+ truncated = true;
54
+ lineBuf = "";
55
+ return;
56
+ }
57
+ }
58
+ for (const line of lines)
59
+ onLine(line);
60
+ return;
61
+ }
37
62
  if (stdout.length < MAX_CAPTURE_BYTES)
38
63
  stdout += String(d);
39
64
  else
@@ -57,7 +82,9 @@ export function runCmd(cmd, args, opts) {
57
82
  clearTimeout(timer);
58
83
  if (opts?.signal)
59
84
  opts.signal.removeEventListener("abort", onAbort);
60
- resolve({ code: code ?? 0, stdout, stderr, ...(timedOut ? { timedOut } : {}), ...(truncated ? { truncated } : {}) });
85
+ if (onLine && !truncated && lineBuf)
86
+ onLine(lineBuf);
87
+ resolve({ code: code ?? -1, stdout, stderr, ...(timedOut ? { timedOut } : {}), ...(truncated ? { truncated } : {}) });
61
88
  });
62
89
  });
63
90
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ffmpeg.js","sourceRoot":"","sources":["../../src/video/ffmpeg.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"node:child_process\";\n\nexport interface RunCmdResult {\n code: number;\n stdout: string;\n stderr: string;\n /** True when the process was killed by the wall-clock timeout (see runCmd\n * opts.timeoutMs). Callers must treat a timed-out result as INVALID —\n * never parse its partial output as an answer. */\n timedOut?: boolean;\n /** True when a captured stream hit the 1 MiB cap — the output is truncated\n * and must not be parsed as complete (e.g. incomplete probe JSON). */\n truncated?: boolean;\n}\n\nexport type FfmpegProgressPhase = \"transcode\" | \"poster\";\n\nexport interface RunFfmpegOptions {\n cwd?: string;\n signal?: AbortSignal;\n /** Optional total duration in milliseconds (used to compute percent). */\n durationMs?: number;\n onProgress?: (p: { phase: FfmpegProgressPhase; percent?: number; outTimeMs?: number }) => void;\n /** Which phase to report in callbacks. */\n phase?: FfmpegProgressPhase;\n}\n\nconst MAX_CAPTURE_BYTES = 1024 * 1024; // 1MB per stream\n\nlet ffmpegChecked: boolean | null = null;\nlet ffmpegVersion: string | null = null;\n\nexport function runCmd(\n cmd: string,\n args: string[],\n opts?: {\n cwd?: string;\n /** Hard wall-clock ceiling; on expiry the child is SIGKILLed and the\n * result resolves with `timedOut: true`. A malformed/adversarial input\n * must never be able to wedge a warm instance (canonical-upload-content-\n * classification hardening, 2026-07-31). */\n timeoutMs?: number;\n /** Abort kills the child immediately (same SIGKILL path as the timeout). */\n signal?: AbortSignal;\n }\n): Promise<RunCmdResult> {\n return new Promise((resolve, reject) => {\n const p = spawn(cmd, args, { cwd: opts?.cwd, stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\n let stdout = \"\";\n let stderr = \"\";\n let timedOut = false;\n let truncated = false;\n\n const kill = () => {\n try {\n p.kill(\"SIGKILL\");\n } catch {\n /* already dead */\n }\n };\n const timer =\n opts?.timeoutMs && opts.timeoutMs > 0\n ? setTimeout(() => {\n timedOut = true;\n kill();\n }, opts.timeoutMs)\n : null;\n const onAbort = () => {\n timedOut = true; // treated identically: the result is invalid\n kill();\n };\n if (opts?.signal) {\n if (opts.signal.aborted) onAbort();\n else opts.signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n\n p.stdout.on(\"data\", (d) => {\n if (stdout.length < MAX_CAPTURE_BYTES) stdout += String(d);\n else truncated = true;\n });\n p.stderr.on(\"data\", (d) => {\n if (stderr.length < MAX_CAPTURE_BYTES) stderr += String(d);\n else truncated = true;\n });\n\n p.on(\"error\", (e) => {\n if (timer) clearTimeout(timer);\n if (opts?.signal) opts.signal.removeEventListener(\"abort\", onAbort);\n reject(e);\n });\n p.on(\"close\", (code) => {\n if (timer) clearTimeout(timer);\n if (opts?.signal) opts.signal.removeEventListener(\"abort\", onAbort);\n resolve({ code: code ?? 0, stdout, stderr, ...(timedOut ? { timedOut } : {}), ...(truncated ? { truncated } : {}) });\n });\n });\n}\n\nfunction abortError(): DOMException {\n return new DOMException(\"Aborted\", \"AbortError\");\n}\n\n// NOTE: ffmpeg prints \"out_time_ms\" but it is microseconds in known builds.\n// We harden parsing and sanity-check values.\nfunction parseOutTimeMicroseconds(value: string): number | undefined {\n const n = Number(value);\n if (!Number.isFinite(n) || n < 0) return undefined;\n\n // Discard absurd values (100 years in microseconds).\n const MAX_REASONABLE_US = 100 * 365 * 24 * 60 * 60 * 1_000_000;\n if (n > MAX_REASONABLE_US) return undefined;\n\n return n;\n}\n\nfunction parseOutTimeHmsToMs(value: string): number | undefined {\n // \"HH:MM:SS.micro\" (micro may be omitted)\n const m = String(value).trim().match(/^(\\d+):([0-5]\\d):([0-5]\\d)(?:\\.(\\d{1,6}))?$/);\n if (!m) return undefined;\n\n const hh = Number(m[1]);\n const mm = Number(m[2]);\n const ss = Number(m[3]);\n if (!Number.isFinite(hh) || !Number.isFinite(mm) || !Number.isFinite(ss)) return undefined;\n\n const frac = m[4] ?? \"\";\n const us = frac ? Number((frac + \"000000\").slice(0, 6)) : 0;\n if (!Number.isFinite(us) || us < 0) return undefined;\n\n const totalMs = (hh * 3600 + mm * 60 + ss) * 1000 + Math.floor(us / 1000);\n return totalMs >= 0 ? totalMs : undefined;\n}\n\nfunction clamp01(n: number) {\n if (!Number.isFinite(n)) return undefined;\n if (n < 0) return 0;\n if (n > 1) return 1;\n return n;\n}\n\n/**\n * Runs ffmpeg with optional AbortSignal + progress reporting.\n * If onProgress is provided, ffmpeg is invoked with \"-progress pipe:1\".\n */\nexport function runFfmpeg(args: string[], opts?: RunFfmpegOptions): Promise<RunCmdResult> {\n const phase: FfmpegProgressPhase = opts?.phase ?? \"transcode\";\n const withProgress = !!opts?.onProgress;\n const ffArgs = withProgress ? [\"-progress\", \"pipe:1\", \"-nostats\", ...args] : args;\n\n return new Promise((resolve, reject) => {\n if (opts?.signal?.aborted) return reject(abortError());\n\n const p = spawn(\"ffmpeg\", ffArgs, { cwd: opts?.cwd, stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\n let stdout = \"\";\n let stderr = \"\";\n let progressBuf = \"\";\n\n // keep progress monotonic\n let lastOutTimeMs: number | undefined;\n\n const onAbort = () => {\n try {\n p.kill(\"SIGKILL\");\n } catch {}\n };\n opts?.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n p.stdout.on(\"data\", (d) => {\n const s = String(d);\n if (stdout.length < MAX_CAPTURE_BYTES) stdout += s;\n\n if (!withProgress) return;\n\n progressBuf += s;\n const lines = progressBuf.split(/\\r?\\n/);\n progressBuf = lines.pop() ?? \"\";\n\n let outTimeMs: number | undefined;\n\n for (const line of lines) {\n const idx = line.indexOf(\"=\");\n if (idx <= 0) continue;\n\n const k = line.slice(0, idx).trim();\n const v = line.slice(idx + 1).trim();\n\n if (k === \"out_time_ms\") {\n const us = parseOutTimeMicroseconds(v);\n if (us !== undefined) outTimeMs = Math.floor(us / 1000);\n } else if (k === \"out_time\" && outTimeMs == null) {\n const ms = parseOutTimeHmsToMs(v);\n if (ms !== undefined) outTimeMs = ms;\n } else if (k === \"progress\" && v === \"end\") {\n opts?.onProgress?.({ phase, percent: 1, outTimeMs: lastOutTimeMs });\n }\n }\n\n if (outTimeMs != null) {\n // monotonic guard\n if (lastOutTimeMs == null || outTimeMs >= lastOutTimeMs) {\n lastOutTimeMs = outTimeMs;\n }\n\n const dur = opts?.durationMs;\n const percent =\n dur && dur > 0 && lastOutTimeMs != null\n ? // clamp to <1 during processing (end event sends 1)\n clamp01(Math.min(0.999, lastOutTimeMs / dur))\n : undefined;\n\n opts?.onProgress?.({ phase, percent, outTimeMs: lastOutTimeMs });\n }\n });\n\n p.stderr.on(\"data\", (d) => {\n const s = String(d);\n if (stderr.length < MAX_CAPTURE_BYTES) stderr += s;\n });\n\n p.on(\"error\", (e) => {\n opts?.signal?.removeEventListener(\"abort\", onAbort);\n reject(e);\n });\n\n p.on(\"close\", (code) => {\n opts?.signal?.removeEventListener(\"abort\", onAbort);\n if (opts?.signal?.aborted) return reject(abortError());\n resolve({ code: code ?? 0, stdout, stderr });\n });\n });\n}\n\nexport async function ensureFfmpegAvailable(): Promise<\n { ok: true; version?: string } | { ok: false; error: unknown }\n> {\n if (ffmpegChecked) return { ok: true, version: ffmpegVersion ?? undefined };\n\n try {\n const r = await runCmd(\"ffmpeg\", [\"-version\"]);\n if (r.code === 0) {\n ffmpegChecked = true;\n ffmpegVersion = (r.stdout || r.stderr || \"\").split(/\\r?\\n/)[0] || null;\n return { ok: true, version: ffmpegVersion ?? undefined };\n }\n return { ok: false, error: new Error(\"ffmpeg not available\") };\n } catch (e) {\n return { ok: false, error: e };\n }\n}\n"]}
1
+ {"version":3,"file":"ffmpeg.js","sourceRoot":"","sources":["../../src/video/ffmpeg.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"node:child_process\";\n\nexport interface RunCmdResult {\n /** The exit code; `-1` when the process was ended by a signal, so a child\n * killed partway (a crash, an outside kill) never reads as a clean exit. */\n code: number;\n stdout: string;\n stderr: string;\n /** True when the process was killed by the wall-clock timeout (see runCmd\n * opts.timeoutMs). Callers must treat a timed-out result as INVALID —\n * never parse its partial output as an answer. */\n timedOut?: boolean;\n /** True when a captured stream hit the 1 MiB cap — the output is truncated\n * and must not be parsed as complete (e.g. incomplete probe JSON). */\n truncated?: boolean;\n}\n\nexport type FfmpegProgressPhase = \"transcode\" | \"poster\";\n\nexport interface RunFfmpegOptions {\n cwd?: string;\n signal?: AbortSignal;\n /** Optional total duration in milliseconds (used to compute percent). */\n durationMs?: number;\n onProgress?: (p: { phase: FfmpegProgressPhase; percent?: number; outTimeMs?: number }) => void;\n /** Which phase to report in callbacks. */\n phase?: FfmpegProgressPhase;\n}\n\nconst MAX_CAPTURE_BYTES = 1024 * 1024; // 1MB per stream\n\n/** Wall-clock ceiling for an ffprobe call that opens a file and reads its\n * stream table (metadata only, never every packet). */\nexport const DEFAULT_PROBE_TIMEOUT_MS = 20_000;\n\nlet ffmpegChecked: boolean | null = null;\nlet ffmpegVersion: string | null = null;\n\nexport function runCmd(\n cmd: string,\n args: string[],\n opts?: {\n cwd?: string;\n /** Hard wall-clock ceiling; on expiry the child is SIGKILLed and the\n * result resolves with `timedOut: true`. A malformed/adversarial input\n * must never be able to wedge a warm instance. */\n timeoutMs?: number;\n /** Abort kills the child immediately (same SIGKILL path as the timeout). */\n signal?: AbortSignal;\n /** Receives stdout one line at a time instead of capturing it, so a\n * listing of any length is read in bounded memory; `stdout` is then\n * empty. A single line over the capture cap sets `truncated`. */\n onStdoutLine?: (line: string) => void;\n }\n): Promise<RunCmdResult> {\n return new Promise((resolve, reject) => {\n const p = spawn(cmd, args, { cwd: opts?.cwd, stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\n let stdout = \"\";\n let stderr = \"\";\n let timedOut = false;\n let truncated = false;\n\n const kill = () => {\n try {\n p.kill(\"SIGKILL\");\n } catch {\n /* already dead */\n }\n };\n const timer =\n opts?.timeoutMs && opts.timeoutMs > 0\n ? setTimeout(() => {\n timedOut = true;\n kill();\n }, opts.timeoutMs)\n : null;\n const onAbort = () => {\n timedOut = true; // treated identically: the result is invalid\n kill();\n };\n if (opts?.signal) {\n if (opts.signal.aborted) onAbort();\n else opts.signal.addEventListener(\"abort\", onAbort, { once: true });\n }\n\n const onLine = opts?.onStdoutLine;\n let lineBuf = \"\";\n // Decodes across chunk boundaries, so a multi-byte character split\n // between two chunks reaches the line intact.\n if (onLine) p.stdout.setEncoding(\"utf8\");\n p.stdout.on(\"data\", (d) => {\n if (onLine) {\n if (truncated) return;\n const lines = (lineBuf + String(d)).split(/\\r?\\n/);\n lineBuf = lines.pop() ?? \"\";\n for (const line of [...lines, lineBuf]) {\n if (line.length > MAX_CAPTURE_BYTES) {\n truncated = true;\n lineBuf = \"\";\n return;\n }\n }\n for (const line of lines) onLine(line);\n return;\n }\n if (stdout.length < MAX_CAPTURE_BYTES) stdout += String(d);\n else truncated = true;\n });\n p.stderr.on(\"data\", (d) => {\n if (stderr.length < MAX_CAPTURE_BYTES) stderr += String(d);\n else truncated = true;\n });\n\n p.on(\"error\", (e) => {\n if (timer) clearTimeout(timer);\n if (opts?.signal) opts.signal.removeEventListener(\"abort\", onAbort);\n reject(e);\n });\n p.on(\"close\", (code) => {\n if (timer) clearTimeout(timer);\n if (opts?.signal) opts.signal.removeEventListener(\"abort\", onAbort);\n if (onLine && !truncated && lineBuf) onLine(lineBuf);\n resolve({ code: code ?? -1, stdout, stderr, ...(timedOut ? { timedOut } : {}), ...(truncated ? { truncated } : {}) });\n });\n });\n}\n\nfunction abortError(): DOMException {\n return new DOMException(\"Aborted\", \"AbortError\");\n}\n\n// NOTE: ffmpeg prints \"out_time_ms\" but it is microseconds in known builds.\n// We harden parsing and sanity-check values.\nfunction parseOutTimeMicroseconds(value: string): number | undefined {\n const n = Number(value);\n if (!Number.isFinite(n) || n < 0) return undefined;\n\n // Discard absurd values (100 years in microseconds).\n const MAX_REASONABLE_US = 100 * 365 * 24 * 60 * 60 * 1_000_000;\n if (n > MAX_REASONABLE_US) return undefined;\n\n return n;\n}\n\nfunction parseOutTimeHmsToMs(value: string): number | undefined {\n // \"HH:MM:SS.micro\" (micro may be omitted)\n const m = String(value).trim().match(/^(\\d+):([0-5]\\d):([0-5]\\d)(?:\\.(\\d{1,6}))?$/);\n if (!m) return undefined;\n\n const hh = Number(m[1]);\n const mm = Number(m[2]);\n const ss = Number(m[3]);\n if (!Number.isFinite(hh) || !Number.isFinite(mm) || !Number.isFinite(ss)) return undefined;\n\n const frac = m[4] ?? \"\";\n const us = frac ? Number((frac + \"000000\").slice(0, 6)) : 0;\n if (!Number.isFinite(us) || us < 0) return undefined;\n\n const totalMs = (hh * 3600 + mm * 60 + ss) * 1000 + Math.floor(us / 1000);\n return totalMs >= 0 ? totalMs : undefined;\n}\n\nfunction clamp01(n: number) {\n if (!Number.isFinite(n)) return undefined;\n if (n < 0) return 0;\n if (n > 1) return 1;\n return n;\n}\n\n/**\n * Runs ffmpeg with optional AbortSignal + progress reporting.\n * If onProgress is provided, ffmpeg is invoked with \"-progress pipe:1\".\n */\nexport function runFfmpeg(args: string[], opts?: RunFfmpegOptions): Promise<RunCmdResult> {\n const phase: FfmpegProgressPhase = opts?.phase ?? \"transcode\";\n const withProgress = !!opts?.onProgress;\n const ffArgs = withProgress ? [\"-progress\", \"pipe:1\", \"-nostats\", ...args] : args;\n\n return new Promise((resolve, reject) => {\n if (opts?.signal?.aborted) return reject(abortError());\n\n const p = spawn(\"ffmpeg\", ffArgs, { cwd: opts?.cwd, stdio: [\"ignore\", \"pipe\", \"pipe\"] });\n\n let stdout = \"\";\n let stderr = \"\";\n let progressBuf = \"\";\n\n // keep progress monotonic\n let lastOutTimeMs: number | undefined;\n\n const onAbort = () => {\n try {\n p.kill(\"SIGKILL\");\n } catch {}\n };\n opts?.signal?.addEventListener(\"abort\", onAbort, { once: true });\n\n p.stdout.on(\"data\", (d) => {\n const s = String(d);\n if (stdout.length < MAX_CAPTURE_BYTES) stdout += s;\n\n if (!withProgress) return;\n\n progressBuf += s;\n const lines = progressBuf.split(/\\r?\\n/);\n progressBuf = lines.pop() ?? \"\";\n\n let outTimeMs: number | undefined;\n\n for (const line of lines) {\n const idx = line.indexOf(\"=\");\n if (idx <= 0) continue;\n\n const k = line.slice(0, idx).trim();\n const v = line.slice(idx + 1).trim();\n\n if (k === \"out_time_ms\") {\n const us = parseOutTimeMicroseconds(v);\n if (us !== undefined) outTimeMs = Math.floor(us / 1000);\n } else if (k === \"out_time\" && outTimeMs == null) {\n const ms = parseOutTimeHmsToMs(v);\n if (ms !== undefined) outTimeMs = ms;\n } else if (k === \"progress\" && v === \"end\") {\n opts?.onProgress?.({ phase, percent: 1, outTimeMs: lastOutTimeMs });\n }\n }\n\n if (outTimeMs != null) {\n // monotonic guard\n if (lastOutTimeMs == null || outTimeMs >= lastOutTimeMs) {\n lastOutTimeMs = outTimeMs;\n }\n\n const dur = opts?.durationMs;\n const percent =\n dur && dur > 0 && lastOutTimeMs != null\n ? // clamp to <1 during processing (end event sends 1)\n clamp01(Math.min(0.999, lastOutTimeMs / dur))\n : undefined;\n\n opts?.onProgress?.({ phase, percent, outTimeMs: lastOutTimeMs });\n }\n });\n\n p.stderr.on(\"data\", (d) => {\n const s = String(d);\n if (stderr.length < MAX_CAPTURE_BYTES) stderr += s;\n });\n\n p.on(\"error\", (e) => {\n opts?.signal?.removeEventListener(\"abort\", onAbort);\n reject(e);\n });\n\n p.on(\"close\", (code) => {\n opts?.signal?.removeEventListener(\"abort\", onAbort);\n if (opts?.signal?.aborted) return reject(abortError());\n resolve({ code: code ?? 0, stdout, stderr });\n });\n });\n}\n\nexport async function ensureFfmpegAvailable(): Promise<\n { ok: true; version?: string } | { ok: false; error: unknown }\n> {\n if (ffmpegChecked) return { ok: true, version: ffmpegVersion ?? undefined };\n\n try {\n const r = await runCmd(\"ffmpeg\", [\"-version\"]);\n if (r.code === 0) {\n ffmpegChecked = true;\n ffmpegVersion = (r.stdout || r.stderr || \"\").split(/\\r?\\n/)[0] || null;\n return { ok: true, version: ffmpegVersion ?? undefined };\n }\n return { ok: false, error: new Error(\"ffmpeg not available\") };\n } catch (e) {\n return { ok: false, error: e };\n }\n}\n"]}
@@ -1,8 +1,9 @@
1
+ import { type MeasureDurationOptions } from "../duration/media-duration.js";
1
2
  export interface VideoProbe {
2
3
  durationSec?: number;
3
4
  width?: number;
4
5
  height?: number;
5
6
  hasAudio?: boolean;
6
7
  }
7
- export declare function probeVideo(inputPath: string): Promise<VideoProbe>;
8
+ export declare function probeVideo(inputPath: string, opts?: MeasureDurationOptions): Promise<VideoProbe>;
8
9
  //# sourceMappingURL=probe.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../../src/video/probe.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,UAAU;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAsCvE"}
1
+ {"version":3,"file":"probe.d.ts","sourceRoot":"","sources":["../../src/video/probe.ts"],"names":[],"mappings":"AACA,OAAO,EAAsB,KAAK,sBAAsB,EAAE,MAAM,+BAA+B,CAAC;AAEhG,MAAM,WAAW,UAAU;IACzB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAsB,UAAU,CAAC,SAAS,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,UAAU,CAAC,CA4BtG"}
@@ -1,30 +1,21 @@
1
- import { runCmd } from "./ffmpeg.js";
2
- export async function probeVideo(inputPath) {
1
+ import { DEFAULT_PROBE_TIMEOUT_MS, runCmd } from "./ffmpeg.js";
2
+ import { measureDurationSec } from "../duration/media-duration.js";
3
+ export async function probeVideo(inputPath, opts) {
3
4
  // ffprobe must exist in runtime environment
4
- const args = [
5
- "-v",
6
- "error",
7
- "-print_format",
8
- "json",
9
- "-show_streams",
10
- "-show_format",
11
- inputPath,
12
- ];
13
- const r = await runCmd("ffprobe", args);
14
- if (r.code !== 0) {
5
+ const args = ["-v", "error", "-print_format", "json", "-show_streams", inputPath];
6
+ const r = await runCmd("ffprobe", args, { timeoutMs: DEFAULT_PROBE_TIMEOUT_MS, signal: opts?.signal });
7
+ if (r.timedOut || r.code !== 0) {
15
8
  throw new Error(`ffprobe failed (${r.code}): ${r.stderr || r.stdout}`);
16
9
  }
17
10
  const json = JSON.parse(r.stdout || "{}");
18
11
  const streams = Array.isArray(json.streams) ? json.streams : [];
19
- const format = json.format ?? {};
20
12
  const v = streams.find((s) => s.codec_type === "video");
21
13
  const a = streams.find((s) => s.codec_type === "audio");
22
- const durationStr = format.duration ?? v?.duration ?? a?.duration;
23
- const durationSec = durationStr ? Number(durationStr) : undefined;
14
+ const durationSec = await measureDurationSec(inputPath, opts);
24
15
  const width = v?.width ? Number(v.width) : undefined;
25
16
  const height = v?.height ? Number(v.height) : undefined;
26
17
  return {
27
- durationSec: Number.isFinite(durationSec) ? durationSec : undefined,
18
+ durationSec,
28
19
  width,
29
20
  height,
30
21
  hasAudio: !!a,
@@ -1 +1 @@
1
- {"version":3,"file":"probe.js","sourceRoot":"","sources":["../../src/video/probe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AASrC,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,SAAiB;IAChD,4CAA4C;IAC5C,MAAM,IAAI,GAAG;QACX,IAAI;QACJ,OAAO;QACP,eAAe;QACf,MAAM;QACN,eAAe;QACf,cAAc;QACd,SAAS;KACV,CAAC;IAEF,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAExC,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;IACzE,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC;IAE1C,MAAM,OAAO,GAAU,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IACvE,MAAM,MAAM,GAAQ,IAAI,CAAC,MAAM,IAAI,EAAE,CAAC;IAEtC,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC;IACxD,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC;IAExD,MAAM,WAAW,GAAG,MAAM,CAAC,QAAQ,IAAI,CAAC,EAAE,QAAQ,IAAI,CAAC,EAAE,QAAQ,CAAC;IAClE,MAAM,WAAW,GAAG,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAElE,MAAM,KAAK,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACrD,MAAM,MAAM,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAExD,OAAO;QACL,WAAW,EAAE,MAAM,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,WAAW,CAAC,CAAC,CAAC,SAAS;QACnE,KAAK;QACL,MAAM;QACN,QAAQ,EAAE,CAAC,CAAC,CAAC;KACd,CAAC;AACJ,CAAC","sourcesContent":["import { runCmd } from \"./ffmpeg.js\";\n\nexport interface VideoProbe {\n durationSec?: number;\n width?: number;\n height?: number;\n hasAudio?: boolean;\n}\n\nexport async function probeVideo(inputPath: string): Promise<VideoProbe> {\n // ffprobe must exist in runtime environment\n const args = [\n \"-v\",\n \"error\",\n \"-print_format\",\n \"json\",\n \"-show_streams\",\n \"-show_format\",\n inputPath,\n ];\n\n const r = await runCmd(\"ffprobe\", args);\n\n if (r.code !== 0) {\n throw new Error(`ffprobe failed (${r.code}): ${r.stderr || r.stdout}`);\n }\n\n const json = JSON.parse(r.stdout || \"{}\");\n\n const streams: any[] = Array.isArray(json.streams) ? json.streams : [];\n const format: any = json.format ?? {};\n\n const v = streams.find((s) => s.codec_type === \"video\");\n const a = streams.find((s) => s.codec_type === \"audio\");\n\n const durationStr = format.duration ?? v?.duration ?? a?.duration;\n const durationSec = durationStr ? Number(durationStr) : undefined;\n\n const width = v?.width ? Number(v.width) : undefined;\n const height = v?.height ? Number(v.height) : undefined;\n\n return {\n durationSec: Number.isFinite(durationSec) ? durationSec : undefined,\n width,\n height,\n hasAudio: !!a,\n };\n}\n"]}
1
+ {"version":3,"file":"probe.js","sourceRoot":"","sources":["../../src/video/probe.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,wBAAwB,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAC/D,OAAO,EAAE,kBAAkB,EAA+B,MAAM,+BAA+B,CAAC;AAShG,MAAM,CAAC,KAAK,UAAU,UAAU,CAAC,SAAiB,EAAE,IAA6B;IAC/E,4CAA4C;IAC5C,MAAM,IAAI,GAAG,CAAC,IAAI,EAAE,OAAO,EAAE,eAAe,EAAE,MAAM,EAAE,eAAe,EAAE,SAAS,CAAC,CAAC;IAElF,MAAM,CAAC,GAAG,MAAM,MAAM,CAAC,SAAS,EAAE,IAAI,EAAE,EAAE,SAAS,EAAE,wBAAwB,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;IAEvG,IAAI,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC,IAAI,MAAM,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC;IACzE,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,IAAI,IAAI,CAAC,CAAC;IAE1C,MAAM,OAAO,GAAU,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC;IAEvE,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC;IACxD,MAAM,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC;IAExD,MAAM,WAAW,GAAG,MAAM,kBAAkB,CAAC,SAAS,EAAE,IAAI,CAAC,CAAC;IAE9D,MAAM,KAAK,GAAG,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IACrD,MAAM,MAAM,GAAG,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAExD,OAAO;QACL,WAAW;QACX,KAAK;QACL,MAAM;QACN,QAAQ,EAAE,CAAC,CAAC,CAAC;KACd,CAAC;AACJ,CAAC","sourcesContent":["import { DEFAULT_PROBE_TIMEOUT_MS, runCmd } from \"./ffmpeg.js\";\nimport { measureDurationSec, type MeasureDurationOptions } from \"../duration/media-duration.js\";\n\nexport interface VideoProbe {\n durationSec?: number;\n width?: number;\n height?: number;\n hasAudio?: boolean;\n}\n\nexport async function probeVideo(inputPath: string, opts?: MeasureDurationOptions): Promise<VideoProbe> {\n // ffprobe must exist in runtime environment\n const args = [\"-v\", \"error\", \"-print_format\", \"json\", \"-show_streams\", inputPath];\n\n const r = await runCmd(\"ffprobe\", args, { timeoutMs: DEFAULT_PROBE_TIMEOUT_MS, signal: opts?.signal });\n\n if (r.timedOut || r.code !== 0) {\n throw new Error(`ffprobe failed (${r.code}): ${r.stderr || r.stdout}`);\n }\n\n const json = JSON.parse(r.stdout || \"{}\");\n\n const streams: any[] = Array.isArray(json.streams) ? json.streams : [];\n\n const v = streams.find((s) => s.codec_type === \"video\");\n const a = streams.find((s) => s.codec_type === \"audio\");\n\n const durationSec = await measureDurationSec(inputPath, opts);\n\n const width = v?.width ? Number(v.width) : undefined;\n const height = v?.height ? Number(v.height) : undefined;\n\n return {\n durationSec,\n width,\n height,\n hasAudio: !!a,\n };\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"video-processor.d.ts","sourceRoot":"","sources":["../../src/video/video-processor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,qBAAqB,EAAE,mBAAmB,EAAoB,MAAM,gCAAgC,CAAC;AAMhI,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAsDvD,wBAAsB,YAAY,CAChC,IAAI,EAAE,mBAAmB,EACzB,GAAG,EAAE;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,EACD,IAAI,CAAC,EAAE,mBAAmB,GACzB,OAAO,CAAC,qBAAqB,CAAC,CAsQhC"}
1
+ {"version":3,"file":"video-processor.d.ts","sourceRoot":"","sources":["../../src/video/video-processor.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAe,qBAAqB,EAAE,mBAAmB,EAAoB,MAAM,gCAAgC,CAAC;AAOhI,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,aAAa,CAAC;AAuDvD,wBAAsB,YAAY,CAChC,IAAI,EAAE,mBAAmB,EACzB,GAAG,EAAE;IACH,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB,EACD,IAAI,CAAC,EAAE,mBAAmB,GACzB,OAAO,CAAC,qBAAqB,CAAC,CA4QhC"}
@@ -2,7 +2,9 @@ import { mkdir, stat } from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { ensureFfmpegAvailable, runFfmpeg } from "./ffmpeg.js";
4
4
  import { probeVideo } from "./probe.js";
5
+ import { unreadableLengthError } from "../duration/media-duration.js";
5
6
  import { safeOutputPathFor } from "../utils/safe-path.js";
7
+ import { TIMED_MEDIA_MAIN_OUTPUT_KEY, VIDEO_POSTER_OUTPUT_KEY } from "@ttt-productions/media-schemas";
6
8
  function matchMime(accepted, actual) {
7
9
  const a = accepted.trim().toLowerCase();
8
10
  const m = actual.trim().toLowerCase();
@@ -92,13 +94,16 @@ export async function processVideo(spec, ctx, opts) {
92
94
  }
93
95
  const outDir = path.dirname(ctx.outputBasePath);
94
96
  await mkdir(outDir, { recursive: true });
95
- const probe = await probeVideo(ctx.inputPath);
97
+ const probe = await probeVideo(ctx.inputPath, { signal: opts?.signal });
96
98
  if (opts?.signal?.aborted) {
97
99
  return { ok: false, mediaType: "video", error: { code: "processing_canceled", message: "Processing canceled." } };
98
100
  }
99
101
  // duration enforcement
100
102
  const maxDur = spec.maxDurationSec ?? spec.video?.maxDurationSec;
101
- if (maxDur && probe.durationSec && probe.durationSec > maxDur) {
103
+ if (maxDur && probe.durationSec === undefined) {
104
+ return { ok: false, mediaType: "video", error: unreadableLengthError("video", maxDur) };
105
+ }
106
+ if (maxDur && probe.durationSec !== undefined && probe.durationSec > maxDur) {
102
107
  return {
103
108
  ok: false,
104
109
  mediaType: "video",
@@ -156,8 +161,8 @@ export async function processVideo(spec, ctx, opts) {
156
161
  };
157
162
  }
158
163
  }
159
- const videoOut = outputPathFor(ctx.outputBasePath, "main", "mp4");
160
- const posterOut = outputPathFor(ctx.outputBasePath, "poster", "jpg");
164
+ const videoOut = outputPathFor(ctx.outputBasePath, TIMED_MEDIA_MAIN_OUTPUT_KEY, "mp4");
165
+ const posterOut = outputPathFor(ctx.outputBasePath, VIDEO_POSTER_OUTPUT_KEY, "jpg");
161
166
  tempFiles.push(videoOut, posterOut);
162
167
  // Build a basic filter graph to enforce size/aspect when auto-format is enabled.
163
168
  // - If exact WxH required: scale+crop to WxH
@@ -246,7 +251,7 @@ export async function processVideo(spec, ctx, opts) {
246
251
  catch { }
247
252
  const outputs = [
248
253
  {
249
- key: "main",
254
+ key: TIMED_MEDIA_MAIN_OUTPUT_KEY,
250
255
  url: `file://${videoOut}`,
251
256
  path: videoOut,
252
257
  mime: mimeFromExt("mp4"),
@@ -263,7 +268,7 @@ export async function processVideo(spec, ctx, opts) {
263
268
  warnings.push("poster_failed");
264
269
  if (psSize > 0) {
265
270
  outputs.push({
266
- key: "poster",
271
+ key: VIDEO_POSTER_OUTPUT_KEY,
267
272
  url: `file://${posterOut}`,
268
273
  path: posterOut,
269
274
  mime: "image/jpeg",
@@ -286,6 +291,9 @@ export async function processVideo(spec, ctx, opts) {
286
291
  };
287
292
  }
288
293
  catch (e) {
294
+ if (opts?.signal?.aborted) {
295
+ return { ok: false, mediaType: "video", error: { code: "processing_canceled", message: "Processing canceled." } };
296
+ }
289
297
  return {
290
298
  ok: false,
291
299
  mediaType: "video",