pup-recorder 0.0.8
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/Cargo.lock +230 -0
- package/Cargo.toml +23 -0
- package/LICENSE +26 -0
- package/README.md +96 -0
- package/build.rs +5 -0
- package/build.ts +55 -0
- package/build_rust.ts +51 -0
- package/dist/cjs/app.cjs +633 -0
- package/dist/cjs/app.cjs.map +7 -0
- package/dist/cjs/cli.cjs +691 -0
- package/dist/cjs/cli.cjs.map +7 -0
- package/dist/cjs/index.cjs +781 -0
- package/dist/cjs/index.cjs.map +7 -0
- package/dist/cli.js +667 -0
- package/dist/cli.js.map +7 -0
- package/dist/index.d.ts +93 -0
- package/dist/index.js +728 -0
- package/dist/index.js.map +7 -0
- package/package.json +37 -0
- package/rust/darwin-arm64.node +0 -0
- package/rust/darwin-x64.node +0 -0
- package/rust/linux-arm64.node +0 -0
- package/rust/linux-x64.node +0 -0
- package/src/app.ts +19 -0
- package/src/base/abort.ts +75 -0
- package/src/base/constants.ts +18 -0
- package/src/base/electron.ts +51 -0
- package/src/base/encoder.ts +35 -0
- package/src/base/env.ts +21 -0
- package/src/base/ffmpeg.ts +188 -0
- package/src/base/frame_sync.ts +139 -0
- package/src/base/image.ts +9 -0
- package/src/base/lazy.ts +20 -0
- package/src/base/limiter.ts +58 -0
- package/src/base/logging.ts +123 -0
- package/src/base/noerr.ts +18 -0
- package/src/base/parser.ts +12 -0
- package/src/base/process.ts +35 -0
- package/src/base/proxy.ts +33 -0
- package/src/base/record.ts +228 -0
- package/src/base/retry.ts +40 -0
- package/src/base/stream.ts +74 -0
- package/src/base/timing.ts +23 -0
- package/src/base/types.ts +19 -0
- package/src/cli.ts +6 -0
- package/src/common.ts +53 -0
- package/src/index.ts +14 -0
- package/src/pup.ts +142 -0
- package/src/rust/lib.rs +105 -0
- package/src/rust/lib.ts +28 -0
- package/tsconfig.json +25 -0
- package/x265/darwin-arm64 +0 -0
- package/x265/darwin-x64 +0 -0
- package/x265/linux-arm64 +0 -0
- package/x265/linux-x64 +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Created by Autokaka (qq1909698494@gmail.com) on 2026/02/05.
|
|
2
|
+
|
|
3
|
+
import { setTimeout } from "timers/promises";
|
|
4
|
+
import { sleep } from "./timing";
|
|
5
|
+
|
|
6
|
+
export interface RetryOptions<Args extends any[], Ret> {
|
|
7
|
+
fn: (...args: Args) => Promise<Ret>;
|
|
8
|
+
maxAttempts?: number;
|
|
9
|
+
timeout?: number;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function useRetry<Args extends any[], Ret>({
|
|
13
|
+
fn,
|
|
14
|
+
maxAttempts = 3,
|
|
15
|
+
timeout,
|
|
16
|
+
}: RetryOptions<Args, Ret>) {
|
|
17
|
+
const timeoutError = new Error(`timeout over ${timeout}ms`);
|
|
18
|
+
return async function (...args: Args) {
|
|
19
|
+
let attempt = 0;
|
|
20
|
+
while (true) {
|
|
21
|
+
try {
|
|
22
|
+
const promises = [fn(...args)];
|
|
23
|
+
if (timeout) {
|
|
24
|
+
promises.push(
|
|
25
|
+
setTimeout(timeout).then(() => {
|
|
26
|
+
throw timeoutError;
|
|
27
|
+
}),
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
return await Promise.race(promises);
|
|
31
|
+
} catch (e) {
|
|
32
|
+
attempt++;
|
|
33
|
+
if (attempt >= maxAttempts) {
|
|
34
|
+
throw e;
|
|
35
|
+
}
|
|
36
|
+
await sleep(Math.pow(2, attempt) * 100 + Math.random() * 100);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
};
|
|
40
|
+
}
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// Created by Autokaka (qq1909698494@gmail.com) on 2026/02/09.
|
|
2
|
+
|
|
3
|
+
import type { ChildProcess } from "child_process";
|
|
4
|
+
|
|
5
|
+
export interface StreamWriter {
|
|
6
|
+
write(buffer: Buffer): Promise<void>;
|
|
7
|
+
end(): void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export function createWriter(stdin: NodeJS.WritableStream): StreamWriter {
|
|
11
|
+
const drainPromise = new Map<NodeJS.WritableStream, Promise<void>>();
|
|
12
|
+
|
|
13
|
+
const waitDrain = (stream: NodeJS.WritableStream) => {
|
|
14
|
+
const existing = drainPromise.get(stream);
|
|
15
|
+
if (existing) return existing;
|
|
16
|
+
const promise = new Promise<void>((resolve, reject) => {
|
|
17
|
+
const cleanup = (fn: () => void) => {
|
|
18
|
+
stream.off("drain", onDrain);
|
|
19
|
+
stream.off("error", onError);
|
|
20
|
+
stream.off("close", onClose);
|
|
21
|
+
drainPromise.delete(stream);
|
|
22
|
+
fn();
|
|
23
|
+
};
|
|
24
|
+
const onDrain = () => cleanup(resolve);
|
|
25
|
+
const onError = (err: Error) => cleanup(() => reject(err));
|
|
26
|
+
const onClose = () => cleanup(() => reject(new Error("stream closed")));
|
|
27
|
+
stream.on("drain", onDrain);
|
|
28
|
+
stream.on("error", onError);
|
|
29
|
+
stream.on("close", onClose);
|
|
30
|
+
});
|
|
31
|
+
drainPromise.set(stream, promise);
|
|
32
|
+
return promise;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const destroyed = () => Reflect.get(stdin, "destroyed");
|
|
36
|
+
return {
|
|
37
|
+
async write(buffer: Buffer) {
|
|
38
|
+
if (destroyed()) {
|
|
39
|
+
throw new Error("stdin destroyed");
|
|
40
|
+
}
|
|
41
|
+
if (!stdin.write(buffer)) {
|
|
42
|
+
await waitDrain(stdin);
|
|
43
|
+
}
|
|
44
|
+
},
|
|
45
|
+
end: () => {
|
|
46
|
+
if (!destroyed()) stdin.end();
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export function pipeline(...procs: ChildProcess[]) {
|
|
52
|
+
for (let i = 0; i < procs.length - 1; i++) {
|
|
53
|
+
const src = procs[i];
|
|
54
|
+
const dst = procs[i + 1];
|
|
55
|
+
if (!src?.stdout || !dst?.stdin) throw new Error("pipeline broken");
|
|
56
|
+
src.stdout.pipe(dst.stdin);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function waitAll(...procs: ChildProcess[]) {
|
|
61
|
+
return Promise.all(
|
|
62
|
+
procs.map(
|
|
63
|
+
(proc) =>
|
|
64
|
+
new Promise<void>((resolve, reject) => {
|
|
65
|
+
proc.on("error", reject);
|
|
66
|
+
proc.on("close", (code) =>
|
|
67
|
+
code === 0
|
|
68
|
+
? resolve()
|
|
69
|
+
: reject(new Error(`exit ${code ?? "null"}`)),
|
|
70
|
+
);
|
|
71
|
+
}),
|
|
72
|
+
),
|
|
73
|
+
);
|
|
74
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// Created by Autokaka (qq1909698494@gmail.com) on 2026/02/09.
|
|
2
|
+
|
|
3
|
+
export function sleep(ms: number) {
|
|
4
|
+
return new Promise<void>((resolve) => setTimeout(resolve, ms));
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export function periodical(
|
|
8
|
+
callback: (count: number) => Promise<void> | void,
|
|
9
|
+
ms: number,
|
|
10
|
+
) {
|
|
11
|
+
let token: NodeJS.Timeout;
|
|
12
|
+
let closed = false;
|
|
13
|
+
async function tick(count: number) {
|
|
14
|
+
await callback(count);
|
|
15
|
+
if (closed) return;
|
|
16
|
+
token = setTimeout(() => tick(count + 1), ms);
|
|
17
|
+
}
|
|
18
|
+
token = setTimeout(() => tick(0), ms);
|
|
19
|
+
return () => {
|
|
20
|
+
closed = true;
|
|
21
|
+
clearTimeout(token);
|
|
22
|
+
};
|
|
23
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// Created by Autokaka (qq1909698494@gmail.com) on 2026/02/06.
|
|
2
|
+
|
|
3
|
+
import type { Size } from "electron";
|
|
4
|
+
|
|
5
|
+
export interface VideoSpec {
|
|
6
|
+
fps: number;
|
|
7
|
+
frames: number;
|
|
8
|
+
size: Size;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface VideoFiles {
|
|
12
|
+
mp4?: string;
|
|
13
|
+
webm?: string;
|
|
14
|
+
mov?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface VideoFilesWithCover extends VideoFiles {
|
|
18
|
+
cover: string;
|
|
19
|
+
}
|
package/src/cli.ts
ADDED
package/src/common.ts
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Created by Autokaka (qq1909698494@gmail.com) on 2026/02/09.
|
|
2
|
+
|
|
3
|
+
import { program } from "commander";
|
|
4
|
+
import { pupUseInnerProxy } from "./base/constants";
|
|
5
|
+
import { logger } from "./base/logging";
|
|
6
|
+
import { noerr } from "./base/noerr";
|
|
7
|
+
import { parseNumber } from "./base/parser";
|
|
8
|
+
import { pargs } from "./base/process";
|
|
9
|
+
import type { RecordOptions } from "./base/record";
|
|
10
|
+
|
|
11
|
+
export const DEFAULT_WIDTH = 1920;
|
|
12
|
+
export const DEFAULT_HEIGHT = 1080;
|
|
13
|
+
export const DEFAULT_FPS = 30;
|
|
14
|
+
export const DEFAULT_DURATION = 5;
|
|
15
|
+
export const DEFAULT_OUT_DIR = "out";
|
|
16
|
+
|
|
17
|
+
export type CLICallback = (
|
|
18
|
+
source: string,
|
|
19
|
+
options: RecordOptions & Record<string, unknown>,
|
|
20
|
+
) => Promise<unknown>;
|
|
21
|
+
|
|
22
|
+
export function makeCLI(name: string, callback: CLICallback) {
|
|
23
|
+
program
|
|
24
|
+
.name(name)
|
|
25
|
+
.argument("<source>", "URL 或 HTML data")
|
|
26
|
+
.option("-w, --width <number>", "视频宽度", `${DEFAULT_WIDTH}`)
|
|
27
|
+
.option("-h, --height <number>", "视频高度", `${DEFAULT_HEIGHT}`)
|
|
28
|
+
.option("-f, --fps <number>", "帧率", `${DEFAULT_FPS}`)
|
|
29
|
+
.option("-t, --duration <number>", "录制时长(秒)", `${DEFAULT_DURATION}`)
|
|
30
|
+
.option("-o, --out-dir <path>", "输出目录", `${DEFAULT_OUT_DIR}`)
|
|
31
|
+
.option("-a, --with-alpha-channel", "输出包含 alpha 通道的视频", false)
|
|
32
|
+
.option(
|
|
33
|
+
"--use-inner-proxy",
|
|
34
|
+
"使用 B 站内网代理加速资源访问",
|
|
35
|
+
pupUseInnerProxy,
|
|
36
|
+
)
|
|
37
|
+
.action(async (source: string, opts) => {
|
|
38
|
+
try {
|
|
39
|
+
await callback(source, {
|
|
40
|
+
width: noerr(parseNumber, DEFAULT_WIDTH)(opts.width),
|
|
41
|
+
height: noerr(parseNumber, DEFAULT_HEIGHT)(opts.height),
|
|
42
|
+
fps: noerr(parseNumber, DEFAULT_FPS)(opts.fps),
|
|
43
|
+
duration: noerr(parseNumber, DEFAULT_DURATION)(opts.duration),
|
|
44
|
+
outDir: opts.outDir ?? DEFAULT_OUT_DIR,
|
|
45
|
+
withAlphaChannel: opts.withAlphaChannel ?? false,
|
|
46
|
+
useInnerProxy: opts.useInnerProxy ?? pupUseInnerProxy,
|
|
47
|
+
});
|
|
48
|
+
} catch (e) {
|
|
49
|
+
logger.fatal(e);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
program.parse(pargs());
|
|
53
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Created by Autokaka (qq1909698494@gmail.com) on 2026/01/30.
|
|
2
|
+
|
|
3
|
+
export * from "./base/constants";
|
|
4
|
+
export * from "./base/env";
|
|
5
|
+
export * from "./base/lazy";
|
|
6
|
+
export * from "./base/limiter";
|
|
7
|
+
export * from "./base/logging";
|
|
8
|
+
export * from "./base/noerr";
|
|
9
|
+
export * from "./base/parser";
|
|
10
|
+
export * from "./base/process";
|
|
11
|
+
export * from "./base/retry";
|
|
12
|
+
export * from "./base/timing";
|
|
13
|
+
|
|
14
|
+
export { pup, type PupOptions } from "./pup";
|
package/src/pup.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// Created by Autokaka (qq1909698494@gmail.com) on 2026/02/09.
|
|
2
|
+
|
|
3
|
+
import { spawn } from "child_process";
|
|
4
|
+
import type { Size } from "electron";
|
|
5
|
+
import { readFile, rm } from "fs/promises";
|
|
6
|
+
import { join } from "path";
|
|
7
|
+
import { AbortLink, type AbortQuery } from "./base/abort";
|
|
8
|
+
import { pupAppPath } from "./base/constants";
|
|
9
|
+
import { runElectronApp } from "./base/electron";
|
|
10
|
+
import { encodeBgraFile, encodeBgraToMov } from "./base/encoder";
|
|
11
|
+
import { createCoverCommand } from "./base/ffmpeg";
|
|
12
|
+
import { ConcurrencyLimiter } from "./base/limiter";
|
|
13
|
+
import { logger } from "./base/logging";
|
|
14
|
+
import { parseNumber } from "./base/parser";
|
|
15
|
+
import { type ProcessHandle } from "./base/process";
|
|
16
|
+
import type { RecordResult } from "./base/record";
|
|
17
|
+
import { waitAll } from "./base/stream";
|
|
18
|
+
import type { VideoFilesWithCover, VideoSpec } from "./base/types";
|
|
19
|
+
import { DEFAULT_HEIGHT, DEFAULT_WIDTH } from "./common";
|
|
20
|
+
|
|
21
|
+
const TAG = "[pup]";
|
|
22
|
+
|
|
23
|
+
export type PupProgressCallback = (progress: number) => Promise<void> | void;
|
|
24
|
+
|
|
25
|
+
export interface PupOptions {
|
|
26
|
+
withAlphaChannel?: boolean;
|
|
27
|
+
width?: number;
|
|
28
|
+
height?: number;
|
|
29
|
+
fps?: number;
|
|
30
|
+
duration?: number;
|
|
31
|
+
outDir?: string;
|
|
32
|
+
cancelQuery?: AbortQuery;
|
|
33
|
+
onProgress?: PupProgressCallback;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const PROGRESS_TAG = " progress: ";
|
|
37
|
+
|
|
38
|
+
function runPupApp(source: string, options: PupOptions) {
|
|
39
|
+
logger.debug(TAG, `runPupApp`, source, options);
|
|
40
|
+
|
|
41
|
+
const args: string[] = [source];
|
|
42
|
+
if (options.width) args.push("--width", `${options.width}`);
|
|
43
|
+
if (options.height) args.push("--height", `${options.height}`);
|
|
44
|
+
if (options.fps) args.push("--fps", `${options.fps}`);
|
|
45
|
+
if (options.duration) args.push("--duration", `${options.duration}`);
|
|
46
|
+
if (options.outDir) args.push("--out-dir", options.outDir);
|
|
47
|
+
if (options.withAlphaChannel) args.push("--with-alpha-channel");
|
|
48
|
+
|
|
49
|
+
const w = options.width ?? DEFAULT_WIDTH;
|
|
50
|
+
const h = options.height ?? DEFAULT_HEIGHT;
|
|
51
|
+
const handle = runElectronApp({ width: w, height: h }, pupAppPath, args);
|
|
52
|
+
const counter = new ConcurrencyLimiter(1);
|
|
53
|
+
handle.process.stdout?.on("data", (data: Buffer) => {
|
|
54
|
+
let message = data.toString().trim();
|
|
55
|
+
let start = message.indexOf(PROGRESS_TAG);
|
|
56
|
+
if (start < 0) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
message = message.slice(start + PROGRESS_TAG.length);
|
|
60
|
+
const end = message.indexOf("%");
|
|
61
|
+
if (end < 0) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
const progressStr = message.slice(0, end);
|
|
65
|
+
const progress = parseNumber(progressStr);
|
|
66
|
+
counter.schedule(async () => {
|
|
67
|
+
await options.onProgress?.(progress);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
return { handle, counter };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export async function pup(source: string, options: PupOptions) {
|
|
74
|
+
logger.debug(TAG, `pup`, source, options);
|
|
75
|
+
|
|
76
|
+
const link = AbortLink.start(options.cancelQuery);
|
|
77
|
+
const outDir = options.outDir ?? "out";
|
|
78
|
+
|
|
79
|
+
const t0 = performance.now();
|
|
80
|
+
const { handle, counter } = runPupApp(source, { ...options, outDir });
|
|
81
|
+
|
|
82
|
+
await link.wait(handle);
|
|
83
|
+
await counter.end();
|
|
84
|
+
logger.info(TAG, `capture cost ${Math.round(performance.now() - t0)}ms`);
|
|
85
|
+
|
|
86
|
+
const metaPath = join(outDir, "record.json");
|
|
87
|
+
const meta = JSON.parse(await readFile(metaPath, "utf-8")) as RecordResult;
|
|
88
|
+
|
|
89
|
+
const { bgraPath, written, options: recordOptions } = meta;
|
|
90
|
+
const { fps, width, height, withAlphaChannel } = recordOptions;
|
|
91
|
+
const size: Size = { width, height };
|
|
92
|
+
|
|
93
|
+
const outputs: VideoFilesWithCover = {
|
|
94
|
+
mp4: withAlphaChannel ? undefined : join(outDir, "output.mp4"),
|
|
95
|
+
webm: withAlphaChannel ? join(outDir, "output.webm") : undefined,
|
|
96
|
+
mov: withAlphaChannel ? join(outDir, "output.mov") : undefined,
|
|
97
|
+
cover: join(outDir, "cover.png"),
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const t1 = performance.now();
|
|
102
|
+
|
|
103
|
+
const spec: VideoSpec = { fps, frames: written, size };
|
|
104
|
+
const handles: ProcessHandle[] = [];
|
|
105
|
+
if (outputs.mp4) {
|
|
106
|
+
handles.push(encodeBgraFile(bgraPath, outputs.mp4, spec, "mp4"));
|
|
107
|
+
}
|
|
108
|
+
if (outputs.webm) {
|
|
109
|
+
handles.push(encodeBgraFile(bgraPath, outputs.webm, spec, "webm"));
|
|
110
|
+
}
|
|
111
|
+
if (outputs.mov) {
|
|
112
|
+
handles.push(encodeBgraToMov(bgraPath, outputs.mov, spec));
|
|
113
|
+
}
|
|
114
|
+
await link.wait(...handles);
|
|
115
|
+
|
|
116
|
+
const coverSrc = outputs.mov ?? outputs.webm ?? outputs.mp4;
|
|
117
|
+
if (coverSrc) {
|
|
118
|
+
const coverCmd = createCoverCommand(coverSrc, outputs.cover);
|
|
119
|
+
await waitAll(
|
|
120
|
+
spawn(coverCmd.command, coverCmd.args, { stdio: "inherit" }),
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
link.stop();
|
|
125
|
+
logger.info(TAG, `encoding cost ${Math.round(performance.now() - t1)}ms`);
|
|
126
|
+
|
|
127
|
+
await Promise.all([
|
|
128
|
+
rm(bgraPath, { force: true }),
|
|
129
|
+
rm(metaPath, { force: true }),
|
|
130
|
+
]);
|
|
131
|
+
return {
|
|
132
|
+
...outputs,
|
|
133
|
+
width,
|
|
134
|
+
height,
|
|
135
|
+
fps,
|
|
136
|
+
duration: Math.ceil(written / fps),
|
|
137
|
+
};
|
|
138
|
+
} catch (error) {
|
|
139
|
+
await rm(outDir, { recursive: true, force: true });
|
|
140
|
+
throw error;
|
|
141
|
+
}
|
|
142
|
+
}
|
package/src/rust/lib.rs
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Created by Autokaka (qq1909698494@gmail.com) on 2026/02/10.
|
|
2
|
+
|
|
3
|
+
use napi::bindgen_prelude::*;
|
|
4
|
+
use napi_derive::napi;
|
|
5
|
+
use std::fs::File;
|
|
6
|
+
use std::io::Write;
|
|
7
|
+
use std::sync::mpsc::{sync_channel, Receiver, SyncSender};
|
|
8
|
+
use std::sync::Mutex;
|
|
9
|
+
use std::thread;
|
|
10
|
+
use tokio::task;
|
|
11
|
+
|
|
12
|
+
#[napi]
|
|
13
|
+
pub struct FixedBufferWriter {
|
|
14
|
+
state: Mutex<Option<WriterState>>,
|
|
15
|
+
recycle_rx: Mutex<Receiver<Vec<u8>>>,
|
|
16
|
+
buffer_capacity: usize,
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
struct WriterState {
|
|
20
|
+
sender: SyncSender<Vec<u8>>,
|
|
21
|
+
thread_handle: thread::JoinHandle<std::io::Result<()>>,
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
#[napi]
|
|
25
|
+
impl FixedBufferWriter {
|
|
26
|
+
#[napi(constructor)]
|
|
27
|
+
pub fn new(path: String, buffer_capacity: u32, queue_depth: Option<u32>) -> Result<Self> {
|
|
28
|
+
let depth = queue_depth.unwrap_or(2) as usize;
|
|
29
|
+
let capacity = buffer_capacity as usize;
|
|
30
|
+
|
|
31
|
+
let (data_tx, data_rx) = sync_channel::<Vec<u8>>(depth);
|
|
32
|
+
let (recycle_tx, recycle_rx) = sync_channel::<Vec<u8>>(depth + 1);
|
|
33
|
+
|
|
34
|
+
let handle = thread::spawn(move || {
|
|
35
|
+
let mut file = File::create(path)?;
|
|
36
|
+
|
|
37
|
+
while let Ok(mut buffer) = data_rx.recv() {
|
|
38
|
+
file.write_all(&buffer)?;
|
|
39
|
+
buffer.clear();
|
|
40
|
+
let _ = recycle_tx.try_send(buffer);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
file.sync_data()?;
|
|
44
|
+
Ok(())
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
Ok(FixedBufferWriter {
|
|
48
|
+
state: Mutex::new(Some(WriterState {
|
|
49
|
+
sender: data_tx,
|
|
50
|
+
thread_handle: handle,
|
|
51
|
+
})),
|
|
52
|
+
recycle_rx: Mutex::new(recycle_rx),
|
|
53
|
+
buffer_capacity: capacity,
|
|
54
|
+
})
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
#[napi]
|
|
58
|
+
pub fn write(&self, buffer: Buffer) -> Result<()> {
|
|
59
|
+
let sender = {
|
|
60
|
+
let guard = self.state.lock().unwrap();
|
|
61
|
+
if let Some(state) = guard.as_ref() {
|
|
62
|
+
state.sender.clone()
|
|
63
|
+
} else {
|
|
64
|
+
return Ok(());
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
let mut vec = {
|
|
69
|
+
let recycle = self.recycle_rx.lock().unwrap();
|
|
70
|
+
recycle
|
|
71
|
+
.try_recv()
|
|
72
|
+
.unwrap_or_else(|_| Vec::with_capacity(self.buffer_capacity))
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
vec.extend_from_slice(buffer.as_ref());
|
|
76
|
+
|
|
77
|
+
sender
|
|
78
|
+
.send(vec)
|
|
79
|
+
.map_err(|e| Error::from_reason(e.to_string()))?;
|
|
80
|
+
|
|
81
|
+
Ok(())
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
#[napi]
|
|
85
|
+
pub async fn close(&self) -> Result<()> {
|
|
86
|
+
let state_opt = {
|
|
87
|
+
let mut guard = self.state.lock().unwrap();
|
|
88
|
+
guard.take()
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
if let Some(state) = state_opt {
|
|
92
|
+
drop(state.sender);
|
|
93
|
+
|
|
94
|
+
let handle = state.thread_handle;
|
|
95
|
+
task::spawn_blocking(move || {
|
|
96
|
+
handle.join().map_err(|_| {
|
|
97
|
+
std::io::Error::new(std::io::ErrorKind::Other, "Thread panicked")
|
|
98
|
+
})?
|
|
99
|
+
})
|
|
100
|
+
.await
|
|
101
|
+
.map_err(|e| Error::from_reason(e.to_string()))??;
|
|
102
|
+
}
|
|
103
|
+
Ok(())
|
|
104
|
+
}
|
|
105
|
+
}
|
package/src/rust/lib.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
// Created by Autokaka (qq1909698494@gmail.com) on 2026/02/10.
|
|
2
|
+
|
|
3
|
+
import { existsSync } from "fs";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
|
|
6
|
+
const { platform, arch } = process;
|
|
7
|
+
|
|
8
|
+
const rustPath = `rust/${platform}-${arch}.node`;
|
|
9
|
+
|
|
10
|
+
const nativeSearchPaths = [
|
|
11
|
+
join(__dirname, `../../${rustPath}`), // process start from src
|
|
12
|
+
join(__dirname, `../${rustPath}`), // process start from dist
|
|
13
|
+
];
|
|
14
|
+
const mod = require(nativeSearchPaths.find(existsSync)!);
|
|
15
|
+
|
|
16
|
+
export interface FixedBufferWriter {
|
|
17
|
+
new (
|
|
18
|
+
path: string,
|
|
19
|
+
bufferSize: number,
|
|
20
|
+
queueDepth?: number,
|
|
21
|
+
): FixedBufferWriter;
|
|
22
|
+
|
|
23
|
+
write(buffer: Buffer): void;
|
|
24
|
+
|
|
25
|
+
close(): Promise<void>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const FixedBufferWriter = mod.FixedBufferWriter as FixedBufferWriter;
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"lib": ["ESNext", "DOM"],
|
|
4
|
+
"target": "ESNext",
|
|
5
|
+
"module": "Preserve",
|
|
6
|
+
"moduleDetection": "force",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
|
|
9
|
+
"moduleResolution": "bundler",
|
|
10
|
+
"allowImportingTsExtensions": true,
|
|
11
|
+
"verbatimModuleSyntax": true,
|
|
12
|
+
"noEmit": true,
|
|
13
|
+
|
|
14
|
+
"strict": true,
|
|
15
|
+
"skipLibCheck": true,
|
|
16
|
+
"noFallthroughCasesInSwitch": true,
|
|
17
|
+
"noUncheckedIndexedAccess": true,
|
|
18
|
+
"noImplicitOverride": true,
|
|
19
|
+
|
|
20
|
+
"noUnusedLocals": true,
|
|
21
|
+
"noUnusedParameters": true,
|
|
22
|
+
"noPropertyAccessFromIndexSignature": true
|
|
23
|
+
},
|
|
24
|
+
"include": ["src", "./*.ts"]
|
|
25
|
+
}
|
|
Binary file
|
package/x265/darwin-x64
ADDED
|
Binary file
|
package/x265/linux-arm64
ADDED
|
Binary file
|
package/x265/linux-x64
ADDED
|
Binary file
|