pwebm 0.0.1-alpha.0 → 0.0.1
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 +118 -5
- package/TODO.md +12 -37
- package/package.json +1 -1
- package/src/args.ts +10 -10
- package/src/ffmpeg.ts +57 -41
- package/src/logger.ts +9 -2
- package/src/main.ts +30 -5
- package/src/queue.ts +9 -11
- package/src/utils.ts +18 -0
package/README.md
CHANGED
|
@@ -1,15 +1,128 @@
|
|
|
1
1
|
# pwebm
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Utility to encode size restricted webm files with ffmpeg.
|
|
4
|
+
|
|
5
|
+
When executed multiple times, the additional encoding requests will be put inside a queue that will be handled in the first instance that was executed.
|
|
6
|
+
|
|
7
|
+
When not specified, the output file name will be the current unix timestamp (with 13 digits plus 3 random additional ones, e.g.: `1741140729397902.webm`), and saved in `~/Movies/pwebm/` (macOs) or `~/Videos/pwebm/` (all others).
|
|
8
|
+
|
|
9
|
+
# Installation
|
|
10
|
+
|
|
11
|
+
This is a CLI that makes use of [Bun](https://bun.sh/) APIs internally, so having bun installed in your system for it to work is a requirement.
|
|
12
|
+
|
|
13
|
+
The package is available in the official npm registry, so you can install it globally with:
|
|
4
14
|
|
|
5
15
|
```bash
|
|
6
|
-
bun
|
|
16
|
+
bun i -g pwebm
|
|
7
17
|
```
|
|
8
18
|
|
|
9
|
-
|
|
19
|
+
Or if you prefer installing the repo directly from GitHub (with the latest commits):
|
|
10
20
|
|
|
11
21
|
```bash
|
|
12
|
-
bun
|
|
22
|
+
bun i -g git+https://github.com/4ndrs/pwebm.git
|
|
13
23
|
```
|
|
14
24
|
|
|
15
|
-
|
|
25
|
+
>[!NOTE]
|
|
26
|
+
>There is no build step for this script; the source code is used and executed as is. You can check, and follow its [entry point](./pwebm) to see how it works.
|
|
27
|
+
|
|
28
|
+
## Historical Background & Purpose
|
|
29
|
+
|
|
30
|
+
In 2022, I wrote a script for the mpv media player called [PureMPV](https://github.com/4ndrs/PureMPV) that would help extracting the currently watched video's information, like the timestamps at certain points, and the the cropping coordinates, which would then later be used for encoding videos with ffmpeg with specific parameters.
|
|
31
|
+
|
|
32
|
+
Up to that point, I mostly spent the time encoding short webm files to keep and share on some imageboards, as well as encoding some mkv files with streams and attachments copied, so I wanted to integrate this functionality in some way with PureMPV. I didn't like the idea of having PureMPV to do the encoding itself, I wanted a seperate process to handle this independently, so I could stop or start another mpv window without affecting the encoding process. I wrote down some of the initial requirements at the time, which were the following:
|
|
33
|
+
|
|
34
|
+
- Encoding webm files with size limits (first crf mode, then bitrate mode on retries)
|
|
35
|
+
- Having a queue for handling multiple encodings
|
|
36
|
+
- Log file tracking everything that is happening (I'm a sucker for these)
|
|
37
|
+
- Generated file names with unix timestamps
|
|
38
|
+
|
|
39
|
+
The encoding with first crf mode, and then bitrate (when failing) was my way of encoding webms, and I wanted to automate this very same process. Having a queue was essential to keep selecting multiple segments to encode without waiting for the previous one to end, this meant I could keep using mpv and then later check at the end the resulting webms. I also loved the idea of having a log file that I could track with `tail -f`. Personally, I have been using tail/multitail on my Linux boxes to track logs for the longest time, so this was hugely inspired by this. When uploading files on some imageboards, the file name generated on the server uses the unix timestamp, so I wanted to keep the same format. I use the same naming format for my screenshots as well.
|
|
40
|
+
|
|
41
|
+
Following these requirements, I wrote an early version of the script in Python named [PureWebM](https://github.com/4ndrs/PureWebM), which was a simple script that would encode size limited webm files, and would retry encoding with a lower bitrate if the limit was reached. It would also handle multiple encoding requests in a queue, and log everything that was happening in a file.
|
|
42
|
+
|
|
43
|
+
The script worked fine for me for 2 years, but maintenance and installation on new machines became a painful experience with Python. The codebase was hard to look at (it was written in my early days with python), and I strongly wanted to improve it using a more modern approach, so I decided to rewrite it from scratch in TypeScript, and this is the result.
|
|
44
|
+
|
|
45
|
+
This new version is a lot more organized, and was written with cross-platform compatibility in mind, like using named pipes on Windows instead of unix sockets, and properly handling paths with `path.join()` instead of using hard coded paths. Unfortunately at the time of writing I haven't been able to test it on Windows yet, but the foundation is there for anyone who wants to try it out.
|
|
46
|
+
|
|
47
|
+
The script was mainly written to use with [PureMPV](https://github.com/4ndrs/PureMPV) and [pwebm-helper](https://github.com/4ndrs/pwebm-helper), but it can be used independently as well, if you are comfortable with the command line.
|
|
48
|
+
|
|
49
|
+
## Usage
|
|
50
|
+
|
|
51
|
+
[Usage Preview](https://github.com/user-attachments/assets/1800f80c-db37-4fd4-9652-3e68aeb645d5)
|
|
52
|
+
|
|
53
|
+
The script has different arguments that can be used to customize the encoding process. They are similar to ffmpeg's arguments, meaning we have `-i` for selecting inputs, `-c:v` for selecting the video codec, `-ss` for the start time, `-to` for the end time, and so on. The script supports both output seeking and input seeking with multiple inputs if needed. Here is an exhaustive list of the currently available arguments:
|
|
54
|
+
|
|
55
|
+
|Argument|Details|
|
|
56
|
+
|----------|------|
|
|
57
|
+
|-h, --help| Show help message and exit|
|
|
58
|
+
|-v, --version| Show version and exit|
|
|
59
|
+
|-kill| Terminate the running pwebm instance, if there is any|
|
|
60
|
+
|-status| Show the current status of the encoding process|
|
|
61
|
+
|-i| The input file to encode|
|
|
62
|
+
|-ss| The start time of the segment to encode|
|
|
63
|
+
|-to| The end time of the segment to encode|
|
|
64
|
+
|-c:v| The video codec to use. Default is **libvpx-vp9**|
|
|
65
|
+
|-crf| The Constant Rate Factor to use. Default is **24**|
|
|
66
|
+
|-lavfi| The set of filters to pass to ffmpeg|
|
|
67
|
+
|-deadline| The deadline passed to ffmpeg for libvpx-vp9. Default is **good**|
|
|
68
|
+
|-cpu-used|The cpu-used passed to ffmpeg for libvpx-vp9. Default is **0**|
|
|
69
|
+
|-subs| Burn the subtitles|
|
|
70
|
+
|-sl, --size-limit| The size limit in MiB. Default is **4**|
|
|
71
|
+
|--video-path| The path to save the video files. Default is **~/Movies/pwebm/** on macOs, and **~/Videos/pwebm/** on everything else|
|
|
72
|
+
|-ep, --extra-params| Extra parameters to pass to ffmpeg|
|
|
73
|
+
|
|
74
|
+
If the codec option `c:v` is set to `libvpx` for v8 webms, or `libvpx-vp9` for vp9 webms, the script will generate a webm with the choosen options and size limit in MiB. If the codec is set to anything else, the script will generate an mkv file with all streams copied, including the attachments, and reencode the video stream with the choosen crf; no size limitations will be applied here.
|
|
75
|
+
|
|
76
|
+
The `subs` option will only trigger on webms, burning the subtitles onto the video stream. The internal implementation of this option is just picking the first input file's subtitles, and applying the subtitle filter to the resulting output. If you need to pick subtitles in a different file, you can use the `-lavfi` option to pass the subtitles filter manually for now.
|
|
77
|
+
|
|
78
|
+
>[!NOTE]
|
|
79
|
+
>The `subs` option doesn't work with input seeking. If you need to seek, make sure you are using output seeking, otherwise the resulting webm will have the subtitles burned at the wrong time.
|
|
80
|
+
|
|
81
|
+
The `size-limit` option sets the limit in MiB for the resulting webm file. If the file exceeds this limit, the script will retry encoding with a lower bitrate, and will keep retrying until the limit is met. The script will also keep track of the amount of retries, and will log this information in the log file. You can use `0` to disable the limit.
|
|
82
|
+
|
|
83
|
+
The `extra-params` option is a way to pass additional parameters to ffmpeg. This is an escape hatch, and can be used to replace internal defaults. Everything is passed as is, and there is no validation by the script, so make sure you are passing valid parameters.
|
|
84
|
+
|
|
85
|
+
### Examples
|
|
86
|
+
```bash
|
|
87
|
+
# Encode a webm without audio, with a size limit of 4 MiB, and save it in the default video path with a random file name
|
|
88
|
+
pwebm -i /tmp/Videos/nijinosaki.mkv
|
|
89
|
+
|
|
90
|
+
# Encode a webm with a specific file name
|
|
91
|
+
pwebm -i /tmp/Videos/nijinosaki.mkv /tmp/Videos/CUTE.webm
|
|
92
|
+
|
|
93
|
+
# Encode a segment with input seeking
|
|
94
|
+
pwebm -ss 00:00:02.268 -to 00:00:10.310 -i /tmp/Videos/nijinosaki.mkv
|
|
95
|
+
|
|
96
|
+
# Encode a segment with output seeking and burnt subtitles
|
|
97
|
+
pwebm -i /tmp/Videos/nijinosaki.mkv -ss 00:00:02.268 -to 00:00:10.310 -subs
|
|
98
|
+
|
|
99
|
+
# Encode a webm with size limit of 6 MiB and audio
|
|
100
|
+
pwebm -i /tmp/Videos/nijinosaki.mkv --size-limit 6 --extra-params -c:a libopus -b:a 128k
|
|
101
|
+
|
|
102
|
+
# Encode an h264 mkv with crf 24 and other streams copied
|
|
103
|
+
pwebm -ss 00:00:02.268 -to 00:00:10.310 -i /tmp/Videos/nijinosaki.mkv -c:v libx264
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The script logs everything it does inside a log file located in `~/.config/pwebm/pwebm.log`, so you can check this file to see what params are used when executing ffmpeg. This file isn't currently rotated automatically in the script, so you might want to setup rotation with `logrotate` or similar.
|
|
107
|
+
|
|
108
|
+
## Configuration File
|
|
109
|
+
|
|
110
|
+
The defaults in the script can be configured with a configuration file located in `~/.config/pwebm/config.toml`. The file is a simple TOML file with the following options:
|
|
111
|
+
|
|
112
|
+
|Key|Equivalent|
|
|
113
|
+
|----------|------|
|
|
114
|
+
|encoder| -c:v|
|
|
115
|
+
|crf| -crf|
|
|
116
|
+
|deadline| -deadline|
|
|
117
|
+
|cpuUsed| -cpu-used|
|
|
118
|
+
|subs|-subs|
|
|
119
|
+
|sizeLimit| --size-limit|
|
|
120
|
+
|videoPath| --video-path|
|
|
121
|
+
|
|
122
|
+
Example:
|
|
123
|
+
```toml
|
|
124
|
+
crf = 28
|
|
125
|
+
subs = true
|
|
126
|
+
sizeLimit = 3
|
|
127
|
+
videoPath = "~/Videos/PureWebM"
|
|
128
|
+
```
|
package/TODO.md
CHANGED
|
@@ -1,28 +1,3 @@
|
|
|
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
1
|
## Stuff
|
|
27
2
|
|
|
28
3
|
- [x] argument parser
|
|
@@ -39,17 +14,17 @@ some considerations:
|
|
|
39
14
|
- [x] name the process
|
|
40
15
|
- [x] version implementation
|
|
41
16
|
- [x] help implementation
|
|
42
|
-
- [
|
|
43
|
-
- [
|
|
44
|
-
- [
|
|
45
|
-
- [
|
|
46
|
-
- [
|
|
47
|
-
- [
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
17
|
+
- [x] args.ts help is missing dynamic default for crf
|
|
18
|
+
- [x] if input is missing after some other arg, the default parse error is shown
|
|
19
|
+
- [x] warn color is messed up for the message shown when the file limit is reached because of default warn color
|
|
20
|
+
- [x] implement better cleaning process before exit
|
|
21
|
+
- [x] release tagged versions in npm
|
|
22
|
+
- [x] 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
|
|
23
|
+
- [x] update readme
|
|
24
|
+
- [x] add subs flag warning to the readme (how it works, output seeking needed, etc)
|
|
25
|
+
|
|
26
|
+
### Might be nice to have
|
|
52
27
|
- [ ] add limit for the amount of tries to redo a conversion
|
|
53
28
|
- [ ] percentage bitrate offset when retrying a conversion with new calcs
|
|
54
|
-
|
|
55
|
-
|
|
29
|
+
- [ ] handle no duration case, maybe adding an additional crf mode instead of bitrate calcs
|
|
30
|
+
- [ ] (helper script) add a keybinding to show the status of the encoding on the mpv window
|
package/package.json
CHANGED
package/src/args.ts
CHANGED
|
@@ -37,15 +37,6 @@ const RECOGNIZED_ARGS = [
|
|
|
37
37
|
];
|
|
38
38
|
|
|
39
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
40
|
const rawArgs: Partial<ArgsSchema> = {};
|
|
50
41
|
|
|
51
42
|
let skip = false;
|
|
@@ -377,6 +368,15 @@ export const parseArgs = async (args: string[]): Promise<ArgsSchema> => {
|
|
|
377
368
|
seeking = undefined;
|
|
378
369
|
}
|
|
379
370
|
|
|
371
|
+
if (!rawArgs.inputs) {
|
|
372
|
+
printUsage();
|
|
373
|
+
|
|
374
|
+
// if the user isn't using any of the quick actions we require the -i flag
|
|
375
|
+
logger.error("Input file is required");
|
|
376
|
+
|
|
377
|
+
process.exit(1);
|
|
378
|
+
}
|
|
379
|
+
|
|
380
380
|
const parsedArgs = ArgsSchema.safeParse(rawArgs);
|
|
381
381
|
|
|
382
382
|
if (!parsedArgs.success) {
|
|
@@ -421,7 +421,7 @@ Options:
|
|
|
421
421
|
-c:v <encoder> The video encoder to use (default is ${config.encoder})
|
|
422
422
|
-deadline {good,best} The deadline for libvpx-vp9; good is the recommended one, best has the best
|
|
423
423
|
compression efficiency but takes the most time (default is ${config.deadline})
|
|
424
|
-
-crf <value> The crf to use (default is
|
|
424
|
+
-crf <value> The crf to use (default is ${config.crf})
|
|
425
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
426
|
the number the faster the encoding will be with a quality trade-off (default is ${config.cpuUsed})
|
|
427
427
|
-subs Burn the subtitles onto the output file; this flag will automatically use
|
package/src/ffmpeg.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { queue } from "./queue";
|
|
|
2
2
|
import { status } from "./status";
|
|
3
3
|
import { logger } from "./logger";
|
|
4
4
|
import { CLI_NAME } from "./constants";
|
|
5
|
+
import { cleanExit } from "./utils";
|
|
5
6
|
import { unlinkSync } from "fs";
|
|
6
7
|
import { ArgsSchema } from "./schema/args";
|
|
7
8
|
import { FFProbeSchema } from "./schema/ffprobe";
|
|
@@ -13,12 +14,13 @@ import path from "path";
|
|
|
13
14
|
|
|
14
15
|
type Subprocess = _Subprocess<"ignore", "pipe", "pipe">;
|
|
15
16
|
|
|
16
|
-
let stderr
|
|
17
|
+
let stderr: string;
|
|
17
18
|
let forceKilled = false;
|
|
19
|
+
let passLogFile: string | undefined;
|
|
18
20
|
let ffmpegProcess: Subprocess | undefined;
|
|
19
21
|
|
|
20
22
|
const encode = async (args: ArgsSchema) => {
|
|
21
|
-
const duration = deduceDuration(args);
|
|
23
|
+
const duration = await deduceDuration(args);
|
|
22
24
|
|
|
23
25
|
const inputs = args.inputs.flatMap((input) => {
|
|
24
26
|
const result = [];
|
|
@@ -114,6 +116,8 @@ const encode = async (args: ArgsSchema) => {
|
|
|
114
116
|
|
|
115
117
|
logger.info("Executing: " + cmd.join(" "));
|
|
116
118
|
|
|
119
|
+
stderr = "";
|
|
120
|
+
|
|
117
121
|
const singlePassProcess = Bun.spawn({ cmd, stderr: "pipe" });
|
|
118
122
|
|
|
119
123
|
ffmpegProcess = singlePassProcess;
|
|
@@ -151,18 +155,17 @@ const encode = async (args: ArgsSchema) => {
|
|
|
151
155
|
await singlePassProcess.exited;
|
|
152
156
|
|
|
153
157
|
if (ffmpegProcess.exitCode !== 0 && !forceKilled) {
|
|
158
|
+
logger.error("\n" + stderr);
|
|
159
|
+
|
|
154
160
|
logger.error(
|
|
155
161
|
"Error processing the single pass, ffmpeg exited with code: " +
|
|
156
162
|
ffmpegProcess.exitCode,
|
|
157
163
|
);
|
|
158
|
-
logger.error(stderr);
|
|
159
164
|
|
|
160
|
-
|
|
165
|
+
await cleanExit(1);
|
|
161
166
|
}
|
|
162
167
|
|
|
163
168
|
if (forceKilled) {
|
|
164
|
-
logKilled();
|
|
165
|
-
|
|
166
169
|
return;
|
|
167
170
|
}
|
|
168
171
|
|
|
@@ -232,7 +235,7 @@ const encode = async (args: ArgsSchema) => {
|
|
|
232
235
|
"-1",
|
|
233
236
|
];
|
|
234
237
|
|
|
235
|
-
|
|
238
|
+
passLogFile = path.join(TEMP_PATH, CLI_NAME + "2pass");
|
|
236
239
|
|
|
237
240
|
const firstPassCmd = [
|
|
238
241
|
...cmd,
|
|
@@ -296,6 +299,8 @@ const encode = async (args: ArgsSchema) => {
|
|
|
296
299
|
|
|
297
300
|
logger.info("Executing: " + firstPassCmd.join(" "));
|
|
298
301
|
|
|
302
|
+
stderr = "";
|
|
303
|
+
|
|
299
304
|
const firstPassProcess = Bun.spawn({ cmd: firstPassCmd, stderr: "pipe" });
|
|
300
305
|
|
|
301
306
|
ffmpegProcess = firstPassProcess;
|
|
@@ -305,18 +310,19 @@ const encode = async (args: ArgsSchema) => {
|
|
|
305
310
|
await firstPassProcess.exited;
|
|
306
311
|
|
|
307
312
|
if (firstPassProcess.exitCode !== 0 && !forceKilled) {
|
|
308
|
-
logger.error("
|
|
313
|
+
logger.error("\n" + stderr);
|
|
314
|
+
|
|
315
|
+
logger.error(
|
|
316
|
+
"Error processing the single pass, ffmpeg exited with code: " +
|
|
317
|
+
ffmpegProcess.exitCode,
|
|
318
|
+
);
|
|
309
319
|
|
|
310
|
-
removePassLogFile(
|
|
320
|
+
removePassLogFile();
|
|
311
321
|
|
|
312
|
-
|
|
322
|
+
await cleanExit(1);
|
|
313
323
|
}
|
|
314
324
|
|
|
315
325
|
if (forceKilled) {
|
|
316
|
-
removePassLogFile(passLogFile);
|
|
317
|
-
|
|
318
|
-
logKilled();
|
|
319
|
-
|
|
320
326
|
return;
|
|
321
327
|
}
|
|
322
328
|
|
|
@@ -336,6 +342,8 @@ const encode = async (args: ArgsSchema) => {
|
|
|
336
342
|
|
|
337
343
|
logger.info("Executing: " + secondPassCmd.join(" "));
|
|
338
344
|
|
|
345
|
+
stderr = "";
|
|
346
|
+
|
|
339
347
|
const secondPassProcess = Bun.spawn({ cmd: secondPassCmd, stderr: "pipe" });
|
|
340
348
|
|
|
341
349
|
ffmpegProcess = secondPassProcess;
|
|
@@ -380,6 +388,7 @@ const encode = async (args: ArgsSchema) => {
|
|
|
380
388
|
`${queue.getStatus()}: {RED}File size is greater than the limit by ${offsetPercentage}% with ${triesCount === 1 ? "crf " + args.crf : "bitrate " + (bitrate / 1000).toFixed(2) + "K"}{/RED}`,
|
|
381
389
|
{
|
|
382
390
|
logToConsole: true,
|
|
391
|
+
noDefaultColors: true,
|
|
383
392
|
fancyConsole: {
|
|
384
393
|
colors: true,
|
|
385
394
|
noNewLine: false,
|
|
@@ -408,6 +417,7 @@ const encode = async (args: ArgsSchema) => {
|
|
|
408
417
|
`${queue.getStatus()}: {RED}Retrying with bitrate ${(bitrate / 1000).toFixed(2)}K{/RED}`,
|
|
409
418
|
{
|
|
410
419
|
logToConsole: true,
|
|
420
|
+
noDefaultColors: true,
|
|
411
421
|
fancyConsole: {
|
|
412
422
|
colors: true,
|
|
413
423
|
noNewLine: false,
|
|
@@ -426,22 +436,18 @@ const encode = async (args: ArgsSchema) => {
|
|
|
426
436
|
await secondPassProcess.exited;
|
|
427
437
|
} while (failed);
|
|
428
438
|
|
|
429
|
-
removePassLogFile(passLogFile);
|
|
430
|
-
|
|
431
439
|
if (ffmpegProcess.exitCode !== 0 && !forceKilled) {
|
|
440
|
+
logger.error("\n" + stderr);
|
|
441
|
+
|
|
432
442
|
logger.error(
|
|
433
443
|
"Error processing the second pass, ffmpeg exited with code: " +
|
|
434
444
|
ffmpegProcess.exitCode,
|
|
435
445
|
);
|
|
436
446
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
process.exit(1);
|
|
447
|
+
await cleanExit(1);
|
|
440
448
|
}
|
|
441
449
|
|
|
442
450
|
if (forceKilled) {
|
|
443
|
-
logKilled();
|
|
444
|
-
|
|
445
451
|
return;
|
|
446
452
|
}
|
|
447
453
|
|
|
@@ -461,6 +467,8 @@ const encode = async (args: ArgsSchema) => {
|
|
|
461
467
|
if (queueIsDone) {
|
|
462
468
|
logger.info("All encodings done");
|
|
463
469
|
}
|
|
470
|
+
|
|
471
|
+
removePassLogFile();
|
|
464
472
|
};
|
|
465
473
|
|
|
466
474
|
const processStderr = async (process: Subprocess) => {
|
|
@@ -469,19 +477,6 @@ const processStderr = async (process: Subprocess) => {
|
|
|
469
477
|
}
|
|
470
478
|
};
|
|
471
479
|
|
|
472
|
-
const logKilled = () => {
|
|
473
|
-
logger.warn("ffmpeg was killed");
|
|
474
|
-
|
|
475
|
-
logger.info(queue.getStatus() + ": {RED}Killed{/RED}", {
|
|
476
|
-
logToConsole: true,
|
|
477
|
-
fancyConsole: {
|
|
478
|
-
colors: true,
|
|
479
|
-
noNewLine: false,
|
|
480
|
-
clearPreviousLine: true,
|
|
481
|
-
},
|
|
482
|
-
});
|
|
483
|
-
};
|
|
484
|
-
|
|
485
480
|
const processStdout = async (
|
|
486
481
|
process: Subprocess,
|
|
487
482
|
onProgress: (progress: ProgressSchema) => void,
|
|
@@ -512,8 +507,8 @@ const processStdout = async (
|
|
|
512
507
|
}
|
|
513
508
|
};
|
|
514
509
|
|
|
515
|
-
const kill = () => {
|
|
516
|
-
if (!ffmpegProcess) {
|
|
510
|
+
const kill = async () => {
|
|
511
|
+
if (!ffmpegProcess || ffmpegProcess.killed) {
|
|
517
512
|
return;
|
|
518
513
|
}
|
|
519
514
|
|
|
@@ -521,15 +516,36 @@ const kill = () => {
|
|
|
521
516
|
|
|
522
517
|
forceKilled = true;
|
|
523
518
|
ffmpegProcess.kill("SIGKILL");
|
|
519
|
+
|
|
520
|
+
await ffmpegProcess.exited;
|
|
521
|
+
|
|
522
|
+
logger.warn("ffmpeg was killed");
|
|
523
|
+
|
|
524
|
+
logger.info(queue.getStatus() + ": {RED}Killed{/RED}", {
|
|
525
|
+
logToConsole: true,
|
|
526
|
+
fancyConsole: {
|
|
527
|
+
colors: true,
|
|
528
|
+
noNewLine: false,
|
|
529
|
+
clearPreviousLine: true,
|
|
530
|
+
},
|
|
531
|
+
});
|
|
532
|
+
|
|
533
|
+
removePassLogFile();
|
|
524
534
|
};
|
|
525
535
|
|
|
526
|
-
const removePassLogFile = (
|
|
527
|
-
|
|
536
|
+
const removePassLogFile = () => {
|
|
537
|
+
if (!passLogFile) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
let file = passLogFile + "-0.log";
|
|
528
542
|
|
|
529
543
|
try {
|
|
530
|
-
logger.info(
|
|
544
|
+
logger.info("Deleting the pass log file: " + file);
|
|
531
545
|
|
|
532
546
|
unlinkSync(file);
|
|
547
|
+
|
|
548
|
+
passLogFile = undefined;
|
|
533
549
|
} catch (error) {
|
|
534
550
|
logger.error("Couldn't delete pass log file: " + file);
|
|
535
551
|
|
|
@@ -571,7 +587,7 @@ const escapeSpecialCharacters = (value: string) =>
|
|
|
571
587
|
// Comma
|
|
572
588
|
.replace(/,/g, "\\,");
|
|
573
589
|
|
|
574
|
-
const deduceDuration = (args: ArgsSchema) => {
|
|
590
|
+
const deduceDuration = async (args: ArgsSchema) => {
|
|
575
591
|
// if output seeking stop time is set with no output start time, the
|
|
576
592
|
// duration will be the stop time
|
|
577
593
|
if (args.output?.stopTime && !args.output?.startTime) {
|
|
@@ -588,7 +604,7 @@ const deduceDuration = (args: ArgsSchema) => {
|
|
|
588
604
|
logger.error("Error reading the input metadata");
|
|
589
605
|
logger.error(error.message);
|
|
590
606
|
|
|
591
|
-
|
|
607
|
+
return await cleanExit(1);
|
|
592
608
|
}
|
|
593
609
|
|
|
594
610
|
// the following is a very simplistic approach to deduce the duration
|
package/src/logger.ts
CHANGED
|
@@ -45,6 +45,7 @@ const CLEAR_LINE = "\r\u001b[K";
|
|
|
45
45
|
type Options = {
|
|
46
46
|
onlyConsole?: boolean; // only logs to console regardless of the allowed level
|
|
47
47
|
logToConsole?: boolean; // logs to console regardless of the allowed level
|
|
48
|
+
noDefaultColors?: boolean; // don't show default colors for warn and error levels
|
|
48
49
|
fancyConsole?: {
|
|
49
50
|
colors?: boolean;
|
|
50
51
|
noNewLine?: boolean;
|
|
@@ -64,11 +65,17 @@ const log = (message: Message, level: Level, options?: Options) => {
|
|
|
64
65
|
consoleLog = (message: string) => print(message, "stdout", skipNewLine);
|
|
65
66
|
break;
|
|
66
67
|
case "WARN":
|
|
67
|
-
|
|
68
|
+
if (!options?.noDefaultColors) {
|
|
69
|
+
message = `{ORANGE}${message}{/ORANGE}`;
|
|
70
|
+
}
|
|
71
|
+
|
|
68
72
|
consoleLog = (message: string) => print(message, "stderr", skipNewLine);
|
|
69
73
|
break;
|
|
70
74
|
case "ERROR":
|
|
71
|
-
|
|
75
|
+
if (!options?.noDefaultColors) {
|
|
76
|
+
message = `{RED}${message}{/RED}`;
|
|
77
|
+
}
|
|
78
|
+
|
|
72
79
|
consoleLog = (message: string) => print(message, "stderr", skipNewLine);
|
|
73
80
|
break;
|
|
74
81
|
case "DEBUG":
|
package/src/main.ts
CHANGED
|
@@ -3,27 +3,52 @@ import { queue } from "./queue";
|
|
|
3
3
|
import { logger } from "./logger";
|
|
4
4
|
import { CLI_NAME } from "./constants";
|
|
5
5
|
import { parseArgs } from "./args";
|
|
6
|
+
import { cleanExit } from "./utils";
|
|
6
7
|
|
|
7
8
|
process.title = CLI_NAME;
|
|
8
9
|
|
|
9
10
|
process.on("SIGINT", () => {
|
|
10
11
|
logger.warn("Received SIGINT, aborting processing");
|
|
11
|
-
|
|
12
|
+
|
|
13
|
+
cleanExit(130);
|
|
12
14
|
});
|
|
13
15
|
|
|
14
16
|
process.on("SIGTERM", () => {
|
|
15
17
|
logger.warn("Received SIGTERM, aborting processing");
|
|
16
|
-
|
|
18
|
+
|
|
19
|
+
cleanExit(143);
|
|
17
20
|
});
|
|
18
21
|
|
|
19
22
|
process.on("SIGHUP", () => {
|
|
20
23
|
logger.warn("Received SIGHUP, aborting processing");
|
|
21
|
-
|
|
24
|
+
|
|
25
|
+
cleanExit(129);
|
|
22
26
|
});
|
|
23
27
|
|
|
24
28
|
process.on("uncaughtException", (error) => {
|
|
25
|
-
|
|
26
|
-
|
|
29
|
+
let message = error.message;
|
|
30
|
+
|
|
31
|
+
message = error.stack ? `${message}\n${error.stack}` : message;
|
|
32
|
+
|
|
33
|
+
logger.error("Uncaught exception: " + message);
|
|
34
|
+
|
|
35
|
+
cleanExit(1);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
process.on("unhandledRejection", (reason) => {
|
|
39
|
+
let message: string;
|
|
40
|
+
|
|
41
|
+
if (reason instanceof Error) {
|
|
42
|
+
message = reason.message;
|
|
43
|
+
|
|
44
|
+
message = reason.stack ? `${message}\n${reason.stack}` : message;
|
|
45
|
+
} else {
|
|
46
|
+
message = JSON.stringify(reason, null, 2);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
logger.error("Unhandled rejection: " + message);
|
|
50
|
+
|
|
51
|
+
cleanExit(1);
|
|
27
52
|
});
|
|
28
53
|
|
|
29
54
|
const args = Bun.argv.slice(2);
|
package/src/queue.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { ffmpeg } from "./ffmpeg";
|
|
2
2
|
import { ArgsSchema } from "./schema/args";
|
|
3
|
-
import { setTimeout } from "timers/promises";
|
|
4
3
|
|
|
5
4
|
const store: {
|
|
6
5
|
total: number;
|
|
@@ -20,13 +19,13 @@ const push = (args: ArgsSchema) => {
|
|
|
20
19
|
};
|
|
21
20
|
|
|
22
21
|
const abortProcessing = async () => {
|
|
23
|
-
store.
|
|
22
|
+
if (!store.processing) {
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
store.abort = true;
|
|
26
27
|
|
|
27
|
-
|
|
28
|
-
await setTimeout(100);
|
|
29
|
-
}
|
|
28
|
+
await ffmpeg.kill();
|
|
30
29
|
};
|
|
31
30
|
|
|
32
31
|
const processQueue = async () => {
|
|
@@ -37,6 +36,10 @@ const processQueue = async () => {
|
|
|
37
36
|
store.processing = true;
|
|
38
37
|
|
|
39
38
|
while (getProcessedCount() < store.total) {
|
|
39
|
+
if (store.abort) {
|
|
40
|
+
break;
|
|
41
|
+
}
|
|
42
|
+
|
|
40
43
|
const current = store.queue.shift();
|
|
41
44
|
|
|
42
45
|
if (!current) {
|
|
@@ -44,13 +47,8 @@ const processQueue = async () => {
|
|
|
44
47
|
}
|
|
45
48
|
|
|
46
49
|
await ffmpeg.encode(current);
|
|
47
|
-
|
|
48
|
-
if (store.abort) {
|
|
49
|
-
break;
|
|
50
|
-
}
|
|
51
50
|
}
|
|
52
51
|
|
|
53
|
-
store.abort = false;
|
|
54
52
|
store.processing = false;
|
|
55
53
|
};
|
|
56
54
|
|
package/src/utils.ts
CHANGED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
import { ipc } from "./ipc";
|
|
2
|
+
import { queue } from "./queue";
|
|
3
|
+
import { logger } from "./logger";
|
|
4
|
+
|
|
1
5
|
export const assertNever = (value: never) => {
|
|
2
6
|
throw new Error(`Unexpected value: ${value}`);
|
|
3
7
|
};
|
|
8
|
+
|
|
9
|
+
// explicit type needed for the control flow but it doesn't work with promises
|
|
10
|
+
// https://github.com/microsoft/TypeScript/issues/34955
|
|
11
|
+
type CleanExit = (code?: number) => Promise<never>;
|
|
12
|
+
|
|
13
|
+
export const cleanExit: CleanExit = async (code = 0) => {
|
|
14
|
+
logger.info("Exiting cleanly...");
|
|
15
|
+
|
|
16
|
+
await queue.abortProcessing();
|
|
17
|
+
|
|
18
|
+
ipc.stopListener();
|
|
19
|
+
|
|
20
|
+
process.exit(code);
|
|
21
|
+
};
|