Tutorials Tutorial

Placeholder Video APIs for Testing a Render Pipeline

Published 2026.08.25 videobycode Editorial Team

The Editframe CLI finishes the TPL-30 payload end to end on its free tier, and seven other tools ran the same template with different trade-offs in speed, output size, and licence terms.

The shortest path from a video API for testing question to a working file is declarative, not a filter graph. The Editframe CLI takes a payload of layers and durations, and its free-tier local render of the TPL-30 template finished in 17.9 s, producing a 1.46 MB MP4 that runs 30.080 s. That run is vbc-2026-08-25-editframe-tpl30-001. Its payload, main.js, appears in full below rather than as a summary, because a paraphrase is not the code that ran.

A placeholder video API for testing does not need a paid account or a render queue to answer this question. Editframe's local mode starts a Vite dev server and a headless instance of the system's own Chrome. The free tier covers companies of three employees or fewer at no cost, and no cloud endpoint takes part anywhere in the run. The same TPL-30 payload, a title card, a captioned still image, and an outro, also ran against seven other tools: ffmpeg, MoviePy, MLT, Remotion, Revideo, Diffusion Studio Core, and Editly. Two of the seven did not return a benchmark-equivalent file. Editly failed before it could render anything. Diffusion Studio Core rendered correctly but stamped a vendor watermark onto the output.

Wall-clock speed is a separate question from setup effort, and the two do not point at the same tool. Among the six tools that produced a usable file, the fastest single-process render belongs to ffmpeg, not Editframe. That trade-off, and what each tool's number actually measures, is the subject of the two sections after the code below.

Versions and prerequisites

One version string sits behind every figure in this comparison, and swapping any of them changes what gets measured. The Editframe run behind 17.9 s used @editframe/cli 0.59.40 and @editframe/elements 0.59.40, bundled by Vite 8.2.2, on Node v25.6.1, driving system Google Chrome 152.0.7977.55. No API key and no account were involved. EF_NO_TELEMETRY=1 suppressed the CLI's telemetry request.

The version matters because Editframe's CLI exposes fps and scale as encode controls, plus a codec selector, and nothing else. It has no CRF flag and no preset flag; a pixel-format flag doesn't exist either. TPL-30 specifies crf 20 and preset medium; ffmpeg, MoviePy, and MLT can match those settings exactly, but Editframe cannot set them at all. Whatever bitrate the browser's own WebCodecs encoder picks is what lands in the file.

Editframe's dev media server caches image assets on first request. tools/editframe/.cache was emptied before this run, so 17.9 s includes that cold-cache cost rather than hiding it behind a warmed-up server. A repeat run against a populated cache would report a different figure. That figure has not been recorded here.

Each of the other seven tools brings its own prerequisite chain, and none of them are interchangeable. ffmpeg 8.0.1 on the measuring machine is a Homebrew build compiled without libfreetype, so its drawtext filter does not exist. The benchmark works around this the same way for every tool: it pre-renders the three on-screen text strings into transparent PNGs, and each tool composites the image instead of drawing type. MoviePy reports dist=2.2.1 and version 2.1.2 in the same install. It runs on Python 3.14.3, with numpy 2.5.1 and Pillow 11.3.0 alongside it, and its FFMPEG_BINARY variable points at the same system ffmpeg the bare ffmpeg run used, not the copy bundled inside imageio-ffmpeg. The two runs share an encoder build rather than two different ones.

MLT's melt CLI is version 7.40.0 against libavformat 63.1.101, driven from a generated XML timeline rather than a Python or JavaScript API. Revideo bundles its own ffmpeg 4.4, separate from and older than the 8.0.1 every other row encoded with, and @revideo/ffmpeg defaults to that bundled binary regardless of what is installed on the host. A script written against one of these version strings and pointed at a different one is not running the same test that produced these numbers.

The code that produced the output

The file behind 17.9 s is quoted below exactly as it ran. It is not paraphrased into pseudo-code, because a rewrite would not be the payload the run recorded.

/**
 * BM-1 (TPL-30) as an Editframe composition.
 *
 * This is the payload, not the runner. tools/editframe/run.mjs drives
 * `editframe render` over this directory, times it, and measures the result.
 *
 * Every layout number is read from ../bm1.json rather than typed here, for the
 * same reason the ffmpeg, MoviePy and Remotion payloads read it: four
 * transcriptions of one benchmark are only comparable if they cannot drift apart.
 *
 * The text is composited from the same pre-rasterised PNGs the other payloads use
 * (bm1.json -> text_rendering), so no web font is loaded and no text shaping
 * happens inside the measured render.
 *
 * Assets resolve through Vite's publicDir, which tools/editframe/vite.config.mjs
 * points at ../_assets — the same directory every other payload reads. A bare
 * filename is therefore served at the site root, exactly as Remotion's
 * --public-dir=../_assets does.
 */

import '@editframe/elements';
import spec from '../bm1.json';

const V = spec.video;
const PAL = spec.palette;

const sceneById = (id) => spec.scenes.find((s) => s.id === id);
const title = sceneById('title');
const body = sceneById('body');
const outro = sceneById('outro');

/** "_assets/text-title.png" -> "/text-title.png" (served from publicDir). */
const asset = (p) => `/${String(p).replace(/^_assets\//, '')}`;

const seconds = (from, to) => `${to - from}s`;

/**
 * An ef-image has no natural duration, so it defaults to 0 and never becomes
 * active. Editframe calls this the editorial choice: a still has to be told how
 * long it is on screen. Each image is given its own scene's length.
 */
const imageDuration = (scene) => seconds(scene.start_s, scene.end_s);

const FILL = 'position:absolute;left:0;top:0;width:100%;height:100%;';
const SCENE = `${FILL}`;

const rule = title.accent_rule;
const bar = body.bar;

document.getElementById('stage').innerHTML = `
  <ef-timegroup
    id="root"
    mode="contain"
    fps="${V.fps}"
    style="position:relative;display:block;width:${V.width}px;height:${V.height}px;background:${PAL.background};overflow:hidden"
  >
    <ef-timegroup mode="sequence" style="${FILL}">

      <!-- Scene 1 — title card -->
      <ef-timegroup mode="fixed" duration="${seconds(title.start_s, title.end_s)}"
        style="${SCENE}background:${title.background};">
        <div style="position:absolute;left:${rule.x}px;top:${rule.y}px;width:${rule.w}px;height:${rule.h}px;background:${rule.color};"></div>
        <ef-image src="${asset(title.text_png)}" duration="${imageDuration(title)}" style="${FILL}object-fit:contain;"></ef-image>
      </ef-timegroup>

      <!-- Scene 2 — still image, caption bar, caption -->
      <ef-timegroup mode="fixed" duration="${seconds(body.start_s, body.end_s)}" style="${SCENE}">
        <ef-image src="${asset(body.image)}" duration="${imageDuration(body)}" style="${FILL}object-fit:${body.image_fit};"></ef-image>
        <div style="position:absolute;left:${bar.x}px;top:${bar.y}px;width:${bar.w}px;height:${bar.h}px;background:${bar.color};opacity:${bar.opacity};"></div>
        <ef-image src="${asset(body.text_png)}" duration="${imageDuration(body)}" style="${FILL}object-fit:contain;"></ef-image>
      </ef-timegroup>

      <!-- Scene 3 — outro -->
      <ef-timegroup mode="fixed" duration="${seconds(outro.start_s, outro.end_s)}"
        style="${SCENE}background:${outro.background};">
        <ef-image src="${asset(outro.text_png)}" duration="${imageDuration(outro)}" style="${FILL}object-fit:contain;"></ef-image>
      </ef-timegroup>

    </ef-timegroup>

    <!-- The music bed is a sibling of the sequence, never a child of it: a
         sequence would treat it as one more beat and double the runtime. -->
    <ef-audio src="${asset(spec.audio.file)}" volume="1" duration="${V.duration_s}s" offset="${spec.audio.start_s}"></ef-audio>
  </ef-timegroup>
`;

This script's text layers are each a pre-rasterised PNG from _assets, not a font loaded at render time. Keeping that constraint is what makes this run comparable to the ffmpeg and MoviePy runs, which share the same restriction for a different reason: a build with no libfreetype. The benchmark's caption bar sits at 1920×160, offset to y=820, at an opacity of 0.55, over the still image between the body scene's start and end marks, 5 s and 25 s. The title and outro scenes run from 0 s to 5 s and from 25 s to 30 s.

Nothing in this script issues a network call. It writes to a local output path, and the CLI's own dev server serves the static assets it composites; no account and no cloud render queue sit between the payload and the file it produces. Adapting this for a real pipeline means replacing the four PNG paths and the background-music WAV with production assets, then setting the codec flag along with fps and scale, explicitly on the command line, since this CLI does not read encode settings out of a shared benchmark definition the way the MLT and ffmpeg payloads read crf and preset out of bm1.json.

What the run produced

The eight recorded runs did not measure one thing at eight speeds. They measured eight different spans of work, and lining them up without saying so would misstate what each number is.

Wall time by tool, TPL-30, single run each
020406080100Diffusion Studio Core6.4sEditframe17.9sFFmpeg 8.0.17.5sMLT (melt) 7.40.010.2sMoviePy 2.2.186.6sRemotion 4.0.50718.2sRevideo 0.11.021.8s

Sourcevideobycode verification runs(TPL-30, measured 2026-08-09 and 2026-08-25)

ToolStatusWall timeOutput sizeMeasured duration
Diffusion Studio Coreunsupported (watermark)6.4 s1.16 MB30.080 s
Editframeok17.9 s1.46 MB30.080 s
Editlyerror (import failed)1.2 sn/an/a
FFmpegok7.5 s0.99 MB30.000 s
MLT (melt)ok10.2 s1.02 MB30.016 s
MoviePyok86.6 s1.01 MB30.000 s
Remotionok18.2 s1.50 MB30.059 s
Revideook21.8 s0.60 MB30.000 s

Sourcevideobycode verification runs, TPL-30 benchmark, measured 2026-08-09 and 2026-08-25

ffmpeg's 7.5 s is the lifetime of a single process running one filter graph through one encode step before it exits. Editframe's 17.9 s, Remotion's 18.2 s, and Revideo's 21.8 s each start a Vite dev server first, then launch a full Chrome or Chromium instance and capture 900 frames through the browser, before encoding even begins. Comparing ffmpeg's number against those three as a ranking of raw renderer speed mixes a bare encode with a browser pipeline. The fairer comparison sits among three single-process tools reading the same PNG assets through a comparable encode step: ffmpeg, MLT's 10.2 s, and MoviePy's 86.6 s.

Output size tells a separate story from wall time. ffmpeg's file lands at 0.99 MB because the benchmark's crf 20 and preset medium settings pass through exactly; MoviePy and MLT, sharing that encoder configuration, land close to the same figure. Editframe, Remotion, and Revideo cannot take a CRF or preset argument at all, only a codec name and, for Revideo, a container format, so each browser encoder settles on its own bitrate. Revideo's file is the smallest of the eight at 0.60 MB. Its run notes record that the AAC track came in under the benchmark's 192 kbps target, because the exporter takes only a format argument and nothing finer; the actual bitrate it chose went uncaptured as a separate metric.

Completed runs land within a fraction of a second of the benchmark's nominal length rather than exactly on it. Editframe and Diffusion Studio Core both measured 30.080 s, and Remotion measured 30.059 s. Only ffmpeg, MoviePy, and Revideo hit the exact 30 s the benchmark specifies. None of these gaps matter for a placeholder clip used to smoke-test a pipeline. A downstream check that asserts an exact frame count against the nominal duration will fail against three of the eight files here, which is worth knowing before wiring one up.

This table's runs each cost $0 to produce, since every tool rendered locally with no consumption pricing attached to the render itself. What a tool costs in production is a different question, and the table below covers that one. Two rows, Editframe and Remotion, carry no self-hosted cost line at all, because both cap free use at three employees rather than charging per render.

ToolLicenceSelf-hosted costPaid planSource
Diffusion Studio CoreMPL-2.0, free with watermarknot applicablelicence-key price unverifiedGitHub README
Editframeproprietary CLI, free ≤3 employeesnot applicableTeam $49/mo, Cloud $99/moeditframe.com/pricing
EditlyMIT$0none publishedGitHub LICENSE
FFmpegLGPL / GPL$0noneffmpeg.org/legal
MLT (melt)LGPL$0noneGitHub COPYING
MoviePyMIT$0noneGitHub
Remotionfree ≤3 employeesnot applicablenot publishedGitHub LICENSE
RevideoMIT$0hosted service unverifiedGitHub LICENSE

Sourcevendor pricing and licence pages; Editframe checked 2026-08-09, Diffusion Studio Core checked 2026-08-25

What breaks, and how to get past it

Two of the eight runs did not return a usable file at all, and three more that reported a clean exit can still produce wrong output under specific conditions the exit code never flags.

Editly never got to a render. Its 0.14.2 release pins two native Node dependencies, node-canvas and headless-gl, and the second loads unconditionally at module load, even for a spec, like this one, that uses no GL transition, because the library's own index.js pulls in a transitions module whose first line imports gl. gl 5.0.3 builds its bundled ANGLE from source and fails to compile against Node 25's V8 headers, with twenty recorded compile errors. Substituting the newest release, 8.1.6, through a pnpm override fails at the same point for the same reason, which rules out a version bump as the fix. The wall time recorded for this run, 1.2 s, measures a failed import, not a render, and does not belong in any speed comparison. Working around this means an older Node runtime that headless-gl still compiles against, or a fork that drops the GL dependency for specs with no transition. Any pipeline that shells out to Editly on Node 25 hits this same failure before a single frame renders, regardless of which spec it points at.

Diffusion Studio Core's free output is not the benchmark file

Diffusion Studio Core's watermark makes the free-tier output not the benchmark: the engine renders correctly and stamps every render with a visible mark, removable only through a paid one-time key. Treat any unlicensed render as a proof of concept, not a deliverable.

Diffusion Studio Core rendered the TPL-30 payload with no account and no network call during the run, and no process contacted a licence server; the watermark is what disqualifies the file anyway. The vendor's own README states the free terms directly: use is free at any volume as long as the "Made with Diffusion Studio" mark stays on the output. That is why this row is recorded as unsupported rather than ok, even though it finished in 6.4 s and produced a 1.16 MB file that otherwise plays back correctly. Teams testing against this tool need to budget for the licence key before treating any free-tier output as representative of production. That key's own price could not be checked past 2026-08-25, when the vendor's pricing URL redirected to its site root with no pricing text on the page. A comparison that reports this tool's wall time next to a licensed one's without flagging the watermark is comparing a proof of concept to a shippable file.

Three tools with an ok status carry a bug each that a status check alone will not catch. An Editframe ef-image element with no explicit duration attribute defaults to zero, so the element never draws, yet the CLI still reports all 900 frames captured and exits clean; the result is a black picture for however long that image was meant to appear. The fix is mechanical: assert that every ef-image in a generated payload carries an explicit duration before the render starts, rather than trusting the frame count. A duration-based sanity check on the finished file catches nothing here either, since the frame count still matches the spec.

MLT has no primitive for a filled rectangle, so both rectangles TPL-30 asks for, the accent rule and the caption bar, become full-frame colour producers cropped by a qtblend transition. qtblend preserves the source aspect ratio unless distort=1 is set; without that flag the accent rule renders as a small dash, and the caption bar renders at a fraction of its intended size, with no warning and a successful exit either way. A pipeline that trusts melt's zero exit code as a pass ships both shapes wrong with no log line to catch it.

Revideo resolves every audio asset's path in Node as outDir/../public/ joined with the element's src value, ignoring Vite's publicDir setting entirely. Point publicDir anywhere else and ffprobe fails to find the file; the exporter then substitutes a generated silent track, and the render still reports success with a full-length AAC stream containing nothing, a failure no ffprobe check catches after the fact. A CI job that only checks for a non-empty output file passes this render even though the soundtrack is silence.

Start from the ffmpeg payload, not a browser-based renderer. It is the only tool measured here with full control over crf and preset, plus pixel format, and it is the fastest of the three single-process encoders. Copy payloads/tpl30/ffmpeg.sh and point the four PNG paths and the WAV file at local assets, then run it once. The first thing to break is text: without libfreetype in the local ffmpeg build, drawtext does not exist, and the fix used throughout this comparison is pre-rasterising each string into a transparent PNG before the filter graph runs.

What we didn't test

TPL-30 is the only benchmark in these records, and every run above answers one question, does a render pipeline finish and produce a file, not the questions below.

  • Only TPL-30 ran; no other benchmark definition exists in the records, so nothing here speaks to longer clips or different scene counts.
  • All eight runs used h264; none tried a different codec.
  • Each tool rendered once on one machine; batch throughput, concurrency, and queueing went unmeasured.
  • No run burned captions into video.
  • Extracting a thumbnail from a finished file wasn't tested either.

Questions people ask

How long did the TPL-30 render actually take, and on what machine?

Wall time depends on the tool: the Editframe CLI's local render finished in 17.9 s and the bare ffmpeg run finished in 7.5 s, both on the same machine used for every run in this comparison. Browser-based tools measure the whole invocation, including a Vite server start and a Chrome launch, not just the encode, so their numbers are not directly comparable to ffmpeg's single-process time.

Why are Diffusion Studio Core's prices shown as unverified?

Diffusion Studio Core's pricing page redirected to the site root with no pricing text when checked on 2026-08-25, so the one-time licence-key price that removes its watermark could not be read from the vendor's own page. No figure is asserted anywhere in this comparison because none could be sourced.

What does Diffusion Studio Core's free tier actually cover?

The free tier covers rendering at any volume with no account and no network call, and the run recorded here finished in 6.4 s with no licence key involved. It stops at the output file itself: every free render carries a visible watermark that only a paid one-time key removes.

References

Ref Editframe pricing (checked 2026-08-09)

Ref Diffusion Studio Core README (checked 2026-08-25)

Ref Editly LICENSE (checked 2026-08-25)

Ref FFmpeg legal (checked 2026-08-09)

Ref MLT Framework COPYING (checked 2026-08-25)

Ref MoviePy repository (checked 2026-08-09)

Ref Remotion LICENSE (checked 2026-08-09)

Ref Revideo LICENSE (checked 2026-08-25)

#video-api#render-pipeline#video-testing#ffmpeg#placeholder-video

Related articles

All articles in Tutorials