m4k 0.1.2 → 0.2.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/README.md +24 -10
- package/lib/audio/audio-processor.d.ts +20 -0
- package/lib/audio/audio-processor.js +47 -0
- package/lib/images/image-processor.d.ts +22 -0
- package/lib/images/{image-optimizer.js → image-processor.js} +24 -9
- package/lib/index.d.ts +4 -2
- package/lib/index.js +4 -2
- package/lib/util/ffmpeg-processor.d.ts +30 -0
- package/lib/{videos/video-optimizer.js → util/ffmpeg-processor.js} +33 -45
- package/lib/util/streams.d.ts +2 -3
- package/lib/util/streams.js +7 -7
- package/lib/videos/video-processor.d.ts +20 -0
- package/lib/videos/video-processor.js +60 -0
- package/package.json +1 -1
- package/lib/images/image-optimizer.d.ts +0 -10
- package/lib/videos/video-optimizer.d.ts +0 -16
package/README.md
CHANGED
|
@@ -3,27 +3,41 @@
|
|
|
3
3
|
A media conversion toolkit based on [ffmpeg](https://ffmpeg.org/) and [sharp](https://sharp.pixelplumbing.com/)/[libvips](https://github.com/libvips/libvips).
|
|
4
4
|
It provides queueing and progress reporting via async iterables.
|
|
5
5
|
|
|
6
|
+
The API is unified for audio, images and videos.
|
|
7
|
+
|
|
6
8
|
## Usage
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
### Processing audio
|
|
11
|
+
|
|
12
|
+
```ts
|
|
13
|
+
import { processAudio, ProcessedFile } from "m4k";
|
|
14
|
+
// input is a file path or an async iterable of Uint8Arrays or a Blob
|
|
15
|
+
for await (const value of processAudio(input, opts)) {
|
|
16
|
+
if (value instanceof ProcessedFile) {
|
|
17
|
+
// do something with the file
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
### Processing images
|
|
9
23
|
|
|
10
24
|
```ts
|
|
11
|
-
import {
|
|
12
|
-
// input is a
|
|
13
|
-
for await (const value of
|
|
14
|
-
if (value instanceof
|
|
25
|
+
import { processImage, ProcessedFile } from "m4k";
|
|
26
|
+
// input is a file path or an async iterable of Uint8Arrays or a Blob
|
|
27
|
+
for await (const value of processImage(input, opts)) {
|
|
28
|
+
if (value instanceof ProcessedFile) {
|
|
15
29
|
// do something with the file
|
|
16
30
|
}
|
|
17
31
|
}
|
|
18
32
|
```
|
|
19
33
|
|
|
20
|
-
|
|
34
|
+
### Processing videos
|
|
21
35
|
|
|
22
36
|
```ts
|
|
23
|
-
import {
|
|
24
|
-
// input is a file path
|
|
25
|
-
for await (const value of
|
|
26
|
-
if (value instanceof
|
|
37
|
+
import { processVideo, ProcessedFile } from "m4k";
|
|
38
|
+
// input is a file path or an async iterable of Uint8Arrays or a Blob
|
|
39
|
+
for await (const value of processVideo(input, opts)) {
|
|
40
|
+
if (value instanceof ProcessedFile) {
|
|
27
41
|
// do something with the file
|
|
28
42
|
}
|
|
29
43
|
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type AudioOptions } from "@m4k/common";
|
|
2
|
+
/**
|
|
3
|
+
* Process a video.
|
|
4
|
+
* @param input - The input video. Can be a file path, a stream or a blob.
|
|
5
|
+
* @param opts - The options for the video processing.
|
|
6
|
+
* @param signal - An optional abort signal.
|
|
7
|
+
* @returns An iterable of the processed videos.
|
|
8
|
+
*/
|
|
9
|
+
export declare function processAudio(input: string | AsyncIterable<Uint8Array> | Blob, opts: AudioOptions, { signal }?: {
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
}): AsyncGenerator<import("@m4k/common").ProcessedFile | {
|
|
12
|
+
progress: number;
|
|
13
|
+
} | {
|
|
14
|
+
position: number;
|
|
15
|
+
}, void, unknown> | null;
|
|
16
|
+
/**
|
|
17
|
+
* The queue for video processing.
|
|
18
|
+
*/
|
|
19
|
+
export declare const audioQueue: import("tasque").Tasque;
|
|
20
|
+
//# sourceMappingURL=audio-processor.d.ts.map
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {} from "@m4k/common";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { createQueue } from "tasque";
|
|
4
|
+
import { processFfmpeg } from "../util/ffmpeg-processor";
|
|
5
|
+
/**
|
|
6
|
+
* Process a video.
|
|
7
|
+
* @param input - The input video. Can be a file path, a stream or a blob.
|
|
8
|
+
* @param opts - The options for the video processing.
|
|
9
|
+
* @param signal - An optional abort signal.
|
|
10
|
+
* @returns An iterable of the processed videos.
|
|
11
|
+
*/
|
|
12
|
+
export function processAudio(input, opts, { signal } = {}) {
|
|
13
|
+
return processFfmpeg(input, () => {
|
|
14
|
+
const inArgs = [];
|
|
15
|
+
if (opts.inputFormat)
|
|
16
|
+
inArgs.push("-f", opts.inputFormat);
|
|
17
|
+
if (opts.seek)
|
|
18
|
+
inArgs.push("-ss", opts.seek.toString());
|
|
19
|
+
if (opts.duration)
|
|
20
|
+
inArgs.push("-t", opts.duration.toString());
|
|
21
|
+
const outArgs = [];
|
|
22
|
+
if (opts.format)
|
|
23
|
+
outArgs.push("-f", opts.format);
|
|
24
|
+
if (opts.bitrate)
|
|
25
|
+
outArgs.push("-b:a", opts.bitrate.toString());
|
|
26
|
+
if (opts.filters)
|
|
27
|
+
outArgs.push("-af", opts.filters);
|
|
28
|
+
if (opts.complexFilters)
|
|
29
|
+
outArgs.push("-filter_complex", opts.complexFilters);
|
|
30
|
+
outArgs.push("-c:a", opts.codec ?? "copy");
|
|
31
|
+
return {
|
|
32
|
+
input: inArgs,
|
|
33
|
+
out: outArgs,
|
|
34
|
+
format: opts.format ?? "mp4",
|
|
35
|
+
};
|
|
36
|
+
}, { signal, queue: audioQueue, tmpDir });
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The queue for video processing.
|
|
40
|
+
*/
|
|
41
|
+
export const audioQueue = createQueue({
|
|
42
|
+
parallelize: Number(process.env.AUDIO_PARALLELIZE ?? 1),
|
|
43
|
+
max: Number(process.env.AUDIO_QUEUE_SIZE ?? 5),
|
|
44
|
+
});
|
|
45
|
+
const tmpDir = `${process.env.M4K_TMP_DIR ?? "/tmp/m4k"}/audio`;
|
|
46
|
+
await mkdir(tmpDir, { recursive: true });
|
|
47
|
+
//# sourceMappingURL=audio-processor.js.map
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { type ImageOptions, ProcessedFile } from "@m4k/common";
|
|
2
|
+
export type Format = Required<ImageOptions>["format"];
|
|
3
|
+
export type Fit = Required<Required<ImageOptions>["resize"]>["fit"];
|
|
4
|
+
/**
|
|
5
|
+
* Process an image.
|
|
6
|
+
* @param input - The input image. Can be a file path, a stream or a blob.
|
|
7
|
+
* @param opts - The options for the image processing.
|
|
8
|
+
* @param signal - An optional abort signal.
|
|
9
|
+
* @returns An iterable of the processed images.
|
|
10
|
+
*/
|
|
11
|
+
export declare function processImage(input: string | AsyncIterable<Uint8Array> | Blob, opts: ImageOptions | ImageOptions[], { signal }?: {
|
|
12
|
+
signal?: AbortSignal;
|
|
13
|
+
}): AsyncGenerator<ProcessedFile | {
|
|
14
|
+
progress: number;
|
|
15
|
+
} | {
|
|
16
|
+
position: number;
|
|
17
|
+
}, void, unknown> | null;
|
|
18
|
+
/**
|
|
19
|
+
* The queue for image processing.
|
|
20
|
+
*/
|
|
21
|
+
export declare const imageQueue: import("tasque").Tasque;
|
|
22
|
+
//# sourceMappingURL=image-processor.d.ts.map
|
|
@@ -1,13 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ProcessedFile } from "@m4k/common";
|
|
2
|
+
import { createReadStream } from "node:fs";
|
|
2
3
|
import sharp from "sharp";
|
|
3
4
|
import { createQueue } from "tasque";
|
|
4
|
-
import {
|
|
5
|
-
|
|
5
|
+
import { exhaustAsyncIterableToWritable } from "../util/streams";
|
|
6
|
+
/**
|
|
7
|
+
* Process an image.
|
|
8
|
+
* @param input - The input image. Can be a file path, a stream or a blob.
|
|
9
|
+
* @param opts - The options for the image processing.
|
|
10
|
+
* @param signal - An optional abort signal.
|
|
11
|
+
* @returns An iterable of the processed images.
|
|
12
|
+
*/
|
|
13
|
+
export function processImage(input, opts, { signal } = {}) {
|
|
6
14
|
const sharpInstance = sharp();
|
|
7
15
|
const iterable = imageQueue.iterate(async function* () {
|
|
8
16
|
opts = Array.isArray(opts) ? opts : [opts];
|
|
9
|
-
|
|
10
|
-
|
|
17
|
+
void exhaustAsyncIterableToWritable(typeof input === "string"
|
|
18
|
+
? createReadStream(input)
|
|
19
|
+
: "stream" in input
|
|
20
|
+
? input.stream()
|
|
21
|
+
: input, sharpInstance);
|
|
22
|
+
const files = opts.map(async ({ colorspace, crop, format = "avif", keepExif, keepIcc, keepMetadata, quality, resize, rotate, }, idx) => {
|
|
11
23
|
let s = sharpInstance.clone();
|
|
12
24
|
s = s.rotate(rotate);
|
|
13
25
|
if (resize)
|
|
@@ -31,12 +43,12 @@ export function optimizeImage(input, opts, signal) {
|
|
|
31
43
|
});
|
|
32
44
|
}
|
|
33
45
|
s = s.toFormat(format, { quality });
|
|
34
|
-
return new
|
|
35
|
-
})
|
|
46
|
+
return new ProcessedFile(`file${idx + 1}.${format}`, `image/${format}`, s);
|
|
47
|
+
});
|
|
36
48
|
let done = 0;
|
|
37
49
|
yield { progress: 0 };
|
|
38
50
|
for (const file of files) {
|
|
39
|
-
yield file;
|
|
51
|
+
yield await file;
|
|
40
52
|
done++;
|
|
41
53
|
yield { progress: Math.round((done / files.length) * 100) };
|
|
42
54
|
}
|
|
@@ -52,8 +64,11 @@ export function optimizeImage(input, opts, signal) {
|
|
|
52
64
|
}
|
|
53
65
|
})();
|
|
54
66
|
}
|
|
67
|
+
/**
|
|
68
|
+
* The queue for image processing.
|
|
69
|
+
*/
|
|
55
70
|
export const imageQueue = createQueue({
|
|
56
71
|
parallelize: Number(process.env.IMAGE_PARALLELIZE ?? 5),
|
|
57
72
|
max: Number(process.env.IMAGE_QUEUE_SIZE ?? 100),
|
|
58
73
|
});
|
|
59
|
-
//# sourceMappingURL=image-
|
|
74
|
+
//# sourceMappingURL=image-processor.js.map
|
package/lib/index.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export * from "@m4k/common";
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
2
|
+
export { audioQueue, processAudio } from "./audio/audio-processor";
|
|
3
|
+
export { imageQueue, processImage } from "./images/image-processor";
|
|
4
|
+
export { getExtension } from "./util/ffmpeg-processor";
|
|
5
|
+
export { processVideo, videoQueue } from "./videos/video-processor";
|
|
4
6
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
export * from "@m4k/common";
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
2
|
+
export { audioQueue, processAudio } from "./audio/audio-processor";
|
|
3
|
+
export { imageQueue, processImage } from "./images/image-processor";
|
|
4
|
+
export { getExtension } from "./util/ffmpeg-processor";
|
|
5
|
+
export { processVideo, videoQueue } from "./videos/video-processor";
|
|
4
6
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ProcessedFile } from "@m4k/common";
|
|
2
|
+
import { type Tasque } from "tasque";
|
|
3
|
+
/**
|
|
4
|
+
* Process a video.
|
|
5
|
+
* @param input - The input video. Can be a file path, a stream or a blob.
|
|
6
|
+
* @param opts - The options for the video processing.
|
|
7
|
+
* @param signal - An optional abort signal.
|
|
8
|
+
* @returns An iterable of the processed videos.
|
|
9
|
+
*/
|
|
10
|
+
export declare function processFfmpeg(input: string | AsyncIterable<Uint8Array> | Blob, buildArgs: () => {
|
|
11
|
+
input: string[];
|
|
12
|
+
out: string[];
|
|
13
|
+
format: string;
|
|
14
|
+
}, { signal, queue, tmpDir, }: {
|
|
15
|
+
signal?: AbortSignal;
|
|
16
|
+
queue: Tasque;
|
|
17
|
+
tmpDir: string;
|
|
18
|
+
}): AsyncGenerator<ProcessedFile | {
|
|
19
|
+
progress: number;
|
|
20
|
+
} | {
|
|
21
|
+
position: number;
|
|
22
|
+
}, void, unknown> | null;
|
|
23
|
+
/**
|
|
24
|
+
* Get the extension for a given format.
|
|
25
|
+
* May not be complete yet. Most of the time, the format will be the same as the extension.
|
|
26
|
+
* @param format - The format to get the extension for.
|
|
27
|
+
* @returns The extension for the given format.
|
|
28
|
+
*/
|
|
29
|
+
export declare function getExtension(format: string): string;
|
|
30
|
+
//# sourceMappingURL=ffmpeg-processor.d.ts.map
|
|
@@ -1,45 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { ProcessedFile } from "@m4k/common";
|
|
2
2
|
import { spawn } from "node:child_process";
|
|
3
3
|
import { getRandomValues } from "node:crypto";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { createWriteStream } from "node:fs";
|
|
5
|
+
import { rm } from "node:fs/promises";
|
|
6
|
+
import {} from "tasque";
|
|
6
7
|
import { mimeTypes } from "../util/mime";
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (opts.audioBitrate)
|
|
22
|
-
outArgs.push("-b:a", opts.audioBitrate.toString());
|
|
23
|
-
if (opts.videoBitrate)
|
|
24
|
-
outArgs.push("-b:v", opts.videoBitrate.toString());
|
|
25
|
-
if (opts.audioFilters)
|
|
26
|
-
outArgs.push("-af", opts.audioFilters);
|
|
27
|
-
if (opts.videoFilters)
|
|
28
|
-
outArgs.push("-vf", opts.videoFilters);
|
|
29
|
-
if (opts.complexFilters)
|
|
30
|
-
outArgs.push("-filter_complex", opts.complexFilters);
|
|
31
|
-
if (opts.aspect)
|
|
32
|
-
outArgs.push("-aspect", opts.aspect.toString());
|
|
33
|
-
if (opts.pad)
|
|
34
|
-
outArgs.push("-pad", opts.pad);
|
|
35
|
-
if (opts.fps)
|
|
36
|
-
outArgs.push("-r", opts.fps.toString());
|
|
37
|
-
if (opts.size)
|
|
38
|
-
outArgs.push("-scale", opts.size);
|
|
39
|
-
outArgs.push("-c:v", opts.videoCodec ?? "copy");
|
|
40
|
-
outArgs.push("-c:a", opts.audioCodec ?? "copy");
|
|
8
|
+
import { exhaustAsyncIterableToWritable } from "../util/streams";
|
|
9
|
+
/**
|
|
10
|
+
* Process a video.
|
|
11
|
+
* @param input - The input video. Can be a file path, a stream or a blob.
|
|
12
|
+
* @param opts - The options for the video processing.
|
|
13
|
+
* @param signal - An optional abort signal.
|
|
14
|
+
* @returns An iterable of the processed videos.
|
|
15
|
+
*/
|
|
16
|
+
export function processFfmpeg(input, buildArgs, { signal, queue, tmpDir, }) {
|
|
17
|
+
const id = randomId();
|
|
18
|
+
const inputPath = typeof input === "string" ? input : createTmp(tmpDir, id, input);
|
|
19
|
+
const iterable = queue.iterate(async function* () {
|
|
20
|
+
const { input: inArgs, out: outArgs, format } = buildArgs();
|
|
21
|
+
const outputPath = `${tmpDir}/out-${id}.${getExtension(format)}`;
|
|
41
22
|
try {
|
|
42
|
-
const child = spawn(ffmpeg, ["-y", ...inArgs, "-i", inputPath, ...outArgs, outputPath], { stdio: ["pipe", "pipe", "pipe"], signal });
|
|
23
|
+
const child = spawn(ffmpeg, ["-y", ...inArgs, "-i", await inputPath, ...outArgs, outputPath], { stdio: ["pipe", "pipe", "pipe"], signal });
|
|
43
24
|
const decoder = new TextDecoder();
|
|
44
25
|
let metadataStr = "";
|
|
45
26
|
let duration = null;
|
|
@@ -77,10 +58,13 @@ export function optimizeVideo(inputPath, opts, signal) {
|
|
|
77
58
|
if (code !== 0) {
|
|
78
59
|
throw new Error(`ffmpeg exited with code ${code}: ${errStr}`);
|
|
79
60
|
}
|
|
80
|
-
yield new
|
|
61
|
+
yield new ProcessedFile(outputPath, mimeTypes[format]);
|
|
81
62
|
}
|
|
82
63
|
finally {
|
|
83
64
|
await rm(outputPath, { force: true });
|
|
65
|
+
if (typeof inputPath !== "string") {
|
|
66
|
+
await rm(await inputPath, { force: true });
|
|
67
|
+
}
|
|
84
68
|
}
|
|
85
69
|
}, signal);
|
|
86
70
|
if (!iterable)
|
|
@@ -103,11 +87,15 @@ export function optimizeVideo(inputPath, opts, signal) {
|
|
|
103
87
|
export function getExtension(format) {
|
|
104
88
|
return extMap[format] ?? format;
|
|
105
89
|
}
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
90
|
+
async function createTmp(tmpDir, id, input) {
|
|
91
|
+
const filePath = `${tmpDir}/in-${id}`;
|
|
92
|
+
const writer = createWriteStream(filePath);
|
|
93
|
+
await exhaustAsyncIterableToWritable("stream" in input ? input.stream() : input, writer);
|
|
94
|
+
return filePath;
|
|
95
|
+
}
|
|
96
|
+
function randomId() {
|
|
97
|
+
return Buffer.from(getRandomValues(new Uint8Array(16))).toString("base64url");
|
|
98
|
+
}
|
|
111
99
|
function parseDuration(metadataStr) {
|
|
112
100
|
const match = metadataStr.match(/Duration: (\d+:\d+:\d+\.\d+)/);
|
|
113
101
|
if (!match)
|
|
@@ -122,4 +110,4 @@ const ffmpeg = process.env.FFMPEG_PATH ?? (await import("@ffmpeg-installer/ffmpe
|
|
|
122
110
|
const extMap = {
|
|
123
111
|
matroska: "mkv",
|
|
124
112
|
};
|
|
125
|
-
//# sourceMappingURL=
|
|
113
|
+
//# sourceMappingURL=ffmpeg-processor.js.map
|
package/lib/util/streams.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare function
|
|
3
|
-
export declare function readableToWeb(stream: Readable): ReadableStream & AsyncIterable<Uint8Array>;
|
|
1
|
+
import type { Writable } from "stream";
|
|
2
|
+
export declare function exhaustAsyncIterableToWritable(it: AsyncIterable<Uint8Array>, writable: Writable): Promise<void>;
|
|
4
3
|
//# sourceMappingURL=streams.d.ts.map
|
package/lib/util/streams.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
export async function exhaustAsyncIterableToWritable(it, writable) {
|
|
2
|
+
for await (const chunk of it) {
|
|
3
|
+
if (!writable.write(chunk))
|
|
4
|
+
await new Promise((resolve) => writable.once("drain", resolve));
|
|
5
|
+
}
|
|
6
|
+
writable.end();
|
|
7
|
+
await new Promise((resolve) => writable.on("finish", resolve));
|
|
8
8
|
}
|
|
9
9
|
//# sourceMappingURL=streams.js.map
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { type VideoOptions } from "@m4k/common";
|
|
2
|
+
/**
|
|
3
|
+
* Process a video.
|
|
4
|
+
* @param input - The input video. Can be a file path, a stream or a blob.
|
|
5
|
+
* @param opts - The options for the video processing.
|
|
6
|
+
* @param signal - An optional abort signal.
|
|
7
|
+
* @returns An iterable of the processed videos.
|
|
8
|
+
*/
|
|
9
|
+
export declare function processVideo(input: string | AsyncIterable<Uint8Array> | Blob, opts: VideoOptions, { signal }?: {
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
}): AsyncGenerator<import("@m4k/common").ProcessedFile | {
|
|
12
|
+
progress: number;
|
|
13
|
+
} | {
|
|
14
|
+
position: number;
|
|
15
|
+
}, void, unknown> | null;
|
|
16
|
+
/**
|
|
17
|
+
* The queue for video processing.
|
|
18
|
+
*/
|
|
19
|
+
export declare const videoQueue: import("tasque").Tasque;
|
|
20
|
+
//# sourceMappingURL=video-processor.d.ts.map
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import {} from "@m4k/common";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
3
|
+
import { createQueue } from "tasque";
|
|
4
|
+
import { processFfmpeg } from "../util/ffmpeg-processor";
|
|
5
|
+
/**
|
|
6
|
+
* Process a video.
|
|
7
|
+
* @param input - The input video. Can be a file path, a stream or a blob.
|
|
8
|
+
* @param opts - The options for the video processing.
|
|
9
|
+
* @param signal - An optional abort signal.
|
|
10
|
+
* @returns An iterable of the processed videos.
|
|
11
|
+
*/
|
|
12
|
+
export function processVideo(input, opts, { signal } = {}) {
|
|
13
|
+
return processFfmpeg(input, () => {
|
|
14
|
+
const inArgs = [];
|
|
15
|
+
if (opts.inputFormat)
|
|
16
|
+
inArgs.push("-f", opts.inputFormat);
|
|
17
|
+
if (opts.seek)
|
|
18
|
+
inArgs.push("-ss", opts.seek.toString());
|
|
19
|
+
if (opts.duration)
|
|
20
|
+
inArgs.push("-t", opts.duration.toString());
|
|
21
|
+
const outArgs = [];
|
|
22
|
+
if (opts.format)
|
|
23
|
+
outArgs.push("-f", opts.format);
|
|
24
|
+
if (opts.audioBitrate)
|
|
25
|
+
outArgs.push("-b:a", opts.audioBitrate.toString());
|
|
26
|
+
if (opts.videoBitrate)
|
|
27
|
+
outArgs.push("-b:v", opts.videoBitrate.toString());
|
|
28
|
+
if (opts.audioFilters)
|
|
29
|
+
outArgs.push("-af", opts.audioFilters);
|
|
30
|
+
if (opts.videoFilters)
|
|
31
|
+
outArgs.push("-vf", opts.videoFilters);
|
|
32
|
+
if (opts.complexFilters)
|
|
33
|
+
outArgs.push("-filter_complex", opts.complexFilters);
|
|
34
|
+
if (opts.aspect)
|
|
35
|
+
outArgs.push("-aspect", opts.aspect.toString());
|
|
36
|
+
if (opts.pad)
|
|
37
|
+
outArgs.push("-pad", opts.pad);
|
|
38
|
+
if (opts.fps)
|
|
39
|
+
outArgs.push("-r", opts.fps.toString());
|
|
40
|
+
if (opts.size)
|
|
41
|
+
outArgs.push("-scale", opts.size);
|
|
42
|
+
outArgs.push("-c:v", opts.videoCodec ?? "copy");
|
|
43
|
+
outArgs.push("-c:a", opts.audioCodec ?? "copy");
|
|
44
|
+
return {
|
|
45
|
+
input: inArgs,
|
|
46
|
+
out: outArgs,
|
|
47
|
+
format: opts.format ?? "mp4",
|
|
48
|
+
};
|
|
49
|
+
}, { signal, queue: videoQueue, tmpDir });
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* The queue for video processing.
|
|
53
|
+
*/
|
|
54
|
+
export const videoQueue = createQueue({
|
|
55
|
+
parallelize: Number(process.env.VIDEO_PARALLELIZE ?? 1),
|
|
56
|
+
max: Number(process.env.VIDEO_QUEUE_SIZE ?? 5),
|
|
57
|
+
});
|
|
58
|
+
const tmpDir = `${process.env.M4K_TMP_DIR ?? "/tmp/m4k"}/video`;
|
|
59
|
+
await mkdir(tmpDir, { recursive: true });
|
|
60
|
+
//# sourceMappingURL=video-processor.js.map
|
package/package.json
CHANGED
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { type ImageOptimizerOptions, ConvertedFile } from "@m4k/common";
|
|
2
|
-
export type Format = Required<ImageOptimizerOptions>["format"];
|
|
3
|
-
export type Fit = Required<Required<ImageOptimizerOptions>["resize"]>["fit"];
|
|
4
|
-
export declare function optimizeImage(input: ReadableStream | Blob, opts: ImageOptimizerOptions | ImageOptimizerOptions[], signal?: AbortSignal): AsyncGenerator<ConvertedFile | {
|
|
5
|
-
progress: number;
|
|
6
|
-
} | {
|
|
7
|
-
position: number;
|
|
8
|
-
}, void, unknown> | null;
|
|
9
|
-
export declare const imageQueue: import("tasque").Tasque;
|
|
10
|
-
//# sourceMappingURL=image-optimizer.d.ts.map
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { ConvertedFile, type VideoOptimizerOptions } from "@m4k/common";
|
|
2
|
-
export declare function optimizeVideo(inputPath: string, opts: VideoOptimizerOptions, signal?: AbortSignal): AsyncGenerator<ConvertedFile | {
|
|
3
|
-
progress: number;
|
|
4
|
-
} | {
|
|
5
|
-
position: number;
|
|
6
|
-
}, void, unknown> | null;
|
|
7
|
-
/**
|
|
8
|
-
* Get the extension for a given format.
|
|
9
|
-
* May not be complete yet. Most of the time, the format will be the same as the extension.
|
|
10
|
-
* @param format - The format to get the extension for.
|
|
11
|
-
* @returns The extension for the given format.
|
|
12
|
-
*/
|
|
13
|
-
export declare function getExtension(format: string): string;
|
|
14
|
-
export declare const videoQueue: import("tasque").Tasque;
|
|
15
|
-
export declare const tmpVideoDir: string;
|
|
16
|
-
//# sourceMappingURL=video-optimizer.d.ts.map
|