← All posts

Video Processing for SaaS: Build vs Buy and Integration Guide

When your SaaS product needs video features, should you build or buy? A practical guide covering integration patterns, cost analysis, and implementation examples.

FFHub·2026-05-11
Video Processing for SaaS: Build vs Buy and Integration Guide

Your SaaS product wasn't supposed to be a video platform. But then a customer asked for video uploads in their course builder. Or your project management tool needs screen recording previews. Or your CMS needs to compress user-uploaded videos before serving them. Suddenly, you're in the video processing business — and you'd rather not be.

This guide helps SaaS product and engineering teams decide how to add video processing capabilities without derailing the product roadmap. We'll cover when to build, when to buy, and how to integrate a cloud video API cleanly into your existing architecture.

When Your SaaS Needs Video Processing

Video processing isn't a feature you seek out. It finds you. Here are the most common triggers:

Learning Management Systems (LMS)

Instructors upload lecture recordings in every format imaginable. Students expect instant playback on any device. You need format conversion, compression, and multi-resolution delivery. This is essentially the same challenge faced by UGC platforms.

Content Management Systems (CMS)

Content editors upload raw video alongside articles. These need compression for storage costs, thumbnail generation for previews, and format standardization for consistent playback.

Project Management / Collaboration Tools

Teams share screen recordings, product demos, and meeting highlights. These need compression (screen recordings can be enormous), preview thumbnails, and sometimes clip extraction for highlights.

Customer Support Platforms

Customers submit video attachments showing bugs or issues. Support agents need quick preview thumbnails and compressed versions that load fast in the ticket view.

E-Commerce / Marketplace Platforms

Sellers upload product videos. These need standardized resolution, compression for page load speed, and thumbnail generation for product listings.

Real Estate / Property Platforms

Agents upload property walkthrough videos. These need compression, thumbnail generation, and sometimes clip extraction for listing previews.

Common Video Processing Needs

Across all these SaaS categories, the needs converge on a surprisingly short list:

NeedDescriptionFFmpeg Example
Format conversionAccept any input, output standard MP4ffmpeg -i input.avi -c:v libx264 -c:a aac output.mp4
CompressionReduce file size for storage and deliveryffmpeg -i input.mp4 -crf 28 -preset medium output.mp4
ThumbnailExtract a preview frameffmpeg -i input.mp4 -ss 2 -frames:v 1 thumb.jpg
Resolution scalingNormalize to target resolutionffmpeg -i input.mp4 -vf "scale=-2:720" output.mp4
Clip extractionExtract a time segmentffmpeg -i input.mp4 -ss 30 -t 15 clip.mp4
Audio extractionPull audio from videoffmpeg -i input.mp4 -vn -c:a aac audio.m4a
WatermarkAdd brand overlayffmpeg -i input.mp4 -i logo.png -filter_complex "overlay=10:10" output.mp4

Most SaaS products need only 2-3 of these. That's important for the build vs. buy decision.

Build vs. Buy: The Decision Matrix

This is the core question. Let's make it concrete.

When to Build (Self-Hosted FFmpeg)

Build your own video processing if all of these are true:

  • You have a dedicated infrastructure/DevOps team
  • Video processing is a core product differentiator
  • You process 100,000+ videos per month (cost sensitivity)
  • You have strict data residency requirements that no cloud provider can meet
  • You can afford 2-4 weeks of engineering time for initial setup plus ongoing maintenance

When to Buy (Cloud API)

Use a cloud video processing API if any of these are true:

  • Your team is under 20 engineers
  • Video is a supporting feature, not the core product
  • You need to ship in days, not weeks
  • Your volume is under 100,000 videos per month
  • You don't want to manage FFmpeg versions, scaling, or failure recovery

Decision Matrix

FactorBuildBuy
Team size20+ engineers, dedicated infra teamAny size
Video volume100K+/monthUnder 100K/month
Time to ship2-4 weeks minimum1-3 days
Video as core featureYes, product differentiatorNo, supporting feature
Data residencyStrict, in-house onlyStandard cloud compliance
Maintenance burdenOngoing (FFmpeg updates, scaling, monitoring)None
Cost at scaleLower marginal costHigher marginal cost
Cost at low volumeHigher (server + engineering)Lower (pay-per-use)

The Honest Math

Let's compare costs for a typical SaaS scenario — a CMS that processes 5,000 videos per month (average 3 minutes, 1080p output):

Self-Hosted:

  • Server: 2x c5.2xlarge ($250/month) or equivalent
  • Engineering setup: 80 hours x $100/hr = $8,000 (one-time)
  • Ongoing maintenance: 10 hours/month x $100/hr = $1,000/month
  • Total first year: $8,000 + ($250 + $1,000) x 12 = $23,000

Cloud API:

  • Processing cost: ~$300/month (5,000 videos x ~$0.06 each)
  • Engineering setup: 8 hours x $100/hr = $800 (one-time)
  • Ongoing maintenance: ~0 hours/month
  • Total first year: $800 + $300 x 12 = $4,400

The break-even point is around 50,000-80,000 videos per month, depending on your engineering costs. Below that, a cloud API wins decisively. For a comparison of cloud API options, see our FFHub vs AWS MediaConvert analysis.

Integration Patterns

There are two main ways to integrate video processing into your SaaS architecture.

Pattern 1: Async with Webhooks (Recommended)

Best for videos longer than 10 seconds or when you need multiple outputs.

User uploads video
        │
        v
┌──────────────┐     ┌──────────────┐
│  Your API    │────>│  Store raw   │
│  Server      │     │  in S3/R2    │
└──────┬───────┘     └──────────────┘
       │
       │ POST /tasks (FFmpeg command + webhook URL)
       v
┌──────────────┐
│  Transcoding │
│  API         │
└──────┬───────┘
       │
       │ ... processing ...
       │
       │ POST webhook callback
       v
┌──────────────┐     ┌──────────────┐
│  Your API    │────>│  Update DB   │
│  (webhook)   │     │  Notify user │
└──────────────┘     └──────────────┘

Implementation:

// 步骤 1: 用户上传时,提交转码任务
app.post("/api/videos/upload", auth, upload.single("video"), async (req, res) => {
  const rawUrl = await uploadToS3(req.file);

  // 在数据库创建记录
  const video = await db.query(
    `INSERT INTO videos (id, user_id, status, source_url, created_at)
     VALUES ($1, $2, 'processing', $3, NOW()) RETURNING *`,
    [generateId(), req.user.id, rawUrl]
  );

  // 提交转码任务
  await fetch("https://api.ffhub.io/v1/tasks", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.FFHUB_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      command: `ffmpeg -i ${rawUrl} -c:v libx264 -crf 23 -preset medium -c:a aac -b:a 128k -movflags +faststart output.mp4`,
      webhook_url: `https://your-app.com/webhooks/video-processed?video_id=${video.id}`,
    }),
  });

  // 同时提交缩略图任务
  await fetch("https://api.ffhub.io/v1/tasks", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.FFHUB_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      command: `ffmpeg -i ${rawUrl} -ss 2 -frames:v 1 -q:v 2 thumbnail.jpg`,
      webhook_url: `https://your-app.com/webhooks/thumbnail-generated?video_id=${video.id}`,
    }),
  });

  res.json({ id: video.id, status: "processing" });
});

// 步骤 2: 接收 Webhook 回调
app.post("/webhooks/video-processed", async (req, res) => {
  const { video_id } = req.query;
  const { status, output_url, error } = req.body;

  if (status === "completed") {
    await db.query(
      `UPDATE videos SET status = 'ready', processed_url = $1, processed_at = NOW() WHERE id = $2`,
      [output_url, video_id]
    );
    // 可选:通知用户视频已就绪
    await notifyUser(video_id, "Your video is ready");
  } else {
    await db.query(
      `UPDATE videos SET status = 'failed', error = $1 WHERE id = $2`,
      [error, video_id]
    );
  }

  res.sendStatus(200);
});

Pattern 2: Sync for Small Files

For very short videos (under 10 seconds) or thumbnails, you might want synchronous processing:

app.post("/api/videos/quick-process", auth, upload.single("video"), async (req, res) => {
  const rawUrl = await uploadToS3(req.file);

  // 提交任务
  const taskRes = await fetch("https://api.ffhub.io/v1/tasks", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.FFHUB_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      command: `ffmpeg -i ${rawUrl} -c:v libx264 -crf 23 -t 10 -movflags +faststart output.mp4`,
    }),
  });
  const { task_id } = await taskRes.json();

  // 轮询等待(设置超时)
  const result = await pollWithTimeout(task_id, 60000); // 60 秒超时

  res.json({
    id: generateId(),
    status: "ready",
    url: result.output_url,
  });
});

Use this sparingly. For anything that might take more than 30 seconds, use the async pattern.

Security Considerations

Video processing in a SaaS context introduces specific security concerns.

Signed URLs

Never expose raw storage URLs to users. Use signed URLs with expiration:

// 生成限时签名 URL
function generateSignedUrl(objectKey, expiresInSeconds = 3600) {
  return s3.getSignedUrl("getObject", {
    Bucket: process.env.S3_BUCKET,
    Key: objectKey,
    Expires: expiresInSeconds,
  });
}

// 提供给前端的 URL 始终带签名
app.get("/api/videos/:id/stream", auth, async (req, res) => {
  const video = await db.query("SELECT * FROM videos WHERE id = $1", [req.params.id]);

  // 鉴权检查
  if (video.user_id !== req.user.id && !req.user.isAdmin) {
    return res.status(403).json({ error: "Forbidden" });
  }

  const signedUrl = generateSignedUrl(video.storage_key, 7200); // 2 小时有效
  res.json({ url: signedUrl });
});

Data Retention

Define and enforce data retention policies:

// 定期清理已处理的源文件
async function cleanupRawFiles() {
  const staleVideos = await db.query(
    `SELECT * FROM videos
     WHERE status = 'ready'
     AND processed_at < NOW() - INTERVAL '7 days'
     AND source_deleted = false`
  );

  for (const video of staleVideos.rows) {
    await deleteFromS3(video.source_url);
    await db.query(
      "UPDATE videos SET source_deleted = true WHERE id = $1",
      [video.id]
    );
  }
}

Input Sanitization

Users can upload malicious files. Validate before processing:

async function validateVideoFile(fileUrl) {
  // 用 FFprobe 验证这确实是一个视频文件
  const probe = await runFFprobe(fileUrl);

  // 检查是否有视频流
  const videoStream = probe.streams.find((s) => s.codec_type === "video");
  if (!videoStream) {
    throw new Error("No video stream found");
  }

  // 检查时长上限(防止滥用)
  if (parseFloat(probe.format.duration) > 3600) {
    throw new Error("Video exceeds 1 hour limit");
  }

  // 检查分辨率上限
  if (videoStream.width > 7680 || videoStream.height > 4320) {
    throw new Error("Resolution exceeds 8K limit");
  }

  return probe;
}

Multi-Tenant Isolation

Ensure one tenant's processing doesn't affect another's:

  • Use unique storage paths per tenant: s3://bucket/tenant-{id}/videos/...
  • Include tenant ID in webhook URLs for verification
  • Set per-tenant processing quotas

Implementation: Full SaaS Video Module

Here's a complete, production-ready video processing module for a SaaS application:

// video-service.js
const API_BASE = "https://api.ffhub.io/v1";

class VideoService {
  constructor(apiKey, webhookBaseUrl) {
    this.apiKey = apiKey;
    this.webhookBase = webhookBaseUrl;
  }

  // 提交视频处理(格式转换 + 压缩 + 缩略图)
  async processUpload(videoId, sourceUrl, options = {}) {
    const {
      maxResolution = 1080,
      crf = 23,
      generateThumbnail = true,
      generatePreview = false,
    } = options;

    const tasks = [];

    // 主视频转码
    tasks.push(
      this.submitTask(
        `ffmpeg -i ${sourceUrl} -vf "scale='min(${maxResolution * 16/9},iw)':'min(${maxResolution},ih)':force_original_aspect_ratio=decrease,pad=ceil(iw/2)*2:ceil(ih/2)*2" -c:v libx264 -crf ${crf} -preset medium -c:a aac -b:a 128k -movflags +faststart output.mp4`,
        `${this.webhookBase}/video-ready?id=${videoId}&type=main`
      )
    );

    // 缩略图
    if (generateThumbnail) {
      tasks.push(
        this.submitTask(
          `ffmpeg -i ${sourceUrl} -ss 2 -frames:v 1 -vf "scale=640:-2" -q:v 2 thumbnail.jpg`,
          `${this.webhookBase}/video-ready?id=${videoId}&type=thumbnail`
        )
      );
    }

    // 动画预览
    if (generatePreview) {
      tasks.push(
        this.submitTask(
          `ffmpeg -i ${sourceUrl} -ss 5 -t 4 -vf "fps=10,scale=320:-2" -c:v libwebp -quality 50 -loop 0 preview.webp`,
          `${this.webhookBase}/video-ready?id=${videoId}&type=preview`
        )
      );
    }

    return Promise.all(tasks);
  }

  // 提取视频片段
  async extractClip(sourceUrl, startTime, duration, videoId) {
    return this.submitTask(
      `ffmpeg -i ${sourceUrl} -ss ${startTime} -t ${duration} -c:v libx264 -crf 23 -c:a aac -movflags +faststart clip.mp4`,
      `${this.webhookBase}/video-ready?id=${videoId}&type=clip`
    );
  }

  // 压缩视频(降低存储成本)
  async compressForStorage(sourceUrl, videoId) {
    return this.submitTask(
      `ffmpeg -i ${sourceUrl} -c:v libx264 -crf 28 -preset slow -c:a aac -b:a 96k -movflags +faststart compressed.mp4`,
      `${this.webhookBase}/video-ready?id=${videoId}&type=compressed`
    );
  }

  async submitTask(command, webhookUrl) {
    const res = await fetch(`${API_BASE}/tasks`, {
      method: "POST",
      headers: {
        Authorization: `Bearer ${this.apiKey}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ command, webhook_url: webhookUrl }),
    });

    if (!res.ok) {
      throw new Error(`API error: ${res.status}`);
    }

    return res.json();
  }
}

export default VideoService;

Usage in your SaaS application:

import VideoService from "./video-service.js";

const videoService = new VideoService(
  process.env.FFHUB_API_KEY,
  "https://your-saas.com/webhooks"
);

// 用户上传处理
app.post("/api/courses/:courseId/videos", auth, upload.single("video"), async (req, res) => {
  const rawUrl = await uploadToStorage(req.file);
  const videoId = generateId();

  await db.query(
    `INSERT INTO videos (id, course_id, user_id, status, source_url)
     VALUES ($1, $2, $3, 'processing', $4)`,
    [videoId, req.params.courseId, req.user.id, rawUrl]
  );

  await videoService.processUpload(videoId, rawUrl, {
    maxResolution: 1080,
    generateThumbnail: true,
    generatePreview: true,
  });

  res.json({ id: videoId, status: "processing" });
});

Cost Projection by SaaS Scale

Here's what video processing costs look like at different SaaS growth stages:

Startup (0-1,000 customers)

  • Video volume: ~500 videos/month
  • Average duration: 3 minutes
  • Processing needs: format conversion + thumbnail
  • Estimated monthly cost: $30-50
  • Engineering investment: 1 day setup

Growth (1,000-10,000 customers)

  • Video volume: ~5,000 videos/month
  • Average duration: 5 minutes
  • Processing needs: conversion + thumbnail + compression
  • Estimated monthly cost: $200-400
  • Engineering investment: 1-2 days for webhook integration

Scale (10,000-100,000 customers)

  • Video volume: ~50,000 videos/month
  • Average duration: 5 minutes
  • Processing needs: conversion + multi-resolution + thumbnail + preview
  • Estimated monthly cost: $1,500-3,000
  • Consider: volume discounts, hybrid processing for base load

Enterprise (100,000+ customers)

  • Video volume: 500,000+ videos/month
  • Processing needs: full pipeline + custom codecs + compliance
  • Estimated monthly cost: $10,000-25,000
  • Consider: self-hosted base load + cloud API for burst, dedicated infrastructure

At every stage, compare the API cost to the engineering time of building and maintaining your own solution. A senior engineer's monthly cost easily exceeds the API cost for the startup and growth stages.

Common SaaS-Specific Patterns

Processing Quotas per Plan

Tier video processing as a feature of your SaaS pricing:

const PLAN_LIMITS = {
  free:       { monthlyVideos: 10,  maxDuration: 60,   maxResolution: 720 },
  starter:    { monthlyVideos: 100, maxDuration: 300,  maxResolution: 1080 },
  business:   { monthlyVideos: 1000, maxDuration: 1800, maxResolution: 1080 },
  enterprise: { monthlyVideos: -1,   maxDuration: 7200, maxResolution: 4320 },
};

async function checkQuota(userId) {
  const user = await getUser(userId);
  const limits = PLAN_LIMITS[user.plan];

  if (limits.monthlyVideos === -1) return true; // 无限制

  const thisMonth = await db.query(
    `SELECT COUNT(*) FROM videos
     WHERE user_id = $1 AND created_at >= date_trunc('month', NOW())`,
    [userId]
  );

  return thisMonth.count < limits.monthlyVideos;
}

Background Processing Queue

Don't block your API on video processing. Use a simple database-backed queue:

// 入队
async function enqueueVideoProcessing(videoId, sourceUrl, options) {
  await db.query(
    `INSERT INTO processing_queue (video_id, source_url, options, status, created_at)
     VALUES ($1, $2, $3, 'pending', NOW())`,
    [videoId, sourceUrl, JSON.stringify(options)]
  );
}

// Worker 进程(每 5 秒轮询一次待处理任务)
async function processQueue() {
  const job = await db.query(
    `UPDATE processing_queue
     SET status = 'processing', started_at = NOW()
     WHERE id = (
       SELECT id FROM processing_queue
       WHERE status = 'pending'
       ORDER BY created_at
       LIMIT 1
       FOR UPDATE SKIP LOCKED
     )
     RETURNING *`
  );

  if (job.rows.length === 0) return;

  const { video_id, source_url, options } = job.rows[0];
  await videoService.processUpload(video_id, source_url, JSON.parse(options));
}

setInterval(processQueue, 5000);

Status API for Frontend

Give your frontend a way to show processing status:

app.get("/api/videos/:id/status", auth, async (req, res) => {
  const video = await db.query(
    "SELECT id, status, thumbnail_url, processed_url FROM videos WHERE id = $1",
    [req.params.id]
  );

  res.json({
    id: video.id,
    status: video.status, // 'processing' | 'ready' | 'failed'
    thumbnail: video.thumbnail_url,
    url: video.status === "ready" ? video.processed_url : null,
  });
});

Conclusion

Adding video processing to your SaaS doesn't have to mean building a video infrastructure team. The decision framework is straightforward:

  1. Identify your actual needs. Most SaaS products need format conversion, compression, and thumbnails. That's it.
  2. Choose build vs. buy based on volume and team size. Under 100K videos/month with a small team? Buy. Over that with a dedicated infra team? Consider building.
  3. Integrate async-first. Use webhooks for anything that takes more than a few seconds. Your users will appreciate not staring at a spinner.
  4. Plan for growth. Start with a cloud API, move to hybrid when volume justifies it.

The code examples in this article give you a working starting point. The VideoService class can be dropped into any Node.js SaaS application and extended as your needs grow.

For the cloud API approach, FFHub.io provides a straightforward FFmpeg API — same commands you'd run locally, executed in the cloud with automatic scaling and no infrastructure to manage. If you're considering the serverless route instead, read about the challenges of running FFmpeg on Lambda first.

Related Articles

Video Processing for SaaS: Build vs Buy and Integration Guide | FFHub