media-exporter-processor
Version:
Media processing API with thumbnail generation and cloud storage
387 lines • 17.8 kB
JavaScript
;
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