m4k 0.1.1 → 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/{dist/images/image-optimizer.js → lib/images/image-processor.js} +24 -8
- package/lib/index.d.ts +6 -0
- package/lib/index.js +6 -0
- package/lib/util/ffmpeg-processor.d.ts +30 -0
- package/{dist/videos/video-optimizer.js → lib/util/ffmpeg-processor.js} +33 -44
- package/{dist → lib}/util/mime.d.ts +1 -0
- package/{dist → lib}/util/mime.js +1 -0
- package/lib/util/streams.d.ts +3 -0
- package/lib/util/streams.js +9 -0
- package/lib/videos/video-processor.d.ts +20 -0
- package/lib/videos/video-processor.js +60 -0
- package/package.json +10 -6
- package/LICENSE +0 -21
- package/dist/images/image-optimizer.d.ts +0 -9
- package/dist/index.d.ts +0 -3
- package/dist/index.js +0 -3
- package/dist/util/streams.d.ts +0 -3
- package/dist/util/streams.js +0 -8
- package/dist/videos/video-optimizer.d.ts +0 -15
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,7 +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
|
});
|
|
74
|
+
//# sourceMappingURL=image-processor.js.map
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from "@m4k/common";
|
|
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";
|
|
6
|
+
//# sourceMappingURL=index.d.ts.map
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export * from "@m4k/common";
|
|
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";
|
|
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,3 +110,4 @@ const ffmpeg = process.env.FFMPEG_PATH ?? (await import("@ffmpeg-installer/ffmpe
|
|
|
122
110
|
const extMap = {
|
|
123
111
|
matroska: "mkv",
|
|
124
112
|
};
|
|
113
|
+
//# sourceMappingURL=ffmpeg-processor.js.map
|
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
}
|
|
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,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "m4k",
|
|
3
|
-
"
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"type": "module",
|
|
5
|
-
"
|
|
6
|
-
"
|
|
7
|
-
"
|
|
5
|
+
"main": "./lib/index.js",
|
|
6
|
+
"types": "./lib/index.d.ts",
|
|
7
|
+
"module": "./lib/index.js",
|
|
8
|
+
"license": "MIT",
|
|
8
9
|
"scripts": {
|
|
9
|
-
"prepublishOnly": "bun
|
|
10
|
-
"build": "tsc",
|
|
10
|
+
"prepublishOnly": "bun run build",
|
|
11
|
+
"build": "tsc -b",
|
|
11
12
|
"typecheck": "tsc --noEmit",
|
|
12
13
|
"test": "bun test"
|
|
13
14
|
},
|
|
@@ -16,6 +17,9 @@
|
|
|
16
17
|
"tasque": "^0.1.2",
|
|
17
18
|
"typescript": "^5.0.0"
|
|
18
19
|
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@m4k/common": "^0.1.0"
|
|
22
|
+
},
|
|
19
23
|
"peerDependencies": {
|
|
20
24
|
"sharp": "^0.33.5"
|
|
21
25
|
},
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2025 Sven
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import { type ImageOptimizerOptions, ConvertedFile } from "@m4k/types";
|
|
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;
|
package/dist/index.d.ts
DELETED
package/dist/index.js
DELETED
package/dist/util/streams.d.ts
DELETED
package/dist/util/streams.js
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
import { Readable } from "node:stream";
|
|
2
|
-
export function readableFromWeb(stream) {
|
|
3
|
-
// Currently node types don't match with TS lib types for ReadableStream, so we need to cast to any.
|
|
4
|
-
return Readable.fromWeb(stream);
|
|
5
|
-
}
|
|
6
|
-
export function readableToWeb(stream) {
|
|
7
|
-
return Readable.toWeb(stream);
|
|
8
|
-
}
|
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
import { ConvertedFile, type VideoOptimizerOptions } from "@m4k/types";
|
|
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;
|