-2

I'm working on a video processing feature in my application where user upload a video with that flow:

  1. Call my API for uploading a video
  2. Generate a video thumbnail
  3. Create an overlay video on top video -> I use ffmpeg and 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
  4. 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);
      }
    });
  }

0

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.