I'm working on a video processing feature in my application where user upload a video with that flow:
- Call my API for uploading a video
- Generate a video thumbnail
- Create an overlay video on top video -> I use
ffmpegand i create a overlay video based on my uploaded one and i link of existed video i need to download my existed video so ffmpeg can processes it - Finally, i put them on AWS S3
I'm facing performance problems, the processing is too slow. I have researched that chunk video in frontend then call the api but it seems not work. And generate thumbnail with some react-native video library or make thumbnail of no available video thumbnail are seem to be too slow.
- I'm currently using React Native, NestJS, ffmpeg for dealing with videos, AWS S3 for storage
I wanna know with step can enhanced or recommend flow should i implement.
There is some of my code:
Create video thumbnail
async createVideoThumbnailVer2(
filePath: string,
outputDes: string,
timestamp: string = '00:00:01',
): Promise<string> {
const thumnailPath = path.join(
outputDes,
`${path.basename(filePath, path.extname(filePath))}-thumbnail.png`,
);
return new Promise<string>((resolve, reject) => {
ffmpeg(filePath)
.screenshots({
timestamps: [timestamp],
filename: path.basename(thumnailPath),
folder: outputDes,
size: '320x240',
})
.on('end', () => {
this.logger.log(`Thumbnail created at ${thumnailPath}`);
resolve(thumnailPath);
})
.on('error', (err) => {
this.logger.error(`Error creating thumbnail: ${err.message}`);
reject(err);
});
});
}
Overlay video on video
async overlayVideoOnVideo(
backgroundVideoPathOrUrl: string,
overlayVideoPathOrUrl: string,
) {
const isRemote = (p: string) => /^https?:\/\//i.test(p);
const downloadToTemp = (urlStr: string) =>
new Promise<string>((resolve, reject) => {
try {
const parsed = new URL(urlStr);
const ext = path.extname(parsed.pathname) || '.mp4';
const tmpFile = path.join(
os.tmpdir(),
`ffmpeg-${Date.now()}-${Math.random().toString(36).slice(2)}${ext}`,
);
const client = parsed.protocol === 'https:' ? https : http;
const req = client.get(urlStr, (res: any) => {
if (res.statusCode && res.statusCode >= 400) {
return reject(
new Error(
`Failed to download ${urlStr}, status ${res.statusCode}`,
),
);
}
const fileStream = fs.createWriteStream(tmpFile);
res.pipe(fileStream);
fileStream.on('finish', () => {
fileStream.close();
resolve(tmpFile);
});
fileStream.on('error', (err) => {
try {
// Prefer fs.rmSync with force option when available, otherwise fall back to unlinkSync
if ((fs as any).rmSync) {
(fs as any).rmSync(tmpFile, { force: true });
} else {
fs.unlinkSync(tmpFile);
}
} catch {
/* ignore cleanup errors */
}
reject(err);
});
});
req.on('error', (err: Error) => {
reject(err);
});
} catch (err) {
reject(err);
}
});
return new Promise<string>(async (resolve, reject) => {
let bgLocal = backgroundVideoPathOrUrl;
let overlayLocal = overlayVideoPathOrUrl;
const tempsToCleanup: string[] = [];
try {
// Download remote files if needed
if (isRemote(backgroundVideoPathOrUrl)) {
this.logger.log(
`Downloading background from ${backgroundVideoPathOrUrl}`,
);
bgLocal = await downloadToTemp(backgroundVideoPathOrUrl);
tempsToCleanup.push(bgLocal);
}
if (isRemote(overlayVideoPathOrUrl)) {
this.logger.log(`Downloading overlay from ${overlayVideoPathOrUrl}`);
overlayLocal = await downloadToTemp(overlayVideoPathOrUrl);
tempsToCleanup.push(overlayLocal);
}
// Validate local existence
if (!fs.existsSync(bgLocal)) {
this.logger.error(`Background video not found: ${bgLocal}`);
return reject(new Error(`Background video not found: ${bgLocal}`));
}
if (!fs.existsSync(overlayLocal)) {
this.logger.error(`Overlay video not found: ${overlayLocal}`);
return reject(new Error(`Overlay video not found: ${overlayLocal}`));
}
const outputDir = path.dirname('uploads/videos');
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, { recursive: true });
}
const filter =
'[1:v]scale=1920x1080,format=yuva420p,colorchannelmixer=aa=0.5[transparent_top];[0:v][transparent_top]overlay[v]';
const args = [
'-i',
bgLocal,
'-i',
overlayLocal,
'-filter_complex',
filter,
'-map',
'[v]',
'-map',
'0:a?',
'-c:v',
'libx264',
'-c:a',
'aac',
'-preset',
'fast',
'-crf',
'23',
'uploads/videos/overlay_output.mp4',
];
const ffmpegProc = spawn('ffmpeg', args);
let stderr = '';
ffmpegProc.stderr.on('data', (data) => {
const msg = data.toString();
stderr += msg;
this.logger.log(`FFmpeg: ${msg}`);
});
ffmpegProc.stdout.on('data', (data) => {
this.logger.log(`FFmpeg stdout: ${data.toString()}`);
});
ffmpegProc.on('close', (code) => {
// cleanup temps
for (const t of tempsToCleanup) {
try {
fs.unlinkSync(t);
} catch {
/* ignore */
}
}
if (code === 0) {
this.logger.log(
`Overlay completed: uploads/videos/overlays/output.mp4`,
);
resolve('uploads/videos/overlay_output.mp4');
} else {
this.logger.error(`FFmpeg exited with code ${code}`);
reject(new Error(`FFmpeg exited with code ${code}. ${stderr}`));
}
});
ffmpegProc.on('error', (err) => {
// cleanup temps
for (const t of tempsToCleanup) {
try {
fs.unlinkSync(t);
} catch {
/* ignore */
}
}
this.logger.error(`FFmpeg process error: ${err.message}`);
reject(err);
});
} catch (err) {
// cleanup temps on failure
for (const t of tempsToCleanup) {
try {
fs.unlinkSync(t);
} catch {
/* ignore */
}
}
this.logger.error(
`Failed to start FFmpeg overlay: ${err.message || err}`,
);
reject(err);
}
});
}