@better-media/plugin-video-streaming 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs ADDED
@@ -0,0 +1,606 @@
1
+ import os from 'os';
2
+ import path from 'path';
3
+ import fs4 from 'fs/promises';
4
+ import { randomUUID } from 'crypto';
5
+ import { spawn } from 'child_process';
6
+
7
+ // src/runtime/runner.ts
8
+
9
+ // src/constants/presets.ts
10
+ var DEFAULT_STREAMING_PRESETS = [
11
+ { name: "360p", height: 360, videoBitrate: "800k", audioBitrate: "96k" },
12
+ { name: "480p", height: 480, videoBitrate: "1400k", audioBitrate: "128k" },
13
+ { name: "720p", height: 720, videoBitrate: "2500k", audioBitrate: "128k" },
14
+ { name: "1080p", height: 1080, videoBitrate: "5000k", audioBitrate: "192k" }
15
+ ];
16
+ var DEFAULT_VIDEO_MIME_TYPES = [
17
+ "video/mp4",
18
+ "video/quicktime",
19
+ "video/x-msvideo",
20
+ "video/x-matroska",
21
+ "video/webm",
22
+ "video/mpeg",
23
+ "video/ogg",
24
+ "video/3gpp",
25
+ "video/x-flv",
26
+ "video/x-ms-wmv"
27
+ ];
28
+
29
+ // src/interfaces/options.interface.ts
30
+ function resolveVideoStreamingOptions(opts) {
31
+ return {
32
+ formats: opts.formats && opts.formats.length > 0 ? opts.formats : ["hls"],
33
+ presets: opts.presets && opts.presets.length > 0 ? opts.presets : [...DEFAULT_STREAMING_PRESETS],
34
+ allowedMimeTypes: opts.allowedMimeTypes ?? [...DEFAULT_VIDEO_MIME_TYPES],
35
+ derivativePrefix: opts.derivativePrefix ?? "streaming",
36
+ segmentDuration: opts.segmentDuration ?? 6,
37
+ persistMediaVersions: opts.persistMediaVersions !== false,
38
+ skipExistingDerivatives: opts.skipExistingDerivatives !== false,
39
+ timeoutMs: opts.timeoutMs ?? 6e5
40
+ };
41
+ }
42
+ async function readBufferForProcessing(context) {
43
+ const fileContent = context.utilities?.fileContent;
44
+ if (fileContent?.buffer) return fileContent.buffer;
45
+ if (fileContent?.tempPath) return fs4.readFile(fileContent.tempPath);
46
+ return context.storage.get(context.file.key);
47
+ }
48
+ function isReferenceUrlMode(context) {
49
+ const url = context.storageLocation?.url;
50
+ return typeof url === "string" && url === context.file.key;
51
+ }
52
+ async function tryImportFfmpeg() {
53
+ try {
54
+ const mod = await import('fluent-ffmpeg');
55
+ return mod.default ?? mod;
56
+ } catch {
57
+ return null;
58
+ }
59
+ }
60
+ async function getFfmpegBinaryPath() {
61
+ const mod = await tryImportFfmpeg();
62
+ if (!mod) return null;
63
+ const configured = mod.getFfmpegPath?.();
64
+ return typeof configured === "string" && configured.length > 0 ? configured : "ffmpeg";
65
+ }
66
+ function parseTimemarkSecs(timemark) {
67
+ const parts = timemark.split(":").map(Number);
68
+ return (parts[0] ?? 0) * 3600 + (parts[1] ?? 0) * 60 + (parts[2] ?? 0);
69
+ }
70
+ function parseDurationSecs(stderr) {
71
+ const match = stderr.match(/Duration:\s*(\d{2}:\d{2}:\d{2}(?:\.\d+)?)/);
72
+ return match ? parseTimemarkSecs(match[1]) : 0;
73
+ }
74
+ function buildScaleFilter(preset) {
75
+ if (preset.width && preset.height) return `scale=${preset.width}:${preset.height}`;
76
+ if (preset.width) return `scale=${preset.width}:-2`;
77
+ if (preset.height) return `scale=-2:${preset.height}`;
78
+ return "scale=-2:-2";
79
+ }
80
+ async function collectFiles(dir, base) {
81
+ const entries = await fs4.readdir(dir, { withFileTypes: true });
82
+ const results = [];
83
+ for (const entry of entries) {
84
+ const full = path.join(dir, entry.name);
85
+ const rel = path.join(base, entry.name);
86
+ if (entry.isDirectory()) {
87
+ results.push(...await collectFiles(full, rel));
88
+ } else {
89
+ results.push(rel);
90
+ }
91
+ }
92
+ return results;
93
+ }
94
+ async function transcodeHLS(inputPath, outputDir, presets, segmentDuration, onProgress) {
95
+ const ffmpeg = await tryImportFfmpeg();
96
+ if (!ffmpeg) throw new FfmpegNotFoundError();
97
+ await Promise.all(
98
+ presets.map((p) => fs4.mkdir(path.join(outputDir, p.name), { recursive: true }))
99
+ );
100
+ let duration = 0;
101
+ await Promise.all(
102
+ presets.map(
103
+ (preset) => new Promise((resolve, reject) => {
104
+ const variantDir = path.join(outputDir, preset.name);
105
+ const playlistPath = path.join(variantDir, "index.m3u8");
106
+ const segmentPattern = path.join(variantDir, "seg%03d.ts");
107
+ const cmd = ffmpeg(inputPath).videoCodec(preset.videoCodec ?? "libx264").audioCodec(preset.audioCodec ?? "aac").videoFilters(buildScaleFilter(preset)).outputOptions([
108
+ `-b:v ${preset.videoBitrate ?? "1000k"}`,
109
+ `-b:a ${preset.audioBitrate ?? "128k"}`,
110
+ "-f hls",
111
+ `-hls_time ${segmentDuration}`,
112
+ "-hls_playlist_type vod",
113
+ "-hls_flags independent_segments",
114
+ `-hls_segment_filename ${segmentPattern}`
115
+ ]).output(playlistPath);
116
+ cmd.on("error", (err) => reject(err));
117
+ cmd.on("end", () => resolve());
118
+ cmd.on("codecData", (data) => {
119
+ if (data.duration) {
120
+ duration = parseTimemarkSecs(data.duration);
121
+ }
122
+ });
123
+ cmd.on("progress", (progress) => {
124
+ onProgress?.({
125
+ preset: preset.name,
126
+ percent: typeof progress.percent === "number" ? Math.min(100, Math.round(progress.percent)) : void 0,
127
+ currentTimeSecs: progress.timemark ? parseTimemarkSecs(progress.timemark) : void 0
128
+ });
129
+ });
130
+ cmd.run();
131
+ })
132
+ )
133
+ );
134
+ const masterLines = ["#EXTM3U", "#EXT-X-VERSION:3"];
135
+ for (const preset of presets) {
136
+ const bandwidth = parseInt((preset.videoBitrate ?? "1000k").replace(/\D/g, ""), 10) * 1e3;
137
+ const resolution = preset.width && preset.height ? `RESOLUTION=${preset.width}x${preset.height}` : "";
138
+ const attrs = [`BANDWIDTH=${bandwidth}`, resolution].filter(Boolean).join(",");
139
+ masterLines.push(`#EXT-X-STREAM-INF:${attrs}`, `${preset.name}/index.m3u8`);
140
+ }
141
+ await fs4.writeFile(path.join(outputDir, "master.m3u8"), masterLines.join("\n") + "\n", "utf8");
142
+ const files = await collectFiles(outputDir, "");
143
+ return { duration, files };
144
+ }
145
+ async function transcodeDASH(inputPath, outputDir, presets, segmentDuration, onProgress) {
146
+ const ffmpegPath = await getFfmpegBinaryPath();
147
+ if (!ffmpegPath) throw new FfmpegNotFoundError();
148
+ const masterPath = path.join(outputDir, "master.mpd");
149
+ const args = ["-y", "-i", inputPath];
150
+ for (let i = 0; i < presets.length; i++) {
151
+ args.push("-map", "0:v:0", "-map", "0:a:0");
152
+ }
153
+ for (let i = 0; i < presets.length; i++) {
154
+ const p = presets[i];
155
+ args.push(
156
+ `-c:v:${i}`,
157
+ p.videoCodec ?? "libx264",
158
+ `-c:a:${i}`,
159
+ p.audioCodec ?? "aac",
160
+ `-filter:v:${i}`,
161
+ buildScaleFilter(p),
162
+ `-b:v:${i}`,
163
+ p.videoBitrate ?? "1000k",
164
+ `-b:a:${i}`,
165
+ p.audioBitrate ?? "128k"
166
+ );
167
+ }
168
+ args.push(
169
+ "-f",
170
+ "dash",
171
+ "-seg_duration",
172
+ String(segmentDuration),
173
+ "-use_template",
174
+ "1",
175
+ "-use_timeline",
176
+ "1",
177
+ "-init_seg_name",
178
+ "init-stream$RepresentationID$.mp4",
179
+ "-media_seg_name",
180
+ "chunk-stream$RepresentationID$-$Number%05d$.m4s",
181
+ masterPath
182
+ );
183
+ const duration = await new Promise((resolve, reject) => {
184
+ const proc = spawn(ffmpegPath, args, { stdio: ["ignore", "ignore", "pipe"] });
185
+ let stderrBuf = "";
186
+ let totalDuration = 0;
187
+ proc.stderr.on("data", (chunk) => {
188
+ const text = chunk.toString();
189
+ stderrBuf += text;
190
+ if (onProgress) {
191
+ if (totalDuration === 0) {
192
+ const dm = text.match(/Duration:\s*(\d{2}:\d{2}:\d{2}(?:\.\d+)?)/);
193
+ if (dm) totalDuration = parseTimemarkSecs(dm[1]);
194
+ }
195
+ for (const segment of text.split(/[\r\n]/).filter(Boolean)) {
196
+ const pm = segment.match(/time=(\d{2}:\d{2}:\d{2}(?:\.\d+)?)/);
197
+ if (pm) {
198
+ const currentTimeSecs = parseTimemarkSecs(pm[1]);
199
+ const percent = totalDuration > 0 ? Math.min(100, Math.round(currentTimeSecs / totalDuration * 100)) : void 0;
200
+ onProgress({ currentTimeSecs, percent });
201
+ }
202
+ }
203
+ }
204
+ });
205
+ proc.on("error", (err) => {
206
+ if (err.code === "ENOENT") {
207
+ reject(new FfmpegNotFoundError());
208
+ } else {
209
+ reject(err);
210
+ }
211
+ });
212
+ proc.on("close", (code) => {
213
+ if (code !== 0) {
214
+ reject(new Error(`ffmpeg DASH transcode failed (exit ${code})
215
+ ${stderrBuf.slice(-2e3)}`));
216
+ } else {
217
+ resolve(parseDurationSecs(stderrBuf));
218
+ }
219
+ });
220
+ });
221
+ const files = await collectFiles(outputDir, "");
222
+ return { duration, files };
223
+ }
224
+ var FfmpegNotFoundError = class extends Error {
225
+ constructor() {
226
+ super(
227
+ "fluent-ffmpeg could not be imported. Install it: pnpm add fluent-ffmpeg. Also ensure the ffmpeg binary is available in PATH."
228
+ );
229
+ this.name = "FfmpegNotFoundError";
230
+ }
231
+ };
232
+ var STREAMING_CONTENT_TYPES = {
233
+ ".m3u8": "application/x-mpegURL",
234
+ ".ts": "video/MP2T",
235
+ ".mpd": "application/dash+xml",
236
+ ".m4s": "video/iso.segment",
237
+ ".mp4": "video/mp4"
238
+ };
239
+ function inferContentType(filename) {
240
+ const ext = path.extname(filename).toLowerCase();
241
+ return STREAMING_CONTENT_TYPES[ext] ?? "application/octet-stream";
242
+ }
243
+ async function uploadDirectory(storage, tempDir, storagePrefix, skipExisting) {
244
+ const uploaded = [];
245
+ await walkAndUpload(storage, tempDir, tempDir, storagePrefix, skipExisting, uploaded);
246
+ return uploaded;
247
+ }
248
+ async function walkAndUpload(storage, rootDir, currentDir, storagePrefix, skipExisting, acc) {
249
+ const entries = await fs4.readdir(currentDir, { withFileTypes: true });
250
+ await Promise.all(
251
+ entries.map(async (entry) => {
252
+ const fullPath = path.join(currentDir, entry.name);
253
+ if (entry.isDirectory()) {
254
+ await walkAndUpload(storage, rootDir, fullPath, storagePrefix, skipExisting, acc);
255
+ return;
256
+ }
257
+ const relative = path.relative(rootDir, fullPath);
258
+ const storageKey = `${storagePrefix}/${relative.split(path.sep).join("/")}`;
259
+ if (skipExisting && await storage.exists(storageKey)) {
260
+ acc.push({ storageKey, size: 0 });
261
+ return;
262
+ }
263
+ const buffer = await fs4.readFile(fullPath);
264
+ await storage.put(storageKey, buffer, { contentType: inferContentType(entry.name) });
265
+ acc.push({ storageKey, size: buffer.length });
266
+ })
267
+ );
268
+ }
269
+
270
+ // src/runtime/next-version.ts
271
+ async function nextMediaVersionStart(database, mediaId) {
272
+ const rows = await database.findMany({
273
+ model: "media_versions",
274
+ where: [{ field: "mediaId", value: mediaId }],
275
+ sortBy: { field: "versionNumber", direction: "desc" },
276
+ limit: 1
277
+ });
278
+ const max = rows[0]?.versionNumber;
279
+ return typeof max === "number" && Number.isFinite(max) ? max + 1 : 1;
280
+ }
281
+
282
+ // src/runtime/runner.ts
283
+ function withTimeout(promise, ms, label) {
284
+ if (!Number.isFinite(ms) || ms <= 0) return promise;
285
+ return new Promise((resolve, reject) => {
286
+ const t = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
287
+ promise.then(
288
+ (v) => {
289
+ clearTimeout(t);
290
+ resolve(v);
291
+ },
292
+ (e) => {
293
+ clearTimeout(t);
294
+ reject(e);
295
+ }
296
+ );
297
+ });
298
+ }
299
+ function masterKeyForFormat(prefix, recordId, format) {
300
+ return format === "hls" ? `${prefix}/${recordId}/hls/master.m3u8` : `${prefix}/${recordId}/dash/master.mpd`;
301
+ }
302
+ function variantPlaylistKey(prefix, recordId, format, presetName) {
303
+ return format === "hls" ? `${prefix}/${recordId}/hls/${presetName}/index.m3u8` : `${prefix}/${recordId}/dash/${presetName}/stream.mpd`;
304
+ }
305
+ async function runVideoStreaming(context, api, opts) {
306
+ const resolved = resolveVideoStreamingOptions(opts);
307
+ if (isReferenceUrlMode(context)) {
308
+ api.emitMetadata({ skipped: "reference-url" });
309
+ return;
310
+ }
311
+ const mime = context.file.mimeType;
312
+ if (!mime || !resolved.allowedMimeTypes.includes(mime)) {
313
+ api.emitMetadata({ skipped: "mime-not-allowed", mimeType: mime });
314
+ return;
315
+ }
316
+ if (resolved.skipExistingDerivatives) {
317
+ const firstMasterKey = masterKeyForFormat(
318
+ resolved.derivativePrefix,
319
+ context.recordId,
320
+ resolved.formats[0]
321
+ );
322
+ if (await context.storage.exists(firstMasterKey)) {
323
+ api.emitMetadata({ skipped: "already-processed", key: firstMasterKey });
324
+ return;
325
+ }
326
+ }
327
+ let buffer;
328
+ try {
329
+ buffer = await withTimeout(
330
+ readBufferForProcessing(context),
331
+ resolved.timeoutMs,
332
+ "readBufferForProcessing"
333
+ );
334
+ } catch (err) {
335
+ api.emitMetadata({
336
+ error: "read-failed",
337
+ message: err instanceof Error ? err.message : String(err)
338
+ });
339
+ return;
340
+ }
341
+ if (buffer == null) {
342
+ api.emitMetadata({ skipped: "no-file-content" });
343
+ return;
344
+ }
345
+ const tempInputDir = path.join(os.tmpdir(), randomUUID());
346
+ await fs4.mkdir(tempInputDir, { recursive: true });
347
+ const ext = context.file.extension ?? ".mp4";
348
+ const inputPath = path.join(tempInputDir, `input${ext}`);
349
+ await fs4.writeFile(inputPath, buffer);
350
+ const presets = opts.resolvePreset ? await Promise.all(
351
+ resolved.presets.map(
352
+ (preset, index) => Promise.resolve(opts.resolvePreset(context, preset, index))
353
+ )
354
+ ) : resolved.presets;
355
+ const streamingVariants = [];
356
+ let videoDuration = 0;
357
+ const tempDirs = [tempInputDir];
358
+ try {
359
+ for (const format of resolved.formats) {
360
+ const tempOutputDir = path.join(os.tmpdir(), randomUUID());
361
+ await fs4.mkdir(tempOutputDir, { recursive: true });
362
+ tempDirs.push(tempOutputDir);
363
+ const storagePrefix = `${resolved.derivativePrefix}/${context.recordId}/${format}`;
364
+ let duration = 0;
365
+ try {
366
+ const transcode = format === "hls" ? transcodeHLS : transcodeDASH;
367
+ const transcodeOnProgress = opts.onProgress ? (event) => opts.onProgress({ format, ...event }) : void 0;
368
+ const result = await withTimeout(
369
+ transcode(
370
+ inputPath,
371
+ tempOutputDir,
372
+ presets,
373
+ resolved.segmentDuration,
374
+ transcodeOnProgress
375
+ ),
376
+ resolved.timeoutMs,
377
+ `transcode-${format}`
378
+ );
379
+ duration = result.duration;
380
+ if (duration > 0) videoDuration = duration;
381
+ } catch (err) {
382
+ if (err instanceof FfmpegNotFoundError) {
383
+ api.emitMetadata({ error: "ffmpeg-not-found", message: err.message });
384
+ return;
385
+ }
386
+ api.emitMetadata({
387
+ error: "transcode-failed",
388
+ format,
389
+ message: err instanceof Error ? err.message : String(err)
390
+ });
391
+ continue;
392
+ }
393
+ try {
394
+ await uploadDirectory(
395
+ context.storage,
396
+ tempOutputDir,
397
+ storagePrefix,
398
+ resolved.skipExistingDerivatives
399
+ );
400
+ } catch (err) {
401
+ api.emitMetadata({
402
+ error: "upload-failed",
403
+ format,
404
+ message: err instanceof Error ? err.message : String(err)
405
+ });
406
+ continue;
407
+ }
408
+ const masterKey = masterKeyForFormat(resolved.derivativePrefix, context.recordId, format);
409
+ streamingVariants.push({ key: masterKey, format });
410
+ if (resolved.persistMediaVersions) {
411
+ let versionCounter = await nextMediaVersionStart(context.database, context.recordId);
412
+ const variantRows = format === "hls" ? presets.map((p) => ({
413
+ storageKey: variantPlaylistKey(
414
+ resolved.derivativePrefix,
415
+ context.recordId,
416
+ format,
417
+ p.name
418
+ ),
419
+ mimeType: "application/x-mpegURL"
420
+ })) : [];
421
+ const rowsToInsert = [
422
+ {
423
+ storageKey: masterKey,
424
+ mimeType: format === "hls" ? "application/x-mpegURL" : "application/dash+xml"
425
+ },
426
+ ...variantRows
427
+ ];
428
+ for (const row of rowsToInsert) {
429
+ try {
430
+ await context.database.create({
431
+ model: "media_versions",
432
+ data: {
433
+ id: randomUUID(),
434
+ mediaId: context.recordId,
435
+ storageKey: row.storageKey,
436
+ mimeType: row.mimeType,
437
+ isOriginal: false,
438
+ type: "compressed",
439
+ versionNumber: versionCounter,
440
+ createdAt: /* @__PURE__ */ new Date()
441
+ }
442
+ });
443
+ versionCounter += 1;
444
+ } catch (err) {
445
+ api.emitMetadata({
446
+ error: "media-versions-persist-failed",
447
+ key: row.storageKey,
448
+ message: err instanceof Error ? err.message : String(err)
449
+ });
450
+ }
451
+ }
452
+ }
453
+ }
454
+ } finally {
455
+ await Promise.allSettled(tempDirs.map((d) => fs4.rm(d, { recursive: true, force: true })));
456
+ }
457
+ if (streamingVariants.length === 0) return;
458
+ api.emitProcessing({
459
+ variants: { streaming: streamingVariants },
460
+ "video-streaming": {
461
+ formats: resolved.formats,
462
+ presets: presets.map((p) => p.name),
463
+ duration: videoDuration || void 0,
464
+ segmentDuration: resolved.segmentDuration
465
+ }
466
+ });
467
+ }
468
+
469
+ // src/playback/resolve-streaming-urls.ts
470
+ var HLS_MIME = "application/x-mpegURL";
471
+ var DASH_MIME = "application/dash+xml";
472
+ async function resolveStreamingUrls(recordId, options) {
473
+ const { database, storage, expiresIn, derivativePrefix = "streaming" } = options;
474
+ if (!storage.getUrl) {
475
+ throw new Error(
476
+ "resolveStreamingUrls requires storage.getUrl to be implemented. The memory adapter does not support URL generation \u2014 use createStreamingProxy instead."
477
+ );
478
+ }
479
+ const rows = await database.findMany({
480
+ model: "media_versions",
481
+ where: [
482
+ { field: "mediaId", value: recordId },
483
+ { field: "mimeType", value: [HLS_MIME, DASH_MIME], operator: "in" }
484
+ ],
485
+ select: ["storageKey", "mimeType"]
486
+ });
487
+ const keyPrefix = `${derivativePrefix}/${recordId}/`;
488
+ const masters = rows.filter(
489
+ (r) => r.storageKey.startsWith(keyPrefix) && (r.storageKey.endsWith("master.m3u8") || r.storageKey.endsWith("master.mpd"))
490
+ );
491
+ const result = {};
492
+ for (const row of masters) {
493
+ const url = await storage.getUrl(
494
+ row.storageKey,
495
+ expiresIn !== void 0 ? { expiresIn } : void 0
496
+ );
497
+ if (row.mimeType === HLS_MIME) {
498
+ result.hls = url;
499
+ } else if (row.mimeType === DASH_MIME) {
500
+ result.dash = url;
501
+ }
502
+ }
503
+ return result;
504
+ }
505
+
506
+ // src/playback/resolve-streaming-status.ts
507
+ async function resolveStreamingStatus(recordId, options) {
508
+ const { storage, derivativePrefix = "streaming" } = options;
509
+ const [hls, dash] = await Promise.all([
510
+ storage.exists(`${derivativePrefix}/${recordId}/hls/master.m3u8`),
511
+ storage.exists(`${derivativePrefix}/${recordId}/dash/master.mpd`)
512
+ ]);
513
+ return { hls, dash, ready: hls || dash };
514
+ }
515
+ var CONTENT_TYPES = {
516
+ ".m3u8": "application/x-mpegURL",
517
+ ".mpd": "application/dash+xml",
518
+ ".ts": "video/MP2T",
519
+ ".mp4": "video/mp4",
520
+ ".m4s": "video/iso.segment"
521
+ };
522
+ var PLAYLIST_EXTS = /* @__PURE__ */ new Set([".m3u8", ".mpd"]);
523
+ var DEFAULT_CACHE_PLAYLIST = "no-cache, no-store";
524
+ var DEFAULT_CACHE_SEGMENT = "public, max-age=31536000, immutable";
525
+ function createStreamingProxy(options) {
526
+ const {
527
+ storage,
528
+ authenticate,
529
+ cacheControl,
530
+ corsOrigin = "*",
531
+ derivativePrefix = "streaming"
532
+ } = options;
533
+ const playlistCache = cacheControl?.playlist ?? DEFAULT_CACHE_PLAYLIST;
534
+ const segmentCache = cacheControl?.segment ?? DEFAULT_CACHE_SEGMENT;
535
+ return {
536
+ async handle({ recordId, filePath, req, method = "GET" }) {
537
+ if (filePath.includes("..")) {
538
+ return new Response("Bad Request", { status: 400 });
539
+ }
540
+ const storageKey = `${derivativePrefix}/${recordId}/${filePath}`;
541
+ if (authenticate) {
542
+ try {
543
+ await authenticate(req, { recordId, storageKey });
544
+ } catch {
545
+ return new Response("Forbidden", { status: 403 });
546
+ }
547
+ }
548
+ const exists = await storage.exists(storageKey);
549
+ if (!exists) {
550
+ return new Response("Not Found", { status: 404 });
551
+ }
552
+ const ext = path.extname(filePath);
553
+ const contentType = CONTENT_TYPES[ext] ?? "application/octet-stream";
554
+ const cacheHeader = PLAYLIST_EXTS.has(ext) ? playlistCache : segmentCache;
555
+ const headers = new Headers({
556
+ "Content-Type": contentType,
557
+ "Cache-Control": cacheHeader,
558
+ "Access-Control-Allow-Origin": corsOrigin,
559
+ "Access-Control-Allow-Methods": "GET, HEAD",
560
+ "Access-Control-Expose-Headers": "Content-Length, Content-Type"
561
+ });
562
+ if (method.toUpperCase() === "HEAD") {
563
+ return new Response(null, { status: 200, headers });
564
+ }
565
+ if (storage.getStream) {
566
+ const stream = await storage.getStream(storageKey);
567
+ if (stream == null) {
568
+ return new Response("Not Found", { status: 404 });
569
+ }
570
+ return new Response(stream, { status: 200, headers });
571
+ }
572
+ const buffer = await storage.get(storageKey);
573
+ if (buffer == null) {
574
+ return new Response("Not Found", { status: 404 });
575
+ }
576
+ return new Response(new Uint8Array(buffer), { status: 200, headers });
577
+ }
578
+ };
579
+ }
580
+
581
+ // src/index.ts
582
+ function videoStreamingPlugin(opts = {}) {
583
+ return {
584
+ name: "video-streaming",
585
+ runtimeManifest: {
586
+ id: "better-media-video-streaming",
587
+ version: "1.0.0",
588
+ trustLevel: "untrusted",
589
+ capabilities: ["file.read", "metadata.write.own", "processing.write.own"],
590
+ namespace: "video-streaming"
591
+ },
592
+ executionMode: "background",
593
+ intensive: true,
594
+ apply(runtime) {
595
+ runtime.hooks["process:run"].tap(
596
+ "video-streaming",
597
+ async (context, api) => runVideoStreaming(context, api, opts),
598
+ { mode: "background" }
599
+ );
600
+ }
601
+ };
602
+ }
603
+
604
+ export { DEFAULT_STREAMING_PRESETS, DEFAULT_VIDEO_MIME_TYPES, createStreamingProxy, resolveStreamingStatus, resolveStreamingUrls, videoStreamingPlugin };
605
+ //# sourceMappingURL=index.mjs.map
606
+ //# sourceMappingURL=index.mjs.map