@twick/cloud-export-video 0.14.8

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/README.md ADDED
@@ -0,0 +1,56 @@
1
+ ## @twick/cloud-export-video
2
+
3
+ Reusable cloud-function package for exporting Twick videos. Includes a core renderer and platform templates for AWS Lambda container images.
4
+
5
+ ### Install
6
+
7
+ ```bash
8
+ npm install -D @twick/cloud-export-video
9
+ ```
10
+
11
+ ### CLI
12
+
13
+ ```bash
14
+ npx twick-export-video help
15
+ ```
16
+
17
+ Commands:
18
+
19
+ - `init [dir]`: Scaffold AWS container template (Dockerfile + handler) into `[dir]` (default `./twick-export-video-aws`). Also writes a minimal `package.json` that depends on this package.
20
+ - `build <image> [dir]`: Build a Docker image from `[dir]` (default `./twick-export-video-aws`).
21
+ - `ecr-login <region> <accountId>`: Log in Docker to your AWS ECR registry.
22
+ - `push <image> <region> <accountId>`: Tag and push the image to ECR. The repository must already exist.
23
+
24
+ ### Typical flow
25
+
26
+ ```bash
27
+ # 1) Scaffold
28
+ npx twick-export-video init
29
+
30
+ # 2) Build an image
31
+ npx twick-export-video build twick-export-video:latest
32
+
33
+ # 3) Login to ECR
34
+ npx twick-export-video ecr-login us-east-1 123456789012
35
+
36
+ # 4) Push (assumes an ECR repo named `twick-export-video` exists)
37
+ npx twick-export-video push twick-export-video:latest us-east-1 123456789012
38
+ ```
39
+
40
+ ### AWS Lambda (container) notes
41
+
42
+ - The Dockerfile is based on `revideo/aws-lambda-base-image` and prepares Chromium and ffmpeg for headless rendering.
43
+ - The handler expects an `event.arguments.input` payload with `{ project, mediaFiles? }`.
44
+ - The response is a `video/mp4` base64 body, or a text file on error.
45
+
46
+ ### Programmatic usage
47
+
48
+ The core renderer is exported at `core/renderer.js`:
49
+
50
+ ```js
51
+ import renderTwickVideo from '@twick/cloud-export-video/core/renderer.js';
52
+
53
+ const resultPath = await renderTwickVideo(project, { outFile: 'my.mp4' });
54
+ ```
55
+
56
+
@@ -0,0 +1,115 @@
1
+ #!/usr/bin/env node
2
+ import { fileURLToPath } from 'url';
3
+ import { dirname, join } from 'path';
4
+ import fs from 'fs';
5
+ import { spawn } from 'child_process';
6
+
7
+ const __filename = fileURLToPath(import.meta.url);
8
+ const __dirname = dirname(__filename);
9
+ const pkgRoot = join(__dirname, '..');
10
+
11
+ function copyTemplate(destDir) {
12
+ const templateDir = join(pkgRoot, 'platform', 'aws');
13
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
14
+
15
+ for (const name of ['Dockerfile', 'handler.js']) {
16
+ const src = join(templateDir, name);
17
+ const dest = join(destDir, name);
18
+ fs.copyFileSync(src, dest);
19
+ }
20
+
21
+ // Minimal package.json to enable docker layer caching (npm ci)
22
+ const pkgJsonPath = join(destDir, 'package.json');
23
+ if (!fs.existsSync(pkgJsonPath)) {
24
+ const pkg = {
25
+ name: 'twick-export-video-runtime',
26
+ private: true,
27
+ type: 'module',
28
+ dependencies: {
29
+ '@twick/cloud-export-video': 'latest',
30
+ puppeteer: '^22.8.0',
31
+ '@ffmpeg-installer/ffmpeg': '^1.1.0'
32
+ }
33
+ };
34
+ fs.writeFileSync(pkgJsonPath, JSON.stringify(pkg, null, 2));
35
+ }
36
+ }
37
+
38
+ function run(cmd, args, opts = {}) {
39
+ return new Promise((resolve, reject) => {
40
+ const ps = typeof cmd === 'string' && Array.isArray(args) && args.length === 0
41
+ ? spawn(cmd, { stdio: 'inherit', shell: true, ...opts })
42
+ : spawn(cmd, args, { stdio: 'inherit', shell: true, ...opts });
43
+ ps.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`${cmd} exited ${code}`))));
44
+ });
45
+ }
46
+
47
+ async function main() {
48
+ const [command, ...rest] = process.argv.slice(2);
49
+
50
+ if (!command || ['-h', '--help', 'help'].includes(command)) {
51
+ console.log(`
52
+ Usage: twick-export-video <command> [options]
53
+
54
+ Commands:
55
+ init [dir] Scaffold AWS container template into [dir] (default: ./twick-export-video-aws)
56
+ build <image> [dir] Docker build image from [dir] (default: ./twick-export-video-aws)
57
+ ecr-login <region> <accountId> Login docker to ECR
58
+ push <image> <region> <accountId> Push image to ECR (repo must exist)
59
+
60
+ Examples:
61
+ twick-export-video init
62
+ twick-export-video build my-repo:latest
63
+ twick-export-video ecr-login us-east-1 123456789012
64
+ twick-export-video push my-repo:latest us-east-1 123456789012
65
+ `);
66
+ return;
67
+ }
68
+
69
+ if (command === 'init') {
70
+ const dir = rest[0] || 'twick-export-video-aws';
71
+ copyTemplate(dir);
72
+ console.log(`✔ Scaffolded AWS runtime into ./${dir}`);
73
+ return;
74
+ }
75
+
76
+ if (command === 'build') {
77
+ const image = rest[0];
78
+ const dir = rest[1] || 'twick-export-video-aws';
79
+ if (!image) throw new Error('Image name required. e.g., my-repo:latest');
80
+ await run('docker', ['build', '-t', image, dir]);
81
+ return;
82
+ }
83
+
84
+ if (command === 'ecr-login') {
85
+ const region = rest[0];
86
+ const accountId = rest[1];
87
+ if (!region || !accountId) throw new Error('Usage: ecr-login <region> <accountId>');
88
+ const registry = `${accountId}.dkr.ecr.${region}.amazonaws.com`;
89
+ await run(`aws ecr get-login-password --region ${region} | docker login --username AWS --password-stdin ${registry}`, []);
90
+ return;
91
+ }
92
+
93
+ if (command === 'push') {
94
+ const image = rest[0];
95
+ const region = rest[1];
96
+ const accountId = rest[2];
97
+ if (!image || !region || !accountId) throw new Error('Usage: push <image> <region> <accountId>');
98
+ const [repo, tag = 'latest'] = image.split(':');
99
+ const registry = `${accountId}.dkr.ecr.${region}.amazonaws.com`;
100
+ const remote = `${registry}/${repo}:${tag}`;
101
+ await run('docker', ['tag', `${repo}:${tag}`, remote]);
102
+ await run('docker', ['push', remote]);
103
+ console.log(`✔ Pushed ${remote}`);
104
+ return;
105
+ }
106
+
107
+ throw new Error(`Unknown command: ${command}`);
108
+ }
109
+
110
+ main().catch((err) => {
111
+ console.error(err.message || err);
112
+ process.exit(1);
113
+ });
114
+
115
+
@@ -0,0 +1,74 @@
1
+ import { renderVideo } from "@twick/renderer";
2
+ import chromium from "@sparticuz/chromium";
3
+ import ffmpegPath from "ffmpeg-static";
4
+ import ffmpeg from "fluent-ffmpeg";
5
+
6
+ chromium.setHeadlessMode = true;
7
+ ffmpeg.setFfmpegPath(ffmpegPath);
8
+
9
+ /**
10
+ * Renders a Twick video with the provided variables and settings.
11
+ * Processes project variables, merges settings with defaults, and
12
+ * generates a video file using the Twick renderer.
13
+ *
14
+ * @param {Object} variables - Project variables containing input configuration
15
+ * @param {Object} settings - Optional render settings to override defaults
16
+ * @returns {Promise<string>} Promise resolving to the path of the rendered video file
17
+ *
18
+ * @example
19
+ * ```js
20
+ * const videoPath = await renderTwickVideo(
21
+ * { input: { properties: { width: 1920, height: 1080 } } },
22
+ * { outFile: "my-video.mp4" }
23
+ * );
24
+ * // videoPath = "./output/my-video.mp4"
25
+ * ```
26
+ */
27
+ const renderTwickVideo = async (variables, settings) => {
28
+ try {
29
+ // Merge user settings with defaults
30
+ const mergedSettings = {
31
+ logProgress: true,
32
+ viteBasePort: 5173,
33
+ outDir: "/tmp/output",
34
+ viteConfig: { cacheDir: "/tmp/.vite" },
35
+ projectSettings: {
36
+ exporter: {
37
+ name: "@twick/core/wasm",
38
+ },
39
+ size: {
40
+ x: variables.properties.width,
41
+ y: variables.properties.height,
42
+ },
43
+ },
44
+ puppeteer: {
45
+ headless: chromium.headless,
46
+ executablePath: await chromium.executablePath(),
47
+ args: chromium.args.filter(
48
+ (arg) =>
49
+ !arg.startsWith("--single-process") &&
50
+ !arg.startsWith("--use-gl=angle") &&
51
+ !arg.startsWith("--use-angle=swiftshader") &&
52
+ !arg.startsWith("--disable-features=")
53
+ ),
54
+ },
55
+ ...settings, // Allow user settings to override defaults
56
+ };
57
+
58
+ const result = await renderVideo({
59
+ projectFile: "@twick/visualizer/dist/project.js",
60
+ variables: variables,
61
+ settings: mergedSettings,
62
+ });
63
+
64
+ console.log("renderVideo Executed:");
65
+ console.log("Render result:", result);
66
+
67
+ return result;
68
+ } catch (error) {
69
+ console.error("Render error:", error);
70
+ throw error;
71
+ }
72
+ };
73
+
74
+ export default renderTwickVideo;
package/package.json ADDED
@@ -0,0 +1,62 @@
1
+ {
2
+ "name": "@twick/cloud-export-video",
3
+ "version": "0.14.8",
4
+ "description": "Twick cloud function for exporting video with platform-specific templates (AWS Lambda container)",
5
+ "type": "module",
6
+ "main": "core/renderer.js",
7
+ "exports": {
8
+ ".": "./core/renderer.js",
9
+ "./aws": "./platform/aws/handler.js",
10
+ "./platform/aws/*": "./platform/aws/*"
11
+ },
12
+ "bin": {
13
+ "twick-export-video": "bin/twick-export-video.js"
14
+ },
15
+ "files": [
16
+ "core",
17
+ "platform",
18
+ "bin",
19
+ "README.md"
20
+ ],
21
+ "scripts": {
22
+ "verify:aws": "node -e \"require('fs').accessSync('platform/aws/Dockerfile'); require('fs').accessSync('platform/aws/handler.js'); console.log('AWS platform assets present')\"",
23
+ "pack:aws": "npm run verify:aws && npm pack",
24
+ "release:aws": "npm run verify:aws && npm publish --access public --tag aws",
25
+ "prepublishOnly": "npm run verify:aws"
26
+ },
27
+ "publishConfig": {
28
+ "access": "public",
29
+ "tag": "aws"
30
+ },
31
+ "keywords": [
32
+ "twick",
33
+ "video",
34
+ "export",
35
+ "lambda",
36
+ "aws",
37
+ "docker"
38
+ ],
39
+ "author": "",
40
+ "license": "SEE LICENSE IN LICENSE.md",
41
+ "engines": {
42
+ "node": ">=18"
43
+ },
44
+ "dependencies": {
45
+ "@sparticuz/chromium": "^129.0.0",
46
+ "@twick/2d": "0.14.8",
47
+ "@twick/core": "0.14.8",
48
+ "@twick/ffmpeg": "0.14.8",
49
+ "@twick/renderer": "0.14.8",
50
+ "@twick/ui": "0.14.8",
51
+ "@twick/vite-plugin": "0.14.8",
52
+ "@twick/visualizer": "0.14.8",
53
+ "ffmpeg-static": "^5.2.0",
54
+ "fluent-ffmpeg": "^2.1.3"
55
+ },
56
+ "devDependencies": {
57
+ "typescript": "~5.4.5"
58
+ },
59
+ "peerDependencies": {
60
+ "puppeteer": ">=22"
61
+ }
62
+ }
@@ -0,0 +1,25 @@
1
+ FROM docker.io/revideo/aws-lambda-base-image:latest
2
+
3
+ # Copy package files for better caching
4
+ COPY package.json package-lock.json* ./
5
+
6
+ RUN npm install
7
+
8
+ RUN npx puppeteer browsers install chrome
9
+
10
+ # Copy source code
11
+ COPY . ./
12
+
13
+
14
+ # Install Puppeteer (if needed)
15
+ RUN node node_modules/puppeteer/install.mjs
16
+
17
+ ENV ROLLUP_CACHE=/tmp/rollup_cache
18
+
19
+ ENV FFMPEG_PATH=/var/task/node_modules/@ffmpeg-installer/linux-x64/ffmpeg
20
+
21
+ ENV HOME=/tmp
22
+
23
+ ENV DONT_WRITE_TO_META_FILES=true
24
+
25
+ CMD ["platform/aws/handler.handler"]
@@ -0,0 +1,141 @@
1
+ import renderTwickVideo from '../../core/renderer.js';
2
+
3
+ /**
4
+ * Handler for processing video project data with files
5
+ *
6
+ * Expected JSON payload:
7
+ * {
8
+ * "project": { ... }, // Video project JSON object
9
+ * "mediaFiles": [ // Optional array of base64-encoded files
10
+ * {
11
+ * "filename": "video.mp4",
12
+ * "contentType": "video/mp4",
13
+ * "data": "base64-encoded-content"
14
+ * }
15
+ * ]
16
+ * }
17
+ *
18
+ * Returns: Processing result with video file
19
+ */
20
+
21
+ export const handler = async (event) => {
22
+ console.log('Video processor function invoked');
23
+ console.log('Event:', JSON.stringify(event));
24
+ const projectData = event.arguments?.input || {};
25
+
26
+ try {
27
+ // Validate required fields
28
+ if (!projectData) {
29
+ return {
30
+ statusCode: 400,
31
+ headers: {
32
+ 'Content-Type': 'application/json',
33
+ 'Access-Control-Allow-Origin': '*',
34
+ },
35
+ body: JSON.stringify({
36
+ error: 'Missing required field: project',
37
+ expectedFormat: {
38
+ project: 'Video project JSON object',
39
+ mediaFiles: 'Optional array of base64-encoded files'
40
+ }
41
+ }),
42
+ };
43
+ }
44
+
45
+ const mediaFiles = projectData.mediaFiles || [];
46
+
47
+ // Log each media file
48
+ mediaFiles.forEach((file, index) => {
49
+ console.log(`Media file ${index + 1}:`, {
50
+ filename: file.filename,
51
+ contentType: file.contentType,
52
+ size: file.data ? Buffer.from(file.data, 'base64').length : 0
53
+ });
54
+ });
55
+
56
+ // Render the video using Twick renderer
57
+ console.log('Starting video rendering...' );
58
+
59
+ let renderedVideoPath;
60
+ let videoBuffer;
61
+
62
+ try {
63
+ // Render the video
64
+ renderedVideoPath = await renderTwickVideo(projectData, {
65
+ outFile: `video-${projectData.properties?.id || Date.now()}.mp4`,
66
+ });
67
+
68
+ // Read the rendered video file
69
+ const fs = await import('fs');
70
+ videoBuffer = fs.readFileSync(renderedVideoPath);
71
+
72
+ console.log('Video rendered successfully:', renderedVideoPath);
73
+ 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
+ } catch (renderError) {
84
+ console.error('Video rendering failed:', renderError);
85
+
86
+ // Fallback to text file if rendering fails
87
+ const errorText = `Video Processing Error
88
+ ======================
89
+ Request ID: ${projectData.properties?.id || 'N/A'}
90
+ Timestamp: ${new Date().toISOString()}
91
+ Status: Rendering Failed
92
+
93
+ Error: ${renderError instanceof Error ? renderError.message : 'Unknown error'}
94
+
95
+ Media Files Received: ${mediaFiles.length}
96
+ ${mediaFiles.map((file, index) => ` ${index + 1}. ${file.filename} (${file.data ? Buffer.from(file.data, 'base64').length : 0} bytes)`).join('\n')}
97
+ `;
98
+
99
+ return {
100
+ statusCode: 500,
101
+ headers: {
102
+ 'Content-Type': 'text/plain',
103
+ 'Content-Disposition': 'attachment; filename="error.txt"',
104
+ 'Access-Control-Allow-Origin': '*',
105
+ },
106
+ body: errorText,
107
+ };
108
+ }
109
+
110
+ // Return the video file
111
+ return {
112
+ statusCode: 200,
113
+ headers: {
114
+ 'Content-Type': 'video/mp4',
115
+ 'Content-Disposition': `attachment; filename="processed-video.mp4"`,
116
+ 'Content-Length': videoBuffer.length,
117
+ 'Access-Control-Allow-Origin': '*',
118
+ 'Access-Control-Allow-Headers': 'Content-Type',
119
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
120
+ },
121
+ body: videoBuffer.toString('base64'),
122
+ isBase64Encoded: true,
123
+ };
124
+ } catch (error) {
125
+ console.error('Error processing video project:', error);
126
+
127
+ return {
128
+ statusCode: 500,
129
+ headers: {
130
+ 'Content-Type': 'application/json',
131
+ 'Access-Control-Allow-Origin': '*',
132
+ 'Access-Control-Allow-Headers': 'Content-Type',
133
+ 'Access-Control-Allow-Methods': 'POST, OPTIONS',
134
+ },
135
+ body: JSON.stringify({
136
+ error: 'Internal server error',
137
+ message: error instanceof Error ? error.message : 'Unknown error',
138
+ }),
139
+ };
140
+ }
141
+ };