@ttt-productions/media-processing-core 0.8.15 → 0.8.17
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/audio/audio-processor.d.ts +2 -2
- package/dist/audio/audio-processor.d.ts.map +1 -1
- package/dist/audio/audio-processor.js +62 -48
- package/dist/audio/audio-processor.js.map +1 -1
- package/dist/audio/probe.d.ts +1 -1
- package/dist/audio/probe.d.ts.map +1 -1
- package/dist/audio/probe.js +6 -3
- package/dist/audio/probe.js.map +1 -1
- package/dist/duration/media-duration.d.ts +2 -2
- package/dist/duration/media-duration.d.ts.map +1 -1
- package/dist/duration/media-duration.js +23 -21
- package/dist/duration/media-duration.js.map +1 -1
- package/dist/image/formats.d.ts +2 -2
- package/dist/image/formats.js +25 -25
- package/dist/image/formats.js.map +1 -1
- package/dist/image/image-processor.d.ts +2 -2
- package/dist/image/image-processor.d.ts.map +1 -1
- package/dist/image/image-processor.js +47 -37
- package/dist/image/image-processor.js.map +1 -1
- package/dist/image/resize.d.ts +2 -2
- package/dist/image/resize.d.ts.map +1 -1
- package/dist/image/resize.js +13 -13
- package/dist/image/resize.js.map +1 -1
- package/dist/index.d.ts +19 -16
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -16
- package/dist/index.js.map +1 -1
- package/dist/inspection/inspect-media.d.ts +13 -13
- package/dist/inspection/inspect-media.d.ts.map +1 -1
- package/dist/inspection/inspect-media.js +196 -140
- package/dist/inspection/inspect-media.js.map +1 -1
- package/dist/io/fs.d.ts +1 -1
- package/dist/io/fs.js +4 -4
- package/dist/io/fs.js.map +1 -1
- package/dist/io/local-input.d.ts +1 -1
- package/dist/io/local-input.d.ts.map +1 -1
- package/dist/io/local-input.js +15 -15
- package/dist/io/local-input.js.map +1 -1
- package/dist/io/types.d.ts.map +1 -1
- package/dist/io/types.js.map +1 -1
- package/dist/moderation/merge.d.ts +1 -1
- package/dist/moderation/merge.d.ts.map +1 -1
- package/dist/moderation/merge.js +1 -1
- package/dist/moderation/merge.js.map +1 -1
- package/dist/moderation/types.d.ts +3 -3
- package/dist/moderation/types.js.map +1 -1
- package/dist/process-media.d.ts +2 -3
- package/dist/process-media.d.ts.map +1 -1
- package/dist/process-media.js +9 -9
- package/dist/process-media.js.map +1 -1
- package/dist/processing/errors.d.ts.map +1 -1
- package/dist/processing/errors.js +1 -1
- package/dist/processing/errors.js.map +1 -1
- package/dist/processing/result.d.ts +2 -2
- package/dist/processing/result.js +1 -1
- package/dist/processing/result.js.map +1 -1
- package/dist/run-pipeline.d.ts +5 -5
- package/dist/run-pipeline.d.ts.map +1 -1
- package/dist/run-pipeline.js +70 -54
- package/dist/run-pipeline.js.map +1 -1
- package/dist/server/download-token.d.ts +14 -0
- package/dist/server/download-token.d.ts.map +1 -0
- package/dist/server/download-token.js +19 -0
- package/dist/server/download-token.js.map +1 -0
- package/dist/server/firebase-media-io.d.ts +2 -2
- package/dist/server/firebase-media-io.js +1 -1
- package/dist/server/firebase-media-io.js.map +1 -1
- package/dist/server/gcs-errors.js +2 -2
- package/dist/server/gcs-errors.js.map +1 -1
- package/dist/server/index.d.ts +7 -5
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +7 -5
- package/dist/server/index.js.map +1 -1
- package/dist/server/pending-media.d.ts +2 -2
- package/dist/server/pending-media.d.ts.map +1 -1
- package/dist/server/pending-media.js +12 -6
- package/dist/server/pending-media.js.map +1 -1
- package/dist/server/r2-retry.d.ts +4 -4
- package/dist/server/r2-retry.d.ts.map +1 -1
- package/dist/server/r2-retry.js +48 -47
- package/dist/server/r2-retry.js.map +1 -1
- package/dist/server/staged-object.d.ts +2 -2
- package/dist/server/staged-object.d.ts.map +1 -1
- package/dist/server/staged-object.js +4 -4
- package/dist/server/staged-object.js.map +1 -1
- package/dist/server/storage-ops.d.ts +3 -5
- package/dist/server/storage-ops.d.ts.map +1 -1
- package/dist/server/storage-ops.js +101 -97
- package/dist/server/storage-ops.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/dist/types.js.map +1 -1
- package/dist/utils/log.d.ts.map +1 -1
- package/dist/utils/log.js +3 -3
- package/dist/utils/log.js.map +1 -1
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +1 -4
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/safe-path.js +8 -8
- package/dist/utils/safe-path.js.map +1 -1
- package/dist/validation/validate-duration.d.ts.map +1 -1
- package/dist/validation/validate-duration.js.map +1 -1
- package/dist/validation/validate-mime.d.ts.map +1 -1
- package/dist/validation/validate-mime.js.map +1 -1
- package/dist/validation/validate-size.d.ts.map +1 -1
- package/dist/validation/validate-size.js.map +1 -1
- package/dist/video/ffmpeg.d.ts +1 -1
- package/dist/video/ffmpeg.d.ts.map +1 -1
- package/dist/video/ffmpeg.js +51 -43
- package/dist/video/ffmpeg.js.map +1 -1
- package/dist/video/probe.d.ts +1 -1
- package/dist/video/probe.d.ts.map +1 -1
- package/dist/video/probe.js +10 -7
- package/dist/video/probe.js.map +1 -1
- package/dist/video/video-processor.d.ts +2 -2
- package/dist/video/video-processor.d.ts.map +1 -1
- package/dist/video/video-processor.js +123 -81
- package/dist/video/video-processor.js.map +1 -1
- package/dist/workspace/temp.js +4 -4
- package/dist/workspace/temp.js.map +1 -1
- package/package.json +2 -2
- package/dist/audio/index.d.ts +0 -3
- package/dist/audio/index.d.ts.map +0 -1
- package/dist/audio/index.js +0 -3
- package/dist/audio/index.js.map +0 -1
- package/dist/image/index.d.ts +0 -4
- package/dist/image/index.d.ts.map +0 -1
- package/dist/image/index.js +0 -4
- package/dist/image/index.js.map +0 -1
- package/dist/video/index.d.ts +0 -4
- package/dist/video/index.d.ts.map +0 -1
- package/dist/video/index.js +0 -4
- package/dist/video/index.js.map +0 -1
|
@@ -25,13 +25,13 @@
|
|
|
25
25
|
// probes as a one-frame AV1 `video` stream);
|
|
26
26
|
// - FAIL-CLOSED: anything unproven is `indeterminate` with the
|
|
27
27
|
// `strict-video-fallback` safety plan — never audio, never publishable.
|
|
28
|
-
import { open } from
|
|
29
|
-
import { z } from
|
|
30
|
-
import { MediaInspectionResultSchema, } from
|
|
31
|
-
import { DEFAULT_PROBE_TIMEOUT_MS, runCmd } from
|
|
32
|
-
import { measureDurationSec } from
|
|
28
|
+
import { open } from 'node:fs/promises';
|
|
29
|
+
import { z } from 'zod';
|
|
30
|
+
import { MediaInspectionResultSchema, } from '@ttt-productions/media-schemas';
|
|
31
|
+
import { DEFAULT_PROBE_TIMEOUT_MS, runCmd } from '../video/ffmpeg.js';
|
|
32
|
+
import { measureDurationSec } from '../duration/media-duration.js';
|
|
33
33
|
/** Bump on any classification-rule change (rides every decision event). */
|
|
34
|
-
export const INSPECTOR_VERSION =
|
|
34
|
+
export const INSPECTOR_VERSION = 'mpc-inspect-2';
|
|
35
35
|
const SIGNATURE_READ_BYTES = 64 * 1024;
|
|
36
36
|
const PROBE_SIZE_BYTES = 4 * 1024 * 1024; // -probesize: metadata, not playback
|
|
37
37
|
const ANALYZE_DURATION_US = 10 * 1000 * 1000;
|
|
@@ -48,42 +48,56 @@ const ProbeStreamSchema = z
|
|
|
48
48
|
.loose();
|
|
49
49
|
const ProbeJsonSchema = z
|
|
50
50
|
.object({
|
|
51
|
-
format: z
|
|
51
|
+
format: z
|
|
52
|
+
.object({ format_name: z.string().max(200).optional() })
|
|
53
|
+
.loose()
|
|
54
|
+
.optional(),
|
|
52
55
|
streams: z.array(ProbeStreamSchema).max(256).optional(),
|
|
53
56
|
})
|
|
54
57
|
.loose();
|
|
55
58
|
// ---- codec normalization (bounded — raw strings never escape) ----
|
|
56
59
|
const CODEC_MAP = {
|
|
57
|
-
h264:
|
|
58
|
-
hevc:
|
|
59
|
-
vp8:
|
|
60
|
-
vp9:
|
|
61
|
-
av1:
|
|
62
|
-
theora:
|
|
63
|
-
mpeg4:
|
|
64
|
-
mjpeg:
|
|
65
|
-
aac:
|
|
66
|
-
alac:
|
|
67
|
-
opus:
|
|
68
|
-
vorbis:
|
|
69
|
-
mp3:
|
|
70
|
-
flac:
|
|
60
|
+
h264: 'h264',
|
|
61
|
+
hevc: 'hevc',
|
|
62
|
+
vp8: 'vp8',
|
|
63
|
+
vp9: 'vp9',
|
|
64
|
+
av1: 'av1',
|
|
65
|
+
theora: 'theora',
|
|
66
|
+
mpeg4: 'mpeg4',
|
|
67
|
+
mjpeg: 'mjpeg',
|
|
68
|
+
aac: 'aac',
|
|
69
|
+
alac: 'alac',
|
|
70
|
+
opus: 'opus',
|
|
71
|
+
vorbis: 'vorbis',
|
|
72
|
+
mp3: 'mp3',
|
|
73
|
+
flac: 'flac',
|
|
71
74
|
};
|
|
72
75
|
function normalizeCodec(name) {
|
|
73
76
|
if (!name)
|
|
74
|
-
return
|
|
77
|
+
return 'other';
|
|
75
78
|
const n = name.toLowerCase();
|
|
76
79
|
if (CODEC_MAP[n])
|
|
77
80
|
return CODEC_MAP[n];
|
|
78
|
-
if (n.startsWith(
|
|
79
|
-
return
|
|
80
|
-
if (n.startsWith(
|
|
81
|
-
return
|
|
82
|
-
return
|
|
81
|
+
if (n.startsWith('pcm_'))
|
|
82
|
+
return 'pcm';
|
|
83
|
+
if (n.startsWith('amr'))
|
|
84
|
+
return 'amr';
|
|
85
|
+
return 'other';
|
|
83
86
|
}
|
|
84
87
|
/** ISO-BMFF brands that declare an IMAGE-item file (HEIF family). Resolved via
|
|
85
88
|
* decoder proof BEFORE the timed-video rule — see the AVIF note above. */
|
|
86
|
-
const ISOBMFF_IMAGE_BRANDS = new Set([
|
|
89
|
+
const ISOBMFF_IMAGE_BRANDS = new Set([
|
|
90
|
+
'avif',
|
|
91
|
+
'avis',
|
|
92
|
+
'heic',
|
|
93
|
+
'heix',
|
|
94
|
+
'heim',
|
|
95
|
+
'heis',
|
|
96
|
+
'hevc',
|
|
97
|
+
'hevx',
|
|
98
|
+
'mif1',
|
|
99
|
+
'msf1',
|
|
100
|
+
]);
|
|
87
101
|
function ascii(b, off, s) {
|
|
88
102
|
if (off + s.length > b.length)
|
|
89
103
|
return false;
|
|
@@ -94,58 +108,62 @@ function ascii(b, off, s) {
|
|
|
94
108
|
}
|
|
95
109
|
export function detectSignatureFamily(b) {
|
|
96
110
|
if (b.length < 12)
|
|
97
|
-
return { family:
|
|
111
|
+
return { family: 'unknown' };
|
|
98
112
|
// images (single-format signatures)
|
|
99
113
|
if (b[0] === 0xff && b[1] === 0xd8 && b[2] === 0xff)
|
|
100
|
-
return { family:
|
|
101
|
-
if (b[0] === 0x89 && ascii(b, 1,
|
|
102
|
-
return { family:
|
|
103
|
-
if (ascii(b, 0,
|
|
104
|
-
return { family:
|
|
105
|
-
if (ascii(b, 0,
|
|
106
|
-
return { family:
|
|
114
|
+
return { family: 'image', formatId: 'jpeg', container: 'jpeg' };
|
|
115
|
+
if (b[0] === 0x89 && ascii(b, 1, 'PNG'))
|
|
116
|
+
return { family: 'image', formatId: 'png', container: 'png' };
|
|
117
|
+
if (ascii(b, 0, 'GIF8'))
|
|
118
|
+
return { family: 'image', formatId: 'gif', container: 'gif' };
|
|
119
|
+
if (ascii(b, 0, 'RIFF') && ascii(b, 8, 'WEBP'))
|
|
120
|
+
return { family: 'image', formatId: 'webp', container: 'webp' };
|
|
107
121
|
if (b[0] === 0x42 && b[1] === 0x4d)
|
|
108
|
-
return { family:
|
|
109
|
-
if ((b[0] === 0x49 && b[1] === 0x49 && b[2] === 0x2a) ||
|
|
110
|
-
|
|
122
|
+
return { family: 'image', formatId: 'bmp', container: 'bmp' };
|
|
123
|
+
if ((b[0] === 0x49 && b[1] === 0x49 && b[2] === 0x2a) ||
|
|
124
|
+
(b[0] === 0x4d && b[1] === 0x4d && b[3] === 0x2a)) {
|
|
125
|
+
return { family: 'image', formatId: 'tiff', container: 'tiff' };
|
|
111
126
|
}
|
|
112
127
|
// SVG: XML text — scan a short prefix for '<svg' (bounded; case-insensitive).
|
|
113
128
|
{
|
|
114
129
|
const prefix = Buffer.from(b.slice(0, Math.min(b.length, 4096)))
|
|
115
|
-
.toString(
|
|
130
|
+
.toString('latin1')
|
|
116
131
|
.toLowerCase();
|
|
117
|
-
if (prefix.includes(
|
|
118
|
-
|
|
132
|
+
if (prefix.includes('<svg') &&
|
|
133
|
+
(prefix.trimStart().startsWith('<?xml') ||
|
|
134
|
+
prefix.trimStart().startsWith('<svg') ||
|
|
135
|
+
prefix.includes('<!doctype svg'))) {
|
|
136
|
+
return { family: 'svg' };
|
|
119
137
|
}
|
|
120
138
|
}
|
|
121
139
|
// containers
|
|
122
140
|
if (b[0] === 0x1a && b[1] === 0x45 && b[2] === 0xdf && b[3] === 0xa3)
|
|
123
|
-
return { family:
|
|
124
|
-
if (ascii(b, 4,
|
|
141
|
+
return { family: 'ebml' };
|
|
142
|
+
if (ascii(b, 4, 'ftyp')) {
|
|
125
143
|
const brand = String.fromCharCode(b[8], b[9], b[10], b[11]).trim().toLowerCase();
|
|
126
|
-
return { family:
|
|
144
|
+
return { family: 'isobmff', brand };
|
|
127
145
|
}
|
|
128
|
-
if (ascii(b, 0,
|
|
129
|
-
return { family:
|
|
130
|
-
if (ascii(b, 0,
|
|
131
|
-
return { family:
|
|
132
|
-
if (ascii(b, 0,
|
|
133
|
-
return { family:
|
|
134
|
-
if (ascii(b, 0,
|
|
135
|
-
return { family:
|
|
146
|
+
if (ascii(b, 0, 'OggS'))
|
|
147
|
+
return { family: 'ogg' };
|
|
148
|
+
if (ascii(b, 0, 'RIFF') && ascii(b, 8, 'AVI '))
|
|
149
|
+
return { family: 'avi' };
|
|
150
|
+
if (ascii(b, 0, 'RIFF') && ascii(b, 8, 'WAVE'))
|
|
151
|
+
return { family: 'wav' };
|
|
152
|
+
if (ascii(b, 0, 'ID3'))
|
|
153
|
+
return { family: 'mp3' };
|
|
136
154
|
if (b[0] === 0xff && (b[1] & 0xe0) === 0xe0)
|
|
137
|
-
return { family:
|
|
138
|
-
if (ascii(b, 0,
|
|
139
|
-
return { family:
|
|
140
|
-
return { family:
|
|
155
|
+
return { family: 'mp3' };
|
|
156
|
+
if (ascii(b, 0, 'fLaC'))
|
|
157
|
+
return { family: 'flac' };
|
|
158
|
+
return { family: 'unknown' };
|
|
141
159
|
}
|
|
142
160
|
let cachedFfprobeVersion;
|
|
143
161
|
async function getFfprobeVersion() {
|
|
144
162
|
if (cachedFfprobeVersion !== undefined)
|
|
145
163
|
return cachedFfprobeVersion ?? undefined;
|
|
146
164
|
try {
|
|
147
|
-
const r = await runCmd(
|
|
148
|
-
const first = (r.stdout || r.stderr).split(/\r?\n/)[0]?.trim() ??
|
|
165
|
+
const r = await runCmd('ffprobe', ['-version'], { timeoutMs: 5_000 });
|
|
166
|
+
const first = (r.stdout || r.stderr).split(/\r?\n/)[0]?.trim() ?? '';
|
|
149
167
|
cachedFfprobeVersion = first ? first.slice(0, 120) : null;
|
|
150
168
|
}
|
|
151
169
|
catch {
|
|
@@ -156,40 +174,42 @@ async function getFfprobeVersion() {
|
|
|
156
174
|
async function defaultProbe(localPath, opts) {
|
|
157
175
|
let r;
|
|
158
176
|
try {
|
|
159
|
-
r = await runCmd(
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
177
|
+
r = await runCmd('ffprobe', [
|
|
178
|
+
'-v',
|
|
179
|
+
'error',
|
|
180
|
+
'-print_format',
|
|
181
|
+
'json',
|
|
182
|
+
'-show_format',
|
|
183
|
+
'-show_streams',
|
|
184
|
+
'-probesize',
|
|
167
185
|
String(PROBE_SIZE_BYTES),
|
|
168
|
-
|
|
186
|
+
'-analyzeduration',
|
|
169
187
|
String(ANALYZE_DURATION_US),
|
|
170
188
|
localPath,
|
|
171
189
|
], { timeoutMs: opts.timeoutMs, signal: opts.signal });
|
|
172
190
|
}
|
|
173
191
|
catch {
|
|
174
|
-
return { ok: false, reason:
|
|
192
|
+
return { ok: false, reason: 'failed' }; // spawn failure (ffprobe missing) — fail closed
|
|
175
193
|
}
|
|
176
194
|
if (r.timedOut)
|
|
177
|
-
return { ok: false, reason:
|
|
195
|
+
return { ok: false, reason: 'timeout' };
|
|
178
196
|
if (r.truncated)
|
|
179
|
-
return { ok: false, reason:
|
|
197
|
+
return { ok: false, reason: 'truncated' };
|
|
180
198
|
if (r.code !== 0)
|
|
181
|
-
return { ok: false, reason:
|
|
199
|
+
return { ok: false, reason: 'failed' };
|
|
182
200
|
try {
|
|
183
|
-
return { ok: true, json: JSON.parse(r.stdout ||
|
|
201
|
+
return { ok: true, json: JSON.parse(r.stdout || '{}') };
|
|
184
202
|
}
|
|
185
203
|
catch {
|
|
186
|
-
return { ok: false, reason:
|
|
204
|
+
return { ok: false, reason: 'bad_json' };
|
|
187
205
|
}
|
|
188
206
|
}
|
|
189
207
|
async function defaultDecodeImage(localPath) {
|
|
190
208
|
try {
|
|
191
|
-
const sharp = (await import(
|
|
192
|
-
const meta = await sharp(localPath, {
|
|
209
|
+
const sharp = (await import('sharp')).default;
|
|
210
|
+
const meta = await sharp(localPath, {
|
|
211
|
+
limitInputPixels: 268402689 /* sharp default ~16k x 16k */,
|
|
212
|
+
}).metadata();
|
|
193
213
|
if (!meta.width || !meta.height)
|
|
194
214
|
return null;
|
|
195
215
|
return { frames: Math.max(1, meta.pages ?? 1) };
|
|
@@ -203,7 +223,13 @@ function baseResult(partial, ffprobeVersion) {
|
|
|
203
223
|
const result = {
|
|
204
224
|
inspectorVersion: INSPECTOR_VERSION,
|
|
205
225
|
...(ffprobeVersion ? { ffprobeVersion } : {}),
|
|
206
|
-
streams: partial.streams ?? {
|
|
226
|
+
streams: partial.streams ?? {
|
|
227
|
+
audio: 0,
|
|
228
|
+
timedVideo: 0,
|
|
229
|
+
attachedPictures: 0,
|
|
230
|
+
imageFrames: 0,
|
|
231
|
+
auxiliary: 0,
|
|
232
|
+
},
|
|
207
233
|
codecs: partial.codecs ?? { audio: [], video: [] },
|
|
208
234
|
status: partial.status,
|
|
209
235
|
container: partial.container,
|
|
@@ -217,9 +243,9 @@ function baseResult(partial, ffprobeVersion) {
|
|
|
217
243
|
}
|
|
218
244
|
function indeterminate(container, reasonCode, ffprobeVersion, formatId) {
|
|
219
245
|
return baseResult({
|
|
220
|
-
status:
|
|
246
|
+
status: 'indeterminate',
|
|
221
247
|
container,
|
|
222
|
-
safetyPlan:
|
|
248
|
+
safetyPlan: 'strict-video-fallback',
|
|
223
249
|
reasonCode,
|
|
224
250
|
...(formatId ? { formatId } : {}),
|
|
225
251
|
}, ffprobeVersion);
|
|
@@ -232,7 +258,7 @@ export async function inspectMedia(args) {
|
|
|
232
258
|
// 1) bounded signature read (header only)
|
|
233
259
|
let head;
|
|
234
260
|
try {
|
|
235
|
-
const fh = await open(args.localPath,
|
|
261
|
+
const fh = await open(args.localPath, 'r');
|
|
236
262
|
try {
|
|
237
263
|
const buf = Buffer.alloc(SIGNATURE_READ_BYTES);
|
|
238
264
|
const { bytesRead } = await fh.read(buf, 0, SIGNATURE_READ_BYTES, 0);
|
|
@@ -243,89 +269,113 @@ export async function inspectMedia(args) {
|
|
|
243
269
|
}
|
|
244
270
|
}
|
|
245
271
|
catch {
|
|
246
|
-
return indeterminate(
|
|
272
|
+
return indeterminate('unknown', 'read_failed', ffprobeVersion);
|
|
247
273
|
}
|
|
248
274
|
const sig = detectSignatureFamily(head);
|
|
249
275
|
// 2) family routing
|
|
250
276
|
switch (sig.family) {
|
|
251
|
-
case
|
|
252
|
-
return indeterminate(
|
|
253
|
-
case
|
|
277
|
+
case 'unknown':
|
|
278
|
+
return indeterminate('unknown', 'unrecognized_signature', ffprobeVersion);
|
|
279
|
+
case 'svg':
|
|
254
280
|
// Scriptable XML "image" — structurally recognized, deliberately NOT a
|
|
255
281
|
// processable format: it can carry script and no sanitize/rasterize step
|
|
256
282
|
// exists. Never classified by decoder capability alone.
|
|
257
283
|
return baseResult({
|
|
258
|
-
status:
|
|
259
|
-
container:
|
|
260
|
-
formatId:
|
|
261
|
-
safetyPlan:
|
|
262
|
-
reasonCode:
|
|
284
|
+
status: 'unsupported',
|
|
285
|
+
container: 'svg',
|
|
286
|
+
formatId: 'svg',
|
|
287
|
+
safetyPlan: 'strict-video-fallback',
|
|
288
|
+
reasonCode: 'unsupported_format:svg',
|
|
263
289
|
}, ffprobeVersion);
|
|
264
|
-
case
|
|
290
|
+
case 'image': {
|
|
265
291
|
// Single-format image signature: decoder proof required — a signature is
|
|
266
292
|
// a candidate, not a classification.
|
|
267
293
|
const decoded = await decodeImage(args.localPath);
|
|
268
294
|
if (!decoded)
|
|
269
|
-
return baseResult({
|
|
295
|
+
return baseResult({
|
|
296
|
+
status: 'malformed',
|
|
297
|
+
container: sig.container,
|
|
298
|
+
formatId: sig.formatId,
|
|
299
|
+
safetyPlan: 'strict-video-fallback',
|
|
300
|
+
reasonCode: 'image_decode_failed',
|
|
301
|
+
}, ffprobeVersion);
|
|
270
302
|
const frames = decoded.frames;
|
|
271
303
|
return baseResult({
|
|
272
|
-
status:
|
|
273
|
-
canonicalKind:
|
|
304
|
+
status: 'definitive',
|
|
305
|
+
canonicalKind: 'image',
|
|
274
306
|
container: sig.container,
|
|
275
307
|
formatId: sig.formatId,
|
|
276
|
-
streams: {
|
|
277
|
-
|
|
278
|
-
|
|
308
|
+
streams: {
|
|
309
|
+
audio: 0,
|
|
310
|
+
timedVideo: 0,
|
|
311
|
+
attachedPictures: 0,
|
|
312
|
+
imageFrames: frames,
|
|
313
|
+
auxiliary: 0,
|
|
314
|
+
},
|
|
315
|
+
safetyPlan: frames > 1 ? 'animated-image-frames' : 'still-image',
|
|
316
|
+
reasonCode: 'ok',
|
|
279
317
|
}, ffprobeVersion);
|
|
280
318
|
}
|
|
281
|
-
case
|
|
319
|
+
case 'isobmff':
|
|
282
320
|
if (ISOBMFF_IMAGE_BRANDS.has(sig.brand)) {
|
|
283
321
|
// IMAGE-FAMILY ISO-BMFF resolves BEFORE the timed-video rule: a valid
|
|
284
322
|
// AVIF probes as a one-frame AV1 `video` stream. Proof = image brand +
|
|
285
323
|
// successful bounded decode + no ordinary timed A/V presentation.
|
|
286
324
|
const probed = await probe(args.localPath, { timeoutMs, signal: args.signal });
|
|
287
325
|
const parsed = probed.ok ? ProbeJsonSchema.safeParse(probed.json) : null;
|
|
288
|
-
const streams = parsed?.success ? parsed.data.streams ?? [] : null;
|
|
289
|
-
const hasAudio = streams ? streams.some((s) => s.codec_type ===
|
|
290
|
-
const formatId = sig.brand.startsWith(
|
|
326
|
+
const streams = parsed?.success ? (parsed.data.streams ?? []) : null;
|
|
327
|
+
const hasAudio = streams ? streams.some((s) => s.codec_type === 'audio') : true; // unknown ⇒ assume worst
|
|
328
|
+
const formatId = sig.brand.startsWith('avi') ? 'avif' : 'heic';
|
|
291
329
|
const decoded = await decodeImage(args.localPath);
|
|
292
330
|
if (!decoded || hasAudio) {
|
|
293
|
-
if (formatId ===
|
|
331
|
+
if (formatId === 'heic' && !decoded && !hasAudio) {
|
|
294
332
|
// Recognized HEIC-family that the runtime cannot decode: explicit
|
|
295
333
|
// unsupported-image (launch policy) — NEVER relabeled video.
|
|
296
|
-
return baseResult({
|
|
334
|
+
return baseResult({
|
|
335
|
+
status: 'unsupported',
|
|
336
|
+
container: 'heif',
|
|
337
|
+
formatId,
|
|
338
|
+
safetyPlan: 'strict-video-fallback',
|
|
339
|
+
reasonCode: 'unsupported_format:heic',
|
|
340
|
+
}, ffprobeVersion);
|
|
297
341
|
}
|
|
298
342
|
// Undecodable/ambiguous image-brand file, or one carrying audio (a
|
|
299
343
|
// real timed presentation wearing an image brand): fail toward the
|
|
300
344
|
// stricter visual path.
|
|
301
|
-
return indeterminate(
|
|
345
|
+
return indeterminate('heif', decoded ? 'image_brand_with_audio' : 'image_decode_failed', ffprobeVersion, formatId);
|
|
302
346
|
}
|
|
303
347
|
const frames = decoded.frames;
|
|
304
348
|
return baseResult({
|
|
305
|
-
status:
|
|
306
|
-
canonicalKind:
|
|
307
|
-
container:
|
|
349
|
+
status: 'definitive',
|
|
350
|
+
canonicalKind: 'image',
|
|
351
|
+
container: 'heif',
|
|
308
352
|
formatId,
|
|
309
|
-
streams: {
|
|
310
|
-
|
|
311
|
-
|
|
353
|
+
streams: {
|
|
354
|
+
audio: 0,
|
|
355
|
+
timedVideo: 0,
|
|
356
|
+
attachedPictures: 0,
|
|
357
|
+
imageFrames: frames,
|
|
358
|
+
auxiliary: 0,
|
|
359
|
+
},
|
|
360
|
+
safetyPlan: frames > 1 ? 'animated-image-frames' : 'still-image',
|
|
361
|
+
reasonCode: 'ok',
|
|
312
362
|
}, ffprobeVersion);
|
|
313
363
|
}
|
|
314
|
-
return classifyAvContainer(args,
|
|
315
|
-
case
|
|
316
|
-
return classifyAvContainer(args,
|
|
317
|
-
case
|
|
318
|
-
return classifyAvContainer(args,
|
|
319
|
-
case
|
|
364
|
+
return classifyAvContainer(args, 'isobmff', 'isobmff', probe, timeoutMs, ffprobeVersion);
|
|
365
|
+
case 'ebml':
|
|
366
|
+
return classifyAvContainer(args, 'webm', 'webm', probe, timeoutMs, ffprobeVersion);
|
|
367
|
+
case 'ogg':
|
|
368
|
+
return classifyAvContainer(args, 'ogg', 'ogg', probe, timeoutMs, ffprobeVersion);
|
|
369
|
+
case 'avi':
|
|
320
370
|
// Real container ffprobe parses; no registry entry (no origin enables it
|
|
321
371
|
// today) — the stream truth still classifies it, policy rejects by format.
|
|
322
|
-
return classifyAvContainer(args,
|
|
323
|
-
case
|
|
324
|
-
return classifyAvContainer(args,
|
|
325
|
-
case
|
|
326
|
-
return classifyAvContainer(args,
|
|
327
|
-
case
|
|
328
|
-
return classifyAvContainer(args,
|
|
372
|
+
return classifyAvContainer(args, 'avi', undefined, probe, timeoutMs, ffprobeVersion);
|
|
373
|
+
case 'wav':
|
|
374
|
+
return classifyAvContainer(args, 'wav', 'wav', probe, timeoutMs, ffprobeVersion);
|
|
375
|
+
case 'mp3':
|
|
376
|
+
return classifyAvContainer(args, 'mp3', 'mp3', probe, timeoutMs, ffprobeVersion);
|
|
377
|
+
case 'flac':
|
|
378
|
+
return classifyAvContainer(args, 'flac', 'flac', probe, timeoutMs, ffprobeVersion);
|
|
329
379
|
}
|
|
330
380
|
}
|
|
331
381
|
async function classifyAvContainer(args, container, formatId, probe, timeoutMs, ffprobeVersion) {
|
|
@@ -335,21 +385,27 @@ async function classifyAvContainer(args, container, formatId, probe, timeoutMs,
|
|
|
335
385
|
}
|
|
336
386
|
const parsed = ProbeJsonSchema.safeParse(probed.json);
|
|
337
387
|
if (!parsed.success)
|
|
338
|
-
return indeterminate(container,
|
|
388
|
+
return indeterminate(container, 'probe_schema_invalid', ffprobeVersion, formatId);
|
|
339
389
|
const rawStreams = parsed.data.streams ?? [];
|
|
340
390
|
if (rawStreams.length === 0)
|
|
341
|
-
return indeterminate(container,
|
|
342
|
-
const counts = {
|
|
391
|
+
return indeterminate(container, 'no_streams', ffprobeVersion, formatId);
|
|
392
|
+
const counts = {
|
|
393
|
+
audio: 0,
|
|
394
|
+
timedVideo: 0,
|
|
395
|
+
attachedPictures: 0,
|
|
396
|
+
imageFrames: 0,
|
|
397
|
+
auxiliary: 0,
|
|
398
|
+
};
|
|
343
399
|
const audioCodecs = new Set();
|
|
344
400
|
const videoCodecs = new Set();
|
|
345
401
|
let sawUnknownType = false;
|
|
346
402
|
for (const s of rawStreams) {
|
|
347
|
-
const type = s.codec_type ??
|
|
348
|
-
if (type ===
|
|
403
|
+
const type = s.codec_type ?? '';
|
|
404
|
+
if (type === 'audio') {
|
|
349
405
|
counts.audio++;
|
|
350
406
|
audioCodecs.add(normalizeCodec(s.codec_name));
|
|
351
407
|
}
|
|
352
|
-
else if (type ===
|
|
408
|
+
else if (type === 'video') {
|
|
353
409
|
// An attached picture (cover art) is NOT a timed presentation — but the
|
|
354
410
|
// disposition flag is untrusted container data, so it is COUNTED and the
|
|
355
411
|
// safety plan forces every such picture through the image hash path
|
|
@@ -362,7 +418,7 @@ async function classifyAvContainer(args, container, formatId, probe, timeoutMs,
|
|
|
362
418
|
videoCodecs.add(normalizeCodec(s.codec_name));
|
|
363
419
|
}
|
|
364
420
|
}
|
|
365
|
-
else if (type ===
|
|
421
|
+
else if (type === 'subtitle' || type === 'data' || type === 'attachment') {
|
|
366
422
|
counts.auxiliary++;
|
|
367
423
|
}
|
|
368
424
|
else {
|
|
@@ -377,41 +433,41 @@ async function classifyAvContainer(args, container, formatId, probe, timeoutMs,
|
|
|
377
433
|
// classification table (fail-closed)
|
|
378
434
|
if (counts.timedVideo > 0) {
|
|
379
435
|
return baseResult({
|
|
380
|
-
status:
|
|
381
|
-
canonicalKind:
|
|
436
|
+
status: 'definitive',
|
|
437
|
+
canonicalKind: 'video',
|
|
382
438
|
container,
|
|
383
439
|
...(formatId ? { formatId } : {}),
|
|
384
440
|
streams: counts,
|
|
385
441
|
codecs,
|
|
386
442
|
...duration,
|
|
387
|
-
safetyPlan:
|
|
388
|
-
reasonCode:
|
|
443
|
+
safetyPlan: 'video-frames',
|
|
444
|
+
reasonCode: 'ok',
|
|
389
445
|
}, ffprobeVersion);
|
|
390
446
|
}
|
|
391
447
|
if (counts.audio > 0 && !sawUnknownType) {
|
|
392
448
|
return baseResult({
|
|
393
|
-
status:
|
|
394
|
-
canonicalKind:
|
|
449
|
+
status: 'definitive',
|
|
450
|
+
canonicalKind: 'audio',
|
|
395
451
|
container,
|
|
396
452
|
...(formatId ? { formatId } : {}),
|
|
397
453
|
streams: counts,
|
|
398
454
|
codecs,
|
|
399
455
|
...duration,
|
|
400
|
-
safetyPlan: counts.attachedPictures > 0 ?
|
|
401
|
-
reasonCode:
|
|
456
|
+
safetyPlan: counts.attachedPictures > 0 ? 'audio-plus-artwork' : 'audio-only',
|
|
457
|
+
reasonCode: 'ok',
|
|
402
458
|
}, ffprobeVersion);
|
|
403
459
|
}
|
|
404
460
|
// auxiliary-only, unknown stream types alongside audio, or nothing usable:
|
|
405
461
|
// never audio — the strict visual fallback owns it.
|
|
406
462
|
return baseResult({
|
|
407
|
-
status:
|
|
463
|
+
status: 'indeterminate',
|
|
408
464
|
container,
|
|
409
465
|
...(formatId ? { formatId } : {}),
|
|
410
466
|
streams: counts,
|
|
411
467
|
codecs,
|
|
412
468
|
...duration,
|
|
413
|
-
safetyPlan:
|
|
414
|
-
reasonCode: sawUnknownType ?
|
|
469
|
+
safetyPlan: 'strict-video-fallback',
|
|
470
|
+
reasonCode: sawUnknownType ? 'unknown_stream_type' : 'auxiliary_only',
|
|
415
471
|
}, ffprobeVersion);
|
|
416
472
|
}
|
|
417
473
|
//# sourceMappingURL=inspect-media.js.map
|