@twick/cloud-export-video 0.14.10 → 0.14.12

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.
@@ -77,7 +77,9 @@ Examples:
77
77
  const image = rest[0];
78
78
  const dir = rest[1] || 'twick-export-video-aws';
79
79
  if (!image) throw new Error('Image name required. e.g., my-repo:latest');
80
- await run('docker', ['build', '-t', image, dir]);
80
+ // Build for linux/amd64 platform to avoid creating multi-arch manifest index
81
+ // This reduces the number of artifacts pushed to the registry
82
+ await run('docker', ['build', '--platform', 'linux/amd64', '-t', image, dir]);
81
83
  return;
82
84
  }
83
85
 
package/core/renderer.js CHANGED
@@ -37,8 +37,8 @@ const renderTwickVideo = async (variables, settings) => {
37
37
  name: "@twick/core/wasm",
38
38
  },
39
39
  size: {
40
- x: variables.properties.width,
41
- y: variables.properties.height,
40
+ x: variables.input.properties.width,
41
+ y: variables.input.properties.height,
42
42
  },
43
43
  },
44
44
  puppeteer: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@twick/cloud-export-video",
3
- "version": "0.14.10",
3
+ "version": "0.14.12",
4
4
  "description": "Twick cloud function for exporting video with platform-specific templates (AWS Lambda container)",
5
5
  "type": "module",
6
6
  "main": "core/renderer.js",
@@ -43,13 +43,14 @@
43
43
  },
44
44
  "dependencies": {
45
45
  "@sparticuz/chromium": "^129.0.0",
46
- "@twick/2d": "0.14.10",
47
- "@twick/core": "0.14.10",
48
- "@twick/ffmpeg": "0.14.10",
49
- "@twick/renderer": "0.14.10",
50
- "@twick/ui": "0.14.10",
51
- "@twick/vite-plugin": "0.14.10",
52
- "@twick/visualizer": "0.14.10",
46
+ "@twick/2d": "0.14.12",
47
+ "@twick/core": "0.14.12",
48
+ "@twick/ffmpeg": "0.14.12",
49
+ "@twick/renderer": "0.14.12",
50
+ "@twick/ui": "0.14.12",
51
+ "@twick/vite-plugin": "0.14.12",
52
+ "@twick/visualizer": "0.14.12",
53
+ "@aws-sdk/client-s3": "^3.620.0",
53
54
  "ffmpeg-static": "^5.2.0",
54
55
  "fluent-ffmpeg": "^2.1.3"
55
56
  },
@@ -1,10 +1,21 @@
1
1
  FROM docker.io/revideo/aws-lambda-base-image:latest
2
2
 
3
+ ARG TWICK_DIST_TAG=latest
4
+
3
5
  # Copy package files for better caching
4
6
  COPY package.json package-lock.json* ./
5
7
 
6
8
  RUN npm install
7
9
 
10
+ RUN npm install --no-save \
11
+ "@twick/2d@${TWICK_DIST_TAG}" \
12
+ "@twick/core@${TWICK_DIST_TAG}" \
13
+ "@twick/ffmpeg@${TWICK_DIST_TAG}" \
14
+ "@twick/renderer@${TWICK_DIST_TAG}" \
15
+ "@twick/ui@${TWICK_DIST_TAG}" \
16
+ "@twick/vite-plugin@${TWICK_DIST_TAG}" \
17
+ "@twick/visualizer@${TWICK_DIST_TAG}"
18
+
8
19
  RUN npx puppeteer browsers install chrome
9
20
 
10
21
  # Copy source code
@@ -1,8 +1,89 @@
1
+ import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
1
2
  import renderTwickVideo from '../../core/renderer.js';
2
3
 
4
+ const s3Client = new S3Client({
5
+ region: process.env.EXPORT_VIDEO_S3_REGION || process.env.AWS_REGION || 'us-east-1',
6
+ endpoint: process.env.EXPORT_VIDEO_S3_ENDPOINT || undefined,
7
+ forcePathStyle: process.env.EXPORT_VIDEO_S3_FORCE_PATH_STYLE === 'true',
8
+ });
9
+
10
+ const ensureEnvVar = (name) => {
11
+ const value = process.env[name];
12
+ if (!value) {
13
+ throw new Error(`Missing required environment variable: ${name}`);
14
+ }
15
+ return value;
16
+ };
17
+
18
+ const normalizePrefix = (prefix = '') => {
19
+ if (!prefix) {
20
+ return '';
21
+ }
22
+ return prefix.endsWith('/') ? prefix : `${prefix}/`;
23
+ };
24
+
25
+ const sanitizeIdentifier = (value) => {
26
+ if (!value) {
27
+ return 'twick-video';
28
+ }
29
+
30
+ let sanitized = String(value)
31
+ .trim()
32
+ .toLowerCase()
33
+ .replace(/[^a-z0-9-_]+/g, '-')
34
+ .replace(/-{2,}/g, '-');
35
+
36
+ // Remove leading and trailing dashes using string methods (avoids ReDoS)
37
+ // Find first non-dash character
38
+ let start = 0;
39
+ while (start < sanitized.length && sanitized[start] === '-') {
40
+ start++;
41
+ }
42
+ // Find last non-dash character
43
+ let end = sanitized.length;
44
+ while (end > start && sanitized[end - 1] === '-') {
45
+ end--;
46
+ }
47
+
48
+ return sanitized.slice(start, end);
49
+ };
50
+
51
+ const buildObjectKey = (projectData, uniqueSuffix) => {
52
+ const projectIdentifier =
53
+ projectData?.properties?.id ??
54
+ projectData?.id ??
55
+ projectData?.project?.properties?.id;
56
+
57
+ const baseName = sanitizeIdentifier(projectIdentifier);
58
+ const suffix = uniqueSuffix ?? Date.now();
59
+
60
+ return `${baseName}-${suffix}.mp4`;
61
+ };
62
+
63
+ const encodeS3Key = (key) =>
64
+ key
65
+ .split('/')
66
+ .map((segment) => encodeURIComponent(segment))
67
+ .join('/');
68
+
69
+ const buildPublicUrl = ({ bucket, key, region, baseUrl }) => {
70
+ if (baseUrl) {
71
+ const trimmedBase = baseUrl.endsWith('/') ? baseUrl.slice(0, -1) : baseUrl;
72
+ return `${trimmedBase}/${encodeS3Key(key)}`;
73
+ }
74
+
75
+ const encodedKey = encodeS3Key(key);
76
+
77
+ if (!region || region === 'us-east-1') {
78
+ return `https://${bucket}.s3.amazonaws.com/${encodedKey}`;
79
+ }
80
+
81
+ return `https://${bucket}.s3.${region}.amazonaws.com/${encodedKey}`;
82
+ };
83
+
3
84
  /**
4
85
  * Handler for processing video project data with files
5
- *
86
+ *
6
87
  * Expected JSON payload:
7
88
  * {
8
89
  * "project": { ... }, // Video project JSON object
@@ -15,13 +96,21 @@ import renderTwickVideo from '../../core/renderer.js';
15
96
  * ]
16
97
  * }
17
98
  *
18
- * Returns: Processing result with video file
99
+ * Environment variables:
100
+ * - EXPORT_VIDEO_S3_BUCKET (required): Destination S3 bucket for rendered videos
101
+ * - EXPORT_VIDEO_S3_PREFIX (optional): Key prefix to prepend before the generated object key
102
+ * - EXPORT_VIDEO_S3_REGION (optional): Region for the S3 client (defaults to AWS_REGION or us-east-1)
103
+ * - EXPORT_VIDEO_PUBLIC_BASE_URL (optional): Custom base URL for returned video links
104
+ * - EXPORT_VIDEO_S3_ENDPOINT (optional): Custom endpoint for S3-compatible storage
105
+ * - EXPORT_VIDEO_S3_FORCE_PATH_STYLE (optional): Set to "true" to force path-style URLs
106
+ *
107
+ * Returns: JSON payload containing the uploaded video URL and metadata
19
108
  */
20
109
 
21
110
  export const handler = async (event) => {
22
111
  console.log('Video processor function invoked');
23
112
  console.log('Event:', JSON.stringify(event));
24
- const projectData = event.arguments?.input || {};
113
+ const projectData = event.arguments || {};
25
114
 
26
115
  try {
27
116
  // Validate required fields
@@ -56,37 +145,28 @@ export const handler = async (event) => {
56
145
  // Render the video using Twick renderer
57
146
  console.log('Starting video rendering...' );
58
147
 
148
+ const fs = await import('fs');
59
149
  let renderedVideoPath;
60
150
  let videoBuffer;
61
151
 
62
152
  try {
63
153
  // Render the video
64
154
  renderedVideoPath = await renderTwickVideo(projectData, {
65
- outFile: `video-${projectData.properties?.id || Date.now()}.mp4`,
155
+ outFile: `video-${projectData.input?.properties?.requestId || `video-${Date.now()}`}.mp4`,
66
156
  });
67
157
 
68
158
  // Read the rendered video file
69
- const fs = await import('fs');
70
159
  videoBuffer = fs.readFileSync(renderedVideoPath);
71
160
 
72
161
  console.log('Video rendered successfully:', renderedVideoPath);
73
162
  console.log('Video size:', videoBuffer.length, 'bytes');
74
-
75
- // Clean up the temporary file
76
- try {
77
- fs.unlinkSync(renderedVideoPath);
78
- console.log('Temporary video file cleaned up');
79
- } catch (cleanupError) {
80
- console.warn('Failed to clean up temporary file:', cleanupError);
81
- }
82
-
83
163
  } catch (renderError) {
84
164
  console.error('Video rendering failed:', renderError);
85
165
 
86
166
  // Fallback to text file if rendering fails
87
167
  const errorText = `Video Processing Error
88
168
  ======================
89
- Request ID: ${projectData.properties?.id || 'N/A'}
169
+ Request ID: ${projectData.input?.properties?.requestId || `video-${Date.now()}`}
90
170
  Timestamp: ${new Date().toISOString()}
91
171
  Status: Rendering Failed
92
172
 
@@ -107,19 +187,93 @@ ${mediaFiles.map((file, index) => ` ${index + 1}. ${file.filename} (${file.data
107
187
  };
108
188
  }
109
189
 
110
- // Return the video file
190
+ const bucket = ensureEnvVar('EXPORT_VIDEO_S3_BUCKET');
191
+ const prefix = normalizePrefix(process.env.EXPORT_VIDEO_S3_PREFIX || '');
192
+ const region = process.env.EXPORT_VIDEO_S3_REGION || process.env.AWS_REGION || 'us-east-1';
193
+ const baseUrl = process.env.EXPORT_VIDEO_PUBLIC_BASE_URL;
194
+ const uniqueSuffix = Date.now();
195
+ const objectKey = `${prefix}${buildObjectKey(projectData, uniqueSuffix)}`;
196
+
197
+ console.log('Uploading rendered video to S3...', {
198
+ bucket,
199
+ key: objectKey,
200
+ region,
201
+ size: videoBuffer.length,
202
+ baseUrl: baseUrl ? '[redacted]' : undefined,
203
+ });
204
+
205
+ try {
206
+ await s3Client.send(
207
+ new PutObjectCommand({
208
+ Bucket: bucket,
209
+ Key: objectKey,
210
+ Body: videoBuffer,
211
+ ContentType: 'video/mp4',
212
+ ContentLength: videoBuffer.length,
213
+ Metadata: {
214
+ 'project-id': String(
215
+ projectData?.input?.properties?.requestId ??
216
+ projectData?.input?.properties?.requestId ??
217
+ `video-${Date.now()}`
218
+ ),
219
+ },
220
+ })
221
+ );
222
+ console.log('Video uploaded to S3 successfully');
223
+ } catch (uploadError) {
224
+ console.error('Video upload failed:', uploadError);
225
+
226
+ try {
227
+ if (renderedVideoPath) {
228
+ fs.unlinkSync(renderedVideoPath);
229
+ }
230
+ } catch (cleanupError) {
231
+ console.warn('Failed to clean up temporary file after upload failure:', cleanupError);
232
+ }
233
+
234
+ return {
235
+ statusCode: 500,
236
+ headers: {
237
+ 'Content-Type': 'application/json',
238
+ 'Access-Control-Allow-Origin': '*',
239
+ 'Access-Control-Allow-Headers': 'Content-Type',
240
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
241
+ },
242
+ body: JSON.stringify({
243
+ error: 'Failed to upload rendered video',
244
+ message: uploadError instanceof Error ? uploadError.message : 'Unknown error',
245
+ }),
246
+ };
247
+ }
248
+
249
+ try {
250
+ if (renderedVideoPath) {
251
+ fs.unlinkSync(renderedVideoPath);
252
+ console.log('Temporary video file cleaned up');
253
+ }
254
+ } catch (cleanupError) {
255
+ console.warn('Failed to clean up temporary file:', cleanupError);
256
+ }
257
+
258
+ const videoUrl = buildPublicUrl({ bucket, key: objectKey, region, baseUrl });
259
+ console.log('Video available at URL:', videoUrl);
260
+
261
+ // Return the video file metadata with URL
111
262
  return {
112
263
  statusCode: 200,
113
264
  headers: {
114
- 'Content-Type': 'video/mp4',
115
- 'Content-Disposition': `attachment; filename="processed-video.mp4"`,
116
- 'Content-Length': videoBuffer.length,
265
+ 'Content-Type': 'application/json',
117
266
  'Access-Control-Allow-Origin': '*',
118
267
  'Access-Control-Allow-Headers': 'Content-Type',
119
268
  'Access-Control-Allow-Methods': 'POST, OPTIONS',
120
269
  },
121
- body: videoBuffer.toString('base64'),
122
- isBase64Encoded: true,
270
+ body: JSON.stringify({
271
+ url: videoUrl,
272
+ bucket,
273
+ key: objectKey,
274
+ size: videoBuffer.length,
275
+ contentType: 'video/mp4',
276
+ }),
123
277
  };
124
278
  } catch (error) {
125
279
  console.error('Error processing video project:', error);