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/LICENSE
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023-2025 4ndrs <andres.degozaru@gmail.com>
|
|
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.
|
|
22
|
+
|
package/README.md
ADDED
package/TODO.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
config, log -> there has to be a way to retrieve the config dir with env variable or some api
|
|
2
|
+
(socket) similar to purewebm; have to be in ~/.config/pwebm/config, ~/.config/pwebm/log
|
|
3
|
+
the socket file (if there is any) goes to /tmp
|
|
4
|
+
|
|
5
|
+
videos dir -> use ~/Videos/pwebm for consistency as default, but make it changeable through
|
|
6
|
+
the config file ASAP, way to many webms in PureWebM to move to another dir
|
|
7
|
+
|
|
8
|
+
filename -> use the unix timestamp with 16 digits (performance.now() + performance.timeOrigin)
|
|
9
|
+
the --name_type flag won't be ported
|
|
10
|
+
|
|
11
|
+
flags -> -v/--version, -h/--help, --status, --kill, -i, -subs, -c:v, -ss, -to, -lavfi,
|
|
12
|
+
--size_limit/-sl, -crf, -cpu-used, -deadline, --extra_params/-ep
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
some considerations:
|
|
16
|
+
- not being v1.0 permits for some leeway with breaking changes
|
|
17
|
+
- default size limit will be increased from 3 to 4
|
|
18
|
+
- if --status or --kill are used other flags will be ignored, consider maybe direct commands?
|
|
19
|
+
- current purewebm needs the start/end times in the metadata of the file to work no matter what
|
|
20
|
+
need to find a different way to get these, or at least not rely on them when they are set
|
|
21
|
+
manually
|
|
22
|
+
- it has to work with PureMPV (through pwebm-helper), tail log is nice but would be also nice
|
|
23
|
+
to have some kind of keybinding on the helper to show the status of the encoding on the mpv
|
|
24
|
+
window
|
|
25
|
+
|
|
26
|
+
## Stuff
|
|
27
|
+
|
|
28
|
+
- [x] argument parser
|
|
29
|
+
- [x] expand tilde and $HOME in config video path
|
|
30
|
+
- [x] check for ffmpeg and ffprobe in path
|
|
31
|
+
- [x] convert webm, retry when limit reached
|
|
32
|
+
- [x] convert any other encoder to mkv copied streams just like purewebm
|
|
33
|
+
- [x] socket file for communication
|
|
34
|
+
- [x] handle queue
|
|
35
|
+
- [x] kill implementation
|
|
36
|
+
- [x] ffmpeg progress
|
|
37
|
+
- [x] status implementation
|
|
38
|
+
- [x] handle signals
|
|
39
|
+
- [x] name the process
|
|
40
|
+
- [x] version implementation
|
|
41
|
+
- [x] help implementation
|
|
42
|
+
- [ ] add automatic releases with github actions (edge + tagged)
|
|
43
|
+
- [ ] add bun bundled executable package releases to the automatic releases
|
|
44
|
+
- [ ] update readme
|
|
45
|
+
- [ ] add subs flag warning to the readme (how it works, output seeking needed, etc)
|
|
46
|
+
- [ ] release tagged versions in npm
|
|
47
|
+
- [ ] it's not part of this repo, but don't forget to improve the helper script for PureMPV (pwebm-helper), it has a missing readme
|
|
48
|
+
|
|
49
|
+
### the following is just extra not implemented on purewebm but was its initial vision (can skip)
|
|
50
|
+
- [ ] implement conversion logger view (save previous conversions to db with last bitrate info)
|
|
51
|
+
- [ ] redo conversions in logger view
|
|
52
|
+
- [ ] add limit for the amount of tries to redo a conversion
|
|
53
|
+
- [ ] percentage bitrate offset when retrying a conversion with new calcs
|
|
54
|
+
|
|
55
|
+
## 2025-03-03
|
package/bun.lock
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"lockfileVersion": 1,
|
|
3
|
+
"workspaces": {
|
|
4
|
+
"": {
|
|
5
|
+
"name": "pwebm",
|
|
6
|
+
"dependencies": {
|
|
7
|
+
"smol-toml": "^1.3.1",
|
|
8
|
+
"zod": "^3.24.2",
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/bun": "^1.2.3",
|
|
12
|
+
"typescript": "^5.7.3",
|
|
13
|
+
},
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
"packages": {
|
|
17
|
+
"@types/bun": ["@types/bun@1.2.3", "", { "dependencies": { "bun-types": "1.2.3" } }, "sha512-054h79ipETRfjtsCW9qJK8Ipof67Pw9bodFWmkfkaUaRiIQ1dIV2VTlheshlBx3mpKr0KeK8VqnMMCtgN9rQtw=="],
|
|
18
|
+
|
|
19
|
+
"@types/node": ["@types/node@22.13.5", "", { "dependencies": { "undici-types": "~6.20.0" } }, "sha512-+lTU0PxZXn0Dr1NBtC7Y8cR21AJr87dLLU953CWA6pMxxv/UDc7jYAY90upcrie1nRcD6XNG5HOYEDtgW5TxAg=="],
|
|
20
|
+
|
|
21
|
+
"@types/ws": ["@types/ws@8.5.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-bd/YFLW+URhBzMXurx7lWByOu+xzU9+kb3RboOteXYDfW+tr+JZa99OyNmPINEGB/ahzKrEuc8rcv4gnpJmxTw=="],
|
|
22
|
+
|
|
23
|
+
"bun-types": ["bun-types@1.2.3", "", { "dependencies": { "@types/node": "*", "@types/ws": "~8.5.10" } }, "sha512-P7AeyTseLKAvgaZqQrvp3RqFM3yN9PlcLuSTe7SoJOfZkER73mLdT2vEQi8U64S1YvM/ldcNiQjn0Sn7H9lGgg=="],
|
|
24
|
+
|
|
25
|
+
"smol-toml": ["smol-toml@1.3.1", "", {}, "sha512-tEYNll18pPKHroYSmLLrksq233j021G0giwW7P3D24jC54pQ5W5BXMsQ/Mvw1OJCmEYDgY+lrzT+3nNUtoNfXQ=="],
|
|
26
|
+
|
|
27
|
+
"typescript": ["typescript@5.7.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw=="],
|
|
28
|
+
|
|
29
|
+
"undici-types": ["undici-types@6.20.0", "", {}, "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg=="],
|
|
30
|
+
|
|
31
|
+
"zod": ["zod@3.24.2", "", {}, "sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ=="],
|
|
32
|
+
}
|
|
33
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pwebm",
|
|
3
|
+
"version": "0.0.1-alpha.0",
|
|
4
|
+
"author": "4ndrs <andres.degozaru@gmail.com>",
|
|
5
|
+
"repository": {
|
|
6
|
+
"type": "git",
|
|
7
|
+
"url": "https://github.com/4ndrs/pwebm.git"
|
|
8
|
+
},
|
|
9
|
+
"module": "index.ts",
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"@types/bun": "^1.2.3",
|
|
12
|
+
"typescript": "^5.7.3"
|
|
13
|
+
},
|
|
14
|
+
"$schema": "https://json.schemastore.org/package.json",
|
|
15
|
+
"bin": {
|
|
16
|
+
"pwebm": "pwebm"
|
|
17
|
+
},
|
|
18
|
+
"bugs": {
|
|
19
|
+
"url": "git+https://github.com/4ndrs/pwebm/issues"
|
|
20
|
+
},
|
|
21
|
+
"description": "Utility to encode size restricted webm files with ffmpeg",
|
|
22
|
+
"homepage": "https://github.com/4ndrs/pwebm",
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"type": "module",
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"smol-toml": "^1.3.1",
|
|
27
|
+
"zod": "^3.24.2"
|
|
28
|
+
}
|
|
29
|
+
}
|
package/pwebm
ADDED
package/src/args.ts
ADDED
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AUTHOR,
|
|
3
|
+
LICENSE,
|
|
4
|
+
VERSION,
|
|
5
|
+
HOMEPAGE,
|
|
6
|
+
CLI_NAME,
|
|
7
|
+
DESCRIPTION,
|
|
8
|
+
COPYRIGHT_YEAR,
|
|
9
|
+
} from "./constants";
|
|
10
|
+
|
|
11
|
+
import { ipc } from "./ipc";
|
|
12
|
+
import { config } from "./config";
|
|
13
|
+
import { logger } from "./logger";
|
|
14
|
+
import { ArgsSchema } from "./schema/args";
|
|
15
|
+
|
|
16
|
+
const RECOGNIZED_ARGS = [
|
|
17
|
+
"-h",
|
|
18
|
+
"--help",
|
|
19
|
+
"-v",
|
|
20
|
+
"--version",
|
|
21
|
+
"-kill",
|
|
22
|
+
"-status",
|
|
23
|
+
"-i",
|
|
24
|
+
"-ss",
|
|
25
|
+
"-to",
|
|
26
|
+
"-lavfi",
|
|
27
|
+
"-c:v",
|
|
28
|
+
"-deadline",
|
|
29
|
+
"-crf",
|
|
30
|
+
"-cpu-used",
|
|
31
|
+
"-subs",
|
|
32
|
+
"-sl",
|
|
33
|
+
"--size-limit",
|
|
34
|
+
"-ep",
|
|
35
|
+
"--extra-params",
|
|
36
|
+
"--video-path",
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export const parseArgs = async (args: string[]): Promise<ArgsSchema> => {
|
|
40
|
+
if (args.length === 0) {
|
|
41
|
+
printUsage();
|
|
42
|
+
|
|
43
|
+
// if the user isn't using any of the quick actions we require the -i flag
|
|
44
|
+
logger.error("Input file is required");
|
|
45
|
+
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const rawArgs: Partial<ArgsSchema> = {};
|
|
50
|
+
|
|
51
|
+
let skip = false;
|
|
52
|
+
let isExtraParam = false;
|
|
53
|
+
let seeking: { startTime?: string; stopTime?: string } | undefined;
|
|
54
|
+
|
|
55
|
+
const skipNext = () => (skip = true);
|
|
56
|
+
|
|
57
|
+
for (let index = 0; index < args.length; index++) {
|
|
58
|
+
const arg = args[index];
|
|
59
|
+
|
|
60
|
+
if (skip) {
|
|
61
|
+
skip = false;
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (isExtraParam) {
|
|
66
|
+
rawArgs.extraParams?.push(arg);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (arg.startsWith("-") && !RECOGNIZED_ARGS.includes(arg)) {
|
|
71
|
+
printUsage();
|
|
72
|
+
|
|
73
|
+
logger.error(`Unrecognized argument: ${arg}`);
|
|
74
|
+
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (["-h", "--help"].includes(arg)) {
|
|
79
|
+
printUsage();
|
|
80
|
+
|
|
81
|
+
process.exit();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (["-v", "--version"].includes(arg)) {
|
|
85
|
+
logger.info(
|
|
86
|
+
`${CLI_NAME} version ${VERSION}\nCopyright (c) ${COPYRIGHT_YEAR} ${AUTHOR}\nLicensed under the ${LICENSE} License\n${HOMEPAGE}`,
|
|
87
|
+
{ onlyConsole: true },
|
|
88
|
+
);
|
|
89
|
+
process.exit();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (arg === "-kill") {
|
|
93
|
+
try {
|
|
94
|
+
await ipc.sendMessage({ type: "kill" });
|
|
95
|
+
|
|
96
|
+
logger.info("Main instance successfully killed", {
|
|
97
|
+
logToConsole: true,
|
|
98
|
+
});
|
|
99
|
+
} catch (error) {
|
|
100
|
+
if (
|
|
101
|
+
error instanceof Error &&
|
|
102
|
+
"code" in error &&
|
|
103
|
+
error.code !== "ENOENT"
|
|
104
|
+
) {
|
|
105
|
+
logger.error("Couldn't kill the main instance");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
process.exit();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (arg === "-status") {
|
|
115
|
+
try {
|
|
116
|
+
const status = await ipc.sendMessage({ type: "status" });
|
|
117
|
+
|
|
118
|
+
logger.info(JSON.stringify(status, null, 2), { onlyConsole: true });
|
|
119
|
+
|
|
120
|
+
logger.info("Status printed to the screen");
|
|
121
|
+
} catch (error) {
|
|
122
|
+
if (
|
|
123
|
+
error instanceof Error &&
|
|
124
|
+
"code" in error &&
|
|
125
|
+
error.code !== "ENOENT"
|
|
126
|
+
) {
|
|
127
|
+
logger.error("Couldn't get the status of the main instance");
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
process.exit();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (!arg.startsWith("-") && !arg.startsWith("--")) {
|
|
137
|
+
if (rawArgs.output?.file) {
|
|
138
|
+
logger.error("Only one output file is allowed");
|
|
139
|
+
|
|
140
|
+
logger.debug(
|
|
141
|
+
`Current output file: ${rawArgs.output.file}, new file: ${arg}`,
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
process.exit(1);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (!rawArgs.output) {
|
|
148
|
+
rawArgs.output = {};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
rawArgs.output.file = arg;
|
|
152
|
+
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (arg === "-ss") {
|
|
157
|
+
if (args[index + 1] === undefined || args[index + 1].startsWith("-")) {
|
|
158
|
+
logMissingArg(arg);
|
|
159
|
+
|
|
160
|
+
process.exit(1);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if (!seeking) {
|
|
164
|
+
seeking = {};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
seeking.startTime = args[index + 1];
|
|
168
|
+
|
|
169
|
+
skipNext();
|
|
170
|
+
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (arg === "-to") {
|
|
175
|
+
if (args[index + 1] === undefined || args[index + 1].startsWith("-")) {
|
|
176
|
+
logMissingArg(arg);
|
|
177
|
+
|
|
178
|
+
process.exit(1);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if (!seeking) {
|
|
182
|
+
seeking = {};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
seeking.stopTime = args[index + 1];
|
|
186
|
+
|
|
187
|
+
skipNext();
|
|
188
|
+
|
|
189
|
+
continue;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (arg === "-i") {
|
|
193
|
+
if (args[index + 1] === undefined || args[index + 1].startsWith("-")) {
|
|
194
|
+
logMissingArg(arg);
|
|
195
|
+
|
|
196
|
+
process.exit(1);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
if (!rawArgs.inputs) {
|
|
200
|
+
rawArgs.inputs = [];
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
rawArgs.inputs.push({
|
|
204
|
+
file: args[index + 1],
|
|
205
|
+
...seeking,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
seeking = undefined;
|
|
209
|
+
|
|
210
|
+
skipNext();
|
|
211
|
+
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (arg === "-subs") {
|
|
216
|
+
rawArgs.subs = true;
|
|
217
|
+
|
|
218
|
+
continue;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (arg === "-sl" || arg === "--size-limit") {
|
|
222
|
+
if (args[index + 1] === undefined || args[index + 1].startsWith("-")) {
|
|
223
|
+
logMissingArg(arg);
|
|
224
|
+
|
|
225
|
+
process.exit(1);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
const sizeLimit = Number(args[index + 1]);
|
|
229
|
+
|
|
230
|
+
if (isNaN(sizeLimit)) {
|
|
231
|
+
logInvalidNumber(arg, sizeLimit);
|
|
232
|
+
|
|
233
|
+
process.exit(1);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
rawArgs.sizeLimit = sizeLimit;
|
|
237
|
+
|
|
238
|
+
skipNext();
|
|
239
|
+
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (arg === "-ep" || arg === "--extra-params") {
|
|
244
|
+
if (args[index + 1] === undefined) {
|
|
245
|
+
logMissingArg(arg);
|
|
246
|
+
|
|
247
|
+
process.exit(1);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
isExtraParam = true;
|
|
251
|
+
|
|
252
|
+
rawArgs.extraParams = [];
|
|
253
|
+
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (arg === "--video-path") {
|
|
258
|
+
if (args[index + 1] === undefined || args[index + 1].startsWith("-")) {
|
|
259
|
+
logMissingArg(arg);
|
|
260
|
+
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
rawArgs.videoPath = args[index + 1];
|
|
265
|
+
|
|
266
|
+
skipNext();
|
|
267
|
+
|
|
268
|
+
continue;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (arg === "-crf") {
|
|
272
|
+
if (args[index + 1] === undefined || args[index + 1].startsWith("-")) {
|
|
273
|
+
logMissingArg(arg);
|
|
274
|
+
|
|
275
|
+
process.exit(1);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const crf = Number(args[index + 1]);
|
|
279
|
+
|
|
280
|
+
if (isNaN(crf)) {
|
|
281
|
+
logInvalidNumber(arg, crf);
|
|
282
|
+
|
|
283
|
+
process.exit(1);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
rawArgs.crf = crf;
|
|
287
|
+
|
|
288
|
+
skipNext();
|
|
289
|
+
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
if (arg === "-cpu-used") {
|
|
294
|
+
if (args[index + 1] === undefined || args[index + 1].startsWith("-")) {
|
|
295
|
+
logMissingArg(arg);
|
|
296
|
+
|
|
297
|
+
process.exit(1);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const cpuUsed = Number(args[index + 1]);
|
|
301
|
+
|
|
302
|
+
if (isNaN(cpuUsed)) {
|
|
303
|
+
logInvalidNumber(arg, cpuUsed);
|
|
304
|
+
|
|
305
|
+
process.exit(1);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
if (ArgsSchema.shape.cpuUsed.safeParse(cpuUsed).success === false) {
|
|
309
|
+
logger.error(
|
|
310
|
+
`The ${arg} flag requires a number between 0 and 5 inclusive. "${cpuUsed}" is out of that range`,
|
|
311
|
+
);
|
|
312
|
+
|
|
313
|
+
process.exit(1);
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
rawArgs.cpuUsed = cpuUsed as 0 | 1 | 2 | 3 | 4 | 5;
|
|
317
|
+
|
|
318
|
+
skipNext();
|
|
319
|
+
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (arg === "-deadline") {
|
|
324
|
+
if (args[index + 1] === undefined || args[index + 1].startsWith("-")) {
|
|
325
|
+
logMissingArg(arg);
|
|
326
|
+
|
|
327
|
+
process.exit(1);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
if (!["good", "best"].includes(args[index + 1])) {
|
|
331
|
+
logger.error(
|
|
332
|
+
`The ${arg} flag requires either "good" or "best". "${args[index + 1]}" is not a valid value`,
|
|
333
|
+
);
|
|
334
|
+
|
|
335
|
+
process.exit(1);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
rawArgs.deadline = args[index + 1] as "good" | "best";
|
|
339
|
+
|
|
340
|
+
skipNext();
|
|
341
|
+
|
|
342
|
+
continue;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
if (arg === "-c:v") {
|
|
346
|
+
if (args[index + 1] === undefined || args[index + 1].startsWith("-")) {
|
|
347
|
+
logMissingArg(arg);
|
|
348
|
+
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
rawArgs.encoder = args[index + 1];
|
|
353
|
+
|
|
354
|
+
skipNext();
|
|
355
|
+
|
|
356
|
+
continue;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (arg === "-lavfi") {
|
|
360
|
+
if (args[index + 1] === undefined || args[index + 1].startsWith("-")) {
|
|
361
|
+
logMissingArg(arg);
|
|
362
|
+
|
|
363
|
+
process.exit(1);
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
rawArgs.lavfi = args[index + 1];
|
|
367
|
+
|
|
368
|
+
skipNext();
|
|
369
|
+
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (seeking) {
|
|
375
|
+
rawArgs.output = { ...rawArgs.output, ...seeking };
|
|
376
|
+
|
|
377
|
+
seeking = undefined;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const parsedArgs = ArgsSchema.safeParse(rawArgs);
|
|
381
|
+
|
|
382
|
+
if (!parsedArgs.success) {
|
|
383
|
+
logger.error("Error parsing the arguments");
|
|
384
|
+
|
|
385
|
+
logger.error(
|
|
386
|
+
JSON.stringify(parsedArgs.error.flatten().fieldErrors, null, 2),
|
|
387
|
+
);
|
|
388
|
+
|
|
389
|
+
process.exit(1);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return parsedArgs.data;
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
const logMissingArg = (arg: string) =>
|
|
396
|
+
logger.error(`The ${arg} flag requires an argument`);
|
|
397
|
+
|
|
398
|
+
const logInvalidNumber = (arg: string, value: number) =>
|
|
399
|
+
logger.error(
|
|
400
|
+
`The ${arg} flag requires a number. "${value}" is not a valid number`,
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
const printUsage = () => {
|
|
404
|
+
const usage = `Usage: ${CLI_NAME} [options] [[infile options] -i infile]... [outfile options] [outfile] [extra params]
|
|
405
|
+
|
|
406
|
+
${DESCRIPTION}
|
|
407
|
+
|
|
408
|
+
Positional arguments:
|
|
409
|
+
[outfile] The output file. If not specified, a generated unix timestamp as filename will be used
|
|
410
|
+
and saved to the directory set in the --video-path option
|
|
411
|
+
|
|
412
|
+
Options:
|
|
413
|
+
-h, --help Show this help message
|
|
414
|
+
-v, --version Show version information
|
|
415
|
+
-kill Terminate the running ${CLI_NAME} instance, if there is any
|
|
416
|
+
-status Show the current status
|
|
417
|
+
-i <input> The input file to encode
|
|
418
|
+
-ss <start_time> The start time (same as ffmpeg's -ss)
|
|
419
|
+
-to <stop_time> The stop time (same as ffmpeg's -to)
|
|
420
|
+
-lavfi <filters> The set of filters to pass to ffmpeg
|
|
421
|
+
-c:v <encoder> The video encoder to use (default is ${config.encoder})
|
|
422
|
+
-deadline {good,best} The deadline for libvpx-vp9; good is the recommended one, best has the best
|
|
423
|
+
compression efficiency but takes the most time (default is ${config.deadline})
|
|
424
|
+
-crf <value> The crf to use (default is 24)
|
|
425
|
+
-cpu-used {0,1,2,3,4,5} The cpu-used for libvpx-vp9; a number between 0 and 5 inclusive, the higher
|
|
426
|
+
the number the faster the encoding will be with a quality trade-off (default is ${config.cpuUsed})
|
|
427
|
+
-subs Burn the subtitles onto the output file; this flag will automatically use
|
|
428
|
+
the subtitles found in the first input file, to use a different file use
|
|
429
|
+
the -lavfi flag with the subtitles filter directly
|
|
430
|
+
-sl, --size-limit <limit> The size limit of the output file in MiB, use 0 for no limit (default is ${config.sizeLimit})
|
|
431
|
+
--video-path <path> The video path where the video files are stored (default is ${config.videoPath})
|
|
432
|
+
this is overridden if the output file is specified
|
|
433
|
+
-ep, --extra-params <params>
|
|
434
|
+
The extra parameters to pass to ffmpeg, these will be appended making it
|
|
435
|
+
possible to override some defaults. This option has to be the last one, everything
|
|
436
|
+
will be passed as is to ffmpeg
|
|
437
|
+
|
|
438
|
+
Examples:
|
|
439
|
+
${CLI_NAME} -i "/tmp/Videos/nijinosaki.mkv" -ss 00:00:02.268 -to 00:00:10.310
|
|
440
|
+
${CLI_NAME} -i "/tmp/Videos/nijinosaki.mkv" --size-limit 6 -subs --extra-params -map 0:a -c:a libopus -b:a 128k`;
|
|
441
|
+
|
|
442
|
+
logger.info(usage, {
|
|
443
|
+
onlyConsole: true,
|
|
444
|
+
});
|
|
445
|
+
};
|
package/src/config.ts
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import path from "path";
|
|
2
|
+
|
|
3
|
+
import { parse } from "smol-toml";
|
|
4
|
+
import { logger } from "./logger";
|
|
5
|
+
import { CONFIG_PATH } from "./paths";
|
|
6
|
+
import { ConfigSchema } from "./schema/config";
|
|
7
|
+
import { CONFIG_FILE_NAME } from "./constants";
|
|
8
|
+
import { existsSync, readFileSync } from "fs";
|
|
9
|
+
|
|
10
|
+
const configPath = path.join(CONFIG_PATH, CONFIG_FILE_NAME);
|
|
11
|
+
|
|
12
|
+
let rawConfig: unknown = {};
|
|
13
|
+
|
|
14
|
+
if (existsSync(configPath)) {
|
|
15
|
+
logger.info("Loading config file " + configPath);
|
|
16
|
+
|
|
17
|
+
try {
|
|
18
|
+
rawConfig = parse(readFileSync(configPath, "utf-8"));
|
|
19
|
+
} catch (error) {
|
|
20
|
+
if (error instanceof Error) {
|
|
21
|
+
logger.error(
|
|
22
|
+
"Error parsing the config file " + configPath + ":\n\n" + error.message,
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
process.exit(1);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
throw error;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const parsedConfig = ConfigSchema.safeParse(rawConfig);
|
|
33
|
+
|
|
34
|
+
if (!parsedConfig.success) {
|
|
35
|
+
logger.error("Error parsing the config file " + configPath);
|
|
36
|
+
|
|
37
|
+
const errors = parsedConfig.error.flatten().fieldErrors;
|
|
38
|
+
|
|
39
|
+
for (const key in errors) {
|
|
40
|
+
logger.error(
|
|
41
|
+
`Error in option "${key}": ` +
|
|
42
|
+
errors[key as keyof typeof errors]?.join("; "),
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const config = parsedConfig.data;
|
package/src/constants.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import packageJson from "../package.json";
|
|
2
|
+
|
|
3
|
+
export const CLI_NAME = packageJson.bin.pwebm;
|
|
4
|
+
export const LOG_FILE_NAME = CLI_NAME + ".log";
|
|
5
|
+
export const CONFIG_FILE_NAME = "config.toml";
|
|
6
|
+
|
|
7
|
+
export const PIPE_NAME = "\\\\.\\pipe\\" + CLI_NAME; // Named pipe for Windows
|
|
8
|
+
export const SOCKET_NAME = CLI_NAME + ".sock";
|
|
9
|
+
|
|
10
|
+
export const AUTHOR = packageJson.author;
|
|
11
|
+
export const VERSION = packageJson.version;
|
|
12
|
+
export const LICENSE = packageJson.license;
|
|
13
|
+
export const HOMEPAGE = packageJson.homepage;
|
|
14
|
+
export const DESCRIPTION = packageJson.description;
|
|
15
|
+
export const COPYRIGHT_YEAR = "2023-2025";
|