Trigger icon
Commands icon
Docker icon
Script icon
If icon
Upload icon
Purge icon
Log icon
Fail icon
SlackIncomingWebhook icon

Video Transcode QA and CDN Publish Pipeline

Transcode new S3 videos into 720p and 1080p with ffmpeg, verify duration, size and resolution in Python, publish to a CDN bucket and purge Cloudflare cache.

Categories
CloudInfrastructure

A video is not ready for delivery the moment it lands in a bucket, it still has to be encoded, checked and published before anyone can watch it. This blueprint watches an Amazon S3 prefix for new source videos, encodes each one into 720p and 1080p MP4 variants with ffmpeg inside a container, and runs a Python QA step that compares ffprobe metadata for duration, resolution and file size against the source. Only variants that clear the gate are copied into the CDN origin bucket, and the Cloudflare cache is purged right after so viewers see the new renditions instead of stale edge copies. Failures anywhere in the pipeline post to Slack through the flow level error handler.

How it works

  1. The new_source_video trigger (io.kestra.plugin.aws.s3.Trigger) polls the incoming/video/ prefix of the source bucket every minute and starts one execution per new object, leaving the object in place with action: NONE.
  2. transcode (io.kestra.plugin.scripts.shell.Commands running on the Docker task runner with a jrottenberg/ffmpeg image) stages the triggered object from internal storage as source.mp4, encodes variant_720p.mp4 and variant_1080p.mp4, and writes ffprobe JSON for the source and both variants plus a sizes.txt manifest.
  3. qa_check (io.kestra.plugin.scripts.python.Script) parses those probe files, checks that each variant keeps the source duration within 10 percent, lands on its expected height, stays non empty and does not grow past twice the source size, then emits a pass or fail verdict with the full violation list through the ::json:: output protocol.
  4. qa_gate (io.kestra.plugin.core.flow.If) branches on outputs.qa_check.vars.verdict, so the publish path only runs when the QA step returned pass.
  5. On the passing path push_720p and push_1080p (io.kestra.plugin.aws.s3.Upload) copy both renditions into the CDN origin bucket, purge_cache (io.kestra.plugin.cloudflare.cache.Purge) invalidates the two rendition URLs, and publish_report (io.kestra.plugin.core.log.Log) writes the publish receipt to the execution log.
  6. On the failing path fail_qa_gate (io.kestra.plugin.core.execution.Fail) ends the run with the violation list, and the errors handler posts a Slack alert for any other failure in the pipeline.

What you get

  • Two CDN ready renditions, 720p and 1080p, produced automatically from every upload without a hand written encoding script.
  • A QA gate that catches truncated encodes, wrong heights and suspicious file sizes before anything reaches the origin bucket.
  • A Cloudflare purge that runs only after the new objects are actually in place, so the edge never refetches too early.
  • Typed flow outputs with the verdict, the violation list, both variant files and the published object keys for downstream flows to consume.
  • A Slack alert on any failure instead of a silent gap in the delivery chain.

Who it's for

  • Media platform teams that need consistent renditions for every uploaded clip.
  • Content and marketing engineering groups delivering video through a CDN.
  • Platform engineers who want encoding, verification and cache invalidation as one auditable execution rather than three separate scripts.

Why orchestrate this with Kestra

The three steps here belong together but usually live in different places, a queue consumer runs ffmpeg, a cron job checks outputs, and a deploy hook purges the edge. Kestra keeps them in one flow with a shared execution id, so the QA verdict, the uploaded keys and the purge result are visible on a single timeline. The If gate is declarative, the failure alert is a flow level policy instead of error handling code, and every artifact is captured as a task output that another flow can read. Adding a rendition or swapping the CDN becomes a YAML change you can review in a pull request.

Prerequisites

  • An S3 source bucket receiving video files under incoming/video/, and a separate origin bucket that sits in front of your CDN.
  • Docker available to the Kestra worker, since the transcode step runs ffmpeg through the Docker task runner.
  • Python 3 available to the worker for the QA script, which uses only the standard library.
  • A Cloudflare zone proxying the CDN host, so the purge task can invalidate the rendition URLs.

Secrets

  • AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY: read by the S3 trigger and by both upload tasks, they need s3:GetObject on the source bucket and s3:PutObject on the origin bucket.
  • CLOUDFLARE_API_TOKEN: token with the Cache Purge permission on the target zone.
  • SLACK_WEBHOOK_URL: incoming webhook used by the flow level error handler.

Quick start

  1. Add the secrets above to your Kestra namespace or environment.
  2. Point variables.source_bucket at your landing bucket and variables.aws_region at its region, then set the cdn_bucket, cdn_host and cloudflare_zone_id inputs to your CDN values.
  3. Drop a video into s3://<source_bucket>/incoming/video/ and watch the flow transcode, verify, publish and purge in one run.
  4. To trigger the same pipeline on demand from an API or a deploy script, add a second trigger next to the S3 one:
triggers:
  - id: on_demand
    type: io.kestra.plugin.core.trigger.Webhook
    key: "{{ secret('TRANSCODE_WEBHOOK_KEY') }}"
  1. Read the verdict and published_keys outputs from the execution page, or from a downstream flow with {{ outputs.media_transcode_cdn_pipeline.vars.verdict }}.

How to extend

  • Add a third 4K rendition by extending the ffmpeg commands, the probe file list and the EXPECTED_VARIANTS map in the QA script.
  • Switch the output container to HLS by emitting .m3u8 playlists and purging the playlist URL instead of the MP4 URLs.
  • Swap the Cloudflare purge for io.kestra.plugin.fastly.purge.Url when Fastly fronts the origin instead.
  • Send a Slack success message with the variant sizes from outputs.transcode.outputFiles after the purge task.
  • Add a io.kestra.plugin.core.trigger.Schedule trigger that re encodes a folder of archived masters on a nightly cadence.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.