@shun-js/remotion-server 0.6.3 → 0.6.5

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shun-js/remotion-server",
3
- "version": "0.6.3",
3
+ "version": "0.6.5",
4
4
  "description": "remotion.cool server",
5
5
  "license": "MIT",
6
6
  "author": "uikoo9 <uikoo9@qq.com>",
@@ -17,10 +17,12 @@
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",
23
- "qiao-file": "^5.0.6",
24
26
  "qiao-log": "^5.1.9",
25
27
  "qiao-timer": "^5.8.4",
26
28
  "qiao-z": "^5.8.9"
@@ -29,5 +31,5 @@
29
31
  "access": "public",
30
32
  "registry": "https://registry.npmjs.org/"
31
33
  },
32
- "gitHead": "57002ceae251dc249f02b53238df64377194928d"
34
+ "gitHead": "5e673cd548dc7ab1b20c57750e3f541fe4fa68cc"
33
35
  }
@@ -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,31 @@
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
+ if (!rows || !rows.length) return;
24
+
25
+ // work
26
+ const work = rows[0];
27
+ await processWork(work);
28
+
29
+ // end
30
+ logger.info(methodName, 'end');
31
+ };
@@ -1,5 +1,5 @@
1
- // model
2
- const { fetchPendingWorks } = require('../model/RemotionModel.js');
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
- // gen remotion video
17
- async function genRemotionVideo() {
18
- const methodName = 'genRemotionVideo';
13
+ // tick
14
+ let start = false;
15
+ exports.tick = async () => {
16
+ logger.info('start', start);
17
+ if (start) return;
19
18
 
20
- // go
21
- logger.info(methodName, 'start');
19
+ start = true;
20
+ logger.info('start', start);
22
21
 
23
- const rows = await fetchPendingWorks();
24
- console.log(rows);
22
+ await genVideo();
25
23
 
26
- // end
27
- logger.info(methodName, 'end');
28
- }
24
+ start = false;
25
+ logger.info('start', start);
26
+ };
@@ -15,63 +15,11 @@ exports.feishuMsg = (msg) => {
15
15
  });
16
16
  };
17
17
 
18
- // is bot
19
- function isBot(req) {
20
- const ua = req.useragent;
21
- const hasOSName = ua && ua.os && ua.os.name;
22
- const isGoogleBot = ua && ua.browser && ua.browser.name === 'Googlebot';
23
- const isMetaBot = ua && ua.browser && ua.browser.name === 'meta-externalagent';
24
- const isSafariBot = ua && ua.engine && ua.engine.name === 'WebKit' && ua.engine.version === '605.1.15';
25
- return !hasOSName || isGoogleBot || isMetaBot || isSafariBot;
26
- }
27
-
28
18
  /**
29
19
  * errorFeishuMsg
30
- * @param {*} req
31
20
  * @param {*} msg
32
21
  * @returns
33
22
  */
34
- exports.errorFeishuMsg = (req, msg) => {
35
- // check
36
- if (isBot(req)) return;
37
-
38
- // msg
39
- const uaJson = JSON.stringify(req.useragent || {});
40
- exports.feishuMsg(`【通知】服务异常,${msg},请查看日志,ua:${uaJson}`);
41
- };
42
-
43
- /**
44
- * chatFeishuMsg
45
- * @param {*} req
46
- * @returns
47
- */
48
- exports.chatFeishuMsg = (req) => {
49
- // check
50
- if (isBot(req)) return;
51
-
52
- // msg
53
- const uaJson = JSON.stringify(req.useragent || {});
54
- const userid = req.headers.userid;
55
- const prompt = decodeURIComponent(req.body.userPrompt);
56
-
57
- const msg = `【通知】/chat被访问\nuserid:${userid}\nua:\n${uaJson}\nprompt:\n${prompt}`;
58
- exports.feishuMsg(msg);
59
- };
60
-
61
- /**
62
- * chatResFeishuMsg
63
- * @param {*} req
64
- * @returns
65
- */
66
- exports.chatResFeishuMsg = (req, chatRes) => {
67
- // check
68
- if (isBot(req)) return;
69
-
70
- // msg
71
- const uaJson = JSON.stringify(req.useragent || {});
72
- const userid = req.headers.userid;
73
- const prompt = decodeURIComponent(req.body.userPrompt);
74
-
75
- const msg = `【通知】/chat生成成功\nuserid:${userid}\nua:\n${uaJson}\nprompt:\n${prompt}\nres:${chatRes}`;
76
- exports.feishuMsg(msg);
23
+ exports.errorFeishuMsg = (msg) => {
24
+ exports.feishuMsg(`【通知】服务异常,${msg},请查看日志。`);
77
25
  };
@@ -0,0 +1,134 @@
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('/tmp', 'remotion-render', 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
+ 'react-dom': '^18.2.0',
45
+ remotion: '^4.0.0',
46
+ },
47
+ };
48
+ fs.writeFileSync(path.join(tempDir, 'package.json'), JSON.stringify(packageJson, null, 2));
49
+
50
+ // 4. 创建 remotion.config.js (可选但推荐)
51
+ const remotionConfig = `
52
+ module.exports = {
53
+ // Remotion 配置
54
+ };
55
+ `;
56
+ fs.writeFileSync(path.join(tempDir, 'remotion.config.js'), remotionConfig);
57
+
58
+ // 5. Bundle 代码
59
+ logger.info(methodName, 'Bundling code...');
60
+ const bundleLocation = await bundle({
61
+ entryPoint,
62
+ webpackOverride: (config) => config,
63
+ });
64
+
65
+ // 6. 获取 composition
66
+ logger.info(methodName, 'Getting composition...');
67
+ const composition = await selectComposition({
68
+ serveUrl: bundleLocation,
69
+ id: 'MyComposition', // 默认 composition ID
70
+ inputProps: {},
71
+ });
72
+
73
+ // 7. 渲染视频
74
+ logger.info(methodName, 'Rendering frames...');
75
+ await renderMedia({
76
+ composition: {
77
+ ...composition,
78
+ width: width,
79
+ height: height,
80
+ fps: fps,
81
+ durationInFrames: composition.durationInFrames || 150, // 默认5秒
82
+ },
83
+ serveUrl: bundleLocation,
84
+ codec: 'h264',
85
+ outputLocation: outputPath,
86
+ inputProps: {},
87
+ onProgress: ({ progress }) => {
88
+ if (progress % 0.1 < 0.01) {
89
+ // 每10%打印一次
90
+ logger.info(`Render progress: ${(progress * 100).toFixed(1)}%`);
91
+ }
92
+ },
93
+ });
94
+
95
+ logger.info(methodName, 'Render completed');
96
+ } finally {
97
+ // 清理临时目录
98
+ if (fs.existsSync(tempDir)) {
99
+ fs.rmSync(tempDir, { recursive: true, force: true });
100
+ }
101
+ }
102
+ };
103
+
104
+ /**
105
+ * 包装用户代码为 Remotion 项目
106
+ */
107
+ function wrapUserCode(sourceCode) {
108
+ return `
109
+ import React from 'react';
110
+ import { Composition, registerRoot, AbsoluteFill, useCurrentFrame, interpolate, useVideoConfig } from 'remotion';
111
+
112
+ // 用户代码
113
+ ${sourceCode}
114
+
115
+ // Root 组件 - Remotion 入口
116
+ const RemotionRoot = () => {
117
+ return (
118
+ <>
119
+ <Composition
120
+ id="MyComposition"
121
+ component={MyComposition}
122
+ durationInFrames={150}
123
+ fps={30}
124
+ width={1920}
125
+ height={1080}
126
+ />
127
+ </>
128
+ );
129
+ };
130
+
131
+ // 注册 Root 组件(Remotion 4.x 要求)
132
+ registerRoot(RemotionRoot);
133
+ `;
134
+ }
@@ -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,92 @@
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
+ // feishu
15
+ const { feishuMsg, errorFeishuMsg } = require('../util/feishu.js');
16
+
17
+ // logger
18
+ const Logger = require('qiao-log');
19
+ const logOptions = require('../log-options.js')();
20
+ const logger = Logger(logOptions);
21
+
22
+ /**
23
+ * processWork
24
+ * @param {*} work
25
+ */
26
+ exports.processWork = async (work) => {
27
+ const methodName = 'processWork';
28
+
29
+ // const
30
+ const workId = work.id;
31
+ const outputPath = path.join(global.QZ_CONFIG.OUTPUT_DIR, `${workId}.mp4`);
32
+ feishuMsg(`${workId} start`);
33
+ logger.info(methodName, `\n[${new Date().toISOString()}] Processing work: ${workId}`);
34
+ logger.info(methodName, `Title: ${work.title}`);
35
+ logger.info(methodName, `Status: ${work.status}, Render Status: ${work.render_status}`);
36
+
37
+ try {
38
+ // 1. 渲染视频
39
+ logger.info(methodName, 'Rendering video...');
40
+ await renderVideo({
41
+ sourceCode: work.source_code,
42
+ outputPath,
43
+ width: work.width || 1920,
44
+ height: work.height || 1080,
45
+ fps: work.fps || 30,
46
+ });
47
+ feishuMsg(`${workId} render ok`);
48
+ logger.info(methodName, 'Video rendered successfully');
49
+
50
+ // 2. 上传到 R2
51
+ logger.info(methodName, 'Uploading to R2...');
52
+ const videoUrl = await uploadToR2({
53
+ filePath: outputPath,
54
+ workId: workId,
55
+ });
56
+ feishuMsg(`${workId} upload ok, ${videoUrl}`);
57
+ logger.info(methodName, 'Video uploaded:', videoUrl);
58
+
59
+ // 3. 获取视频信息
60
+ fs.statSync(outputPath);
61
+ const durationInFrames = Math.round(5 * (work.fps || 30)); // 暂时硬编码为5秒
62
+
63
+ // 4. 更新数据库为渲染完成
64
+ await updateRenderStatus(workId, 'completed', {
65
+ video_url: videoUrl,
66
+ render_completed_at: new Date().toISOString(),
67
+ duration_in_frames: durationInFrames,
68
+ duration_seconds: durationInFrames / (work.fps || 30),
69
+ });
70
+ feishuMsg(`${workId} db ok`);
71
+ logger.info(methodName, `✅ Work ${workId} completed successfully`);
72
+ logger.info(methodName, ` Status: ${work.status}, Render Status: completed`);
73
+
74
+ // 5. 清理本地文件
75
+ fs.unlinkSync(outputPath);
76
+ feishuMsg(`${workId} clear ok`);
77
+ } catch (error) {
78
+ errorFeishuMsg(`${workId} \n ${error}`);
79
+ logger.error(methodName, `❌ Error processing work ${workId}:`, error);
80
+
81
+ // 更新为渲染失败状态
82
+ await updateRenderStatus(workId, 'failed', {
83
+ render_error: error.message,
84
+ render_completed_at: new Date().toISOString(),
85
+ });
86
+
87
+ // 清理可能存在的文件
88
+ if (fs.existsSync(outputPath)) {
89
+ fs.unlinkSync(outputPath);
90
+ }
91
+ }
92
+ };
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
- });