@better-media/plugin-video-processing 0.1.1
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/LICENSE +21 -0
- package/dist/index.d.mts +88 -0
- package/dist/index.d.ts +88 -0
- package/dist/index.js +528 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +518 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +66 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,528 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var os = require('os');
|
|
4
|
+
var path = require('path');
|
|
5
|
+
var fs2 = require('fs/promises');
|
|
6
|
+
var crypto = require('crypto');
|
|
7
|
+
|
|
8
|
+
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
9
|
+
|
|
10
|
+
var os__default = /*#__PURE__*/_interopDefault(os);
|
|
11
|
+
var path__default = /*#__PURE__*/_interopDefault(path);
|
|
12
|
+
var fs2__default = /*#__PURE__*/_interopDefault(fs2);
|
|
13
|
+
|
|
14
|
+
// src/runtime/runner.ts
|
|
15
|
+
|
|
16
|
+
// src/constants/defaults.ts
|
|
17
|
+
var DEFAULT_THUMBNAIL_PRESETS = [
|
|
18
|
+
{ at: "10%", format: "jpeg", width: 640 }
|
|
19
|
+
];
|
|
20
|
+
var DEFAULT_VIDEO_MIME_TYPES = [
|
|
21
|
+
"video/mp4",
|
|
22
|
+
"video/quicktime",
|
|
23
|
+
"video/x-msvideo",
|
|
24
|
+
"video/x-matroska",
|
|
25
|
+
"video/webm",
|
|
26
|
+
"video/mpeg",
|
|
27
|
+
"video/ogg",
|
|
28
|
+
"video/3gpp",
|
|
29
|
+
"video/x-flv",
|
|
30
|
+
"video/x-ms-wmv"
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// src/interfaces/options.interface.ts
|
|
34
|
+
function resolveVideoProcessingOptions(opts) {
|
|
35
|
+
return {
|
|
36
|
+
thumbnails: opts.thumbnails === false ? false : opts.thumbnails && opts.thumbnails.length > 0 ? opts.thumbnails : [...DEFAULT_THUMBNAIL_PRESETS],
|
|
37
|
+
transcode: opts.transcode ?? [],
|
|
38
|
+
allowedMimeTypes: opts.allowedMimeTypes ?? [...DEFAULT_VIDEO_MIME_TYPES],
|
|
39
|
+
maxInputBytes: opts.maxInputBytes,
|
|
40
|
+
ffmpegPath: opts.ffmpegPath,
|
|
41
|
+
ffprobePath: opts.ffprobePath,
|
|
42
|
+
derivativePrefix: opts.derivativePrefix ?? "processing",
|
|
43
|
+
persistMediaVersions: opts.persistMediaVersions !== false,
|
|
44
|
+
skipExistingDerivatives: opts.skipExistingDerivatives !== false,
|
|
45
|
+
timeoutMs: opts.timeoutMs ?? 3e5
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
async function readBufferForProcessing(context) {
|
|
49
|
+
const fileContent = context.utilities?.fileContent;
|
|
50
|
+
if (fileContent?.buffer) return fileContent.buffer;
|
|
51
|
+
if (fileContent?.tempPath) return fs2__default.default.readFile(fileContent.tempPath);
|
|
52
|
+
return context.storage.get(context.file.key);
|
|
53
|
+
}
|
|
54
|
+
function isReferenceUrlMode(context) {
|
|
55
|
+
const url = context.storageLocation?.url;
|
|
56
|
+
return typeof url === "string" && url === context.file.key;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// src/runtime/ffmpeg.ts
|
|
60
|
+
async function tryImportFfmpeg() {
|
|
61
|
+
try {
|
|
62
|
+
const mod = await import('fluent-ffmpeg');
|
|
63
|
+
return mod.default ?? mod;
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
function parseFramerate(str) {
|
|
69
|
+
if (!str) return void 0;
|
|
70
|
+
const parts = str.split("/");
|
|
71
|
+
if (parts.length !== 2) return void 0;
|
|
72
|
+
const num = parseFloat(parts[0]);
|
|
73
|
+
const den = parseFloat(parts[1]);
|
|
74
|
+
if (!den || !Number.isFinite(num) || !Number.isFinite(den)) return void 0;
|
|
75
|
+
const fps = Math.round(num / den * 100) / 100;
|
|
76
|
+
return fps > 0 ? fps : void 0;
|
|
77
|
+
}
|
|
78
|
+
function buildScaleFilter(width, height) {
|
|
79
|
+
if (width && height) return `scale=${width}:${height}`;
|
|
80
|
+
if (width) return `scale=${width}:-2`;
|
|
81
|
+
if (height) return `scale=-2:${height}`;
|
|
82
|
+
return void 0;
|
|
83
|
+
}
|
|
84
|
+
function parseResolution(resolution) {
|
|
85
|
+
if (!resolution) return {};
|
|
86
|
+
const match = resolution.match(/^(\d+)x(\d+)$/i);
|
|
87
|
+
if (!match) return {};
|
|
88
|
+
return { width: parseInt(match[1], 10), height: parseInt(match[2], 10) };
|
|
89
|
+
}
|
|
90
|
+
function resolveTimestamp(at, durationSecs) {
|
|
91
|
+
if (typeof at === "number") return Math.max(0, at);
|
|
92
|
+
if (typeof at === "string" && at.endsWith("%")) {
|
|
93
|
+
const pct = parseFloat(at) / 100;
|
|
94
|
+
return Math.max(0, pct * durationSecs);
|
|
95
|
+
}
|
|
96
|
+
return Math.max(0, parseFloat(at) || 0);
|
|
97
|
+
}
|
|
98
|
+
async function probeVideo(inputPath, opts = {}) {
|
|
99
|
+
const ffmpeg = await tryImportFfmpeg();
|
|
100
|
+
if (!ffmpeg) throw new FfmpegNotFoundError();
|
|
101
|
+
if (opts.ffprobePath) ffmpeg.setFfprobePath(opts.ffprobePath);
|
|
102
|
+
return new Promise((resolve, reject) => {
|
|
103
|
+
ffmpeg.ffprobe(inputPath, (err, data) => {
|
|
104
|
+
if (err) return reject(err);
|
|
105
|
+
const videoStream = data.streams.find((s) => s.codec_type === "video");
|
|
106
|
+
const audioStream = data.streams.find((s) => s.codec_type === "audio");
|
|
107
|
+
const durationStr = data.format?.duration;
|
|
108
|
+
resolve({
|
|
109
|
+
duration: durationStr ? parseFloat(String(durationStr)) : 0,
|
|
110
|
+
width: videoStream?.width,
|
|
111
|
+
height: videoStream?.height,
|
|
112
|
+
videoCodec: videoStream?.codec_name,
|
|
113
|
+
audioCodec: audioStream?.codec_name,
|
|
114
|
+
framerate: parseFramerate(videoStream?.r_frame_rate),
|
|
115
|
+
bitrate: data.format?.bit_rate ? parseInt(String(data.format.bit_rate), 10) : void 0
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
async function extractThumbnail(inputPath, outputPath, preset, durationSecs, opts = {}) {
|
|
121
|
+
const ffmpeg = await tryImportFfmpeg();
|
|
122
|
+
if (!ffmpeg) throw new FfmpegNotFoundError();
|
|
123
|
+
const seekSecs = resolveTimestamp(preset.at, durationSecs);
|
|
124
|
+
const format = preset.format ?? "jpeg";
|
|
125
|
+
const scaleFilter = buildScaleFilter(preset.width, preset.height);
|
|
126
|
+
return new Promise((resolve, reject) => {
|
|
127
|
+
const cmd = ffmpeg(inputPath);
|
|
128
|
+
if (opts.ffmpegPath) cmd.setFfmpegPath(opts.ffmpegPath);
|
|
129
|
+
cmd.seekInput(seekSecs).outputOptions(["-vframes", "1"]);
|
|
130
|
+
if (scaleFilter) cmd.videoFilter(scaleFilter);
|
|
131
|
+
if (format === "webp") {
|
|
132
|
+
cmd.videoCodec("libwebp").outputOptions(["-quality", "85"]);
|
|
133
|
+
} else if (format === "png") {
|
|
134
|
+
cmd.outputFormat("image2").videoCodec("png");
|
|
135
|
+
} else {
|
|
136
|
+
cmd.outputFormat("image2").videoCodec("mjpeg").outputOptions(["-q:v", "2"]);
|
|
137
|
+
}
|
|
138
|
+
cmd.output(outputPath);
|
|
139
|
+
cmd.on("error", (err) => reject(err));
|
|
140
|
+
cmd.on("end", () => resolve({ width: preset.width, height: preset.height }));
|
|
141
|
+
cmd.run();
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
async function transcodeVideo(inputPath, outputPath, preset, opts = {}) {
|
|
145
|
+
const ffmpeg = await tryImportFfmpeg();
|
|
146
|
+
if (!ffmpeg) throw new FfmpegNotFoundError();
|
|
147
|
+
const videoCodec = preset.videoCodec ?? (preset.format === "webm" ? "libvpx-vp9" : "libx264");
|
|
148
|
+
const audioCodec = preset.audioCodec ?? (preset.format === "webm" ? "libopus" : "aac");
|
|
149
|
+
const { width, height } = parseResolution(preset.resolution);
|
|
150
|
+
const scaleFilter = buildScaleFilter(width, height);
|
|
151
|
+
let duration = 0;
|
|
152
|
+
return new Promise((resolve, reject) => {
|
|
153
|
+
const cmd = ffmpeg(inputPath);
|
|
154
|
+
if (opts.ffmpegPath) cmd.setFfmpegPath(opts.ffmpegPath);
|
|
155
|
+
cmd.videoCodec(videoCodec).audioCodec(audioCodec);
|
|
156
|
+
if (preset.videoBitrate) cmd.videoBitrate(preset.videoBitrate);
|
|
157
|
+
if (preset.audioBitrate) cmd.audioBitrate(preset.audioBitrate);
|
|
158
|
+
if (scaleFilter) cmd.videoFilter(scaleFilter);
|
|
159
|
+
cmd.output(outputPath);
|
|
160
|
+
cmd.on("error", (err) => reject(err));
|
|
161
|
+
cmd.on("codecData", (data) => {
|
|
162
|
+
if (data.duration) {
|
|
163
|
+
const parts = data.duration.split(":").map(Number);
|
|
164
|
+
duration = (parts[0] ?? 0) * 3600 + (parts[1] ?? 0) * 60 + (parts[2] ?? 0);
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
cmd.on("end", () => resolve({ duration }));
|
|
168
|
+
cmd.run();
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
var FfmpegNotFoundError = class extends Error {
|
|
172
|
+
constructor() {
|
|
173
|
+
super(
|
|
174
|
+
"fluent-ffmpeg could not be imported. Install it: pnpm add fluent-ffmpeg. Also ensure the ffmpeg binary is available in PATH."
|
|
175
|
+
);
|
|
176
|
+
this.name = "FfmpegNotFoundError";
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// src/runtime/next-version.ts
|
|
181
|
+
async function nextMediaVersionStart(database, mediaId) {
|
|
182
|
+
const rows = await database.findMany({
|
|
183
|
+
model: "media_versions",
|
|
184
|
+
where: [{ field: "mediaId", value: mediaId }],
|
|
185
|
+
sortBy: { field: "versionNumber", direction: "desc" },
|
|
186
|
+
limit: 1
|
|
187
|
+
});
|
|
188
|
+
const max = rows[0]?.versionNumber;
|
|
189
|
+
return typeof max === "number" && Number.isFinite(max) ? max + 1 : 1;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/runtime/runner.ts
|
|
193
|
+
function withTimeout(promise, ms, label) {
|
|
194
|
+
if (!Number.isFinite(ms) || ms <= 0) return promise;
|
|
195
|
+
return new Promise((resolve, reject) => {
|
|
196
|
+
const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
|
|
197
|
+
promise.then(
|
|
198
|
+
(v) => {
|
|
199
|
+
clearTimeout(t);
|
|
200
|
+
resolve(v);
|
|
201
|
+
},
|
|
202
|
+
(e) => {
|
|
203
|
+
clearTimeout(t);
|
|
204
|
+
reject(e);
|
|
205
|
+
}
|
|
206
|
+
);
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
function thumbnailExt(format) {
|
|
210
|
+
switch (format) {
|
|
211
|
+
case "png":
|
|
212
|
+
return ".png";
|
|
213
|
+
case "webp":
|
|
214
|
+
return ".webp";
|
|
215
|
+
default:
|
|
216
|
+
return ".jpg";
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
function thumbnailMime(format) {
|
|
220
|
+
switch (format) {
|
|
221
|
+
case "png":
|
|
222
|
+
return "image/png";
|
|
223
|
+
case "webp":
|
|
224
|
+
return "image/webp";
|
|
225
|
+
default:
|
|
226
|
+
return "image/jpeg";
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
function transcodeMime(format) {
|
|
230
|
+
return format === "webm" ? "video/webm" : "video/mp4";
|
|
231
|
+
}
|
|
232
|
+
async function runVideoProcessing(context, api, opts) {
|
|
233
|
+
const resolved = resolveVideoProcessingOptions(opts);
|
|
234
|
+
if (isReferenceUrlMode(context)) {
|
|
235
|
+
api.emitMetadata({ skipped: "reference-url" });
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
const mime = context.file.mimeType;
|
|
239
|
+
if (!mime || !resolved.allowedMimeTypes.includes(mime)) {
|
|
240
|
+
api.emitMetadata({ skipped: "mime-not-allowed", mimeType: mime });
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
let buffer;
|
|
244
|
+
try {
|
|
245
|
+
buffer = await withTimeout(
|
|
246
|
+
readBufferForProcessing(context),
|
|
247
|
+
resolved.timeoutMs,
|
|
248
|
+
"readBufferForProcessing"
|
|
249
|
+
);
|
|
250
|
+
} catch (err) {
|
|
251
|
+
api.emitMetadata({
|
|
252
|
+
error: "read-failed",
|
|
253
|
+
message: err instanceof Error ? err.message : String(err)
|
|
254
|
+
});
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
if (buffer == null) {
|
|
258
|
+
api.emitMetadata({ skipped: "no-file-content" });
|
|
259
|
+
return;
|
|
260
|
+
}
|
|
261
|
+
if (resolved.maxInputBytes != null && buffer.length > resolved.maxInputBytes) {
|
|
262
|
+
api.emitMetadata({
|
|
263
|
+
skipped: "input-too-large",
|
|
264
|
+
size: buffer.length,
|
|
265
|
+
maxInputBytes: resolved.maxInputBytes
|
|
266
|
+
});
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
const tempDir = path__default.default.join(os__default.default.tmpdir(), crypto.randomUUID());
|
|
270
|
+
await fs2__default.default.mkdir(tempDir, { recursive: true });
|
|
271
|
+
const ext = context.file.extension ?? ".mp4";
|
|
272
|
+
const inputPath = path__default.default.join(tempDir, `input${ext}`);
|
|
273
|
+
await fs2__default.default.writeFile(inputPath, buffer);
|
|
274
|
+
try {
|
|
275
|
+
await runWithInput(context, api, resolved, inputPath, tempDir);
|
|
276
|
+
} finally {
|
|
277
|
+
await fs2__default.default.rm(tempDir, { recursive: true, force: true });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
async function runWithInput(context, api, resolved, inputPath, tempDir) {
|
|
281
|
+
let probe;
|
|
282
|
+
try {
|
|
283
|
+
probe = await withTimeout(
|
|
284
|
+
probeVideo(inputPath, { ffprobePath: resolved.ffprobePath }),
|
|
285
|
+
resolved.timeoutMs,
|
|
286
|
+
"probeVideo"
|
|
287
|
+
);
|
|
288
|
+
} catch (err) {
|
|
289
|
+
if (err instanceof FfmpegNotFoundError) {
|
|
290
|
+
api.emitMetadata({ error: "ffmpeg-not-found", message: err.message });
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
api.emitMetadata({
|
|
294
|
+
error: "probe-failed",
|
|
295
|
+
message: err instanceof Error ? err.message : String(err)
|
|
296
|
+
});
|
|
297
|
+
return;
|
|
298
|
+
}
|
|
299
|
+
const ffOpts = { ffmpegPath: resolved.ffmpegPath };
|
|
300
|
+
const prefix = resolved.derivativePrefix;
|
|
301
|
+
const recordId = context.recordId;
|
|
302
|
+
const thumbnailResults = [];
|
|
303
|
+
if (resolved.thumbnails !== false) {
|
|
304
|
+
let versionCounter = await nextMediaVersionStart(context.database, recordId);
|
|
305
|
+
for (let i = 0; i < resolved.thumbnails.length; i++) {
|
|
306
|
+
const preset = resolved.thumbnails[i];
|
|
307
|
+
const format = preset.format ?? "jpeg";
|
|
308
|
+
const ext = thumbnailExt(preset.format);
|
|
309
|
+
const storageKey = `${prefix}/${recordId}/thumbnails/thumb_${i}${ext}`;
|
|
310
|
+
if (resolved.skipExistingDerivatives && await context.storage.exists(storageKey)) {
|
|
311
|
+
thumbnailResults.push({
|
|
312
|
+
key: storageKey,
|
|
313
|
+
format,
|
|
314
|
+
width: preset.width,
|
|
315
|
+
height: preset.height,
|
|
316
|
+
index: i
|
|
317
|
+
});
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
const outputPath = path__default.default.join(tempDir, `thumb_${i}${ext}`);
|
|
321
|
+
let dims = {};
|
|
322
|
+
try {
|
|
323
|
+
dims = await withTimeout(
|
|
324
|
+
extractThumbnail(inputPath, outputPath, preset, probe.duration, ffOpts),
|
|
325
|
+
resolved.timeoutMs,
|
|
326
|
+
`extractThumbnail[${i}]`
|
|
327
|
+
);
|
|
328
|
+
} catch (err) {
|
|
329
|
+
if (err instanceof FfmpegNotFoundError) {
|
|
330
|
+
api.emitMetadata({ error: "ffmpeg-not-found", message: err.message });
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
api.emitMetadata({
|
|
334
|
+
error: "thumbnail-failed",
|
|
335
|
+
index: i,
|
|
336
|
+
message: err instanceof Error ? err.message : String(err)
|
|
337
|
+
});
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
let thumbBuf;
|
|
341
|
+
try {
|
|
342
|
+
thumbBuf = await fs2__default.default.readFile(outputPath);
|
|
343
|
+
} catch (err) {
|
|
344
|
+
api.emitMetadata({
|
|
345
|
+
error: "thumbnail-read-failed",
|
|
346
|
+
index: i,
|
|
347
|
+
message: err instanceof Error ? err.message : String(err)
|
|
348
|
+
});
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
await context.storage.put(storageKey, thumbBuf);
|
|
353
|
+
} catch (err) {
|
|
354
|
+
api.emitMetadata({
|
|
355
|
+
error: "storage-put-failed",
|
|
356
|
+
key: storageKey,
|
|
357
|
+
message: err instanceof Error ? err.message : String(err)
|
|
358
|
+
});
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
thumbnailResults.push({
|
|
362
|
+
key: storageKey,
|
|
363
|
+
format,
|
|
364
|
+
width: dims.width,
|
|
365
|
+
height: dims.height,
|
|
366
|
+
index: i
|
|
367
|
+
});
|
|
368
|
+
if (resolved.persistMediaVersions) {
|
|
369
|
+
try {
|
|
370
|
+
await context.database.create({
|
|
371
|
+
model: "media_versions",
|
|
372
|
+
data: {
|
|
373
|
+
id: crypto.randomUUID(),
|
|
374
|
+
mediaId: recordId,
|
|
375
|
+
storageKey,
|
|
376
|
+
mimeType: thumbnailMime(preset.format),
|
|
377
|
+
size: thumbBuf.length,
|
|
378
|
+
width: dims.width,
|
|
379
|
+
height: dims.height,
|
|
380
|
+
isOriginal: false,
|
|
381
|
+
type: "thumbnail",
|
|
382
|
+
versionNumber: versionCounter,
|
|
383
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
384
|
+
}
|
|
385
|
+
});
|
|
386
|
+
versionCounter += 1;
|
|
387
|
+
} catch (err) {
|
|
388
|
+
api.emitMetadata({
|
|
389
|
+
error: "media-versions-persist-failed",
|
|
390
|
+
key: storageKey,
|
|
391
|
+
message: err instanceof Error ? err.message : String(err)
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const transcodeResults = [];
|
|
398
|
+
if (resolved.transcode.length > 0) {
|
|
399
|
+
let versionCounter = await nextMediaVersionStart(context.database, recordId);
|
|
400
|
+
for (const preset of resolved.transcode) {
|
|
401
|
+
const storageKey = `${prefix}/${recordId}/transcode/${preset.name}.${preset.format}`;
|
|
402
|
+
if (resolved.skipExistingDerivatives && await context.storage.exists(storageKey)) {
|
|
403
|
+
transcodeResults.push({
|
|
404
|
+
key: storageKey,
|
|
405
|
+
name: preset.name,
|
|
406
|
+
format: preset.format,
|
|
407
|
+
duration: probe.duration
|
|
408
|
+
});
|
|
409
|
+
continue;
|
|
410
|
+
}
|
|
411
|
+
const outputPath = path__default.default.join(tempDir, `transcode_${preset.name}.${preset.format}`);
|
|
412
|
+
let transcoded;
|
|
413
|
+
try {
|
|
414
|
+
transcoded = await withTimeout(
|
|
415
|
+
transcodeVideo(inputPath, outputPath, preset, ffOpts),
|
|
416
|
+
resolved.timeoutMs,
|
|
417
|
+
`transcodeVideo[${preset.name}]`
|
|
418
|
+
);
|
|
419
|
+
} catch (err) {
|
|
420
|
+
if (err instanceof FfmpegNotFoundError) {
|
|
421
|
+
api.emitMetadata({ error: "ffmpeg-not-found", message: err.message });
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
api.emitMetadata({
|
|
425
|
+
error: "transcode-failed",
|
|
426
|
+
name: preset.name,
|
|
427
|
+
message: err instanceof Error ? err.message : String(err)
|
|
428
|
+
});
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
431
|
+
let outBuf;
|
|
432
|
+
try {
|
|
433
|
+
outBuf = await fs2__default.default.readFile(outputPath);
|
|
434
|
+
} catch (err) {
|
|
435
|
+
api.emitMetadata({
|
|
436
|
+
error: "transcode-read-failed",
|
|
437
|
+
name: preset.name,
|
|
438
|
+
message: err instanceof Error ? err.message : String(err)
|
|
439
|
+
});
|
|
440
|
+
continue;
|
|
441
|
+
}
|
|
442
|
+
try {
|
|
443
|
+
await context.storage.put(storageKey, outBuf);
|
|
444
|
+
} catch (err) {
|
|
445
|
+
api.emitMetadata({
|
|
446
|
+
error: "storage-put-failed",
|
|
447
|
+
key: storageKey,
|
|
448
|
+
message: err instanceof Error ? err.message : String(err)
|
|
449
|
+
});
|
|
450
|
+
continue;
|
|
451
|
+
}
|
|
452
|
+
transcodeResults.push({
|
|
453
|
+
key: storageKey,
|
|
454
|
+
name: preset.name,
|
|
455
|
+
format: preset.format,
|
|
456
|
+
duration: transcoded.duration
|
|
457
|
+
});
|
|
458
|
+
if (resolved.persistMediaVersions) {
|
|
459
|
+
try {
|
|
460
|
+
await context.database.create({
|
|
461
|
+
model: "media_versions",
|
|
462
|
+
data: {
|
|
463
|
+
id: crypto.randomUUID(),
|
|
464
|
+
mediaId: recordId,
|
|
465
|
+
storageKey,
|
|
466
|
+
mimeType: transcodeMime(preset.format),
|
|
467
|
+
size: outBuf.length,
|
|
468
|
+
isOriginal: false,
|
|
469
|
+
type: "compressed",
|
|
470
|
+
versionNumber: versionCounter,
|
|
471
|
+
createdAt: /* @__PURE__ */ new Date()
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
versionCounter += 1;
|
|
475
|
+
} catch (err) {
|
|
476
|
+
api.emitMetadata({
|
|
477
|
+
error: "media-versions-persist-failed",
|
|
478
|
+
key: storageKey,
|
|
479
|
+
message: err instanceof Error ? err.message : String(err)
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
api.emitMetadata({
|
|
486
|
+
duration: probe.duration,
|
|
487
|
+
videoCodec: probe.videoCodec,
|
|
488
|
+
audioCodec: probe.audioCodec,
|
|
489
|
+
width: probe.width,
|
|
490
|
+
height: probe.height,
|
|
491
|
+
framerate: probe.framerate,
|
|
492
|
+
bitrate: probe.bitrate
|
|
493
|
+
});
|
|
494
|
+
const processingPatch = {};
|
|
495
|
+
if (thumbnailResults.length > 0) processingPatch.thumbnails = thumbnailResults;
|
|
496
|
+
if (transcodeResults.length > 0) processingPatch.transcode = transcodeResults;
|
|
497
|
+
if (Object.keys(processingPatch).length > 0) api.emitProcessing(processingPatch);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// src/index.ts
|
|
501
|
+
function videoProcessingPlugin(opts = {}) {
|
|
502
|
+
const executionMode = opts.executionMode ?? "background";
|
|
503
|
+
return {
|
|
504
|
+
name: "video-processing",
|
|
505
|
+
runtimeManifest: {
|
|
506
|
+
id: "better-media-video-processing",
|
|
507
|
+
version: "1.0.0",
|
|
508
|
+
trustLevel: "untrusted",
|
|
509
|
+
capabilities: ["file.read", "metadata.write.own", "processing.write.own"],
|
|
510
|
+
namespace: "video-processing"
|
|
511
|
+
},
|
|
512
|
+
executionMode,
|
|
513
|
+
intensive: true,
|
|
514
|
+
apply(runtime) {
|
|
515
|
+
runtime.hooks["process:run"].tap(
|
|
516
|
+
"video-processing",
|
|
517
|
+
async (context, api) => runVideoProcessing(context, api, opts),
|
|
518
|
+
{ mode: executionMode }
|
|
519
|
+
);
|
|
520
|
+
}
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
exports.DEFAULT_THUMBNAIL_PRESETS = DEFAULT_THUMBNAIL_PRESETS;
|
|
525
|
+
exports.DEFAULT_VIDEO_MIME_TYPES = DEFAULT_VIDEO_MIME_TYPES;
|
|
526
|
+
exports.videoProcessingPlugin = videoProcessingPlugin;
|
|
527
|
+
//# sourceMappingURL=index.js.map
|
|
528
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/constants/defaults.ts","../src/interfaces/options.interface.ts","../src/runtime/buffer.ts","../src/runtime/ffmpeg.ts","../src/runtime/next-version.ts","../src/runtime/runner.ts","../src/index.ts"],"names":["fs","path","os","randomUUID"],"mappings":"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","file":"index.js","sourcesContent":["import type { ThumbnailPreset } from \"../interfaces/options.interface\";\n\nexport const DEFAULT_THUMBNAIL_PRESETS: ThumbnailPreset[] = [\n { at: \"10%\", format: \"jpeg\", width: 640 },\n];\n\nexport const DEFAULT_VIDEO_MIME_TYPES: string[] = [\n \"video/mp4\",\n \"video/quicktime\",\n \"video/x-msvideo\",\n \"video/x-matroska\",\n \"video/webm\",\n \"video/mpeg\",\n \"video/ogg\",\n \"video/3gpp\",\n \"video/x-flv\",\n \"video/x-ms-wmv\",\n];\n","import { DEFAULT_THUMBNAIL_PRESETS, DEFAULT_VIDEO_MIME_TYPES } from \"../constants/defaults\";\n\n/** A single thumbnail to extract from the video. */\nexport interface ThumbnailPreset {\n /**\n * Position to extract.\n * - number: seconds from start (e.g. 5 = 5s)\n * - string ending with \"%\": percentage of total duration (e.g. \"10%\")\n * - other string: parsed as seconds (e.g. \"5.5\")\n */\n at: number | string;\n /** Output image format. Default: \"jpeg\". */\n format?: \"jpeg\" | \"png\" | \"webp\";\n /** Output width in pixels. Aspect ratio preserved when only one dimension is set. */\n width?: number;\n /** Output height in pixels. */\n height?: number;\n}\n\n/** A single transcode target. */\nexport interface TranscodePreset {\n /** Identifier used in storage key and media_versions. Must be stable. */\n name: string;\n /** Output container format. */\n format: \"mp4\" | \"webm\";\n /** Video codec. Defaults: libx264 (mp4), libvpx-vp9 (webm). */\n videoCodec?: string;\n /** Audio codec. Defaults: aac (mp4), libopus (webm). */\n audioCodec?: string;\n /** Video bitrate, e.g. \"1000k\". */\n videoBitrate?: string;\n /** Audio bitrate, e.g. \"128k\". */\n audioBitrate?: string;\n /** Resolution as \"WxH\", e.g. \"1280x720\". Omit to keep source resolution. */\n resolution?: string;\n}\n\nexport interface VideoProcessingPluginOptions {\n /** Execution mode. Default: \"background\". */\n executionMode?: \"sync\" | \"background\";\n\n /**\n * Thumbnail presets to extract.\n * Pass `false` to disable thumbnail extraction entirely.\n * Default: one JPEG at 10% of duration, 640px wide.\n */\n thumbnails?: ThumbnailPreset[] | false;\n\n /**\n * Transcode presets to produce. Omit or pass an empty array for no transcoding.\n * Each preset produces one output file stored as a media_versions row.\n */\n transcode?: TranscodePreset[];\n\n /** Only process files whose mimeType is in this list. Default: common video types. */\n allowedMimeTypes?: string[];\n\n /**\n * Skip the plugin if the source file is larger than this (bytes).\n * Useful for very large uploads that shouldn't block the sync path.\n * Default: no limit.\n */\n maxInputBytes?: number;\n\n /** Override the ffmpeg binary path. Default: resolved from PATH. */\n ffmpegPath?: string;\n\n /** Override the ffprobe binary path. Default: resolved from PATH. */\n ffprobePath?: string;\n\n /** Storage key prefix for derivative files. Default: \"processing\". */\n derivativePrefix?: string;\n\n /**\n * Insert rows into media_versions for thumbnails and transcoded outputs.\n * Default: true.\n */\n persistMediaVersions?: boolean;\n\n /**\n * Skip a thumbnail or transcode if the storage key already exists.\n * Default: true.\n */\n skipExistingDerivatives?: boolean;\n\n /** Timeout for the full processing run in milliseconds. Default: 300_000 (5 min). */\n timeoutMs?: number;\n}\n\nexport type ResolvedVideoProcessingOptions = Required<\n Pick<\n VideoProcessingPluginOptions,\n | \"allowedMimeTypes\"\n | \"derivativePrefix\"\n | \"persistMediaVersions\"\n | \"skipExistingDerivatives\"\n | \"timeoutMs\"\n >\n> & {\n thumbnails: ThumbnailPreset[] | false;\n transcode: TranscodePreset[];\n maxInputBytes: number | undefined;\n ffmpegPath: string | undefined;\n ffprobePath: string | undefined;\n};\n\nexport function resolveVideoProcessingOptions(\n opts: VideoProcessingPluginOptions\n): ResolvedVideoProcessingOptions {\n return {\n thumbnails:\n opts.thumbnails === false\n ? false\n : opts.thumbnails && opts.thumbnails.length > 0\n ? opts.thumbnails\n : [...DEFAULT_THUMBNAIL_PRESETS],\n transcode: opts.transcode ?? [],\n allowedMimeTypes: opts.allowedMimeTypes ?? [...DEFAULT_VIDEO_MIME_TYPES],\n maxInputBytes: opts.maxInputBytes,\n ffmpegPath: opts.ffmpegPath,\n ffprobePath: opts.ffprobePath,\n derivativePrefix: opts.derivativePrefix ?? \"processing\",\n persistMediaVersions: opts.persistMediaVersions !== false,\n skipExistingDerivatives: opts.skipExistingDerivatives !== false,\n timeoutMs: opts.timeoutMs ?? 300_000,\n };\n}\n","import fs from \"node:fs/promises\";\nimport type { PipelineContext } from \"@better-media/core\";\n\nexport async function readBufferForProcessing(context: PipelineContext): Promise<Buffer | null> {\n const fileContent = context.utilities?.fileContent;\n if (fileContent?.buffer) return fileContent.buffer;\n if (fileContent?.tempPath) return fs.readFile(fileContent.tempPath);\n return context.storage.get(context.file.key);\n}\n\nexport function isReferenceUrlMode(context: PipelineContext): boolean {\n const url = context.storageLocation?.url;\n return typeof url === \"string\" && url === context.file.key;\n}\n","import type { ThumbnailPreset, TranscodePreset } from \"../interfaces/options.interface\";\n\nexport interface VideoProbeResult {\n duration: number;\n width?: number;\n height?: number;\n videoCodec?: string;\n audioCodec?: string;\n framerate?: number;\n bitrate?: number;\n}\n\nasync function tryImportFfmpeg(): Promise<typeof import(\"fluent-ffmpeg\") | null> {\n try {\n const mod = await import(\"fluent-ffmpeg\");\n return mod.default ?? (mod as unknown as typeof import(\"fluent-ffmpeg\"));\n } catch {\n return null;\n }\n}\n\nfunction parseFramerate(str?: string): number | undefined {\n if (!str) return undefined;\n const parts = str.split(\"/\");\n if (parts.length !== 2) return undefined;\n const num = parseFloat(parts[0]!);\n const den = parseFloat(parts[1]!);\n if (!den || !Number.isFinite(num) || !Number.isFinite(den)) return undefined;\n const fps = Math.round((num / den) * 100) / 100;\n return fps > 0 ? fps : undefined;\n}\n\nfunction buildScaleFilter(width?: number, height?: number): string | undefined {\n if (width && height) return `scale=${width}:${height}`;\n if (width) return `scale=${width}:-2`;\n if (height) return `scale=-2:${height}`;\n return undefined;\n}\n\nfunction parseResolution(resolution?: string): { width?: number; height?: number } {\n if (!resolution) return {};\n const match = resolution.match(/^(\\d+)x(\\d+)$/i);\n if (!match) return {};\n return { width: parseInt(match[1]!, 10), height: parseInt(match[2]!, 10) };\n}\n\nexport function resolveTimestamp(at: number | string, durationSecs: number): number {\n if (typeof at === \"number\") return Math.max(0, at);\n if (typeof at === \"string\" && at.endsWith(\"%\")) {\n const pct = parseFloat(at) / 100;\n return Math.max(0, pct * durationSecs);\n }\n return Math.max(0, parseFloat(at as string) || 0);\n}\n\nexport async function probeVideo(\n inputPath: string,\n opts: { ffprobePath?: string } = {}\n): Promise<VideoProbeResult> {\n const ffmpeg = await tryImportFfmpeg();\n if (!ffmpeg) throw new FfmpegNotFoundError();\n\n if (opts.ffprobePath) ffmpeg.setFfprobePath(opts.ffprobePath);\n\n return new Promise((resolve, reject) => {\n ffmpeg.ffprobe(inputPath, (err, data) => {\n if (err) return reject(err);\n\n const videoStream = data.streams.find((s) => s.codec_type === \"video\");\n const audioStream = data.streams.find((s) => s.codec_type === \"audio\");\n const durationStr = data.format?.duration;\n\n resolve({\n duration: durationStr ? parseFloat(String(durationStr)) : 0,\n width: videoStream?.width,\n height: videoStream?.height,\n videoCodec: videoStream?.codec_name,\n audioCodec: audioStream?.codec_name,\n framerate: parseFramerate(videoStream?.r_frame_rate),\n bitrate: data.format?.bit_rate ? parseInt(String(data.format.bit_rate), 10) : undefined,\n });\n });\n });\n}\n\nexport async function extractThumbnail(\n inputPath: string,\n outputPath: string,\n preset: ThumbnailPreset,\n durationSecs: number,\n opts: { ffmpegPath?: string } = {}\n): Promise<{ width?: number; height?: number }> {\n const ffmpeg = await tryImportFfmpeg();\n if (!ffmpeg) throw new FfmpegNotFoundError();\n\n const seekSecs = resolveTimestamp(preset.at, durationSecs);\n const format = preset.format ?? \"jpeg\";\n const scaleFilter = buildScaleFilter(preset.width, preset.height);\n\n return new Promise((resolve, reject) => {\n const cmd = ffmpeg(inputPath);\n if (opts.ffmpegPath) cmd.setFfmpegPath(opts.ffmpegPath);\n\n cmd.seekInput(seekSecs).outputOptions([\"-vframes\", \"1\"]);\n\n if (scaleFilter) cmd.videoFilter(scaleFilter);\n\n if (format === \"webp\") {\n cmd.videoCodec(\"libwebp\").outputOptions([\"-quality\", \"85\"]);\n } else if (format === \"png\") {\n cmd.outputFormat(\"image2\").videoCodec(\"png\");\n } else {\n cmd.outputFormat(\"image2\").videoCodec(\"mjpeg\").outputOptions([\"-q:v\", \"2\"]);\n }\n\n cmd.output(outputPath);\n cmd.on(\"error\", (err: Error) => reject(err));\n cmd.on(\"end\", () => resolve({ width: preset.width, height: preset.height }));\n cmd.run();\n });\n}\n\nexport async function transcodeVideo(\n inputPath: string,\n outputPath: string,\n preset: TranscodePreset,\n opts: { ffmpegPath?: string } = {}\n): Promise<{ duration: number }> {\n const ffmpeg = await tryImportFfmpeg();\n if (!ffmpeg) throw new FfmpegNotFoundError();\n\n const videoCodec = preset.videoCodec ?? (preset.format === \"webm\" ? \"libvpx-vp9\" : \"libx264\");\n const audioCodec = preset.audioCodec ?? (preset.format === \"webm\" ? \"libopus\" : \"aac\");\n const { width, height } = parseResolution(preset.resolution);\n const scaleFilter = buildScaleFilter(width, height);\n\n let duration = 0;\n\n return new Promise((resolve, reject) => {\n const cmd = ffmpeg(inputPath);\n if (opts.ffmpegPath) cmd.setFfmpegPath(opts.ffmpegPath);\n\n cmd.videoCodec(videoCodec).audioCodec(audioCodec);\n\n if (preset.videoBitrate) cmd.videoBitrate(preset.videoBitrate);\n if (preset.audioBitrate) cmd.audioBitrate(preset.audioBitrate);\n if (scaleFilter) cmd.videoFilter(scaleFilter);\n\n cmd.output(outputPath);\n cmd.on(\"error\", (err: Error) => reject(err));\n cmd.on(\"codecData\", (data: { duration?: string }) => {\n if (data.duration) {\n const parts = data.duration.split(\":\").map(Number);\n duration = (parts[0] ?? 0) * 3600 + (parts[1] ?? 0) * 60 + (parts[2] ?? 0);\n }\n });\n cmd.on(\"end\", () => resolve({ duration }));\n cmd.run();\n });\n}\n\nexport class FfmpegNotFoundError extends Error {\n constructor() {\n super(\n \"fluent-ffmpeg could not be imported. Install it: pnpm add fluent-ffmpeg. \" +\n \"Also ensure the ffmpeg binary is available in PATH.\"\n );\n this.name = \"FfmpegNotFoundError\";\n }\n}\n","import type { DatabaseAdapter } from \"@better-media/core\";\n\nexport async function nextMediaVersionStart(\n database: DatabaseAdapter,\n mediaId: string\n): Promise<number> {\n const rows = await database.findMany({\n model: \"media_versions\",\n where: [{ field: \"mediaId\", value: mediaId }],\n sortBy: { field: \"versionNumber\", direction: \"desc\" },\n limit: 1,\n });\n const max = rows[0]?.versionNumber;\n return typeof max === \"number\" && Number.isFinite(max) ? max + 1 : 1;\n}\n","import os from \"node:os\";\nimport path from \"node:path\";\nimport fs from \"node:fs/promises\";\nimport { randomUUID } from \"node:crypto\";\nimport type { PipelineContext, PluginApi } from \"@better-media/core\";\nimport type {\n VideoProcessingPluginOptions,\n ThumbnailPreset,\n TranscodePreset,\n} from \"../interfaces/options.interface\";\nimport { resolveVideoProcessingOptions } from \"../interfaces/options.interface\";\nimport { isReferenceUrlMode, readBufferForProcessing } from \"./buffer\";\nimport { probeVideo, extractThumbnail, transcodeVideo, FfmpegNotFoundError } from \"./ffmpeg\";\nimport { nextMediaVersionStart } from \"./next-version\";\n\nfunction withTimeout<T>(promise: Promise<T>, ms: number, label: string): Promise<T> {\n if (!Number.isFinite(ms) || ms <= 0) return promise;\n return new Promise<T>((resolve, reject) => {\n const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);\n promise.then(\n (v) => {\n clearTimeout(t);\n resolve(v);\n },\n (e) => {\n clearTimeout(t);\n reject(e);\n }\n );\n });\n}\n\nfunction thumbnailExt(format: ThumbnailPreset[\"format\"]): string {\n switch (format) {\n case \"png\":\n return \".png\";\n case \"webp\":\n return \".webp\";\n default:\n return \".jpg\";\n }\n}\n\nfunction thumbnailMime(format: ThumbnailPreset[\"format\"]): string {\n switch (format) {\n case \"png\":\n return \"image/png\";\n case \"webp\":\n return \"image/webp\";\n default:\n return \"image/jpeg\";\n }\n}\n\nfunction transcodeMime(format: TranscodePreset[\"format\"]): string {\n return format === \"webm\" ? \"video/webm\" : \"video/mp4\";\n}\n\nexport async function runVideoProcessing(\n context: PipelineContext,\n api: PluginApi,\n opts: VideoProcessingPluginOptions\n): Promise<void> {\n const resolved = resolveVideoProcessingOptions(opts);\n\n if (isReferenceUrlMode(context)) {\n api.emitMetadata({ skipped: \"reference-url\" });\n return;\n }\n\n const mime = context.file.mimeType;\n if (!mime || !resolved.allowedMimeTypes.includes(mime)) {\n api.emitMetadata({ skipped: \"mime-not-allowed\", mimeType: mime });\n return;\n }\n\n let buffer: Buffer | null;\n try {\n buffer = await withTimeout(\n readBufferForProcessing(context),\n resolved.timeoutMs,\n \"readBufferForProcessing\"\n );\n } catch (err: unknown) {\n api.emitMetadata({\n error: \"read-failed\",\n message: err instanceof Error ? err.message : String(err),\n });\n return;\n }\n\n if (buffer == null) {\n api.emitMetadata({ skipped: \"no-file-content\" });\n return;\n }\n\n if (resolved.maxInputBytes != null && buffer.length > resolved.maxInputBytes) {\n api.emitMetadata({\n skipped: \"input-too-large\",\n size: buffer.length,\n maxInputBytes: resolved.maxInputBytes,\n });\n return;\n }\n\n // Write buffer to a temp file so ffmpeg can read it\n const tempDir = path.join(os.tmpdir(), randomUUID());\n await fs.mkdir(tempDir, { recursive: true });\n const ext = context.file.extension ?? \".mp4\";\n const inputPath = path.join(tempDir, `input${ext}`);\n await fs.writeFile(inputPath, buffer);\n\n try {\n await runWithInput(context, api, resolved, inputPath, tempDir);\n } finally {\n await fs.rm(tempDir, { recursive: true, force: true });\n }\n}\n\nasync function runWithInput(\n context: PipelineContext,\n api: PluginApi,\n resolved: ReturnType<typeof resolveVideoProcessingOptions>,\n inputPath: string,\n tempDir: string\n): Promise<void> {\n // Probe video for metadata\n let probe: Awaited<ReturnType<typeof probeVideo>>;\n try {\n probe = await withTimeout(\n probeVideo(inputPath, { ffprobePath: resolved.ffprobePath }),\n resolved.timeoutMs,\n \"probeVideo\"\n );\n } catch (err: unknown) {\n if (err instanceof FfmpegNotFoundError) {\n api.emitMetadata({ error: \"ffmpeg-not-found\", message: err.message });\n return;\n }\n api.emitMetadata({\n error: \"probe-failed\",\n message: err instanceof Error ? err.message : String(err),\n });\n return;\n }\n\n const ffOpts = { ffmpegPath: resolved.ffmpegPath };\n const prefix = resolved.derivativePrefix;\n const recordId = context.recordId;\n\n // ---------- Thumbnails ----------\n const thumbnailResults: Array<{\n key: string;\n format: string;\n width?: number;\n height?: number;\n index: number;\n }> = [];\n\n if (resolved.thumbnails !== false) {\n let versionCounter = await nextMediaVersionStart(context.database, recordId);\n\n for (let i = 0; i < resolved.thumbnails.length; i++) {\n const preset = resolved.thumbnails[i]!;\n const format = preset.format ?? \"jpeg\";\n const ext = thumbnailExt(preset.format);\n const storageKey = `${prefix}/${recordId}/thumbnails/thumb_${i}${ext}`;\n\n if (resolved.skipExistingDerivatives && (await context.storage.exists(storageKey))) {\n thumbnailResults.push({\n key: storageKey,\n format,\n width: preset.width,\n height: preset.height,\n index: i,\n });\n continue;\n }\n\n const outputPath = path.join(tempDir, `thumb_${i}${ext}`);\n let dims: { width?: number; height?: number } = {};\n try {\n dims = await withTimeout(\n extractThumbnail(inputPath, outputPath, preset, probe.duration, ffOpts),\n resolved.timeoutMs,\n `extractThumbnail[${i}]`\n );\n } catch (err: unknown) {\n if (err instanceof FfmpegNotFoundError) {\n api.emitMetadata({ error: \"ffmpeg-not-found\", message: err.message });\n return;\n }\n api.emitMetadata({\n error: \"thumbnail-failed\",\n index: i,\n message: err instanceof Error ? err.message : String(err),\n });\n continue;\n }\n\n let thumbBuf: Buffer;\n try {\n thumbBuf = await fs.readFile(outputPath);\n } catch (err: unknown) {\n api.emitMetadata({\n error: \"thumbnail-read-failed\",\n index: i,\n message: err instanceof Error ? err.message : String(err),\n });\n continue;\n }\n\n try {\n await context.storage.put(storageKey, thumbBuf);\n } catch (err: unknown) {\n api.emitMetadata({\n error: \"storage-put-failed\",\n key: storageKey,\n message: err instanceof Error ? err.message : String(err),\n });\n continue;\n }\n\n thumbnailResults.push({\n key: storageKey,\n format,\n width: dims.width,\n height: dims.height,\n index: i,\n });\n\n if (resolved.persistMediaVersions) {\n try {\n await context.database.create({\n model: \"media_versions\",\n data: {\n id: randomUUID(),\n mediaId: recordId,\n storageKey,\n mimeType: thumbnailMime(preset.format),\n size: thumbBuf.length,\n width: dims.width,\n height: dims.height,\n isOriginal: false,\n type: \"thumbnail\",\n versionNumber: versionCounter,\n createdAt: new Date(),\n },\n });\n versionCounter += 1;\n } catch (err: unknown) {\n api.emitMetadata({\n error: \"media-versions-persist-failed\",\n key: storageKey,\n message: err instanceof Error ? err.message : String(err),\n });\n }\n }\n }\n }\n\n // ---------- Transcode ----------\n const transcodeResults: Array<{\n key: string;\n name: string;\n format: string;\n duration: number;\n }> = [];\n\n if (resolved.transcode.length > 0) {\n let versionCounter = await nextMediaVersionStart(context.database, recordId);\n\n for (const preset of resolved.transcode) {\n const storageKey = `${prefix}/${recordId}/transcode/${preset.name}.${preset.format}`;\n\n if (resolved.skipExistingDerivatives && (await context.storage.exists(storageKey))) {\n transcodeResults.push({\n key: storageKey,\n name: preset.name,\n format: preset.format,\n duration: probe.duration,\n });\n continue;\n }\n\n const outputPath = path.join(tempDir, `transcode_${preset.name}.${preset.format}`);\n let transcoded: { duration: number };\n try {\n transcoded = await withTimeout(\n transcodeVideo(inputPath, outputPath, preset, ffOpts),\n resolved.timeoutMs,\n `transcodeVideo[${preset.name}]`\n );\n } catch (err: unknown) {\n if (err instanceof FfmpegNotFoundError) {\n api.emitMetadata({ error: \"ffmpeg-not-found\", message: err.message });\n return;\n }\n api.emitMetadata({\n error: \"transcode-failed\",\n name: preset.name,\n message: err instanceof Error ? err.message : String(err),\n });\n continue;\n }\n\n let outBuf: Buffer;\n try {\n outBuf = await fs.readFile(outputPath);\n } catch (err: unknown) {\n api.emitMetadata({\n error: \"transcode-read-failed\",\n name: preset.name,\n message: err instanceof Error ? err.message : String(err),\n });\n continue;\n }\n\n try {\n await context.storage.put(storageKey, outBuf);\n } catch (err: unknown) {\n api.emitMetadata({\n error: \"storage-put-failed\",\n key: storageKey,\n message: err instanceof Error ? err.message : String(err),\n });\n continue;\n }\n\n transcodeResults.push({\n key: storageKey,\n name: preset.name,\n format: preset.format,\n duration: transcoded.duration,\n });\n\n if (resolved.persistMediaVersions) {\n try {\n await context.database.create({\n model: \"media_versions\",\n data: {\n id: randomUUID(),\n mediaId: recordId,\n storageKey,\n mimeType: transcodeMime(preset.format),\n size: outBuf.length,\n isOriginal: false,\n type: \"compressed\",\n versionNumber: versionCounter,\n createdAt: new Date(),\n },\n });\n versionCounter += 1;\n } catch (err: unknown) {\n api.emitMetadata({\n error: \"media-versions-persist-failed\",\n key: storageKey,\n message: err instanceof Error ? err.message : String(err),\n });\n }\n }\n }\n }\n\n // Emit probe metadata and processing results\n api.emitMetadata({\n duration: probe.duration,\n videoCodec: probe.videoCodec,\n audioCodec: probe.audioCodec,\n width: probe.width,\n height: probe.height,\n framerate: probe.framerate,\n bitrate: probe.bitrate,\n });\n\n const processingPatch: Record<string, unknown> = {};\n if (thumbnailResults.length > 0) processingPatch.thumbnails = thumbnailResults;\n if (transcodeResults.length > 0) processingPatch.transcode = transcodeResults;\n if (Object.keys(processingPatch).length > 0) api.emitProcessing(processingPatch);\n}\n","import type { PipelinePlugin, MediaRuntime, PipelineContext, PluginApi } from \"@better-media/core\";\nimport type { VideoProcessingPluginOptions } from \"./interfaces/options.interface\";\nimport { runVideoProcessing } from \"./runtime/runner\";\n\nexport type {\n VideoProcessingPluginOptions,\n ThumbnailPreset,\n TranscodePreset,\n} from \"./interfaces/options.interface\";\nexport { DEFAULT_THUMBNAIL_PRESETS, DEFAULT_VIDEO_MIME_TYPES } from \"./constants/defaults\";\n\n/**\n * Video processing plugin: thumbnail extraction, transcoding, and metadata via ffmpeg.\n *\n * Requires `fluent-ffmpeg` peer dependency and the `ffmpeg`/`ffprobe` binaries in PATH.\n */\nexport function videoProcessingPlugin(opts: VideoProcessingPluginOptions = {}): PipelinePlugin {\n const executionMode = opts.executionMode ?? \"background\";\n\n return {\n name: \"video-processing\",\n runtimeManifest: {\n id: \"better-media-video-processing\",\n version: \"1.0.0\",\n trustLevel: \"untrusted\",\n capabilities: [\"file.read\", \"metadata.write.own\", \"processing.write.own\"],\n namespace: \"video-processing\",\n },\n executionMode,\n intensive: true,\n apply(runtime: MediaRuntime) {\n runtime.hooks[\"process:run\"].tap(\n \"video-processing\",\n async (context: PipelineContext, api: PluginApi) => runVideoProcessing(context, api, opts),\n { mode: executionMode }\n );\n },\n };\n}\n"]}
|