@uploadbox/video 0.4.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/ffutils.d.ts +39 -0
- package/dist/ffutils.d.ts.map +1 -0
- package/dist/ffutils.js +82 -0
- package/dist/ffutils.js.map +1 -0
- package/dist/hooks/video-processing.d.ts +47 -0
- package/dist/hooks/video-processing.d.ts.map +1 -0
- package/dist/hooks/video-processing.js +115 -0
- package/dist/hooks/video-processing.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/metadata.d.ts +19 -0
- package/dist/metadata.d.ts.map +1 -0
- package/dist/metadata.js +50 -0
- package/dist/metadata.js.map +1 -0
- package/dist/provider.d.ts +44 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +2 -0
- package/dist/provider.js.map +1 -0
- package/dist/providers/external.d.ts +40 -0
- package/dist/providers/external.d.ts.map +1 -0
- package/dist/providers/external.js +94 -0
- package/dist/providers/external.js.map +1 -0
- package/dist/providers/ffmpeg.d.ts +27 -0
- package/dist/providers/ffmpeg.d.ts.map +1 -0
- package/dist/providers/ffmpeg.js +282 -0
- package/dist/providers/ffmpeg.js.map +1 -0
- package/dist/providers/lambda.d.ts +49 -0
- package/dist/providers/lambda.d.ts.map +1 -0
- package/dist/providers/lambda.js +80 -0
- package/dist/providers/lambda.js.map +1 -0
- package/dist/react/index.d.ts +3 -0
- package/dist/react/index.d.ts.map +1 -0
- package/dist/react/index.js +2 -0
- package/dist/react/index.js.map +1 -0
- package/dist/react/use-video-player.d.ts +110 -0
- package/dist/react/use-video-player.d.ts.map +1 -0
- package/dist/react/use-video-player.js +319 -0
- package/dist/react/use-video-player.js.map +1 -0
- package/dist/types.d.ts +90 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +45 -0
- package/dist/types.js.map +1 -0
- package/package.json +83 -0
- package/src/ffutils.ts +128 -0
- package/src/hooks/video-processing.ts +160 -0
- package/src/index.ts +18 -0
- package/src/metadata.ts +57 -0
- package/src/provider.ts +46 -0
- package/src/providers/external.ts +122 -0
- package/src/providers/ffmpeg.ts +365 -0
- package/src/providers/lambda.ts +112 -0
- package/src/react/index.ts +7 -0
- package/src/react/use-video-player.ts +444 -0
- package/src/types.ts +130 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/** Subset of ffprobe JSON output we use. */
|
|
2
|
+
export interface FfprobeStream {
|
|
3
|
+
codec_type?: string;
|
|
4
|
+
codec_name?: string;
|
|
5
|
+
width?: number;
|
|
6
|
+
height?: number;
|
|
7
|
+
r_frame_rate?: string;
|
|
8
|
+
duration?: string;
|
|
9
|
+
bit_rate?: string;
|
|
10
|
+
sample_rate?: string;
|
|
11
|
+
channels?: number;
|
|
12
|
+
}
|
|
13
|
+
export interface FfprobeFormat {
|
|
14
|
+
duration?: string;
|
|
15
|
+
bit_rate?: string;
|
|
16
|
+
}
|
|
17
|
+
export interface FfprobeData {
|
|
18
|
+
streams: FfprobeStream[];
|
|
19
|
+
format: FfprobeFormat;
|
|
20
|
+
}
|
|
21
|
+
export interface FfprobeOptions {
|
|
22
|
+
/** Path to the ffprobe binary. @default "ffprobe" */
|
|
23
|
+
ffprobePath?: string;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Run ffprobe on a file path or readable stream and return parsed JSON output.
|
|
27
|
+
*/
|
|
28
|
+
export declare function ffprobe(input: string | NodeJS.ReadableStream, opts?: FfprobeOptions): Promise<FfprobeData>;
|
|
29
|
+
export interface RunFFmpegOptions {
|
|
30
|
+
/** Path to the ffmpeg binary. @default "ffmpeg" */
|
|
31
|
+
ffmpegPath?: string;
|
|
32
|
+
/** Signal to abort the process. */
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Run ffmpeg with the given argument list. Resolves on exit code 0, rejects otherwise.
|
|
37
|
+
*/
|
|
38
|
+
export declare function runFFmpeg(args: string[], opts?: RunFFmpegOptions): Promise<void>;
|
|
39
|
+
//# sourceMappingURL=ffutils.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ffutils.d.ts","sourceRoot":"","sources":["../src/ffutils.ts"],"names":[],"mappings":"AAEA,4CAA4C;AAC5C,MAAM,WAAW,aAAa;IAC5B,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,cAAc;IAC7B,qDAAqD;IACrD,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;GAEG;AACH,wBAAgB,OAAO,CACrB,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,cAAc,EACrC,IAAI,CAAC,EAAE,cAAc,GACpB,OAAO,CAAC,WAAW,CAAC,CA2CtB;AAED,MAAM,WAAW,gBAAgB;IAC/B,mDAAmD;IACnD,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,mCAAmC;IACnC,MAAM,CAAC,EAAE,WAAW,CAAC;CACtB;AAED;;GAEG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,IAAI,CAAC,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoChF"}
|
package/dist/ffutils.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
import { execFile, spawn } from "node:child_process";
|
|
2
|
+
/**
|
|
3
|
+
* Run ffprobe on a file path or readable stream and return parsed JSON output.
|
|
4
|
+
*/
|
|
5
|
+
export function ffprobe(input, opts) {
|
|
6
|
+
const bin = opts?.ffprobePath ?? "ffprobe";
|
|
7
|
+
const baseArgs = ["-v", "quiet", "-print_format", "json", "-show_format", "-show_streams"];
|
|
8
|
+
if (typeof input === "string") {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
execFile(bin, [...baseArgs, input], { maxBuffer: 10 * 1024 * 1024 }, (err, stdout) => {
|
|
11
|
+
if (err)
|
|
12
|
+
return reject(new Error(`ffprobe failed: ${err.message}`));
|
|
13
|
+
try {
|
|
14
|
+
resolve(JSON.parse(stdout));
|
|
15
|
+
}
|
|
16
|
+
catch {
|
|
17
|
+
reject(new Error("ffprobe returned invalid JSON"));
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
// Readable stream → pipe to stdin
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
const proc = spawn(bin, [...baseArgs, "-i", "pipe:0"], {
|
|
25
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
26
|
+
});
|
|
27
|
+
let stdout = "";
|
|
28
|
+
let stderr = "";
|
|
29
|
+
proc.stdout.on("data", (d) => (stdout += d.toString()));
|
|
30
|
+
proc.stderr.on("data", (d) => (stderr += d.toString()));
|
|
31
|
+
proc.on("error", (err) => reject(new Error(`ffprobe failed: ${err.message}`)));
|
|
32
|
+
proc.on("close", (code) => {
|
|
33
|
+
if (code !== 0)
|
|
34
|
+
return reject(new Error(`ffprobe exited with code ${code}: ${stderr}`));
|
|
35
|
+
try {
|
|
36
|
+
resolve(JSON.parse(stdout));
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
reject(new Error("ffprobe returned invalid JSON"));
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
input.pipe(proc.stdin);
|
|
43
|
+
proc.stdin.on("error", () => {
|
|
44
|
+
// ignore EPIPE — ffprobe may close stdin early
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Run ffmpeg with the given argument list. Resolves on exit code 0, rejects otherwise.
|
|
50
|
+
*/
|
|
51
|
+
export function runFFmpeg(args, opts) {
|
|
52
|
+
const bin = opts?.ffmpegPath ?? "ffmpeg";
|
|
53
|
+
return new Promise((resolve, reject) => {
|
|
54
|
+
const proc = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
55
|
+
let stderr = "";
|
|
56
|
+
proc.stderr.on("data", (d) => (stderr += d.toString()));
|
|
57
|
+
const onAbort = () => {
|
|
58
|
+
proc.kill("SIGKILL");
|
|
59
|
+
};
|
|
60
|
+
if (opts?.signal) {
|
|
61
|
+
if (opts.signal.aborted) {
|
|
62
|
+
proc.kill("SIGKILL");
|
|
63
|
+
return reject(new Error("Cancelled"));
|
|
64
|
+
}
|
|
65
|
+
opts.signal.addEventListener("abort", onAbort, { once: true });
|
|
66
|
+
}
|
|
67
|
+
proc.on("error", (err) => {
|
|
68
|
+
opts?.signal?.removeEventListener("abort", onAbort);
|
|
69
|
+
reject(new Error(`ffmpeg failed: ${err.message}`));
|
|
70
|
+
});
|
|
71
|
+
proc.on("close", (code) => {
|
|
72
|
+
opts?.signal?.removeEventListener("abort", onAbort);
|
|
73
|
+
if (code !== 0) {
|
|
74
|
+
// Extract last meaningful line from stderr for error message
|
|
75
|
+
const lastLine = stderr.trim().split("\n").pop() ?? "";
|
|
76
|
+
return reject(new Error(`ffmpeg exited with code ${code}: ${lastLine}`));
|
|
77
|
+
}
|
|
78
|
+
resolve();
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=ffutils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ffutils.js","sourceRoot":"","sources":["../src/ffutils.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { ProcessingHook } from "@uploadbox/core";
|
|
2
|
+
import type { TranscodingProvider } from "../provider.js";
|
|
3
|
+
import type { QualityPreset } from "../types.js";
|
|
4
|
+
interface VideoProcessingHookOptions {
|
|
5
|
+
/** Quality presets to generate. Defaults to all QUALITY_PRESETS. */
|
|
6
|
+
qualities?: QualityPreset[];
|
|
7
|
+
/** HLS segment duration in seconds. @default 6 */
|
|
8
|
+
segmentDuration?: number;
|
|
9
|
+
/** Whether to generate a poster thumbnail. @default true */
|
|
10
|
+
generateThumbnail?: boolean;
|
|
11
|
+
/** Whether to generate a sprite sheet with WebVTT. @default true */
|
|
12
|
+
generateSpriteSheet?: boolean;
|
|
13
|
+
/** Timeout in milliseconds. @default 1800000 (30 minutes) */
|
|
14
|
+
timeoutMs?: number;
|
|
15
|
+
/**
|
|
16
|
+
* Function to determine the S3 output prefix for HLS content.
|
|
17
|
+
* Receives the file key and should return the prefix path.
|
|
18
|
+
* @default Derives from file key: `video/{fileKey}/`
|
|
19
|
+
*/
|
|
20
|
+
getOutputPrefix?: (fileKey: string) => string;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Creates a processing hook that triggers video transcoding after upload.
|
|
24
|
+
*
|
|
25
|
+
* Implements the `@uploadbox/core` `ProcessingHook` interface. Only runs
|
|
26
|
+
* for files with a `video/*` MIME type.
|
|
27
|
+
*
|
|
28
|
+
* @param provider - The transcoding provider to use (FFmpeg, Lambda, External).
|
|
29
|
+
* @param options - Transcoding options.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* ```ts
|
|
33
|
+
* import { createVideoProcessingHook } from "@uploadbox/video/hooks/video-processing";
|
|
34
|
+
* import { createFFmpegProvider } from "@uploadbox/video/providers/ffmpeg";
|
|
35
|
+
*
|
|
36
|
+
* const provider = createFFmpegProvider();
|
|
37
|
+
* const videoHook = createVideoProcessingHook(provider, {
|
|
38
|
+
* qualities: [QUALITY_PRESETS["720p"], QUALITY_PRESETS["1080p"]],
|
|
39
|
+
* });
|
|
40
|
+
*
|
|
41
|
+
* // Use with processing pipeline
|
|
42
|
+
* const processing = { hooks: [videoHook] };
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare function createVideoProcessingHook(provider: TranscodingProvider, options?: VideoProcessingHookOptions): ProcessingHook;
|
|
46
|
+
export {};
|
|
47
|
+
//# sourceMappingURL=video-processing.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video-processing.d.ts","sourceRoot":"","sources":["../../src/hooks/video-processing.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAA2C,MAAM,iBAAiB,CAAC;AAC/F,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,gBAAgB,CAAC;AAC1D,OAAO,KAAK,EAAsB,aAAa,EAAE,MAAM,aAAa,CAAC;AAIrE,UAAU,0BAA0B;IAClC,oEAAoE;IACpE,SAAS,CAAC,EAAE,aAAa,EAAE,CAAC;IAC5B,kDAAkD;IAClD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,4DAA4D;IAC5D,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,oEAAoE;IACpE,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,6DAA6D;IAC7D,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB;;;;OAIG;IACH,eAAe,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;CAC/C;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,yBAAyB,CACvC,QAAQ,EAAE,mBAAmB,EAC7B,OAAO,GAAE,0BAA+B,GACvC,cAAc,CA4GhB"}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { QUALITY_PRESETS } from "../types.js";
|
|
2
|
+
import { extractVideoMetadata } from "../metadata.js";
|
|
3
|
+
/**
|
|
4
|
+
* Creates a processing hook that triggers video transcoding after upload.
|
|
5
|
+
*
|
|
6
|
+
* Implements the `@uploadbox/core` `ProcessingHook` interface. Only runs
|
|
7
|
+
* for files with a `video/*` MIME type.
|
|
8
|
+
*
|
|
9
|
+
* @param provider - The transcoding provider to use (FFmpeg, Lambda, External).
|
|
10
|
+
* @param options - Transcoding options.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { createVideoProcessingHook } from "@uploadbox/video/hooks/video-processing";
|
|
15
|
+
* import { createFFmpegProvider } from "@uploadbox/video/providers/ffmpeg";
|
|
16
|
+
*
|
|
17
|
+
* const provider = createFFmpegProvider();
|
|
18
|
+
* const videoHook = createVideoProcessingHook(provider, {
|
|
19
|
+
* qualities: [QUALITY_PRESETS["720p"], QUALITY_PRESETS["1080p"]],
|
|
20
|
+
* });
|
|
21
|
+
*
|
|
22
|
+
* // Use with processing pipeline
|
|
23
|
+
* const processing = { hooks: [videoHook] };
|
|
24
|
+
* ```
|
|
25
|
+
*/
|
|
26
|
+
export function createVideoProcessingHook(provider, options = {}) {
|
|
27
|
+
const { qualities = Object.values(QUALITY_PRESETS), segmentDuration = 6, generateThumbnail = true, generateSpriteSheet = true, timeoutMs = 30 * 60 * 1000, getOutputPrefix = (fileKey) => {
|
|
28
|
+
const baseName = fileKey.replace(/\.[^.]+$/, "");
|
|
29
|
+
return `video/${baseName}`;
|
|
30
|
+
}, } = options;
|
|
31
|
+
return {
|
|
32
|
+
name: "video-processing",
|
|
33
|
+
timeoutMs,
|
|
34
|
+
shouldRun(ctx) {
|
|
35
|
+
return ctx.file.type.startsWith("video/");
|
|
36
|
+
},
|
|
37
|
+
async run(ctx) {
|
|
38
|
+
try {
|
|
39
|
+
// Extract metadata to filter presets by source resolution
|
|
40
|
+
const { GetObjectCommand } = await import("@aws-sdk/client-s3");
|
|
41
|
+
const response = await ctx.s3Client.send(new GetObjectCommand({
|
|
42
|
+
Bucket: ctx.config.bucket,
|
|
43
|
+
Key: ctx.file.key,
|
|
44
|
+
}));
|
|
45
|
+
// Write to temp file for ffprobe
|
|
46
|
+
const fs = await import("fs/promises");
|
|
47
|
+
const os = await import("os");
|
|
48
|
+
const path = await import("path");
|
|
49
|
+
const tmpFile = path.join(os.tmpdir(), `uploadbox-probe-${crypto.randomUUID()}`);
|
|
50
|
+
const body = await response.Body?.transformToByteArray();
|
|
51
|
+
if (!body) {
|
|
52
|
+
return { success: false, error: "Failed to download video from S3" };
|
|
53
|
+
}
|
|
54
|
+
await fs.writeFile(tmpFile, body);
|
|
55
|
+
let metadata;
|
|
56
|
+
try {
|
|
57
|
+
metadata = await extractVideoMetadata(tmpFile);
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
await fs.rm(tmpFile, { force: true }).catch(() => { });
|
|
61
|
+
}
|
|
62
|
+
// Filter quality presets: only include presets at or below source resolution
|
|
63
|
+
const filteredQualities = qualities.filter((q) => q.height <= metadata.height);
|
|
64
|
+
// Always include at least the lowest quality
|
|
65
|
+
if (filteredQualities.length === 0 && qualities.length > 0) {
|
|
66
|
+
const lowest = [...qualities].sort((a, b) => a.height - b.height)[0];
|
|
67
|
+
if (lowest)
|
|
68
|
+
filteredQualities.push(lowest);
|
|
69
|
+
}
|
|
70
|
+
const outputPrefix = getOutputPrefix(ctx.file.key);
|
|
71
|
+
const transcodingOptions = {
|
|
72
|
+
qualities: filteredQualities,
|
|
73
|
+
segmentDuration,
|
|
74
|
+
generateThumbnail,
|
|
75
|
+
generateSpriteSheet,
|
|
76
|
+
s3OutputPrefix: outputPrefix,
|
|
77
|
+
};
|
|
78
|
+
// Submit transcoding job
|
|
79
|
+
const { jobId } = await provider.submit({
|
|
80
|
+
sourceKey: ctx.file.key,
|
|
81
|
+
sourceBucket: ctx.config.bucket,
|
|
82
|
+
outputBucket: ctx.config.bucket,
|
|
83
|
+
options: transcodingOptions,
|
|
84
|
+
s3Config: {
|
|
85
|
+
region: ctx.config.region,
|
|
86
|
+
endpoint: ctx.config.endpoint,
|
|
87
|
+
accessKeyId: ctx.config.accessKeyId,
|
|
88
|
+
secretAccessKey: ctx.config.secretAccessKey,
|
|
89
|
+
forcePathStyle: ctx.config.forcePathStyle,
|
|
90
|
+
},
|
|
91
|
+
});
|
|
92
|
+
return {
|
|
93
|
+
success: true,
|
|
94
|
+
data: {
|
|
95
|
+
jobId,
|
|
96
|
+
providerName: provider.name,
|
|
97
|
+
masterPlaylistKey: `${outputPrefix}/master.m3u8`,
|
|
98
|
+
thumbnailKey: generateThumbnail ? `${outputPrefix}/thumbnail.jpg` : undefined,
|
|
99
|
+
spriteSheetKey: generateSpriteSheet ? `${outputPrefix}/sprites/sprite-sheet.jpg` : undefined,
|
|
100
|
+
spriteVttKey: generateSpriteSheet ? `${outputPrefix}/sprites/sprites.vtt` : undefined,
|
|
101
|
+
qualities: filteredQualities.map((q) => q.label),
|
|
102
|
+
metadata,
|
|
103
|
+
},
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
catch (err) {
|
|
107
|
+
return {
|
|
108
|
+
success: false,
|
|
109
|
+
error: `Video processing failed: ${err.message}`,
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=video-processing.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"video-processing.js","sourceRoot":"","sources":["../../src/hooks/video-processing.ts"],"names":[],"mappings":"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"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { VideoMetadata, TranscodingStatus, QualityPreset, TranscodingJob, TranscodingOptions, TranscodingProgress, TranscodingProgressCallback, } from "./types.js";
|
|
2
|
+
export { QUALITY_PRESETS, CDN_CACHE_HEADERS } from "./types.js";
|
|
3
|
+
export type { TranscodingProvider, TranscodingSubmitInput, } from "./provider.js";
|
|
4
|
+
export { extractVideoMetadata } from "./metadata.js";
|
|
5
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,aAAa,EACb,cAAc,EACd,kBAAkB,EAClB,mBAAmB,EACnB,2BAA2B,GAC5B,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEhE,YAAY,EACV,mBAAmB,EACnB,sBAAsB,GACvB,MAAM,eAAe,CAAC;AAEvB,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAUA,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAOhE,OAAO,EAAE,oBAAoB,EAAE,MAAM,eAAe,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { VideoMetadata } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Extract video metadata (dimensions, duration, codec, etc.) using ffprobe.
|
|
4
|
+
*
|
|
5
|
+
* Requires `ffprobe` to be installed on the system (available on `$PATH`).
|
|
6
|
+
*
|
|
7
|
+
* @param input - Path to the video file or a readable stream.
|
|
8
|
+
* @returns Extracted video metadata.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { extractVideoMetadata } from "@uploadbox/video";
|
|
13
|
+
*
|
|
14
|
+
* const metadata = await extractVideoMetadata("/tmp/video.mp4");
|
|
15
|
+
* console.log(metadata.width, metadata.height, metadata.durationSeconds);
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export declare function extractVideoMetadata(input: string | NodeJS.ReadableStream): Promise<VideoMetadata>;
|
|
19
|
+
//# sourceMappingURL=metadata.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAGhD;;;;;;;;;;;;;;;GAeG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,MAAM,GAAG,MAAM,CAAC,cAAc,GACpC,OAAO,CAAC,aAAa,CAAC,CAmCxB"}
|
package/dist/metadata.js
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { ffprobe } from "./ffutils.js";
|
|
2
|
+
/**
|
|
3
|
+
* Extract video metadata (dimensions, duration, codec, etc.) using ffprobe.
|
|
4
|
+
*
|
|
5
|
+
* Requires `ffprobe` to be installed on the system (available on `$PATH`).
|
|
6
|
+
*
|
|
7
|
+
* @param input - Path to the video file or a readable stream.
|
|
8
|
+
* @returns Extracted video metadata.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```ts
|
|
12
|
+
* import { extractVideoMetadata } from "@uploadbox/video";
|
|
13
|
+
*
|
|
14
|
+
* const metadata = await extractVideoMetadata("/tmp/video.mp4");
|
|
15
|
+
* console.log(metadata.width, metadata.height, metadata.durationSeconds);
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
export async function extractVideoMetadata(input) {
|
|
19
|
+
const data = await ffprobe(input);
|
|
20
|
+
const videoStream = data.streams?.find((s) => s.codec_type === "video");
|
|
21
|
+
const audioStream = data.streams?.find((s) => s.codec_type === "audio");
|
|
22
|
+
if (!videoStream) {
|
|
23
|
+
throw new Error("No video stream found in file");
|
|
24
|
+
}
|
|
25
|
+
// Parse frame rate from r_frame_rate (e.g. "30/1" or "30000/1001")
|
|
26
|
+
let frameRate = 30;
|
|
27
|
+
if (videoStream.r_frame_rate) {
|
|
28
|
+
const [num, den] = videoStream.r_frame_rate.split("/").map(Number);
|
|
29
|
+
if (num && den)
|
|
30
|
+
frameRate = Math.round((num / den) * 100) / 100;
|
|
31
|
+
}
|
|
32
|
+
const metadata = {
|
|
33
|
+
width: videoStream.width ?? 0,
|
|
34
|
+
height: videoStream.height ?? 0,
|
|
35
|
+
durationSeconds: parseFloat(data.format?.duration ?? videoStream.duration ?? "0"),
|
|
36
|
+
codec: videoStream.codec_name ?? "unknown",
|
|
37
|
+
frameRate,
|
|
38
|
+
bitrate: parseInt(data.format?.bit_rate ?? videoStream.bit_rate ?? "0", 10),
|
|
39
|
+
audio: audioStream
|
|
40
|
+
? {
|
|
41
|
+
codec: audioStream.codec_name ?? "unknown",
|
|
42
|
+
sampleRate: parseInt(audioStream.sample_rate ?? "0", 10),
|
|
43
|
+
channels: audioStream.channels ?? 0,
|
|
44
|
+
bitrate: parseInt(audioStream.bit_rate ?? "0", 10),
|
|
45
|
+
}
|
|
46
|
+
: undefined,
|
|
47
|
+
};
|
|
48
|
+
return metadata;
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=metadata.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"metadata.js","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAEvC;;;;;;;;;;;;;;;GAeG;AACH,MAAM,CAAC,KAAK,UAAU,oBAAoB,CACxC,KAAqC;IAErC,MAAM,IAAI,GAAG,MAAM,OAAO,CAAC,KAAK,CAAC,CAAC;IAElC,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC;IACxE,MAAM,WAAW,GAAG,IAAI,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,KAAK,OAAO,CAAC,CAAC;IAExE,IAAI,CAAC,WAAW,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;IACnD,CAAC;IAED,mEAAmE;IACnE,IAAI,SAAS,GAAG,EAAE,CAAC;IACnB,IAAI,WAAW,CAAC,YAAY,EAAE,CAAC;QAC7B,MAAM,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,WAAW,CAAC,YAAY,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;QACnE,IAAI,GAAG,IAAI,GAAG;YAAE,SAAS,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC,GAAG,GAAG,CAAC;IAClE,CAAC;IAED,MAAM,QAAQ,GAAkB;QAC9B,KAAK,EAAE,WAAW,CAAC,KAAK,IAAI,CAAC;QAC7B,MAAM,EAAE,WAAW,CAAC,MAAM,IAAI,CAAC;QAC/B,eAAe,EAAE,UAAU,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,IAAI,WAAW,CAAC,QAAQ,IAAI,GAAG,CAAC;QACjF,KAAK,EAAE,WAAW,CAAC,UAAU,IAAI,SAAS;QAC1C,SAAS;QACT,OAAO,EAAE,QAAQ,CAAC,IAAI,CAAC,MAAM,EAAE,QAAQ,IAAI,WAAW,CAAC,QAAQ,IAAI,GAAG,EAAE,EAAE,CAAC;QAC3E,KAAK,EAAE,WAAW;YAChB,CAAC,CAAC;gBACE,KAAK,EAAE,WAAW,CAAC,UAAU,IAAI,SAAS;gBAC1C,UAAU,EAAE,QAAQ,CAAC,WAAW,CAAC,WAAW,IAAI,GAAG,EAAE,EAAE,CAAC;gBACxD,QAAQ,EAAE,WAAW,CAAC,QAAQ,IAAI,CAAC;gBACnC,OAAO,EAAE,QAAQ,CAAC,WAAW,CAAC,QAAQ,IAAI,GAAG,EAAE,EAAE,CAAC;aACnD;YACH,CAAC,CAAC,SAAS;KACd,CAAC;IAEF,OAAO,QAAQ,CAAC;AAClB,CAAC"}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { TranscodingOptions, TranscodingJob, TranscodingProgressCallback } from "./types.js";
|
|
2
|
+
/** Input passed to a transcoding provider's submit method. */
|
|
3
|
+
export interface TranscodingSubmitInput {
|
|
4
|
+
/** S3 key of the source video file. */
|
|
5
|
+
sourceKey: string;
|
|
6
|
+
/** S3 bucket containing the source file. */
|
|
7
|
+
sourceBucket: string;
|
|
8
|
+
/** S3 bucket for transcoded output (may be the same as source). */
|
|
9
|
+
outputBucket: string;
|
|
10
|
+
/** Transcoding options controlling quality presets, segments, etc. */
|
|
11
|
+
options: TranscodingOptions;
|
|
12
|
+
/** S3 connection configuration. */
|
|
13
|
+
s3Config: {
|
|
14
|
+
region?: string;
|
|
15
|
+
endpoint?: string;
|
|
16
|
+
accessKeyId: string;
|
|
17
|
+
secretAccessKey: string;
|
|
18
|
+
forcePathStyle?: boolean;
|
|
19
|
+
};
|
|
20
|
+
/** Optional callback for progress updates. */
|
|
21
|
+
onProgress?: TranscodingProgressCallback;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Interface for pluggable transcoding backends.
|
|
25
|
+
*
|
|
26
|
+
* Implementations handle the actual video transcoding work — locally via FFmpeg,
|
|
27
|
+
* remotely via AWS Lambda, or through external services like Mux or Cloudflare Stream.
|
|
28
|
+
*/
|
|
29
|
+
export interface TranscodingProvider {
|
|
30
|
+
/** Human-readable provider name for logging. */
|
|
31
|
+
name: string;
|
|
32
|
+
/**
|
|
33
|
+
* Submit a video for transcoding.
|
|
34
|
+
* Returns a job ID that can be used to track status.
|
|
35
|
+
*/
|
|
36
|
+
submit(input: TranscodingSubmitInput): Promise<{
|
|
37
|
+
jobId: string;
|
|
38
|
+
}>;
|
|
39
|
+
/** Get the current status of a transcoding job. */
|
|
40
|
+
getJobStatus(jobId: string): Promise<TranscodingJob>;
|
|
41
|
+
/** Cancel a running transcoding job. */
|
|
42
|
+
cancel(jobId: string): Promise<void>;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=provider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.d.ts","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,cAAc,EAAE,2BAA2B,EAAE,MAAM,YAAY,CAAC;AAElG,8DAA8D;AAC9D,MAAM,WAAW,sBAAsB;IACrC,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,4CAA4C;IAC5C,YAAY,EAAE,MAAM,CAAC;IACrB,mEAAmE;IACnE,YAAY,EAAE,MAAM,CAAC;IACrB,sEAAsE;IACtE,OAAO,EAAE,kBAAkB,CAAC;IAC5B,mCAAmC;IACnC,QAAQ,EAAE;QACR,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,WAAW,EAAE,MAAM,CAAC;QACpB,eAAe,EAAE,MAAM,CAAC;QACxB,cAAc,CAAC,EAAE,OAAO,CAAC;KAC1B,CAAC;IACF,8CAA8C;IAC9C,UAAU,CAAC,EAAE,2BAA2B,CAAC;CAC1C;AAED;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IAClC,gDAAgD;IAChD,IAAI,EAAE,MAAM,CAAC;IAEb;;;OAGG;IACH,MAAM,CAAC,KAAK,EAAE,sBAAsB,GAAG,OAAO,CAAC;QAAE,KAAK,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IAElE,mDAAmD;IACnD,YAAY,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC,CAAC;IAErD,wCAAwC;IACxC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACtC"}
|
package/dist/provider.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provider.js","sourceRoot":"","sources":["../src/provider.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { TranscodingProvider } from "../provider.js";
|
|
2
|
+
import type { TranscodingJob } from "../types.js";
|
|
3
|
+
interface ExternalProviderOptions {
|
|
4
|
+
/** Base URL for the external transcoding API. */
|
|
5
|
+
apiUrl: string;
|
|
6
|
+
/** API key for authentication. */
|
|
7
|
+
apiKey: string;
|
|
8
|
+
/** Webhook secret for verifying incoming status callbacks. */
|
|
9
|
+
webhookSecret?: string;
|
|
10
|
+
/**
|
|
11
|
+
* Map external API response to TranscodingJob format.
|
|
12
|
+
* If not provided, the response is assumed to already match.
|
|
13
|
+
*/
|
|
14
|
+
mapResponse?: (response: Record<string, unknown>) => TranscodingJob;
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Generic external transcoding provider.
|
|
18
|
+
*
|
|
19
|
+
* Adapter for external services like Mux, Cloudflare Stream, or any API
|
|
20
|
+
* that accepts a source URL and returns transcoded HLS outputs.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* import { createExternalProvider } from "@uploadbox/video/providers/external";
|
|
25
|
+
*
|
|
26
|
+
* const provider = createExternalProvider({
|
|
27
|
+
* apiUrl: "https://api.mux.com/video/v1/assets",
|
|
28
|
+
* apiKey: process.env.MUX_TOKEN_ID + ":" + process.env.MUX_TOKEN_SECRET,
|
|
29
|
+
* mapResponse: (res) => ({
|
|
30
|
+
* jobId: res.id as string,
|
|
31
|
+
* status: mapMuxStatus(res.status as string),
|
|
32
|
+
* progress: res.status === "ready" ? 100 : 50,
|
|
33
|
+
* outputKeys: [],
|
|
34
|
+
* }),
|
|
35
|
+
* });
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
export declare function createExternalProvider(options: ExternalProviderOptions): TranscodingProvider;
|
|
39
|
+
export {};
|
|
40
|
+
//# sourceMappingURL=external.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"external.d.ts","sourceRoot":"","sources":["../../src/providers/external.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAA0B,MAAM,gBAAgB,CAAC;AAClF,OAAO,KAAK,EAAE,cAAc,EAAqB,MAAM,aAAa,CAAC;AAErE,UAAU,uBAAuB;IAC/B,iDAAiD;IACjD,MAAM,EAAE,MAAM,CAAC;IACf,kCAAkC;IAClC,MAAM,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB;;;OAGG;IACH,WAAW,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,cAAc,CAAC;CACrE;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,uBAAuB,GAC/B,mBAAmB,CAgFrB"}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic external transcoding provider.
|
|
3
|
+
*
|
|
4
|
+
* Adapter for external services like Mux, Cloudflare Stream, or any API
|
|
5
|
+
* that accepts a source URL and returns transcoded HLS outputs.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```ts
|
|
9
|
+
* import { createExternalProvider } from "@uploadbox/video/providers/external";
|
|
10
|
+
*
|
|
11
|
+
* const provider = createExternalProvider({
|
|
12
|
+
* apiUrl: "https://api.mux.com/video/v1/assets",
|
|
13
|
+
* apiKey: process.env.MUX_TOKEN_ID + ":" + process.env.MUX_TOKEN_SECRET,
|
|
14
|
+
* mapResponse: (res) => ({
|
|
15
|
+
* jobId: res.id as string,
|
|
16
|
+
* status: mapMuxStatus(res.status as string),
|
|
17
|
+
* progress: res.status === "ready" ? 100 : 50,
|
|
18
|
+
* outputKeys: [],
|
|
19
|
+
* }),
|
|
20
|
+
* });
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function createExternalProvider(options) {
|
|
24
|
+
const { apiUrl, apiKey, mapResponse } = options;
|
|
25
|
+
function mapJob(data) {
|
|
26
|
+
if (mapResponse)
|
|
27
|
+
return mapResponse(data);
|
|
28
|
+
return {
|
|
29
|
+
jobId: data["jobId"] ?? data["id"] ?? "",
|
|
30
|
+
status: data["status"] ?? "pending",
|
|
31
|
+
progress: data["progress"] ?? 0,
|
|
32
|
+
outputKeys: data["outputKeys"] ?? [],
|
|
33
|
+
error: data["error"],
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
return {
|
|
37
|
+
name: "external",
|
|
38
|
+
async submit(input) {
|
|
39
|
+
const response = await fetch(apiUrl, {
|
|
40
|
+
method: "POST",
|
|
41
|
+
headers: {
|
|
42
|
+
"Content-Type": "application/json",
|
|
43
|
+
Authorization: `Bearer ${apiKey}`,
|
|
44
|
+
},
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
sourceKey: input.sourceKey,
|
|
47
|
+
sourceBucket: input.sourceBucket,
|
|
48
|
+
outputBucket: input.outputBucket,
|
|
49
|
+
options: input.options,
|
|
50
|
+
s3Config: {
|
|
51
|
+
region: input.s3Config.region,
|
|
52
|
+
endpoint: input.s3Config.endpoint,
|
|
53
|
+
},
|
|
54
|
+
}),
|
|
55
|
+
});
|
|
56
|
+
if (!response.ok) {
|
|
57
|
+
const text = await response.text().catch(() => "");
|
|
58
|
+
throw new Error(`External transcoding API returned ${response.status}: ${text.slice(0, 500)}`);
|
|
59
|
+
}
|
|
60
|
+
const data = (await response.json());
|
|
61
|
+
const jobId = data["jobId"] ?? data["id"];
|
|
62
|
+
if (!jobId)
|
|
63
|
+
throw new Error("External API did not return a jobId");
|
|
64
|
+
return { jobId };
|
|
65
|
+
},
|
|
66
|
+
async getJobStatus(jobId) {
|
|
67
|
+
const response = await fetch(`${apiUrl}/${jobId}`, {
|
|
68
|
+
headers: {
|
|
69
|
+
Authorization: `Bearer ${apiKey}`,
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
if (!response.ok) {
|
|
73
|
+
return {
|
|
74
|
+
jobId,
|
|
75
|
+
status: "failed",
|
|
76
|
+
progress: 0,
|
|
77
|
+
outputKeys: [],
|
|
78
|
+
error: `API returned ${response.status}`,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const data = (await response.json());
|
|
82
|
+
return mapJob(data);
|
|
83
|
+
},
|
|
84
|
+
async cancel(jobId) {
|
|
85
|
+
await fetch(`${apiUrl}/${jobId}/cancel`, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: {
|
|
88
|
+
Authorization: `Bearer ${apiKey}`,
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=external.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"external.js","sourceRoot":"","sources":["../../src/providers/external.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { TranscodingProvider } from "../provider.js";
|
|
2
|
+
interface FFmpegProviderOptions {
|
|
3
|
+
/** Path to ffmpeg binary. @default "ffmpeg" */
|
|
4
|
+
ffmpegPath?: string;
|
|
5
|
+
/** Path to ffprobe binary. @default "ffprobe" */
|
|
6
|
+
ffprobePath?: string;
|
|
7
|
+
/** Temporary directory for intermediate files. @default os.tmpdir() */
|
|
8
|
+
tmpDir?: string;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Local FFmpeg transcoding provider.
|
|
12
|
+
*
|
|
13
|
+
* Downloads the source video from S3, transcodes it using a local FFmpeg binary,
|
|
14
|
+
* and uploads HLS segments, playlists, thumbnails, and sprites back to S3.
|
|
15
|
+
*
|
|
16
|
+
* Best for development and small-scale self-hosted deployments.
|
|
17
|
+
*
|
|
18
|
+
* @example
|
|
19
|
+
* ```ts
|
|
20
|
+
* import { createFFmpegProvider } from "@uploadbox/video/providers/ffmpeg";
|
|
21
|
+
*
|
|
22
|
+
* const provider = createFFmpegProvider({ ffmpegPath: "/usr/bin/ffmpeg" });
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export declare function createFFmpegProvider(options?: FFmpegProviderOptions): TranscodingProvider;
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=ffmpeg.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ffmpeg.d.ts","sourceRoot":"","sources":["../../src/providers/ffmpeg.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,mBAAmB,EAA0B,MAAM,gBAAgB,CAAC;AAUlF,UAAU,qBAAqB;IAC7B,+CAA+C;IAC/C,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,uEAAuE;IACvE,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAUD;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,GAAE,qBAA0B,GAClC,mBAAmB,CAwTrB"}
|