pwebm 0.0.1-alpha.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/LICENSE +22 -0
- package/README.md +15 -0
- package/TODO.md +55 -0
- package/bun.lock +33 -0
- package/package.json +29 -0
- package/pwebm +2 -0
- package/src/args.ts +445 -0
- package/src/config.ts +49 -0
- package/src/constants.ts +15 -0
- package/src/ffmpeg.ts +685 -0
- package/src/ipc.ts +182 -0
- package/src/logger.ts +131 -0
- package/src/main.ts +86 -0
- package/src/paths.ts +44 -0
- package/src/queue.ts +68 -0
- package/src/schema/args.ts +36 -0
- package/src/schema/config.ts +23 -0
- package/src/schema/ffmpeg.ts +21 -0
- package/src/schema/ffprobe.ts +57 -0
- package/src/schema/ipc.ts +43 -0
- package/src/schema/status.ts +30 -0
- package/src/status.ts +53 -0
- package/src/utils.ts +3 -0
- package/tsconfig.json +19 -0
package/src/ipc.ts
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
import { queue } from "./queue";
|
|
5
|
+
import { status } from "./status";
|
|
6
|
+
import { logger } from "./logger";
|
|
7
|
+
import { TEMP_PATH } from "./paths";
|
|
8
|
+
import { unlinkSync } from "fs";
|
|
9
|
+
import { assertNever } from "./utils";
|
|
10
|
+
import { PIPE_NAME, SOCKET_NAME } from "./constants";
|
|
11
|
+
import { IPCSchema, ResponseSchema } from "./schema/ipc";
|
|
12
|
+
|
|
13
|
+
import type { StatusSchema } from "./schema/status";
|
|
14
|
+
import type { UnixSocketListener } from "bun";
|
|
15
|
+
|
|
16
|
+
// windows doesn't have unix sockets but named pipes
|
|
17
|
+
const SOCKET_FILE =
|
|
18
|
+
os.platform() === "win32" ? PIPE_NAME : path.join(TEMP_PATH, SOCKET_NAME);
|
|
19
|
+
|
|
20
|
+
let listener: UnixSocketListener<undefined> | undefined;
|
|
21
|
+
|
|
22
|
+
const startListener = () => {
|
|
23
|
+
if (listener) {
|
|
24
|
+
logger.warn("Listener already started");
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
logger.info("Listening for connections on " + SOCKET_FILE);
|
|
29
|
+
|
|
30
|
+
listener = Bun.listen({
|
|
31
|
+
unix: SOCKET_FILE,
|
|
32
|
+
socket: {
|
|
33
|
+
data: async (socket, rawData) => {
|
|
34
|
+
const parsedData = IPCSchema.safeParse(JSON.parse(rawData.toString()));
|
|
35
|
+
|
|
36
|
+
if (!parsedData.success) {
|
|
37
|
+
logger.warn("Invalid data received through the socket. Ignoring...");
|
|
38
|
+
|
|
39
|
+
socket.end();
|
|
40
|
+
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const { type, data } = parsedData.data;
|
|
45
|
+
|
|
46
|
+
switch (type) {
|
|
47
|
+
case "kill":
|
|
48
|
+
logger.info("Received kill signal, aborting the queue");
|
|
49
|
+
|
|
50
|
+
await queue.abortProcessing();
|
|
51
|
+
|
|
52
|
+
socket.end(
|
|
53
|
+
JSON.stringify({
|
|
54
|
+
type: "kill",
|
|
55
|
+
success: true,
|
|
56
|
+
} satisfies ResponseSchema),
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
break;
|
|
60
|
+
case "enqueue":
|
|
61
|
+
logger.info(
|
|
62
|
+
"Received new encoding parameters, adding to the queue",
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
queue.push(data);
|
|
66
|
+
|
|
67
|
+
socket.end(
|
|
68
|
+
JSON.stringify({
|
|
69
|
+
type: "enqueue",
|
|
70
|
+
success: true,
|
|
71
|
+
} satisfies ResponseSchema),
|
|
72
|
+
);
|
|
73
|
+
break;
|
|
74
|
+
case "status":
|
|
75
|
+
logger.info("Received status request, sending the current status");
|
|
76
|
+
|
|
77
|
+
socket.end(
|
|
78
|
+
JSON.stringify({
|
|
79
|
+
type: "status",
|
|
80
|
+
success: true,
|
|
81
|
+
data: status.getStatus(),
|
|
82
|
+
} satisfies ResponseSchema),
|
|
83
|
+
);
|
|
84
|
+
break;
|
|
85
|
+
default:
|
|
86
|
+
assertNever(type);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
socket.end();
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
const stopListener = () => {
|
|
96
|
+
if (!listener) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
listener.stop();
|
|
101
|
+
listener = undefined;
|
|
102
|
+
|
|
103
|
+
// cleanup socket file if not on windows
|
|
104
|
+
if (os.platform() !== "win32") {
|
|
105
|
+
try {
|
|
106
|
+
logger.info("Deleting the socket file: " + SOCKET_FILE);
|
|
107
|
+
|
|
108
|
+
unlinkSync(SOCKET_FILE);
|
|
109
|
+
} catch (error) {
|
|
110
|
+
logger.error("Error deleting socket file: " + SOCKET_FILE);
|
|
111
|
+
|
|
112
|
+
logger.error(
|
|
113
|
+
error instanceof Error ? error.message : JSON.stringify(error, null, 2),
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
// overloading
|
|
120
|
+
type SendMessage = {
|
|
121
|
+
(message: Exclude<IPCSchema, { type: "status" }>): Promise<void>;
|
|
122
|
+
(message: Extract<IPCSchema, { type: "status" }>): Promise<StatusSchema>;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const sendMessage: SendMessage = async (message) =>
|
|
126
|
+
new Promise(async (resolve, reject) => {
|
|
127
|
+
try {
|
|
128
|
+
const socket = await Bun.connect({
|
|
129
|
+
unix: SOCKET_FILE,
|
|
130
|
+
socket: {
|
|
131
|
+
data: (socket, rawData) => {
|
|
132
|
+
socket.end();
|
|
133
|
+
|
|
134
|
+
const parsedData = ResponseSchema.safeParse(
|
|
135
|
+
JSON.parse(rawData.toString()),
|
|
136
|
+
);
|
|
137
|
+
|
|
138
|
+
if (!parsedData.success) {
|
|
139
|
+
// couldn't parse
|
|
140
|
+
logger.error("Invalid response received through the socket");
|
|
141
|
+
|
|
142
|
+
return reject();
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (!parsedData.data.success) {
|
|
146
|
+
// request did not succeed
|
|
147
|
+
return reject();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (parsedData.data.type === "status") {
|
|
151
|
+
const { data } = parsedData.data;
|
|
152
|
+
|
|
153
|
+
return resolve(data);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return resolve(undefined as void & StatusSchema);
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
socket.write(JSON.stringify(message));
|
|
162
|
+
} catch (error) {
|
|
163
|
+
if (
|
|
164
|
+
error instanceof Error &&
|
|
165
|
+
"code" in error &&
|
|
166
|
+
error.code === "ENOENT" &&
|
|
167
|
+
message.type !== "enqueue"
|
|
168
|
+
) {
|
|
169
|
+
logger.info("No current main instance running", {
|
|
170
|
+
logToConsole: true,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
reject(error);
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
export const ipc = {
|
|
179
|
+
sendMessage,
|
|
180
|
+
stopListener,
|
|
181
|
+
startListener,
|
|
182
|
+
};
|
package/src/logger.ts
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
import { CONFIG_PATH } from "./paths";
|
|
5
|
+
import { CLI_NAME, LOG_FILE_NAME } from "./constants";
|
|
6
|
+
import { existsSync, mkdirSync, appendFileSync } from "fs";
|
|
7
|
+
|
|
8
|
+
type Level = "INFO" | "ERROR" | "WARN" | "DEBUG";
|
|
9
|
+
|
|
10
|
+
const LEVELS_FOR_CONSOLE: Level[] = ["ERROR"];
|
|
11
|
+
|
|
12
|
+
const logFile = path.join(CONFIG_PATH, LOG_FILE_NAME);
|
|
13
|
+
|
|
14
|
+
if (!existsSync(CONFIG_PATH)) {
|
|
15
|
+
mkdirSync(CONFIG_PATH, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const writeToFile = (message: string, level: Level) => {
|
|
19
|
+
const now = new Date();
|
|
20
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
21
|
+
const year = now.getFullYear();
|
|
22
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
23
|
+
const hours = String(now.getHours()).padStart(2, "0");
|
|
24
|
+
const minutes = String(now.getMinutes()).padStart(2, "0");
|
|
25
|
+
const seconds = String(now.getSeconds()).padStart(2, "0");
|
|
26
|
+
const milliseconds = String(now.getMilliseconds()).padStart(3, "0");
|
|
27
|
+
|
|
28
|
+
const timestamp = `${year}-${month}-${day} ${hours}:${minutes}:${seconds}.${milliseconds}`;
|
|
29
|
+
const logMessage = `${timestamp} ${os.hostname()} ${CLI_NAME}[${process.pid}]: ${level}: ${message}\n`;
|
|
30
|
+
|
|
31
|
+
appendFileSync(logFile, logMessage, "utf-8");
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
const COLORS = {
|
|
35
|
+
RED: "\u001b[1;91m",
|
|
36
|
+
BLUE: "\u001b[1;94m",
|
|
37
|
+
GREEN: "\u001b[1;92m",
|
|
38
|
+
YELLOW: "\u001b[1;93m",
|
|
39
|
+
ORANGE: "\u001b[1;38;5;208m",
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
const END_COLOR = "\u001b[0m";
|
|
43
|
+
const CLEAR_LINE = "\r\u001b[K";
|
|
44
|
+
|
|
45
|
+
type Options = {
|
|
46
|
+
onlyConsole?: boolean; // only logs to console regardless of the allowed level
|
|
47
|
+
logToConsole?: boolean; // logs to console regardless of the allowed level
|
|
48
|
+
fancyConsole?: {
|
|
49
|
+
colors?: boolean;
|
|
50
|
+
noNewLine?: boolean;
|
|
51
|
+
clearPreviousLine?: boolean;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
type Message = Parameters<typeof print>[0];
|
|
56
|
+
|
|
57
|
+
const log = (message: Message, level: Level, options?: Options) => {
|
|
58
|
+
let consoleLog: typeof console.log;
|
|
59
|
+
|
|
60
|
+
const skipNewLine = options?.fancyConsole?.noNewLine;
|
|
61
|
+
|
|
62
|
+
switch (level) {
|
|
63
|
+
case "INFO":
|
|
64
|
+
consoleLog = (message: string) => print(message, "stdout", skipNewLine);
|
|
65
|
+
break;
|
|
66
|
+
case "WARN":
|
|
67
|
+
message = `{ORANGE}${message}{/ORANGE}`;
|
|
68
|
+
consoleLog = (message: string) => print(message, "stderr", skipNewLine);
|
|
69
|
+
break;
|
|
70
|
+
case "ERROR":
|
|
71
|
+
message = `{RED}${message}{/RED}`;
|
|
72
|
+
consoleLog = (message: string) => print(message, "stderr", skipNewLine);
|
|
73
|
+
break;
|
|
74
|
+
case "DEBUG":
|
|
75
|
+
consoleLog = (message: string) => print(message, "stdout", skipNewLine);
|
|
76
|
+
break;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (options?.fancyConsole?.colors || level === "ERROR" || level === "WARN") {
|
|
80
|
+
Object.keys(COLORS).forEach((color) => {
|
|
81
|
+
const endColorRegex = new RegExp(`\\{\/${color}\\}`, "g");
|
|
82
|
+
const startColorRegex = new RegExp(`\\{${color}\\}`, "g");
|
|
83
|
+
|
|
84
|
+
message = message.replace(
|
|
85
|
+
startColorRegex,
|
|
86
|
+
COLORS[color as keyof typeof COLORS],
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
message = message.replace(endColorRegex, END_COLOR);
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// i don't want the following in the log file
|
|
94
|
+
let consoleMessage = message;
|
|
95
|
+
|
|
96
|
+
if (options?.fancyConsole?.clearPreviousLine) {
|
|
97
|
+
consoleMessage = CLEAR_LINE + message;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (options?.onlyConsole) {
|
|
101
|
+
consoleLog(consoleMessage);
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (LEVELS_FOR_CONSOLE.includes(level) || options?.logToConsole) {
|
|
106
|
+
consoleLog(consoleMessage);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
writeToFile(message, level);
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
const print = (
|
|
113
|
+
message: string,
|
|
114
|
+
output: "stdout" | "stderr",
|
|
115
|
+
skipNewLine?: boolean,
|
|
116
|
+
) => {
|
|
117
|
+
const newLine = skipNewLine ? "" : "\n";
|
|
118
|
+
|
|
119
|
+
process[output].write(message + newLine);
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const logger: {
|
|
123
|
+
[key in Lowercase<Level>]: (message: Message, options?: Options) => void;
|
|
124
|
+
} = {
|
|
125
|
+
info: (message, options) => log(message, "INFO", options),
|
|
126
|
+
warn: (message, options) => log(message, "WARN", options),
|
|
127
|
+
error: (message, options) => log(message, "ERROR", options),
|
|
128
|
+
debug: (message, options) => log(message, "DEBUG", options),
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
logger.info("Started");
|
package/src/main.ts
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { ipc } from "./ipc";
|
|
2
|
+
import { queue } from "./queue";
|
|
3
|
+
import { logger } from "./logger";
|
|
4
|
+
import { CLI_NAME } from "./constants";
|
|
5
|
+
import { parseArgs } from "./args";
|
|
6
|
+
|
|
7
|
+
process.title = CLI_NAME;
|
|
8
|
+
|
|
9
|
+
process.on("SIGINT", () => {
|
|
10
|
+
logger.warn("Received SIGINT, aborting processing");
|
|
11
|
+
queue.abortProcessing();
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
process.on("SIGTERM", () => {
|
|
15
|
+
logger.warn("Received SIGTERM, aborting processing");
|
|
16
|
+
queue.abortProcessing();
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
process.on("SIGHUP", () => {
|
|
20
|
+
logger.warn("Received SIGHUP, aborting processing");
|
|
21
|
+
queue.abortProcessing();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
process.on("uncaughtException", (error) => {
|
|
25
|
+
logger.error("Uncaught exception: " + error.message);
|
|
26
|
+
queue.abortProcessing();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const args = Bun.argv.slice(2);
|
|
30
|
+
|
|
31
|
+
logger.info("argv: " + args.join(" "));
|
|
32
|
+
|
|
33
|
+
const parsedArgs = await parseArgs(args);
|
|
34
|
+
|
|
35
|
+
// let's check if ffmpeg and ffprobe are available before encoding anything
|
|
36
|
+
try {
|
|
37
|
+
const ffmpegProcess = Bun.spawnSync(["ffmpeg", "-hide_banner", "-version"]);
|
|
38
|
+
|
|
39
|
+
if (!ffmpegProcess.success) {
|
|
40
|
+
throw new Error(ffmpegProcess.stderr.toString());
|
|
41
|
+
}
|
|
42
|
+
} catch (error) {
|
|
43
|
+
logger.error(
|
|
44
|
+
"Error checking ffmpeg executable" +
|
|
45
|
+
(error instanceof Error ? `: ${error.message}` : ""),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
process.exit(1);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
const ffprobeProcess = Bun.spawnSync(["ffprobe", "-hide_banner", "-version"]);
|
|
53
|
+
|
|
54
|
+
if (!ffprobeProcess.success) {
|
|
55
|
+
throw new Error(ffprobeProcess.stderr.toString());
|
|
56
|
+
}
|
|
57
|
+
} catch (error) {
|
|
58
|
+
logger.error(
|
|
59
|
+
"Error checking ffprobe executable" +
|
|
60
|
+
(error instanceof Error ? `: ${error.message}` : ""),
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
// try sending the args to the running process if there is one
|
|
68
|
+
await ipc.sendMessage({
|
|
69
|
+
type: "enqueue",
|
|
70
|
+
data: parsedArgs,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
logger.info("Sent the encoding parameters to the already running instance", {
|
|
74
|
+
logToConsole: true,
|
|
75
|
+
});
|
|
76
|
+
} catch (error) {
|
|
77
|
+
logger.warn("No running instance found, starting a new queue");
|
|
78
|
+
|
|
79
|
+
queue.push(parsedArgs);
|
|
80
|
+
|
|
81
|
+
ipc.startListener();
|
|
82
|
+
|
|
83
|
+
await queue.processQueue();
|
|
84
|
+
|
|
85
|
+
ipc.stopListener();
|
|
86
|
+
}
|
package/src/paths.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import os from "os";
|
|
2
|
+
import path from "path";
|
|
3
|
+
|
|
4
|
+
import { CLI_NAME } from "./constants";
|
|
5
|
+
|
|
6
|
+
const home = os.homedir();
|
|
7
|
+
|
|
8
|
+
const tempPath = path.join(os.tmpdir());
|
|
9
|
+
const configPath = path.join(home, ".config", CLI_NAME);
|
|
10
|
+
|
|
11
|
+
let videoPath: string;
|
|
12
|
+
let nullDevicePath: string;
|
|
13
|
+
|
|
14
|
+
switch (os.platform()) {
|
|
15
|
+
case "darwin":
|
|
16
|
+
nullDevicePath = "/dev/null";
|
|
17
|
+
videoPath = path.join(home, "Movies", CLI_NAME);
|
|
18
|
+
break;
|
|
19
|
+
case "win32":
|
|
20
|
+
nullDevicePath = "NUL";
|
|
21
|
+
videoPath = path.join(home, "Videos", CLI_NAME);
|
|
22
|
+
break;
|
|
23
|
+
case "linux":
|
|
24
|
+
default:
|
|
25
|
+
nullDevicePath = "/dev/null";
|
|
26
|
+
videoPath = path.join(home, "Videos", CLI_NAME);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const expandHome = <T extends string | undefined>(value: T) => {
|
|
30
|
+
if (value?.startsWith("~")) {
|
|
31
|
+
return value.replace("~", home);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (value?.startsWith("$HOME")) {
|
|
35
|
+
return value.replace("$HOME", home);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return value;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
export const TEMP_PATH = tempPath;
|
|
42
|
+
export const CONFIG_PATH = configPath;
|
|
43
|
+
export const DEFAULT_VIDEO_PATH = videoPath;
|
|
44
|
+
export const NULL_DEVICE_PATH = nullDevicePath;
|
package/src/queue.ts
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { ffmpeg } from "./ffmpeg";
|
|
2
|
+
import { ArgsSchema } from "./schema/args";
|
|
3
|
+
import { setTimeout } from "timers/promises";
|
|
4
|
+
|
|
5
|
+
const store: {
|
|
6
|
+
total: number;
|
|
7
|
+
abort: boolean;
|
|
8
|
+
queue: ArgsSchema[];
|
|
9
|
+
processing: boolean;
|
|
10
|
+
} = {
|
|
11
|
+
total: 0,
|
|
12
|
+
queue: [],
|
|
13
|
+
abort: false,
|
|
14
|
+
processing: false,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
const push = (args: ArgsSchema) => {
|
|
18
|
+
store.queue.push(args);
|
|
19
|
+
store.total++;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const abortProcessing = async () => {
|
|
23
|
+
store.abort = true;
|
|
24
|
+
|
|
25
|
+
ffmpeg.kill();
|
|
26
|
+
|
|
27
|
+
while (store.processing) {
|
|
28
|
+
await setTimeout(100);
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
const processQueue = async () => {
|
|
33
|
+
if (store.processing) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
store.processing = true;
|
|
38
|
+
|
|
39
|
+
while (getProcessedCount() < store.total) {
|
|
40
|
+
const current = store.queue.shift();
|
|
41
|
+
|
|
42
|
+
if (!current) {
|
|
43
|
+
continue;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
await ffmpeg.encode(current);
|
|
47
|
+
|
|
48
|
+
if (store.abort) {
|
|
49
|
+
break;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
store.abort = false;
|
|
54
|
+
store.processing = false;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
const getTotalCount = () => store.total;
|
|
58
|
+
const getProcessedCount = () => store.total - store.queue.length;
|
|
59
|
+
const getStatus = () => `Encoding ${getProcessedCount()} of ${store.total}`;
|
|
60
|
+
|
|
61
|
+
export const queue = {
|
|
62
|
+
push,
|
|
63
|
+
getStatus,
|
|
64
|
+
processQueue,
|
|
65
|
+
getTotalCount,
|
|
66
|
+
abortProcessing,
|
|
67
|
+
getProcessedCount,
|
|
68
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { config } from "../config";
|
|
3
|
+
import { expandHome } from "../paths";
|
|
4
|
+
import { ConfigSchema } from "./config";
|
|
5
|
+
|
|
6
|
+
export const ArgsSchema = ConfigSchema.merge(
|
|
7
|
+
z.object({
|
|
8
|
+
inputs: z.array(
|
|
9
|
+
z.object({
|
|
10
|
+
file: z.string().transform(expandHome),
|
|
11
|
+
stopTime: z.string().optional(),
|
|
12
|
+
startTime: z.string().optional(),
|
|
13
|
+
}),
|
|
14
|
+
),
|
|
15
|
+
output: z
|
|
16
|
+
.object({
|
|
17
|
+
file: z.string().optional().transform(expandHome),
|
|
18
|
+
stopTime: z.string().optional(),
|
|
19
|
+
startTime: z.string().optional(),
|
|
20
|
+
})
|
|
21
|
+
.optional(),
|
|
22
|
+
lavfi: z.string().optional(),
|
|
23
|
+
extraParams: z.array(z.string()).optional(),
|
|
24
|
+
// the next ones come from the config schema
|
|
25
|
+
// we are just replacing the defaults with the values loaded from the config file
|
|
26
|
+
crf: ConfigSchema.shape.crf.default(config.crf),
|
|
27
|
+
subs: ConfigSchema.shape.subs.default(config.subs),
|
|
28
|
+
encoder: ConfigSchema.shape.encoder.default(config.encoder),
|
|
29
|
+
cpuUsed: ConfigSchema.shape.cpuUsed.default(config.cpuUsed),
|
|
30
|
+
deadline: ConfigSchema.shape.deadline.default(config.deadline),
|
|
31
|
+
sizeLimit: ConfigSchema.shape.sizeLimit.default(config.sizeLimit),
|
|
32
|
+
videoPath: ConfigSchema.shape.videoPath.default(config.videoPath),
|
|
33
|
+
}),
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
export type ArgsSchema = z.infer<typeof ArgsSchema>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { DEFAULT_VIDEO_PATH, expandHome } from "../paths";
|
|
3
|
+
|
|
4
|
+
export const ConfigSchema = z.object({
|
|
5
|
+
subs: z.boolean().default(false),
|
|
6
|
+
encoder: z.string().default("libvpx-vp9"),
|
|
7
|
+
sizeLimit: z.number().default(4),
|
|
8
|
+
crf: z.number().default(24),
|
|
9
|
+
cpuUsed: z
|
|
10
|
+
.union([
|
|
11
|
+
z.literal(0),
|
|
12
|
+
z.literal(1),
|
|
13
|
+
z.literal(2),
|
|
14
|
+
z.literal(3),
|
|
15
|
+
z.literal(4),
|
|
16
|
+
z.literal(5),
|
|
17
|
+
])
|
|
18
|
+
.default(0),
|
|
19
|
+
deadline: z.enum(["good", "best"]).default("good"),
|
|
20
|
+
videoPath: z.string().default(DEFAULT_VIDEO_PATH).transform(expandHome),
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
export type ConfigSchema = z.infer<typeof ConfigSchema>;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
export const ProgressSchema = z
|
|
4
|
+
.object({
|
|
5
|
+
progress: z.union([z.literal("continue"), z.literal("end")]),
|
|
6
|
+
total_size: z
|
|
7
|
+
.string()
|
|
8
|
+
.transform(Number)
|
|
9
|
+
.refine((value) => !isNaN(value), { message: "Invalid number" }),
|
|
10
|
+
out_time_us: z
|
|
11
|
+
.string()
|
|
12
|
+
.transform(Number)
|
|
13
|
+
.refine((value) => !isNaN(value), { message: "Invalid number" }),
|
|
14
|
+
})
|
|
15
|
+
.transform((data) => ({
|
|
16
|
+
outTime: data.out_time_us / 1_000_000, // seconds
|
|
17
|
+
progress: data.progress,
|
|
18
|
+
totalSize: data.total_size, // bytes
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
export type ProgressSchema = z.infer<typeof ProgressSchema>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const Format = z.object({
|
|
4
|
+
format_name: z.string(),
|
|
5
|
+
format_long_name: z.string(),
|
|
6
|
+
size: z.string().transform(Number),
|
|
7
|
+
duration: z.string().transform(Number),
|
|
8
|
+
start_time: z.string(),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
const Stream = z.object({
|
|
12
|
+
index: z.number(),
|
|
13
|
+
codec_name: z.string(),
|
|
14
|
+
codec_long_name: z.string(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
const Video = Stream.extend({
|
|
18
|
+
codec_type: z.literal("video"),
|
|
19
|
+
width: z.number(),
|
|
20
|
+
height: z.number(),
|
|
21
|
+
pix_fmt: z.string(),
|
|
22
|
+
start_time: z.string().optional(),
|
|
23
|
+
duration: z.string().transform(Number).optional(),
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
const Audio = Stream.extend({
|
|
27
|
+
codec_type: z.literal("audio"),
|
|
28
|
+
start_time: z.string().optional(),
|
|
29
|
+
duration: z.string().transform(Number).optional(),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const Subtitle = Stream.extend({
|
|
33
|
+
codec_type: z.literal("subtitle"),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const Attachment = Stream.merge(
|
|
37
|
+
z.object({
|
|
38
|
+
codec_type: z.literal("attachment"),
|
|
39
|
+
codec_name: Stream.shape.codec_name.optional(),
|
|
40
|
+
codec_long_name: Stream.shape.codec_long_name.optional(),
|
|
41
|
+
}),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const Data = Stream.merge(
|
|
45
|
+
z.object({
|
|
46
|
+
codec_type: z.literal("data"),
|
|
47
|
+
codec_name: Stream.shape.codec_name.optional(),
|
|
48
|
+
codec_long_name: Stream.shape.codec_long_name.optional(),
|
|
49
|
+
}),
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
export const FFProbeSchema = z.object({
|
|
53
|
+
format: Format,
|
|
54
|
+
streams: z.array(z.union([Video, Audio, Subtitle, Attachment, Data])),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
export type FFProbeSchema = z.infer<typeof FFProbeSchema>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { ArgsSchema } from "./args";
|
|
3
|
+
import { StatusSchema } from "./status";
|
|
4
|
+
|
|
5
|
+
const KillRequest = z.object({
|
|
6
|
+
type: z.literal("kill"),
|
|
7
|
+
data: z.undefined(),
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
const StatusRequest = z.object({
|
|
11
|
+
type: z.literal("status"),
|
|
12
|
+
data: z.undefined(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const EnqueueRequest = z.object({
|
|
16
|
+
type: z.literal("enqueue"),
|
|
17
|
+
data: ArgsSchema,
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
const SimpleResponse = z.object({
|
|
21
|
+
type: z.union([z.literal("enqueue"), z.literal("kill")]),
|
|
22
|
+
success: z.boolean(),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const StatusOKResponse = z.object({
|
|
26
|
+
type: z.literal("status"),
|
|
27
|
+
data: StatusSchema,
|
|
28
|
+
success: z.literal(true),
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const StatusFAILResponse = z.object({
|
|
32
|
+
type: z.literal("status"),
|
|
33
|
+
data: z.undefined(),
|
|
34
|
+
success: z.literal(false),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const StatusResponse = z.union([StatusOKResponse, StatusFAILResponse]);
|
|
38
|
+
|
|
39
|
+
export const IPCSchema = z.union([KillRequest, StatusRequest, EnqueueRequest]);
|
|
40
|
+
export const ResponseSchema = z.union([SimpleResponse, StatusResponse]);
|
|
41
|
+
|
|
42
|
+
export type IPCSchema = z.infer<typeof IPCSchema>;
|
|
43
|
+
export type ResponseSchema = z.infer<typeof ResponseSchema>;
|