← All posts

Video-Pipeline für SaaS: Build vs. Buy in der Praxis

Wenn dein SaaS plötzlich Video-Features braucht — selber bauen oder kaufen? Praxis-Guide mit Integrationsmustern, Kostenanalyse und Code-Beispielen.

FFHub·2026-05-11
Video-Pipeline für SaaS: Build vs. Buy in der Praxis

Dein SaaS sollte gar keine Video-Plattform werden. Aber dann fragt ein Kunde nach Video-Uploads im Kursbaukasten. Oder dein Projektmanagement-Tool braucht Screen-Recording-Previews. Oder dein CMS muss User-Videos vor der Auslieferung komprimieren. Plötzlich bist du im Video-Processing-Business — und willst es eigentlich nicht sein.

Dieser Guide hilft SaaS-Produkt- und Engineering-Teams, Video-Processing einzubauen, ohne die Roadmap zu zerlegen. Wir gehen durch wann bauen, wann kaufen, und wie du eine Cloud-Video-API sauber in deine bestehende Architektur integrierst.

Wann dein SaaS Video-Processing braucht

Video-Processing ist kein Feature, das du suchst. Es findet dich. Die häufigsten Auslöser:

Learning Management Systems (LMS)

Dozenten laden Vorlesungs-Recordings in jedem erdenklichen Format hoch. Studierende erwarten sofortige Wiedergabe auf jedem Gerät. Du brauchst Format-Konvertierung, Komprimierung und Multi-Resolution-Auslieferung. Im Kern dasselbe Problem wie bei UGC-Plattformen.

Content Management Systems (CMS)

Redakteure laden Rohvideos zu Artikeln hoch. Das braucht Komprimierung für Storage-Kosten, Thumbnail-Generierung für Previews und Format-Standardisierung für konsistente Wiedergabe.

Projektmanagement / Collaboration Tools

Teams teilen Screen Recordings, Produkt-Demos und Meeting-Highlights. Komprimierung ist Pflicht (Screen Recordings können riesig sein), Preview-Thumbnails sowieso, manchmal auch Clip-Extraktion für Highlights.

Customer-Support-Plattformen

Kunden hängen Video-Anhänge an, die Bugs zeigen. Support-Agents brauchen schnelle Preview-Thumbnails und komprimierte Versionen, die im Ticket schnell laden.

E-Commerce / Marktplatz-Plattformen

Verkäufer laden Produktvideos hoch. Brauchen einheitliche Auflösung, Komprimierung für Page-Load-Speed und Thumbnail-Generierung für Listings.

Real Estate / Immobilien-Plattformen

Makler laden Walkthrough-Videos hoch. Komprimierung, Thumbnail-Generierung und manchmal Clip-Extraktion für Listing-Previews.

Übliche Video-Processing-Anforderungen

Über alle SaaS-Kategorien hinweg konvergieren die Anforderungen auf eine überraschend kurze Liste:

BedarfBeschreibungFFmpeg-Beispiel
Format-KonvertierungBeliebigen Input akzeptieren, Standard-MP4 ausliefernffmpeg -i input.avi -c:v libx264 -c:a aac output.mp4
KomprimierungDateigröße für Storage und Auslieferung reduzierenffmpeg -i input.mp4 -crf 28 -preset medium output.mp4
ThumbnailPreview-Frame extrahierenffmpeg -i input.mp4 -ss 2 -frames:v 1 thumb.jpg
Resolution ScalingAuf Ziel-Auflösung normalisierenffmpeg -i input.mp4 -vf "scale=-2:720" output.mp4
Clip-ExtraktionZeitsegment rausschneidenffmpeg -i input.mp4 -ss 30 -t 15 clip.mp4
Audio-ExtraktionAudio aus Video ziehenffmpeg -i input.mp4 -vn -c:a aac audio.m4a
WasserzeichenBrand-Overlay hinzufügenffmpeg -i input.mp4 -i logo.png -filter_complex "overlay=10:10" output.mp4

Die meisten SaaS-Produkte brauchen davon nur 2-3. Wichtig für die Build-vs-Buy-Entscheidung.

Build vs. Buy: Die Entscheidungsmatrix

Das ist die Kernfrage. Machen wir's konkret.

Wann bauen (Self-Hosted FFmpeg)

Bau dein eigenes Video-Processing nur, wenn alle folgenden Punkte zutreffen:

  • Du hast ein dediziertes Infrastruktur-/DevOps-Team
  • Video-Processing ist ein Kern-Differenzierer deines Produkts
  • Du verarbeitest 100.000+ Videos pro Monat (Kostensensitivität)
  • Du hast strikte Data-Residency-Anforderungen, die kein Cloud-Provider erfüllt
  • Du kannst dir 2-4 Wochen Engineering für initiales Setup plus laufende Wartung leisten

Wann kaufen (Cloud API)

Nutze eine Cloud-Video-API, wenn eines dieser Dinge zutrifft:

  • Dein Team ist unter 20 Engineers
  • Video ist ein Support-Feature, nicht das Kernprodukt
  • Du musst in Tagen liefern, nicht in Wochen
  • Dein Volumen liegt unter 100.000 Videos pro Monat
  • Du willst FFmpeg-Versionen, Skalierung und Failure-Recovery nicht managen

Entscheidungsmatrix

FaktorBuildBuy
Teamgröße20+ Engineers, dediziertes Infra-TeamBeliebig
Video-Volumen100K+/MonatUnter 100K/Monat
Time-to-ShipMind. 2-4 Wochen1-3 Tage
Video als Kern-FeatureJa, DifferenziererNein, Support-Feature
Data ResidencyStrikt, nur in-houseStandard Cloud Compliance
WartungsaufwandLaufend (FFmpeg-Updates, Skalierung, Monitoring)Keiner
Kosten im MaßstabNiedrigere GrenzkostenHöhere Grenzkosten
Kosten bei niedrigem VolumenHöher (Server + Engineering)Niedriger (Pay-per-Use)

Die ehrliche Mathematik

Vergleichen wir Kosten für ein typisches SaaS-Szenario — ein CMS, das 5.000 Videos pro Monat verarbeitet (Durchschnitt 3 Minuten, 1080p Output):

Self-Hosted:

  • Server: 2x c5.2xlarge ($250/Monat) oder äquivalent
  • Engineering Setup: 80 Stunden x $100/h = $8.000 (einmalig)
  • Laufende Wartung: 10 Stunden/Monat x $100/h = $1.000/Monat
  • Erstes Jahr: $8.000 + ($250 + $1.000) x 12 = $23.000

Cloud API:

  • Verarbeitungskosten: ~$300/Monat (5.000 Videos x ~$0,06)
  • Engineering Setup: 8 Stunden x $100/h = $800 (einmalig)
  • Laufende Wartung: ~0 Stunden/Monat
  • Erstes Jahr: $800 + $300 x 12 = $4.400

Der Break-Even liegt je nach Engineering-Kosten bei rund 50.000-80.000 Videos pro Monat. Darunter gewinnt die Cloud API klar. Einen Vergleich verschiedener Cloud-API-Optionen findest du in unserer FFHub vs AWS MediaConvert Analyse.

Integrationsmuster

Es gibt zwei Hauptwege, Video-Processing in deine SaaS-Architektur einzubauen.

Muster 1: Async mit Webhooks (empfohlen)

Am besten für Videos länger als 10 Sekunden oder wenn du mehrere Outputs brauchst.

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 │
└──────────────┘     └──────────────┘

Implementierung:

// Schritt 1: Beim Upload des Nutzers den Transcoding-Task absenden
app.post("/api/videos/upload", auth, upload.single("video"), async (req, res) => {
  const rawUrl = await uploadToS3(req.file);

  // Datensatz in der Datenbank anlegen
  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]
  );

  // Transcoding-Task absenden
  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: `https://your-app.com/webhooks/video-processed?video_id=${video.id}`,
    }),
  });

  // Gleichzeitig den Thumbnail-Task absenden
  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: `https://your-app.com/webhooks/thumbnail-generated?video_id=${video.id}`,
    }),
  });

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

// Schritt 2: Webhook-Callback entgegennehmen
app.post("/webhooks/video-processed", async (req, res) => {
  const { video_id } = req.query;
  const { status, outputs, error } = req.body.data;

  if (status === "succeeded") {
    await db.query(
      `UPDATE videos SET status = 'ready', processed_url = $1, processed_at = NOW() WHERE id = $2`,
      [outputs[0].url, video_id]
    );
    // Optional: den Nutzer benachrichtigen, dass das Video bereit ist
    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);
});

Muster 2: Sync für kleine Files

Für sehr kurze Videos (unter 10 Sekunden) oder Thumbnails kann synchron sinnvoll sein:

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

  // Task absenden
  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();

  // Pollen und warten (mit Timeout)
  const result = await pollWithTimeout(task_id, 60000); // 60s Timeout

  res.json({
    id: generateId(),
    status: "ready",
    url: result.outputs[0].url,
  });
});

Sparsam einsetzen. Alles, was länger als 30 Sekunden dauern könnte, läuft async.

Sicherheits-Aspekte

Video-Processing in einem SaaS-Kontext bringt spezifische Sicherheitsthemen.

Signed URLs

Niemals rohe Storage-URLs an User rausgeben. Nimm Signed URLs mit Ablauf:

// Zeitlich begrenzte signierte URL erzeugen
function generateSignedUrl(objectKey, expiresInSeconds = 3600) {
  return s3.getSignedUrl("getObject", {
    Bucket: process.env.S3_BUCKET,
    Key: objectKey,
    Expires: expiresInSeconds,
  });
}

// URLs für das Frontend sind immer signiert
app.get("/api/videos/:id/stream", auth, async (req, res) => {
  const video = await db.query("SELECT * FROM videos WHERE id = $1", [req.params.id]);

  // Berechtigungsprüfung
  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 Stunden gültig
  res.json({ url: signedUrl });
});

Data Retention

Definiere und erzwinge Data-Retention-Policies:

// Verarbeitete Quelldateien regelmäßig aufräumen
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-Validation

User können bösartige Files hochladen. Validiere vor der Verarbeitung:

async function validateVideoFile(fileUrl) {
  // Mit FFprobe prüfen, ob es wirklich eine Videodatei ist
  const probe = await runFFprobe(fileUrl);

  // Prüfen, ob ein Video-Stream vorhanden ist
  const videoStream = probe.streams.find((s) => s.codec_type === "video");
  if (!videoStream) {
    throw new Error("No video stream found");
  }

  // Maximale Länge prüfen (Missbrauchsschutz)
  if (parseFloat(probe.format.duration) > 3600) {
    throw new Error("Video exceeds 1 hour limit");
  }

  // Maximale Auflösung prüfen
  if (videoStream.width > 7680 || videoStream.height > 4320) {
    throw new Error("Resolution exceeds 8K limit");
  }

  return probe;
}

Multi-Tenant-Isolation

Stell sicher, dass die Verarbeitung eines Tenants die eines anderen nicht beeinflusst:

  • Pro Tenant einen eigenen Storage-Pfad: s3://bucket/tenant-{id}/videos/...
  • Tenant-ID in Webhook-URLs zur Verifikation
  • Pro-Tenant-Verarbeitungsquoten

Praxisbeispiel: Vollständiges SaaS-Video-Modul

Hier ein produktionsreifes Video-Processing-Modul für eine SaaS-Anwendung:

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

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

  // Verarbeitung absenden (Konvertierung + Komprimierung + Thumbnail)
  async processUpload(videoId, sourceUrl, options = {}) {
    const {
      maxResolution = 1080,
      crf = 23,
      generateThumbnail = true,
      generatePreview = false,
    } = options;

    const tasks = [];

    // Haupt-Transcode
    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`
      )
    );

    // Thumbnail
    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`
        )
      );
    }

    // Animierte Vorschau
    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);
  }

  // Clip ausschneiden
  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`
    );
  }

  // Video komprimieren (spart Speicherkosten)
  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: webhookUrl }),
    });

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

    return res.json();
  }
}

export default VideoService;

Verwendung in deiner SaaS-Anwendung:

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

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

// Upload des Nutzers verarbeiten
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" });
});

Kostenprognose nach SaaS-Phase

So sehen Video-Processing-Kosten in den verschiedenen Wachstumsphasen aus:

Startup (0-1.000 Kunden)

  • Video-Volumen: ~500 Videos/Monat
  • Durchschnittliche Dauer: 3 Minuten
  • Bedarf: Format-Konvertierung + Thumbnail
  • Geschätzte Monatskosten: $30-50
  • Engineering: 1 Tag Setup

Growth (1.000-10.000 Kunden)

  • Video-Volumen: ~5.000 Videos/Monat
  • Durchschnittliche Dauer: 5 Minuten
  • Bedarf: Konvertierung + Thumbnail + Komprimierung
  • Geschätzte Monatskosten: $200-400
  • Engineering: 1-2 Tage für Webhook-Integration

Scale (10.000-100.000 Kunden)

  • Video-Volumen: ~50.000 Videos/Monat
  • Durchschnittliche Dauer: 5 Minuten
  • Bedarf: Konvertierung + Multi-Resolution + Thumbnail + Preview
  • Geschätzte Monatskosten: $1.500-3.000
  • Überlegungen: Volume-Discounts, Hybrid für Grundlast

Enterprise (100.000+ Kunden)

  • Video-Volumen: 500.000+ Videos/Monat
  • Bedarf: Volle Pipeline + Custom Codecs + Compliance
  • Geschätzte Monatskosten: $10.000-25.000
  • Überlegungen: Self-Hosted für Grundlast + Cloud API für Burst, dedizierte Infrastruktur

In jeder Phase: Vergleich API-Kosten gegen Engineering-Zeit fürs Bauen und Pflegen einer eigenen Lösung. Ein Senior Engineer kostet pro Monat schon mehr als die API-Kosten in der Startup- und Growth-Phase.

SaaS-spezifische Patterns

Verarbeitungs-Quotas pro Plan

Mach Video-Processing zum Bestandteil deines Pricings:

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; // unbegrenzt

  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-Verarbeitungs-Queue

Blockier deine API nicht mit Video-Processing. Nimm eine simple, datenbankgestützte Queue:

// In die Queue legen
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-Prozess (fragt alle 5 Sekunden offene Jobs ab)
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 fürs Frontend

Gib deinem Frontend einen Weg, den Verarbeitungsstatus anzuzeigen:

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,
  });
});

Fazit

Video-Processing in deinem SaaS hinzuzufügen, heißt nicht zwingend, ein Video-Infrastruktur-Team aufzubauen. Das Entscheidungs-Framework ist klar:

  1. Identifiziere deinen tatsächlichen Bedarf. Die meisten SaaS-Produkte brauchen Format-Konvertierung, Komprimierung und Thumbnails. Mehr nicht.
  2. Wähl Build vs. Buy nach Volumen und Teamgröße. Unter 100K Videos/Monat mit kleinem Team? Buy. Darüber mit dediziertem Infra-Team? Bauen erwägen.
  3. Async First integrieren. Webhooks für alles, was länger als ein paar Sekunden dauert. Deine User danken dir, dass sie keinen Spinner anstarren müssen.
  4. Plane für Wachstum. Start mit Cloud API, wechsel zu Hybrid, wenn das Volumen es rechtfertigt.

Die Code-Beispiele hier geben dir einen funktionierenden Startpunkt. Die VideoService-Klasse lässt sich in jede Node.js-SaaS einbauen und mit dem Bedarf erweitern.

Für den Cloud-API-Ansatz bietet FFHub.io eine simple FFmpeg-API — die gleichen Commands, die du lokal laufen lassen würdest, ausgeführt in der Cloud mit automatischer Skalierung und ohne Infrastruktur. Falls du stattdessen über Serverless nachdenkst, lies erst über die Probleme von FFmpeg auf Lambda.

Verwandte Artikel