mb-rrvideo-server 1.0.2
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/.dockerignore +48 -0
- package/CHANGELOG.md +335 -0
- package/DOCKER.md +374 -0
- package/Dockerfile +98 -0
- package/README.md +334 -0
- package/bin/rrvideo-server +14 -0
- package/config.example.json +49 -0
- package/docker-compose.yml +44 -0
- package/ecosystem.config.js +24 -0
- package/ecosystem.docker.config.js +23 -0
- package/package.json +46 -0
- package/src/config.js +171 -0
- package/src/index.js +138 -0
- package/src/logger.js +144 -0
- package/src/routes/convert.js +818 -0
- package/src/routes/merge.js +258 -0
- package/src/routes/receive.js +84 -0
- package/src/storage.js +154 -0
- package/src/task.js +120 -0
- package/src/utils/callback.js +73 -0
- package/src/utils/video.js +234 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 视频处理工具模块
|
|
3
|
+
* 负责执行 rrvideos 命令进行视频转换和合并
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const { spawn } = require('child_process');
|
|
7
|
+
const { execSync } = require('child_process');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* 转义命令行参数
|
|
12
|
+
*/
|
|
13
|
+
function escapeArg(str) {
|
|
14
|
+
if (str == null) return '';
|
|
15
|
+
return `"${String(str).replace(/"/g, '\\"')}"`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 执行 rrvideos 命令
|
|
20
|
+
* @param {string} command - 完整的命令字符串
|
|
21
|
+
* @param {function} onLog - 日志回调函数 (type, data)
|
|
22
|
+
* @param {object} customEnv - 自定义环境变量
|
|
23
|
+
* @returns {ChildProcess} 子进程对象
|
|
24
|
+
*/
|
|
25
|
+
function runRrvideosCommand(command, onLog, customEnv) {
|
|
26
|
+
if (process.env.TEST_MODE === '1') {
|
|
27
|
+
// 测试模式:模拟进度与完成
|
|
28
|
+
setTimeout(() => onLog && onLog('stdout', '[TEST] progress: 10%'), 50);
|
|
29
|
+
setTimeout(() => onLog && onLog('stdout', '[TEST] progress: 50%'), 100);
|
|
30
|
+
setTimeout(() => onLog && onLog('stdout', '[TEST] progress: 90%'), 150);
|
|
31
|
+
setTimeout(() => onLog && onLog('close', { code: 0, signal: null }), 220);
|
|
32
|
+
return { kill: () => {}, pid: 12345 };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const spawnOptions = {
|
|
36
|
+
shell: true,
|
|
37
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
// 使用自定义环境变量或继承当前进程环境
|
|
41
|
+
if (customEnv) {
|
|
42
|
+
spawnOptions.env = customEnv;
|
|
43
|
+
} else {
|
|
44
|
+
spawnOptions.env = Object.assign({}, process.env);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const child = spawn(command, spawnOptions);
|
|
48
|
+
|
|
49
|
+
// 流式捕获 stdout/stderr
|
|
50
|
+
if (child.stdout) {
|
|
51
|
+
child.stdout.on('data', (data) => {
|
|
52
|
+
const output = String(data).trim();
|
|
53
|
+
if (output) {
|
|
54
|
+
onLog && onLog('stdout', output);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (child.stderr) {
|
|
60
|
+
child.stderr.on('data', (data) => {
|
|
61
|
+
const output = String(data).trim();
|
|
62
|
+
if (output) {
|
|
63
|
+
onLog && onLog('stderr', output);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// close 事件带上 signal
|
|
69
|
+
child.on('close', (code, signal) => {
|
|
70
|
+
onLog && onLog('close', { code, signal });
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return child;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* 获取 rrvideos 命令路径
|
|
78
|
+
* @returns {string} rrvideos 可执行文件路径
|
|
79
|
+
*/
|
|
80
|
+
function getRrvideosPath() {
|
|
81
|
+
try {
|
|
82
|
+
const rrvideosPath = execSync('which rrvideos', { encoding: 'utf-8' }).trim();
|
|
83
|
+
return rrvideosPath;
|
|
84
|
+
} catch (e) {
|
|
85
|
+
throw new Error('rrvideos 命令未找到,请确保已安装 mb-rrvideo');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* 获取 node 命令路径(与 rrvideos 同目录)
|
|
91
|
+
* @returns {string} node 可执行文件路径
|
|
92
|
+
*/
|
|
93
|
+
function getNodePath() {
|
|
94
|
+
const rrvideosPath = getRrvideosPath();
|
|
95
|
+
const rrvideosDir = path.dirname(rrvideosPath);
|
|
96
|
+
return path.join(rrvideosDir, 'node');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* 生成临时 rrvideo 配置文件
|
|
101
|
+
* @param {object} rrvideoConfig - rrvideo 配置对象(从 config.rrvideo 读取)
|
|
102
|
+
* @returns {string} 临时配置文件路径
|
|
103
|
+
*/
|
|
104
|
+
function generateTempConfigFile(rrvideoConfig) {
|
|
105
|
+
const fs = require('fs');
|
|
106
|
+
const os = require('os');
|
|
107
|
+
const path = require('path');
|
|
108
|
+
|
|
109
|
+
// 创建临时目录
|
|
110
|
+
const tempDir = path.join(os.tmpdir(), 'rrvideo-server');
|
|
111
|
+
if (!fs.existsSync(tempDir)) {
|
|
112
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 生成唯一的临时文件名
|
|
116
|
+
const tempFile = path.join(tempDir, `rrvideo-config-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
|
|
117
|
+
|
|
118
|
+
// 写入配置(rrvideos 期望的是扁平的 JSON,不需要 rrvideo 包裹层)
|
|
119
|
+
fs.writeFileSync(tempFile, JSON.stringify(rrvideoConfig, null, 2));
|
|
120
|
+
|
|
121
|
+
return tempFile;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* 删除临时配置文件
|
|
126
|
+
* @param {string} configPath - 临时配置文件路径
|
|
127
|
+
*/
|
|
128
|
+
function cleanupTempConfigFile(configPath) {
|
|
129
|
+
try {
|
|
130
|
+
const fs = require('fs');
|
|
131
|
+
if (configPath && fs.existsSync(configPath)) {
|
|
132
|
+
fs.unlinkSync(configPath);
|
|
133
|
+
}
|
|
134
|
+
} catch (e) {
|
|
135
|
+
console.error(`清理临时配置文件失败: ${e.message}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* 构建转换命令
|
|
141
|
+
* @param {Array} inputList - 输入文件路径列表
|
|
142
|
+
* @param {Array} outputList - 输出文件路径列表
|
|
143
|
+
* @param {object|string} config - 配置对象或配置文件路径
|
|
144
|
+
* @returns {object} { command: string, tempConfigFile: string|null }
|
|
145
|
+
*/
|
|
146
|
+
function buildConvertCommand(inputList, outputList, config) {
|
|
147
|
+
const nodePath = getNodePath();
|
|
148
|
+
const rrvideosPath = getRrvideosPath();
|
|
149
|
+
|
|
150
|
+
let command = `${nodePath} --expose-gc ${rrvideosPath}`;
|
|
151
|
+
inputList.forEach((p) => { command += ` --input ${escapeArg(p)}`; });
|
|
152
|
+
outputList.forEach((p) => { command += ` --output ${escapeArg(p)}`; });
|
|
153
|
+
command += ` --format mp4`;
|
|
154
|
+
|
|
155
|
+
let tempConfigFile = null;
|
|
156
|
+
|
|
157
|
+
if (config) {
|
|
158
|
+
if (typeof config === 'string') {
|
|
159
|
+
// 如果是字符串,当作文件路径直接使用
|
|
160
|
+
command += ` --config ${escapeArg(config)}`;
|
|
161
|
+
} else if (typeof config === 'object') {
|
|
162
|
+
// 如果是对象,生成临时配置文件
|
|
163
|
+
tempConfigFile = generateTempConfigFile(config);
|
|
164
|
+
command += ` --config ${escapeArg(tempConfigFile)}`;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return { command, tempConfigFile };
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* 构建合并命令
|
|
173
|
+
* @param {Array} videos - 视频文件路径列表
|
|
174
|
+
* @param {string} output - 输出文件路径
|
|
175
|
+
* @param {object} options - 合并选项
|
|
176
|
+
* @returns {string} 完整的命令字符串
|
|
177
|
+
*/
|
|
178
|
+
function buildMergeCommand(videos, output, options = {}) {
|
|
179
|
+
const nodePath = getNodePath();
|
|
180
|
+
const rrvideosPath = getRrvideosPath();
|
|
181
|
+
|
|
182
|
+
const format = options.format || 'mp4';
|
|
183
|
+
const method = options.method || 'concat';
|
|
184
|
+
const autoMode = options.auto_mode || 'smart';
|
|
185
|
+
const headers = options.headers || {};
|
|
186
|
+
|
|
187
|
+
let command = `${nodePath} --expose-gc ${rrvideosPath} merge --output ${escapeArg(output)} --format ${escapeArg(format)} --method ${escapeArg(method)} --auto-mode ${escapeArg(autoMode)}`;
|
|
188
|
+
|
|
189
|
+
videos.forEach((v) => {
|
|
190
|
+
command += ` --videos ${escapeArg(v)}`;
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
Object.keys(headers).forEach((k) => {
|
|
194
|
+
const pair = `${k}:${headers[k]}`;
|
|
195
|
+
command += ` --headers ${escapeArg(pair)}`;
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
return command;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* 设置文件权限
|
|
203
|
+
* @param {string} directoryPath - 目录路径
|
|
204
|
+
* @returns {Promise<void>}
|
|
205
|
+
*/
|
|
206
|
+
function setPermissions(directoryPath) {
|
|
207
|
+
return new Promise((resolve, reject) => {
|
|
208
|
+
const { exec } = require('child_process');
|
|
209
|
+
const command = `chmod -R o+r ${directoryPath}/*`;
|
|
210
|
+
exec(command, (error, stdout, stderr) => {
|
|
211
|
+
if (error) {
|
|
212
|
+
console.error(`chmod执行错误: ${error}`);
|
|
213
|
+
reject(error);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (stderr) {
|
|
217
|
+
console.error(`chmod标准错误输出: ${stderr}`);
|
|
218
|
+
}
|
|
219
|
+
console.log(`chmod标准输出: ${stdout}`);
|
|
220
|
+
resolve(stdout);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
module.exports = {
|
|
226
|
+
runRrvideosCommand,
|
|
227
|
+
getRrvideosPath,
|
|
228
|
+
getNodePath,
|
|
229
|
+
buildConvertCommand,
|
|
230
|
+
buildMergeCommand,
|
|
231
|
+
setPermissions,
|
|
232
|
+
escapeArg,
|
|
233
|
+
cleanupTempConfigFile
|
|
234
|
+
};
|