@shun-js/remotion-server 0.6.3 → 0.6.4
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/package.json +5 -2
- package/server/model/RemotionModel.js +25 -3
- package/server/service/RemotionService.js +32 -0
- package/server/task/RemotionTask.js +13 -15
- package/server/util/renderer.js +122 -0
- package/server/util/uploader.js +51 -0
- package/server/util/work.js +86 -0
- package/server/1 +0 -177
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shun-js/remotion-server",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "remotion.cool server",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"author": "uikoo9 <uikoo9@qq.com>",
|
|
@@ -17,6 +17,9 @@
|
|
|
17
17
|
"app.js"
|
|
18
18
|
],
|
|
19
19
|
"dependencies": {
|
|
20
|
+
"@aws-sdk/client-s3": "^3.978.0",
|
|
21
|
+
"@remotion/bundler": "^4.0.414",
|
|
22
|
+
"@remotion/renderer": "^4.0.414",
|
|
20
23
|
"@shun-js/shun-config": "^0.3.1",
|
|
21
24
|
"@shun-js/shun-service": "^0.3.1",
|
|
22
25
|
"@supabase/supabase-js": "^2.93.3",
|
|
@@ -29,5 +32,5 @@
|
|
|
29
32
|
"access": "public",
|
|
30
33
|
"registry": "https://registry.npmjs.org/"
|
|
31
34
|
},
|
|
32
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "260326ec9d0cb743db2fb84bb240828ee4caab84"
|
|
33
36
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// supabase
|
|
2
2
|
const { createClient } = require('@supabase/supabase-js');
|
|
3
|
+
const supabase = createClient(global.QZ_CONFIG.SUPABASE_URL, global.QZ_CONFIG.SUPABASE_SERVICE_KEY);
|
|
3
4
|
|
|
4
5
|
// logger
|
|
5
6
|
const Logger = require('qiao-log');
|
|
@@ -12,10 +13,8 @@ const logger = Logger(logOptions);
|
|
|
12
13
|
*/
|
|
13
14
|
exports.fetchPendingWorks = async () => {
|
|
14
15
|
const methodName = 'fetchPendingWorks';
|
|
15
|
-
try {
|
|
16
|
-
// supabase
|
|
17
|
-
const supabase = createClient(global.QZ_CONFIG.SUPABASE_URL, global.QZ_CONFIG.SUPABASE_SERVICE_KEY);
|
|
18
16
|
|
|
17
|
+
try {
|
|
19
18
|
// query
|
|
20
19
|
const { data, error } = await supabase.rpc('get_pending_works_locked', {
|
|
21
20
|
max_count: 1,
|
|
@@ -34,3 +33,26 @@ exports.fetchPendingWorks = async () => {
|
|
|
34
33
|
return [];
|
|
35
34
|
}
|
|
36
35
|
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* 更新作品渲染状态
|
|
39
|
+
*/
|
|
40
|
+
exports.updateRenderStatus = async (workId, renderStatus, updates = {}) => {
|
|
41
|
+
const methodName = 'updateRenderStatus';
|
|
42
|
+
try {
|
|
43
|
+
const { error } = await supabase
|
|
44
|
+
.from('works')
|
|
45
|
+
.update({
|
|
46
|
+
render_status: renderStatus,
|
|
47
|
+
updated_at: new Date().toISOString(),
|
|
48
|
+
...updates,
|
|
49
|
+
})
|
|
50
|
+
.eq('id', workId);
|
|
51
|
+
|
|
52
|
+
if (error) {
|
|
53
|
+
logger.error(methodName, `Error updating work ${workId} to ${renderStatus}:`, error);
|
|
54
|
+
}
|
|
55
|
+
} catch (error) {
|
|
56
|
+
logger.error(methodName, 'Unexpected error updating render status:', error);
|
|
57
|
+
}
|
|
58
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// model
|
|
2
|
+
const { fetchPendingWorks } = require('../model/RemotionModel.js');
|
|
3
|
+
|
|
4
|
+
// go
|
|
5
|
+
const { processWork } = require('../util/work.js');
|
|
6
|
+
|
|
7
|
+
// logger
|
|
8
|
+
const Logger = require('qiao-log');
|
|
9
|
+
const logOptions = require('../log-options.js')();
|
|
10
|
+
const logger = Logger(logOptions);
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* genVideo
|
|
14
|
+
*/
|
|
15
|
+
exports.genVideo = async () => {
|
|
16
|
+
const methodName = 'genVideo';
|
|
17
|
+
|
|
18
|
+
// go
|
|
19
|
+
logger.info(methodName, 'start');
|
|
20
|
+
|
|
21
|
+
// rows
|
|
22
|
+
const rows = await fetchPendingWorks();
|
|
23
|
+
logger.info(methodName, 'rows', rows);
|
|
24
|
+
if (!rows || !rows.length) return;
|
|
25
|
+
|
|
26
|
+
// work
|
|
27
|
+
const work = rows[0];
|
|
28
|
+
await processWork(work);
|
|
29
|
+
|
|
30
|
+
// end
|
|
31
|
+
logger.info(methodName, 'end');
|
|
32
|
+
};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
const {
|
|
1
|
+
// service
|
|
2
|
+
const { genVideo } = require('../service/RemotionService.js');
|
|
3
3
|
|
|
4
4
|
// logger
|
|
5
5
|
const Logger = require('qiao-log');
|
|
@@ -9,20 +9,18 @@ const logger = Logger(logOptions);
|
|
|
9
9
|
// RemotionTask.js
|
|
10
10
|
exports.runAndInit = true;
|
|
11
11
|
exports.time = '* 1 * * *';
|
|
12
|
-
exports.tick = async () => {
|
|
13
|
-
await genRemotionVideo();
|
|
14
|
-
};
|
|
15
12
|
|
|
16
|
-
//
|
|
17
|
-
|
|
18
|
-
|
|
13
|
+
// tick
|
|
14
|
+
let start = false;
|
|
15
|
+
exports.tick = async () => {
|
|
16
|
+
logger.info('start', start);
|
|
17
|
+
if (start) return;
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
logger.info(
|
|
19
|
+
start = true;
|
|
20
|
+
logger.info('start', start);
|
|
22
21
|
|
|
23
|
-
|
|
24
|
-
console.log(rows);
|
|
22
|
+
await genVideo();
|
|
25
23
|
|
|
26
|
-
|
|
27
|
-
logger.info(
|
|
28
|
-
}
|
|
24
|
+
start = false;
|
|
25
|
+
logger.info('start', start);
|
|
26
|
+
};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// fs
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
// remotion
|
|
6
|
+
const { bundle } = require('@remotion/bundler');
|
|
7
|
+
const { renderMedia, selectComposition } = require('@remotion/renderer');
|
|
8
|
+
|
|
9
|
+
// logger
|
|
10
|
+
const Logger = require('qiao-log');
|
|
11
|
+
const logOptions = require('../log-options.js')();
|
|
12
|
+
const logger = Logger(logOptions);
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* 渲染 Remotion 视频
|
|
16
|
+
* @param {Object} options
|
|
17
|
+
* @param {string} options.sourceCode - 用户的 React 代码
|
|
18
|
+
* @param {string} options.outputPath - 输出视频路径
|
|
19
|
+
* @param {number} options.width - 视频宽度
|
|
20
|
+
* @param {number} options.height - 视频高度
|
|
21
|
+
* @param {number} options.fps - 帧率
|
|
22
|
+
*/
|
|
23
|
+
exports.renderVideo = async ({ sourceCode, outputPath, width, height, fps }) => {
|
|
24
|
+
const methodName = 'renderVideo';
|
|
25
|
+
|
|
26
|
+
// const
|
|
27
|
+
const tempDir = path.join(__dirname, 'temp', Date.now().toString());
|
|
28
|
+
const entryPoint = path.join(tempDir, 'src', 'Root.jsx');
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
// 1. 创建临时项目结构
|
|
32
|
+
fs.mkdirSync(path.join(tempDir, 'src'), { recursive: true });
|
|
33
|
+
|
|
34
|
+
// 2. 写入用户代码
|
|
35
|
+
const wrappedCode = wrapUserCode(sourceCode);
|
|
36
|
+
fs.writeFileSync(entryPoint, wrappedCode);
|
|
37
|
+
|
|
38
|
+
// 3. 创建 package.json
|
|
39
|
+
const packageJson = {
|
|
40
|
+
name: 'temp-remotion-project',
|
|
41
|
+
version: '1.0.0',
|
|
42
|
+
dependencies: {
|
|
43
|
+
react: '^18.2.0',
|
|
44
|
+
remotion: '^4.0.0',
|
|
45
|
+
},
|
|
46
|
+
};
|
|
47
|
+
fs.writeFileSync(path.join(tempDir, 'package.json'), JSON.stringify(packageJson, null, 2));
|
|
48
|
+
|
|
49
|
+
// 4. Bundle 代码
|
|
50
|
+
logger.info(methodName, 'Bundling code...');
|
|
51
|
+
const bundleLocation = await bundle({
|
|
52
|
+
entryPoint,
|
|
53
|
+
webpackOverride: (config) => config,
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// 5. 获取 composition
|
|
57
|
+
logger.info(methodName, 'Getting composition...');
|
|
58
|
+
const composition = await selectComposition({
|
|
59
|
+
serveUrl: bundleLocation,
|
|
60
|
+
id: 'MyComposition', // 默认 composition ID
|
|
61
|
+
inputProps: {},
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// 6. 渲染视频
|
|
65
|
+
logger.info(methodName, 'Rendering frames...');
|
|
66
|
+
await renderMedia({
|
|
67
|
+
composition: {
|
|
68
|
+
...composition,
|
|
69
|
+
width: width,
|
|
70
|
+
height: height,
|
|
71
|
+
fps: fps,
|
|
72
|
+
durationInFrames: composition.durationInFrames || 150, // 默认5秒
|
|
73
|
+
},
|
|
74
|
+
serveUrl: bundleLocation,
|
|
75
|
+
codec: 'h264',
|
|
76
|
+
outputLocation: outputPath,
|
|
77
|
+
inputProps: {},
|
|
78
|
+
onProgress: ({ progress }) => {
|
|
79
|
+
if (progress % 0.1 < 0.01) {
|
|
80
|
+
// 每10%打印一次
|
|
81
|
+
logger.info(`Render progress: ${(progress * 100).toFixed(1)}%`);
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
logger.info(methodName, 'Render completed');
|
|
87
|
+
} finally {
|
|
88
|
+
// 清理临时目录
|
|
89
|
+
if (fs.existsSync(tempDir)) {
|
|
90
|
+
fs.rmSync(tempDir, { recursive: true, force: true });
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* 包装用户代码为 Remotion 项目
|
|
97
|
+
*/
|
|
98
|
+
function wrapUserCode(sourceCode) {
|
|
99
|
+
return `
|
|
100
|
+
import React from 'react';
|
|
101
|
+
import { Composition, AbsoluteFill, useCurrentFrame, interpolate, useVideoConfig } from 'remotion';
|
|
102
|
+
|
|
103
|
+
// 用户代码
|
|
104
|
+
${sourceCode}
|
|
105
|
+
|
|
106
|
+
// Root 组件 - Remotion 入口
|
|
107
|
+
export const RemotionRoot = () => {
|
|
108
|
+
return (
|
|
109
|
+
<>
|
|
110
|
+
<Composition
|
|
111
|
+
id="MyComposition"
|
|
112
|
+
component={MyComposition}
|
|
113
|
+
durationInFrames={150}
|
|
114
|
+
fps={30}
|
|
115
|
+
width={1920}
|
|
116
|
+
height={1080}
|
|
117
|
+
/>
|
|
118
|
+
</>
|
|
119
|
+
);
|
|
120
|
+
};
|
|
121
|
+
`;
|
|
122
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
// fs
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
|
|
4
|
+
// s3
|
|
5
|
+
const { S3Client, PutObjectCommand } = require('@aws-sdk/client-s3');
|
|
6
|
+
const s3Client = new S3Client({
|
|
7
|
+
region: 'auto',
|
|
8
|
+
endpoint: global.QZ_CONFIG.R2_ENDPOINT,
|
|
9
|
+
credentials: {
|
|
10
|
+
accessKeyId: global.QZ_CONFIG.R2_ACCESS_KEY_ID,
|
|
11
|
+
secretAccessKey: global.QZ_CONFIG.R2_SECRET_ACCESS_KEY,
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
// logger
|
|
16
|
+
const Logger = require('qiao-log');
|
|
17
|
+
const logOptions = require('../log-options.js')();
|
|
18
|
+
const logger = Logger(logOptions);
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* 上传文件到 Cloudflare R2
|
|
22
|
+
* @param {Object} options
|
|
23
|
+
* @param {string} options.filePath - 本地文件路径
|
|
24
|
+
* @param {string} options.workId - 作品 ID
|
|
25
|
+
* @returns {Promise<string>} 文件的��开 URL
|
|
26
|
+
*/
|
|
27
|
+
exports.uploadToR2 = async ({ filePath, workId }) => {
|
|
28
|
+
const methodName = 'uploadToR2';
|
|
29
|
+
|
|
30
|
+
// const
|
|
31
|
+
const fileName = `${workId}.mp4`;
|
|
32
|
+
const fileContent = fs.readFileSync(filePath);
|
|
33
|
+
|
|
34
|
+
// command
|
|
35
|
+
const command = new PutObjectCommand({
|
|
36
|
+
Bucket: global.QZ_CONFIG.R2_BUCKET_NAME,
|
|
37
|
+
Key: fileName,
|
|
38
|
+
Body: fileContent,
|
|
39
|
+
ContentType: 'video/mp4',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
// go
|
|
43
|
+
try {
|
|
44
|
+
await s3Client.send(command);
|
|
45
|
+
|
|
46
|
+
// 返回公开 URL
|
|
47
|
+
return `${global.QZ_CONFIG.NEXT_PUBLIC_R2_PUBLIC_URL}/${fileName}`;
|
|
48
|
+
} catch (error) {
|
|
49
|
+
logger.error(methodName, 'Error uploading to R2:', error);
|
|
50
|
+
}
|
|
51
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
// fs
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
|
|
5
|
+
// renderer
|
|
6
|
+
const { renderVideo } = require('./renderer.js');
|
|
7
|
+
|
|
8
|
+
// upload
|
|
9
|
+
const { uploadToR2 } = require('./uploader.js');
|
|
10
|
+
|
|
11
|
+
// model
|
|
12
|
+
const { updateRenderStatus } = require('../model/RemotionModel.js');
|
|
13
|
+
|
|
14
|
+
// logger
|
|
15
|
+
const Logger = require('qiao-log');
|
|
16
|
+
const logOptions = require('../log-options.js')();
|
|
17
|
+
const logger = Logger(logOptions);
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* processWork
|
|
21
|
+
* @param {*} work
|
|
22
|
+
*/
|
|
23
|
+
exports.processWork = async (work) => {
|
|
24
|
+
const methodName = 'processWork';
|
|
25
|
+
|
|
26
|
+
// const
|
|
27
|
+
const workId = work.id;
|
|
28
|
+
const outputPath = path.join(global.QZ_CONFIG.OUTPUT_DIR, `${workId}.mp4`);
|
|
29
|
+
logger.info(methodName, `\n[${new Date().toISOString()}] Processing work: ${workId}`);
|
|
30
|
+
logger.info(methodName, `Title: ${work.title}`);
|
|
31
|
+
logger.info(methodName, `Status: ${work.status}, Render Status: ${work.render_status}`);
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
// 1. 渲染视频
|
|
35
|
+
logger.info(methodName, 'Rendering video...');
|
|
36
|
+
await renderVideo({
|
|
37
|
+
sourceCode: work.source_code,
|
|
38
|
+
outputPath,
|
|
39
|
+
width: work.width || 1920,
|
|
40
|
+
height: work.height || 1080,
|
|
41
|
+
fps: work.fps || 30,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
logger.info(methodName, 'Video rendered successfully');
|
|
45
|
+
|
|
46
|
+
// 2. 上传到 R2
|
|
47
|
+
logger.info(methodName, 'Uploading to R2...');
|
|
48
|
+
const videoUrl = await uploadToR2({
|
|
49
|
+
filePath: outputPath,
|
|
50
|
+
workId: workId,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
logger.info(methodName, 'Video uploaded:', videoUrl);
|
|
54
|
+
|
|
55
|
+
// 3. 获取视频信息
|
|
56
|
+
fs.statSync(outputPath);
|
|
57
|
+
const durationInFrames = Math.round(5 * (work.fps || 30)); // 暂时硬编码为5秒
|
|
58
|
+
|
|
59
|
+
// 4. 更新数据库为渲染完成
|
|
60
|
+
await updateRenderStatus(workId, 'completed', {
|
|
61
|
+
video_url: videoUrl,
|
|
62
|
+
render_completed_at: new Date().toISOString(),
|
|
63
|
+
duration_in_frames: durationInFrames,
|
|
64
|
+
duration_seconds: durationInFrames / (work.fps || 30),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
logger.info(methodName, `✅ Work ${workId} completed successfully`);
|
|
68
|
+
logger.info(methodName, ` Status: ${work.status}, Render Status: completed`);
|
|
69
|
+
|
|
70
|
+
// 5. 清理本地文件
|
|
71
|
+
fs.unlinkSync(outputPath);
|
|
72
|
+
} catch (error) {
|
|
73
|
+
logger.error(methodName, `❌ Error processing work ${workId}:`, error);
|
|
74
|
+
|
|
75
|
+
// 更新为渲染失败状态
|
|
76
|
+
await updateRenderStatus(workId, 'failed', {
|
|
77
|
+
render_error: error.message,
|
|
78
|
+
render_completed_at: new Date().toISOString(),
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
// 清理可能存在的文件
|
|
82
|
+
if (fs.existsSync(outputPath)) {
|
|
83
|
+
fs.unlinkSync(outputPath);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
package/server/1
DELETED
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
require('dotenv').config();
|
|
2
|
-
const { renderVideo } = require('./renderer');
|
|
3
|
-
const { uploadToR2 } = require('./uploader');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const path = require('path');
|
|
6
|
-
|
|
7
|
-
// 配置
|
|
8
|
-
const CONFIG = {
|
|
9
|
-
POLL_INTERVAL: 60 * 1000, // 60秒检查一次
|
|
10
|
-
MAX_CONCURRENT: 1, // 同时只渲染1个视频
|
|
11
|
-
OUTPUT_DIR: path.join(__dirname, 'output'),
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
// 确保输出目录存在
|
|
15
|
-
if (!fs.existsSync(CONFIG.OUTPUT_DIR)) {
|
|
16
|
-
fs.mkdirSync(CONFIG.OUTPUT_DIR, { recursive: true });
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
let isProcessing = false;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* 更新作品状态
|
|
23
|
-
*/
|
|
24
|
-
async function updateWorkStatus(workId, status, updates = {}) {
|
|
25
|
-
try {
|
|
26
|
-
const { error } = await supabase
|
|
27
|
-
.from('works')
|
|
28
|
-
.update({
|
|
29
|
-
status,
|
|
30
|
-
updated_at: new Date().toISOString(),
|
|
31
|
-
...updates,
|
|
32
|
-
})
|
|
33
|
-
.eq('id', workId);
|
|
34
|
-
|
|
35
|
-
if (error) {
|
|
36
|
-
console.error(`Error updating work ${workId} to ${status}:`, error);
|
|
37
|
-
}
|
|
38
|
-
} catch (error) {
|
|
39
|
-
console.error('Unexpected error updating work status:', error);
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
/**
|
|
44
|
-
* 处理单个作品的渲染
|
|
45
|
-
*/
|
|
46
|
-
async function processWork(work) {
|
|
47
|
-
const workId = work.id;
|
|
48
|
-
const outputPath = path.join(CONFIG.OUTPUT_DIR, `${workId}.mp4`);
|
|
49
|
-
|
|
50
|
-
console.log(`\n[${new Date().toISOString()}] Processing work: ${workId}`);
|
|
51
|
-
console.log(`Title: ${work.title}`);
|
|
52
|
-
|
|
53
|
-
try {
|
|
54
|
-
// 1. 更新状态为 rendering
|
|
55
|
-
await updateWorkStatus(workId, 'rendering', {
|
|
56
|
-
render_started_at: new Date().toISOString(),
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
// 2. 渲染视频
|
|
60
|
-
console.log('Rendering video...');
|
|
61
|
-
await renderVideo({
|
|
62
|
-
sourceCode: work.source_code,
|
|
63
|
-
outputPath,
|
|
64
|
-
width: work.width || 1920,
|
|
65
|
-
height: work.height || 1080,
|
|
66
|
-
fps: work.fps || 30,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
console.log('Video rendered successfully');
|
|
70
|
-
|
|
71
|
-
// 3. 上传到 R2
|
|
72
|
-
console.log('Uploading to R2...');
|
|
73
|
-
const videoUrl = await uploadToR2({
|
|
74
|
-
filePath: outputPath,
|
|
75
|
-
workId: workId,
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
console.log('Video uploaded:', videoUrl);
|
|
79
|
-
|
|
80
|
-
// 4. 获取视频信息
|
|
81
|
-
const stats = fs.statSync(outputPath);
|
|
82
|
-
const durationInFrames = Math.round(5 * (work.fps || 30)); // 暂时硬编码为5秒
|
|
83
|
-
|
|
84
|
-
// 5. 更新数据库
|
|
85
|
-
await updateWorkStatus(workId, 'published', {
|
|
86
|
-
video_url: videoUrl,
|
|
87
|
-
render_completed_at: new Date().toISOString(),
|
|
88
|
-
published_at: new Date().toISOString(),
|
|
89
|
-
duration_in_frames: durationInFrames,
|
|
90
|
-
duration_seconds: durationInFrames / (work.fps || 30),
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
console.log(`✅ Work ${workId} completed successfully`);
|
|
94
|
-
|
|
95
|
-
// 6. 清理本地文件
|
|
96
|
-
fs.unlinkSync(outputPath);
|
|
97
|
-
} catch (error) {
|
|
98
|
-
console.error(`❌ Error processing work ${workId}:`, error);
|
|
99
|
-
|
|
100
|
-
// 更新为失败状态
|
|
101
|
-
await updateWorkStatus(workId, 'render_failed', {
|
|
102
|
-
render_error: error.message,
|
|
103
|
-
render_completed_at: new Date().toISOString(),
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
// 清理可能存在的文件
|
|
107
|
-
if (fs.existsSync(outputPath)) {
|
|
108
|
-
fs.unlinkSync(outputPath);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* 主处理循环
|
|
115
|
-
*/
|
|
116
|
-
async function processQueue() {
|
|
117
|
-
if (isProcessing) {
|
|
118
|
-
console.log('Already processing, skipping...');
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
isProcessing = true;
|
|
123
|
-
|
|
124
|
-
try {
|
|
125
|
-
const works = await fetchPendingWorks();
|
|
126
|
-
|
|
127
|
-
if (works.length === 0) {
|
|
128
|
-
console.log(`[${new Date().toISOString()}] No pending works`);
|
|
129
|
-
} else {
|
|
130
|
-
console.log(`[${new Date().toISOString()}] Found ${works.length} pending work(s)`);
|
|
131
|
-
|
|
132
|
-
for (const work of works) {
|
|
133
|
-
await processWork(work);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
} catch (error) {
|
|
137
|
-
console.error('Error in process queue:', error);
|
|
138
|
-
} finally {
|
|
139
|
-
isProcessing = false;
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
/**
|
|
144
|
-
* 启动 Worker
|
|
145
|
-
*/
|
|
146
|
-
async function start() {
|
|
147
|
-
console.log('='.repeat(60));
|
|
148
|
-
console.log('Remotion Render Worker Started');
|
|
149
|
-
console.log('='.repeat(60));
|
|
150
|
-
console.log(`Poll Interval: ${CONFIG.POLL_INTERVAL / 1000}s`);
|
|
151
|
-
console.log(`Max Concurrent: ${CONFIG.MAX_CONCURRENT}`);
|
|
152
|
-
console.log(`Output Directory: ${CONFIG.OUTPUT_DIR}`);
|
|
153
|
-
console.log('='.repeat(60));
|
|
154
|
-
|
|
155
|
-
// 立即执行一次
|
|
156
|
-
await processQueue();
|
|
157
|
-
|
|
158
|
-
// 定期执行
|
|
159
|
-
setInterval(processQueue, CONFIG.POLL_INTERVAL);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// 优雅退出
|
|
163
|
-
process.on('SIGINT', () => {
|
|
164
|
-
console.log('\nReceived SIGINT, shutting down gracefully...');
|
|
165
|
-
process.exit(0);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
process.on('SIGTERM', () => {
|
|
169
|
-
console.log('\nReceived SIGTERM, shutting down gracefully...');
|
|
170
|
-
process.exit(0);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
// 启动
|
|
174
|
-
start().catch((error) => {
|
|
175
|
-
console.error('Failed to start worker:', error);
|
|
176
|
-
process.exit(1);
|
|
177
|
-
});
|