@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 +56 -0
- package/bin/twick-export-video.js +115 -0
- package/core/renderer.js +74 -0
- package/package.json +62 -0
- package/platform/aws/Dockerfile +25 -0
- package/platform/aws/handler.js +141 -0
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
|
+
|
package/core/renderer.js
ADDED
|
@@ -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
|
+
};
|