UNPKG

media-exporter-processor

Version:

Media processing API with thumbnail generation and cloud storage

387 lines 17.8 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.VideoProcessingService = void 0; const child_process_1 = require("child_process"); const fs_1 = require("fs"); class VideoProcessingService { constructor(thumbnailService, uploadService) { this.thumbnailService = thumbnailService; this.uploadService = uploadService; } async processVideo(videoBuffer, queryParams, originalFilename) { // Ensure temp directory exists const tempDir = "/tmp"; try { await fs_1.promises.access(tempDir); } catch { await fs_1.promises.mkdir(tempDir, { recursive: true }); } const tempFile = `/tmp/video-${Date.now()}-${Math.random() .toString(36) .substring(2)}.mp4`; try { // Write video to temporary file console.log(`Writing video buffer (${videoBuffer.length} bytes) to ${tempFile}`); await fs_1.promises.writeFile(tempFile, videoBuffer); // Verify file was written and is accessible const stats = await fs_1.promises.stat(tempFile); console.log(`Temp file created: ${tempFile} (${stats.size} bytes)`); // Double-check file can be read await fs_1.promises.access(tempFile, fs_1.constants.R_OK); console.log(`Temp file is readable: ${tempFile}`); // Add a small delay to ensure file system sync await new Promise((resolve) => setTimeout(resolve, 100)); // Parse and validate parameters const metadata = this.parseVideoMetadata(queryParams, originalFilename); // Extract video duration const duration = await this.extractVideoDuration(tempFile); console.log(`Video duration: ${duration} seconds`); // Generate thumbnails FIRST from the original video console.log(`Generating thumbnails from: ${tempFile}`); const thumbnails = await this.thumbnailService.generateThumbnails(tempFile); console.log(`Generated ${thumbnails.length} thumbnails`); // Add metadata to video using exiftool const processedVideoBuffer = await this.addMetadataToVideo(tempFile, metadata); // Upload video and thumbnails to R2 console.log(`Starting upload to R2 storage...`); const result = await this.uploadService.uploadVideoWithThumbnails(processedVideoBuffer, originalFilename || `video-${Date.now()}.mp4`, duration, thumbnails, queryParams.prefix, this.createVideoMetadataHeaders(metadata)); console.log(`Upload completed successfully. Video key: ${result.video.key}`); // Add duration to the result return { ...result, duration, }; } finally { // Clean up temporary file try { await fs_1.promises.unlink(tempFile); } catch (error) { // Ignore cleanup errors } } } parseVideoMetadata(queryParams, originalFilename) { const latitude = queryParams.lat || 37.7749; const longitude = queryParams.lon || -122.4194; const altitude = queryParams.alt; let creationDate; if (queryParams.timestamp) { // Try to parse as Unix timestamp first (if it's a number) if (/^\d+$/.test(queryParams.timestamp)) { creationDate = new Date(parseInt(queryParams.timestamp) * 1000); } else { // Try to parse as ISO string creationDate = new Date(queryParams.timestamp); } // Validate the date if (isNaN(creationDate.getTime())) { throw new Error("Invalid timestamp format. Use ISO string or Unix timestamp."); } } else { // Default to current date if no timestamp provided creationDate = new Date(); } return { latitude, longitude, altitude, creationDate, originalFilename, }; } async addMetadataToVideo(videoPath, metadata) { const outputPath = `${videoPath}-processed.mp4`; try { await this.runExiftool(videoPath, outputPath, metadata); return await fs_1.promises.readFile(outputPath); } finally { try { await fs_1.promises.unlink(outputPath); } catch (error) { // Ignore cleanup errors } } } async runExiftool(inputPath, outputPath, metadata) { console.log(`Using direct ExifTool binary approach`); // Debug: Check what's available in the file system try { const rootContents = await fs_1.promises.readdir("/"); console.log(`Contents of /:`, rootContents); } catch (error) { console.log(`Could not read / directory:`, error); } // Debug: Check what's available in /opt try { const optContents = await fs_1.promises.readdir("/opt"); console.log(`Contents of /opt:`, optContents); if (optContents.includes("bin")) { const optBinContents = await fs_1.promises.readdir("/opt/bin"); console.log(`Contents of /opt/bin:`, optBinContents); } if (optContents.includes("lib")) { const optLibContents = await fs_1.promises.readdir("/opt/lib"); console.log(`Contents of /opt/lib:`, optLibContents); // Check for ExifTool in perl5 directories try { const perl5Contents = await fs_1.promises.readdir("/opt/lib/perl5"); console.log(`Contents of /opt/lib/perl5:`, perl5Contents); if (perl5Contents.includes("site_perl")) { const sitePerlContents = await fs_1.promises.readdir("/opt/lib/perl5/site_perl"); console.log(`Contents of /opt/lib/perl5/site_perl:`, sitePerlContents); // Check the version directory if (sitePerlContents.includes("5.34.1")) { const versionContents = await fs_1.promises.readdir("/opt/lib/perl5/site_perl/5.34.1"); console.log(`Contents of /opt/lib/perl5/site_perl/5.34.1:`, versionContents); // Look for ExifTool or Image directory if (versionContents.includes("Image")) { const imageContents = await fs_1.promises.readdir("/opt/lib/perl5/site_perl/5.34.1/Image"); console.log(`Contents of /opt/lib/perl5/site_perl/5.34.1/Image:`, imageContents); } // Look for exiftool script if (versionContents.includes("exiftool")) { console.log(`Found exiftool script in site_perl!`); } } } } catch (error) { console.log(`Could not read /opt/lib/perl5:`, error); } } } catch (error) { console.log(`Could not read /opt directory:`, error); } const { latitude, longitude, altitude, creationDate } = metadata; const alt = altitude !== undefined ? altitude : 0; console.log(`Copying file from ${inputPath} to ${outputPath}`); // Copy the file first await fs_1.promises.copyFile(inputPath, outputPath); console.log(`Writing metadata: GPS(${latitude}, ${longitude}, ${alt}), Date: ${creationDate.toISOString()}`); // Use ExifTool binary directly via spawn return new Promise((resolve, reject) => { const args = [ "-overwrite_original", "-api", "QuickTimeUTC=1", "-m", "-P", `-GPSLatitude=${latitude}`, `-GPSLongitude=${longitude}`, `-GPSAltitude=${alt}`, `-CreateDate=${creationDate .toISOString() .replace("T", " ") .replace("Z", "")}`, `-Title=Video with Location`, `-Description=Video with GPS coordinates: ${latitude}, ${longitude}${altitude ? `, ${altitude}m` : ""} - Created: ${creationDate.toISOString()}`, `-Comment=GPS: ${latitude.toFixed(6)}, ${longitude.toFixed(6)}, ${alt.toFixed(1)}m`, outputPath, ]; console.log(`Running ExifTool: /opt/bin/exiftool ${args.join(" ")}`); // Debug: Check ExifTool layer structure const debugPaths = ["/opt/exiftool", "/opt/exiftool/bin", "/opt/lib"]; for (const debugPath of debugPaths) { try { const contents = require("fs").readdirSync(debugPath); console.log(`Contents of ${debugPath}:`, contents); } catch (error) { console.log(`Could not read ${debugPath}:`, error); } } // Check if perl is available in ALL possible locations const perlPaths = [ "/usr/bin/perl", "/bin/perl", "/opt/bin/perl", "/opt/exiftool/bin/perl", "/opt/exiftool/perl", "/opt/perl/bin/perl", ]; let foundPerlPath = null; for (const perlPath of perlPaths) { try { require("fs").accessSync(perlPath, require("fs").constants.F_OK); console.log(`Found Perl at: ${perlPath}`); if (!foundPerlPath) foundPerlPath = perlPath; // Use first found } catch (error) { console.log(`Perl not found at: ${perlPath}`); } } if (foundPerlPath) { console.log(`Will use Perl at: ${foundPerlPath}`); } else { console.log(`No Perl found anywhere! Trying system perl...`); foundPerlPath = "perl"; // fallback to PATH } // Try different possible ExifTool paths const possiblePaths = [ "/opt/exiftool/bin/exiftool", // Your layer path (confirmed working) "/opt/bin/exiftool", // Alternative layer path "/bin/exiftool", "/opt/exiftool", "exiftool", // fallback to PATH ]; // Try to find a working ExifTool path synchronously let exiftoolPath = possiblePaths[0]; for (const path of possiblePaths) { try { require("fs").accessSync(path, require("fs").constants.F_OK); exiftoolPath = path; console.log(`Found ExifTool at: ${exiftoolPath}`); break; } catch (error) { console.log(`ExifTool not found at: ${path}`); } } console.log(`Using ExifTool at: ${exiftoolPath}`); // Debug: Check what's actually in /opt/lib64 (we saw this directory exists) try { const lib64Contents = require("fs").readdirSync("/opt/lib64"); console.log(`Contents of /opt/lib64:`, lib64Contents); // Check each file for libnsl const libnslFiles = lib64Contents.filter((file) => file.includes("libnsl")); if (libnslFiles.length > 0) { console.log(`Found libnsl files in /opt/lib64:`, libnslFiles); } else { console.log(`No libnsl files found in /opt/lib64`); } } catch (error) { console.log(`Could not read /opt/lib64:`, error); } // Also check system lib64 try { const systemLib64Contents = require("fs").readdirSync("/lib64"); console.log(`Contents of /lib64:`, systemLib64Contents.slice(0, 10), `... (showing first 10)`); const systemLibnslFiles = systemLib64Contents.filter((file) => file.includes("libnsl")); if (systemLibnslFiles.length > 0) { console.log(`Found libnsl files in /lib64:`, systemLibnslFiles); } } catch (error) { console.log(`Could not read /lib64:`, error); } // Use the correct paths we discovered from debug output const env = { ...process.env, PATH: "/opt/bin:" + (process.env.PATH || ""), PERL5LIB: "/opt/lib/perl5:/opt/lib/perl5/site_perl", LD_LIBRARY_PATH: "/opt/lib64:/opt/lib:/lib64:/usr/lib64", // Added /opt/lib64 first! }; console.log(`Using ExifTool: ${exiftoolPath} ${args.join(" ")}`); console.log(`Environment PATH: ${env.PATH}`); console.log(`Environment PERL5LIB: ${env.PERL5LIB}`); console.log(`Environment LD_LIBRARY_PATH: ${env.LD_LIBRARY_PATH}`); const exiftoolProcess = (0, child_process_1.spawn)(exiftoolPath, args, { env }); let stdout = ""; let stderr = ""; exiftoolProcess.stdout.on("data", (data) => { stdout += data.toString(); }); exiftoolProcess.stderr.on("data", (data) => { stderr += data.toString(); }); exiftoolProcess.on("close", (code) => { console.log(`ExifTool process completed with code: ${code}`); console.log(`ExifTool stdout: ${stdout}`); if (stderr) { console.log(`ExifTool stderr: ${stderr}`); } if (code === 0) { console.log(`ExifTool write operation completed successfully`); resolve(); } else { reject(new Error(`ExifTool failed with code ${code}: ${stderr || stdout}`)); } }); exiftoolProcess.on("error", (error) => { console.error(`ExifTool spawn error:`, error); reject(new Error(`ExifTool spawn error: ${error.message}`)); }); }); } async extractVideoDuration(videoPath) { return new Promise((resolve, reject) => { const ffprobePath = this.getFFprobePath(); const args = [ "-v", "quiet", "-show_entries", "format=duration", "-of", "csv=p=0", videoPath, ]; console.log(`Running FFprobe: ${ffprobePath} ${args.join(" ")}`); const ffprobeProcess = (0, child_process_1.spawn)(ffprobePath, args); let stdout = ""; let stderr = ""; ffprobeProcess.stdout.on("data", (data) => { stdout += data.toString(); }); ffprobeProcess.stderr.on("data", (data) => { stderr += data.toString(); }); ffprobeProcess.on("close", (code) => { if (code === 0) { const duration = parseFloat(stdout.trim()); if (!isNaN(duration)) { const roundedDuration = Math.round(duration); console.log(`FFprobe completed successfully. Duration: ${roundedDuration}s`); resolve(roundedDuration); } else { console.error(`FFprobe returned invalid duration: ${stdout}`); resolve(0); // Fallback to 0 if duration cannot be parsed } } else { console.error(`FFprobe failed with code ${code}: ${stderr}`); resolve(0); // Fallback to 0 if FFprobe fails } }); ffprobeProcess.on("error", (error) => { console.error(`FFprobe spawn error: ${error.message}`); resolve(0); // Fallback to 0 if FFprobe cannot be spawned }); }); } getFFprobePath() { // Check for FFprobe in common locations (usually comes with FFmpeg) const ffprobePaths = [ "/opt/bin/ffprobe", // Lambda layer path "/opt/ffmpeg/bin/ffprobe", // Alternative layer path "/usr/bin/ffprobe", // System path "ffprobe", // Fallback to PATH ]; return ffprobePaths[0]; // Start with layer path - will be validated at runtime } createVideoMetadataHeaders(metadata) { return { "x-amz-meta-latitude": metadata.latitude.toString(), "x-amz-meta-longitude": metadata.longitude.toString(), "x-amz-meta-altitude": metadata.altitude?.toString() || "", "x-amz-meta-creation-date": metadata.creationDate.toISOString(), "x-amz-meta-original-filename": metadata.originalFilename || "", }; } } exports.VideoProcessingService = VideoProcessingService; //# sourceMappingURL=VideoProcessingService.js.map