@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shun-js/remotion-server",
3
- "version": "0.6.3",
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": "57002ceae251dc249f02b53238df64377194928d"
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
- // 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
+ };
@@ -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
- });