cmake --build build --config Release && ctest --test-dir build -C Release
Every push and pull request builds on Windows, Linux and macOS and runs this same line on Linux, under Xvfb so the headless binary uses the GL path that ships. So a suite that only passes on the machine that wrote it will be caught, and a suite whose fixture, device or model weights are absent has to skip and say so rather than fail: a runner has no camera, no graphics card and no Whisper checkpoint, and every section here is written to survive that. Keep that property when adding tests.
ctest generates its own media — two files with known content, a moving bar over a
gradient and a tone at a known level, differing in size, aspect, frame rate and length,
and a third with no audio stream in it at all, which is not the same file as one
whose soundtrack is quiet and is the only thing that separates "the mix" from "a mix
nothing feeds" — and runs every suite against them. Four more are each about a stream
the rest take for granted: one with no video stream in it at all, the mirror of
the silent one and the only thing separating the composite from a composite nothing
feeds; one whose pictures carry a display matrix, which is the only thing
separating a clip laid out upright from one laid out on its side; one with a
gpmd data track, which is a stream that is neither picture, sound nor cues,
is identified by its fourcc alone, and carries real GPMF — a payload whose
SCAL divisors are the difference between 9.81 m/s² and 981; and one with a dvdsub track, whose cues are
pictures of characters and therefore cannot be converted, burned in or read for what
they say. None can be faked with content: a picture that
happens to be black is not an absent one, a picture that happens to be tall is not a
rotated one, a track full of bytes is not a track something can still find by
name or read, and a text track with an odd payload is still a text track. Nothing is checked in and nothing depends on what a file you happened to have
lying around contains.
A UI suite is given one more thing before it starts: no leftovers. localStorage is
written beside the application, so ui/.storage.json outlives the run that wrote it — which is
what makes remembered settings work and what makes a suite unrepeatable, since every reader in
ui/ is written to sanitise a blob left by an older version of this code and a suite that
finds one is not testing the input it states. ctest removes the file first, as a fixture of
the same kind as the media. Running a suite by hand does not, so delete it yourself if a result
surprises you — a failure caused this way names whatever the leftover state confused, not the
file that confused it.
Each suite also runs standalone against any real file, which is how to check behaviour against footage the fixtures do not resemble:
./build/Release/ffmpeg-bro-decodetest <file> [--rotated <file>] [--sound-only <file>]
./build/Release/ffmpeg-bro-exporttest <file> [<file2>] # renderer: geometry, opacity, mix, cancel
./build/Release/ffmpeg-bro-captest <file> # muxers, demuxers, protocols, devices, decoders
./build/Release/ffmpeg-bro-inputtest <file> [<rotated>] [<cues>] # an -i: forced demuxer, options, window, token, filters
./build/Release/ffmpeg-bro-seqtest <fixture-dir> # sequences, stills, -stream_loop, concat, image output
./build/Release/ffmpeg-bro-playbacktest <file> # the preview's cadence: which call makes a picture, and for which moment
./build/Release/ffmpeg-bro-capturetest out [<fixture-dir>] # devices: an endless input, recording one, a session of several, a file laid over one
./build/Release/ffmpeg-bro-hwtest <file> [<av1.mp4>] # the GPU: what is here, is it the same picture, what does each path cost — and, with an AV1 file, that a codec whose default decoder is software-only still opens on a device
./build/Release/ffmpeg-bro-datatest <telemetry.mp4> [<real-gopro.MP4>] # a data track: which parser, what GPMF says, and a payload it may not trust
./build/Release/ffmpeg-bro-markstest <marks.m4a> [<silent.mp4>] [<sound.m4a>] # a soundtrack: is a transient found, is it at the right second, is the tone the frequency it was written at
./build/Release/ffmpeg-bro-proxytest <file> [<portrait>] # the file a scrub is answered out of: is every frame really a keyframe, is it the size and shape asked for, is it the same length
./build/Release/ffmpeg-bro-headless ui/ tests/ui_player.js -- <file> [<file2>] [<rotated>] [<sound-only>]
./build/Release/ffmpeg-bro-headless ui/ tests/ui_sources.js -- <file>
./build/Release/ffmpeg-bro-headless ui/ tests/ui_hardware.js -- <file>
./build/Release/ffmpeg-bro-headless ui/ tests/ui_sequence.js -- <fixture-dir>
./build/Release/ffmpeg-bro-headless ui/ tests/ui_export.js -- <file> [<video-only>] [<sound-only>] [<with-data-stream>]
./build/Release/ffmpeg-bro-headless ui/ tests/ui_report.js -- <file>
./build/Release/ffmpeg-bro-headless ui/ tests/ui_measure.js -- <file>
./build/Release/ffmpeg-bro-headless ui/ tests/ui_subtitles.js -- <fixture-dir>
./build/Release/ffmpeg-bro-headless ui/ tests/ui_document.js -- <file> [<file2>]
./build/Release/ffmpeg-bro-headless ui/ tests/ui_output.js -- <file>
./build/Release/ffmpeg-bro-headless ui/ tests/ui_find.js -- <file>
./build/Release/ffmpeg-bro-headless ui/ tests/ui_load.js -- <file>
./build/Release/ffmpeg-bro-headless supercut/ tests/supercut.js -- <file>
./build/Release/ffmpeg-bro-headless ui/ tests/ui_capture.js # needs no media
./build/Release/ffmpeg-bro-headless ui/ tests/ui_filtergraph.js # needs no media
./build/Release/ffmpeg-bro-headless ui/ tests/ui_graph.js [-- <file>] # media only for the last four sections
hwtest has the same problem one further on: CI has no graphics card, and
unlike a camera there is no lavfi to stand in for one. So it splits what the
other suites do not. The assertions run everywhere and are about the shape of
the answer — enumeration answers something, a type reported present can be
created and is shared rather than remade, a type reported absent refuses with a
sentence, a codec the device cannot decode is refused before a packet is read —
and every one of them is reachable on a machine with nothing. How many devices
of a type there are is checked the same way: the indices the enumeration
reported can all be opened, the first one past the end refuses, and they start at
0 and are contiguous. One card satisfies that as well as two do, and a machine
with none skips it — asserting two would be asserting something about the
hardware, which is the line this whole suite is drawn along. The numbers are
printed and never asserted on, because a threshold on them would be a
statement about the machine rather than about the code; where they belong is
this README, beside the name of the hardware they came from.
The one equivalence check is worth its own note. A hardware decoder is not bit-exact with a software one and is not required to be — NVDEC and the CPU implement the same standard and differ in rounding and in deblocking arithmetic, all of it within what H.264 permits — so what is asserted is a PSNR floor of 40 dB. Two conformant decoders of one bitstream land in the forties; every mistake that could be made here (a frame out of step, a plane swapped, a download that lost the colour tags) lands under twenty. There is a great deal of room between those and the threshold sits in it.
capturetest and ui_capture.js have a problem the others do not: CI has no
camera. The vehicle is lavfi — libavfilter's input device, -f lavfi -i testsrc=size=320x240:rate=25 — which is a device in exactly the way gdigrab is
(registered by avdevice_register_all(), opened by a forced -f, reporting no
duration, never ending) and is openable on any machine. It is not the same
mechanism as a source filter inside a filtergraph, which the Graph stage also has:
testsrc as a filter is a node with no input pad, and the lavfi
device wraps a whole graph up as a demuxer so libavformat can read it as an -i.
Two different places in the pipeline spelt almost identically.
Being the vehicle makes it worth one assertion of its own: the card's <video> is required to
reach a picture 1280 wide, which is a claim about the crossing rather than about the UI.
lavfi's packets are still wrapped_avframe — the probe is asserted to say so — so what changed
is that such a frame now survives the trip into the engine, and a preview that came back empty
would fail here rather than be worked around with a refusal on the card.
The machine's real devices are asked about as well, and whatever the answer is it is asserted rather than skipped: gdigrab is either in this build or it is not, and if it is then opening it either produces frames or says why it did not. A test that quietly passed because it found no camera would be worse than no test. On Windows with a desktop session, gdigrab opens — including headless — so what runs here is the real screen grabber.
A session — several devices at once — needs one thing more from the vehicle: a lavfi input
produces as fast as it can be read, and two of them free-running would exercise none of what
several inputs are about. So the session sections pace each one with the realtime/arealtime
filter, which is what makes the wall clock the thing under test rather than a formality, and
they skip by name where a build has not got it. What they assert is what several inputs added:
that two pictures composited by the graph both reach the file, that two sounds mixed by it do,
that a session of picture and sound together lines up, and that the refusals fire — several
inputs with no graph, and a device that produces nothing failing the job naming itself rather
than waiting for ever.
Several files out of one recording is asserted by reading them back, because a
size check alone passes for a second copy of the master and the whole claim is that
it is not one. capturetest records one graph — split, one branch cropped — into
two files and checks that the second is 160×120 where the first is 320×240 and that
their mean colours differ, which a tee of the same encode could not do; then it
records a proxy beside a master with no graph at all, where the two means agree and
only the size differs, which is the other reason for a second file. ui_capture.js
does the same through the stage: a split and a crop wired on the Graph stage, an
Also-write row pointed at the output of its own, the command bar asserted to map both
pads on one line, the clash of two files at one path caught before the press, and
then the recording run — the second file half the width of the first, off the disk.
ui_player.js drops real files on the real UI, plays them, scrubs, steps to the
last picture in the file, zooms the timeline, moves and deletes a clip, scales
and crops the picture, works the controls, and screenshots the viewer into
out/. Pass a second file to exercise the multi-clip transport. It also checks
the control strip's geometry — that every icon button drew its icon, that the
transport buttons are one width, that the transport is on the window's centre
line and the zoom controls on the timeline's left edge — because a mistyped
icon name or a stray width breaks none of the behaviour and all of the look.
Its last section needs no fixture at all, which is the whole point of it: a generator laid
out on the timeline is a clip whose pictures
libavfilter makes, so it is dragged, trimmed and played with nothing on disk involved. Four
things there are checked because they are the four places a generator is not a file — the list
of them is libavfilter's registry and not a table here, its length is a decision that a trim
raises, it takes no -i number, and it is never the master clock. What it derives to is in
ui_graph.js, what a document does with it in ui_document.js, and the render it produces in
ui_export.js.
Its second-to-last section needs no network, which is the same trick the other way round:
reading a clip for the span on screen rather than whole is decided by one flag on the input
(remote, ui/inputs.js), so the section sets it by hand on a ten-second fixture and
everything downstream of it — the settle, the grid, the strips, the honest blank where nothing
has been read — is the code a Twitch VOD goes through, against a file whose content a test can
check. The assertion the whole design is for is the one that pumps a second and a half of a
timeline that is holding still and requires that nothing was read.
ui_subtitles.js is the three things people mean by subtitles, each of which is
a different mechanism: the cue file arriving as an -i and being recognised
from what libavformat found in it, the stream row that carries or converts it,
the container narrowing the codec menu, the command bar printing the -i, the
-map and the -c:s, and the burn-in placed as an ordinary node with its path
escaped the way libavfilter needs it. It renders both and reads the results
back, because a subtitle track that is described correctly and not written is
the failure worth catching.
Both burn-in points are driven: the one over the whole canvas from the Sources stage, and the
one on a clip from its properties panel — where what is checked is the anchor (above the
setpts, which is the clock the cues are on), the si= that is not written because the
track is the first of its kind despite being stream 2, and the clip's element ending up pointed
at a /@fx/ view whose chain starts with the filter. The stream that proves si= counts
subtitle streams rather than streams is written out in the test as a shape rather than made as
a file, because two subtitle tracks in one container is not a fact any fixture here exists for;
the end-to-end half runs against the mp4 the same test rendered a page earlier.
The window is driven twice over the same two numbers, once with the row carrying and once with it converting, because the two keep different cues: at an in-point of 4.5 s a conversion keeps one of the fixture's three and zeroes the output at 4.5, and a copy keeps two and zeroes it at 4. Both claims are made against a rendered file in the export suite as well as against the panel here, since which cues survive a window is exactly the sort of thing a UI can be confidently wrong about on its own.
The fixture generator writes cues.srt and cues.ass beside the video, with the cues placed
so that a burn-in is measurable: a second of picture with nothing over it, a second with a
line over it, a second with nothing again. The export suite renders the same seconds with the
filter and without it and compares them at both moments — 99 dB apart before the cue and 31 dB
during it. Either half alone proves nothing, because a filter that did nothing passes the first
and a filter that ruined every frame passes the second.
One of cues.srt's three cues is marked up (<i>third cue</i>), which is there for the
decoded half: a cue does not arrive as its words but as an ASS dialogue line, so a reader that
printed the {\i1} override codes instead of the line passes against the two plain cues and
fails only against that one.
picture-cues.mkv is the fixture for the other family — a dvdsub track beside
a picture, written by the generator with libavcodec's own dvdsub encoder. It is
the only fixture that reaches the three refusals a bitmap track earns (it cannot
become text, cannot be burned in, and has no words to read) and the only one a
drawn bitmap cue can be rendered from, because a text track with an odd payload
is still a text track. Its cues are at the same three moments the sidecars use,
each an opaque box in the lower third: what a check can ask about a picture of
text is that pixels changed where the box is and did not change where it is not.
The drawn half is measured three times rather than twice, and the third is the one nothing
else would catch: two renders of the same seconds, one with the input's cues pad wired into an
overlay and one without, compared before the cue (99 dB), during it (14 dB) and after it
expires (99 dB again). A sub2video that paints each cue and never sends the cleared frame that
ends one passes the first two and leaves the subtitle on screen for the rest of the render.
ui_document.js is the whole edit through a file and back — see
The document. The shape is a round trip, and the step that makes
it mean anything is the one in the middle: after saving, the suite starts a new
document, opens a different file, and only then reads the saved one back. Skip
that and every assertion below passes for free, because the model was never
actually cleared.
What it checks is mostly identity, which is why it takes two videos rather than one. A
filter is inserted against clip:<id>/after-scale and a source node is placed naming the
second input's id; after the round trip both have to point at the same shot and the same file,
and one file cannot tell a renumbering from a correct answer. It also asserts the two negatives
that the design turns on — that a clip in the file carries no probe and no name, because both
are its input's answer, and that a snapshot does not change when the model does, because an
undo stack is a list of them and one that shared its objects would be N copies of the present.
The failure cases are driven too: a document whose input path has been edited to something that
is not there opens short, with the input carrying libav's message and the clips of it named
as left out, and a file that is not JSON at all is refused by name rather than by a stack
trace.
The cues section of it is the same identity argument applied to the one part of a document
that is content rather than a description of a file. Two tracks are made rather than one, so
that "the ids come back" is a fact and not a coincidence about the number 1; a cue is given a
dialogue line with {\i1} and a non-default style in it, so that what is asserted after the
round trip is the styling and not only the words; a track made after the open has to take an
id the document did not use, which is the check a fresh counter would fail; and the reader is
handed a hand-edited subtitles list — an id that is a word, a start that is a letter, a
negative end, a null and a string among the cues — because a document is a text file people
open.
The undo half of the same suite drives the three rules that decide what a step is, in the
change channel's own vocabulary rather than through a synthesised drag: twenty move events
and one moved are one step, three edits inside half a second are one and three either side
of the gap are two, and a change that changed nothing is none. The assertion worth naming is
the cheap-looking one — that a clip is still being decoded by the same element after an undo —
because that is the whole reason open() reconciles instead of rebuilding, and nothing else in
the suite would notice if it stopped.
Then the other track, which is the boundary asserted from both sides: a press on a stage
about the timeline leaves the form alone, and a press on the encode side is about the form and
adds no step to the edit's stack at all. It is driven through the Start from row, because a
preset is the press this exists for — the test walks the row until one of them actually changes
what will be written, since which preset differs from the default depends on what this build
can encode. What comes back has to be the codec, the rate control and the quality it was, and
the control in front of you has to be showing it: the form draws from settings, so an undo
changes the model behind its back and a redraw that stopped happening would leave the preset's
value on screen under the old settings. Arriving on the encode side is asserted not to be a
step.
data_test.cpp is a data track read, and it is the one suite here whose bulk is
about input nobody wrote in good faith. Every length, repeat count and
nesting depth in a GPMF payload comes from the file, so a malformed or hostile
.mp4 is the case the parser is designed for and not an edge of it — a parser
that segfaulted on a bad file would be a worse outcome than a camera's telemetry
never being plotted, which was the alternative to writing this.
So a good payload is put through it damaged in five ways, and what is required of each is not that it parses but that it refuses and returns: truncated at every four-byte boundary in it (493 cases — a camera that lost power mid-write leaves exactly this); every one of its 25 item headers given an oversized length in turn, the declared struct size and the declared repeat count scribbled separately because a check on their product that trusted either one would pass one and fail the other; the largest length a header can express (16.7 MB) put in front of a two-kilobyte buffer; a nesting bomb two hundred levels deep, which has to stop at the parser's own depth cap rather than at the C stack's; four kilobytes of zeros, which is every item nested, empty and named with NULs and is the shape that loops a walk advancing by the payload rather than by the header; a megabyte of pseudo-random bytes in two hundred packets; and every byte of the first kilobyte flipped three ways, 3072 cases, of which 2980 still parse into something and 92 are refused. After each one, whatever survived is checked for internal consistency — an item whose value count disagreed with its own sample and component counts would be a caller indexing off the end of it.
The rest of it is the format, each rule with a value that says the rule was followed: a
broadcast SCAL over three components (an axis that is exactly 9.81 was divided and one that
is 981 was not), a per-component SCAL with five different divisors in it (an altitude of
123.456 m proves the third was used and not the first), a float under a divisor that must
come back undivided, a SCAL whose count fits nothing and must therefore be applied to
nothing, an item of repeat zero that must advance the cursor rather than loop it, and a ?
complex item that must be stepped over by its declared length rather than read.
The second argument is a real camera file and there is deliberately no fixture for it: a
fixture is written by this repository and therefore cannot prove that a real HERO8 payload
parses. Given one, the suite prints every series it found with its sample count, rate, units
and range, and asserts the two things about a real recording that are checkable without knowing
where the camera was — an accelerometer that reaches a number an accelerometer has rather than
one in the hundreds of thousands, and a latitude that is on the Earth rather than in the
hundreds of millions. Without one it says so and skips, so the suite runs on a machine with no
GoPro in the drawer. -DGOPRO_FILE=<path> at configure time is how a machine that has one
points ctest at it.
A file that carries a data track nothing here parses is skipped and not failed, and that
distinction was found by running this over seventy-one real files rather than reasoned about:
an older GoPro, or a newer one with telemetry switched off, writes a tmcd and no gpmd, and
"this file has no track a parser answers for" is a true thing about the file rather than a
fault. The skip names the tags that got no parser, so the seam is as visible there as it is
with a gpmd present. Of those seventy-one files, twenty-one carry real GPMF and all twenty-
one parse with no packet refused — 40 series each, and the largest (a 4 GB recording, 1058
payloads) read in 45 ms.
markstest is the native measurement suite.
The measurement is almost entirely a number of seconds, because that is what the feature
is: a mark is a place, and a detector that finds a transient a second late is worse than
useless. marks.m4a is the only fixture here in which anything ever happens — three
broadband transients at 1, 3 and 5 seconds, a 1000 Hz tone from 6.0 to 7.5, and two seconds of
quiet after it — and each of those parts is there for one assertion. The clicks are two seconds
apart, which is forty times the detector's own refractory period, so a mark near one is
unambiguously about that one. The tolerance is 120 ms and it is arithmetic rather than tuning:
a 25 ms analysis window, a mark stamped at its start so it lands early rather than late, and an
AAC transform of 1024 samples on top. The tone's frequency is the one physical measurement in
the whole feature — a detector that reports a run and gets the pitch wrong is the failure that
still looks plausible — and the two seconds of bed after it are what separates a detector from
one that marks everything.
The bed being stationary noise rather than a quiet tone is not a detail. The first version of the fixture used a 137 Hz tone amplitude-modulated at 3.1 Hz, and PCEN — which divides each mel channel by its own smoothed energy — turned that swell into real spectral flux: eight onsets came out of the first second of "silence", and the refractory period of the last of them swallowed the genuine transient at 1.0 s. The suite passed, on a spurious mark. Anything added to that file has to be stationary or has to be one of the things being detected.
Two onsets in the first quarter-second survive even with a stationary bed, and they are
asserted rather than suppressed: the flux baseline is an EMA starting at zero, so the earliest
frames clear the bar trivially. What is required is that they are distinguishable — flux
0.055 and 0.077 against 3.4–3.5 for a real transient — because filtering them out here would
make this and bro.sense.analyze() disagree about the same file.
Both halves of this suite used to carry a second path, for a build configured
-DBRO_WITH_SOUNDML=OFF, and both are gone: that configuration is refused at configure time
now, so there is one behaviour to assert. It was worth removing for a reason worth remembering
— the branch was never once configured, built or run, only checked by compiling two files with
the macro forced off, so the suite that asserted it was asserting a claim nothing had ever
exercised.
ui_find.js is the Find panel, and it is the one suite here that
builds its own store. A corpus is tens of gigabytes and is not in this
repository, so a suite reading the real one would be testing whatever happened to
be on the machine — and the panel reads a single fixed path, so writing a fixture
there would overwrite somebody's actual manifest. useCorpus exists for exactly
that: the suite writes a manifest and a nineteen-word transcript beside the
fixtures and points the panel at it.
Every word in that transcript is there for one assertion. The phrase is said three times and
one of them is written youcross, which is the flattening; a fourth is you crossing, which
the boundary rule has to exclude; and the last three words sit forty seconds after the rest, so
there are two stretches of talking and the gap between them is what decides whether that is one
run or two. The cues point at the ordinary fixture, so Add really opens a file and really
lays out a clip — what is not checked is that the words match the sound, and they do not.
Whether a transcript is right is transcribe's question; the panel's is whether a time in one
becomes a clip of that time.
supercut.js is the other application, and it is the one suite
whose first argument is a different app directory — supercut/ rather than
ui/, which is what makes it a second application rather than a mode of the
first. It builds its own store for ui_find.js's reasons and then does the
whole job: a phrase found, two moments put in the mix, and a file written.
The four card gestures are driven with real MouseEvents on the real cards,
down on the grab point and move/up on <body>. They are the whole point of that
application and each of them is a place to grab — a suite that called the
functions behind them would pass with every grab point wired to the wrong thing.
Two moves per drag rather than one, because the first has to cross the few pixels
that separate a click from a drag: a gesture that only works when the pointer
teleports is not a gesture.
The assertion to keep is the one about growing a clip. ui/project.js stops a trim at the
neighbour, which is right on a timeline and would mean no clip could ever be made longer in a
packed sequence — so mix.js moves the neighbours out of reach, runs the real edit and packs
the result. Every section of that suite ends by checking the row is still butted end to end,
because every edit in it could break that and nothing else would notice.
ui_output.js is the render on the program monitor instead of the clips — see
The output, instead of the clips.
Nothing in it compares pixels, and that is deliberate rather than a gap: a
screenshot is for the record and the picture on the screen is the host's. What is
checkable is every claim the mode makes about itself, and each of those is a
thing that could silently stop being true — that the preview is the render's own
source and says which of the two renderers it is, that a moved playhead is a new
source rather than a seek, that the clips underneath are parked and not playing,
and that a graph libavfilter refuses arrives as libavfilter's own sentence on the
stage rather than as black.
Two of the checks are there because the behaviour broke in review rather than because it looked interesting: an edit made while the preview is playing has to go on playing — a re-point hands the element a new src, and a new src is a paused element at zero — and a preview whose range ends before the timeline does has to stop there rather than hand over to the clip after it.
A third is there because it broke in use: the picture has to go on being made. It plays for
four seconds and watches the element's own clock, and what it asserts is only that the moment
never stands still — no rate, because a machine that renders at a tenth of the speed should
pass. playbacktest is the same failure taken one layer down and made deterministic:
OutputReader driven by hand, with the screen readings a real element would have produced,
asserting which calls make a picture and which moment each is for. The two are worth having
separately — the invariant is a property of the reader and can be pinned exactly there, but
nothing at that layer can see the deadlock between three threads that made it matter.
The three sections in the middle are the three things one element per clip structurally cannot
show, driven one at a time: a filter over the whole canvas, a filter that resizes a clip's
picture below the point where the clip is placed — asserted from both ends, the viewer
refusing it by name and the render showing it without complaint — and a testsrc with nothing
on the timeline at all, which is a picture where the viewer has no element to put one in.
The level strip beside the viewer is asserted for the one thing a strip of bars
cannot say about itself, which is which of two things it is reading — see
The meter beside the picture. With the
preview off it has to say monitor, because bro's mixer is what is summing the clips
and nothing else has a mix to measure; with it on, output, and the loudest-so-far
number has to come off the floor, which is the assertion that a reading is arriving
rather than that the bars exist. Then the channel count: two bars for a stereo output
and one for an output written in mono, which is the half of the old Not-yet entry
that said the meter was per clip rather than per output channel. That the peak is a
true peak is asserted on the Capture stage instead, where the signal can be written
down as an expression.
ui_load.js is the suite for a large edit, and everything in it is a claim
nothing else can make: an edit of many clips holds few decoders, every clip
under the playhead has one whatever that costs, the lanes survive a decoder being
let go, a measurement landing is not an edit, playback runs on the render, and a
gesture on the Graph stage costs what the gesture changed. It is built from one
file cut many times, because residency is about how many decoders are open and a
decoder per clip is a decoder per clip whether or not two of them read the same
path.
Its timing assertions are all ratios or worst frames, never totals. The same total spread over a hundred frames and delivered in one are the same number and completely different to use, so what is asserted is the worst single frame; and "this is not quadratic" has no honest form other than asking the same question of two sizes and comparing, so the graph's derivation is timed at the full edit and at a fifth of it and checked against how much bigger the graph actually got. That one fails at about 20× on the array-walking model and passes at about 5× on the indexed one, which is a threshold that cannot be met by a fast machine or missed by a slow one.
The last section is the fold — a clip's derived run drawn as one card, see When there is too much of it to read. Three of its four checks are about the fold not being a hiding place rather than about it being fast: a filter you inserted is on the screen as itself, a derived node you locked holds its whole run open, and the stage's own line says how many clips were collapsed and why any were not. The fourth is that walking to the stage returns without laying the graph out, because a press that does not return until the work is done is a press that looks like it failed.
captest is what this build can write, read, reach and capture, and it prints
as much as it asserts: how many muxers, which of them write pictures, which
protocols are in and out, what capture devices the machine has. The numbers
are printed rather than demanded, because how many muxers a build has depends
on how it was configured; what is asserted is the shape — that a muxer's
extensions are split rather than handed over as one string, that the name a
picker puts in -f is the name libavformat answers to, that an option table is
the muxer's own and not the last one asked for — and then it renders into
Matroska and MPEG-TS by name and opens what came out, because a picker over a
hundred and eighty is only worth having if what it offers can be written.
inputtest is what an -i is: a demuxer forced and a name this build does not
have refused, an option reaching the demuxer and an unknown one stopping the open
with the key named, -ss and -t moving the input's own clock — checked in
pixels, by asking a seeked reader for its zero and an unseeked one for the same
moment — and the token bro's media backend opens a registered input by.
The last third of it is the other token: an input with filters on it, which is what a
<video> on the timeline plays when the clip carries any. Asserted by decoding the frames back
through bro's own registry rather than by reading a size, because a chain that quietly did
nothing would report the same width as one that worked. negate is the filter, because its
effect can be predicted — the picture that comes out has to be 255 minus the one that went
in, which a chain that ran nothing cannot land on by accident — and then the same filter
carrying enable='gt(t,10)' twice with nothing changed but the view's shift, which is the
only way to see from outside that a filter is on the render's clock and not the file's: at zero
the first frame is before the span and comes back untouched, and moved twenty seconds along it
comes back inverted. Three things are asserted about that one, because a clock has three ways
to be wrong — the frame is inverted, it comes back stamped at its own second rather than the
render's, and the same filter written in front of the setpts instead of behind it is
untouched again, which is where the render puts a filter inserted after the decode.
Then a subtitles burn-in, which is the only filter here whose whole job is to be
different at different moments: the same view is decoded inside a cue and between two, and it
has to differ from the file at the first and equal it exactly at the second. Either check alone
passes for a bug — one that never drew passes the second, one hard-wired to draw always fails
it, and one whose clock is out by seconds fails both. The path is escaped by hand in C++ rather
than by calling the UI's filterPath, so that the escaping is stated in both languages
rather than agreed with itself.
Then a crop for the sizes, a seek for the graph being rebuilt across one, volume=0 for the
sound half with the picture checked to be untouched and undecoded beside it, a filter option
nothing has for the refusal, and a clip shot sideways for the turn: the chain sees the picture
the right way up and the track then asks for no turn of its own, both halves asserted because
either alone passes for a bug.
seqtest is the inputs whose content is assembled, and most of what it asserts
is what the grouping refuses: a lone numbered file is a still, a folder of two
runs and a stray picture is two sequences, and an unpadded run crossing from one
digit to two is one input rather than two. Then that a sequence's length is its
-framerate and nothing else, that a still probes as no time at all and is as
long as its -t and no longer, that -stream_loop 1 is twice through and then
the end, that a concat list with no durations in it reports none — and the round
trip, since what the writer means by a sequence and what the reader means by one
have to be the same thing or every half of this works alone and none of it works
together.
ui_sequence.js is the same subject from the drop inwards: twelve files becoming
one -i with its options printed in front of it, a still that plays because it
is held and is refused when it is not, a sequence played through the same
<video> everything else uses, and the Write stage listing the filenames a run
will be written as before anything is rendered.
ui_sources.js follows one input the length of the stage: typed in as a path
with no clip near it, forced to a demuxer picked out of libavformat's own list,
given -probesize out of the option column, given -skip_frame out of the
decoder's column beside it — a separate bag, because a decoder is a separate
object with a separate table — cut to a two-second window, used on
the timeline, and then found in the spec with the clip pointing at it by index —
and every one of those printed by the command bar in front of its -i,
because the same words after it are output options meaning something else. It
also adds a URL, to check that it survives as written rather than being resolved
against the document, that the protocol is named, and that its own option table
is offered — and then that adding it returned at once, that the card says
Connecting with a Stop, and that pressing Stop settles the input as stopped.
Nothing in either suite reaches a network, and neither needs one. The address
is 192.0.2.1, which RFC 5737 reserves for documentation and which is therefore
never assigned to anything, so what is asserted is what this code does while an
open is going nowhere. input_test.cpp does the same at the native level against
a closed port on the loopback address — which on this platform is a genuinely
blocking open, since libav sits in the poll until the protocol's own
open_timeout — and checks the two halves that matter: a one-second deadline
ends the open in about a second rather than when libav gives up, and a stop
against a sixty-second deadline lands in a tenth of one, which is only possible
if the press actually reached the interrupt callback. A machine with no route at
all answers immediately, and the UI half then says so and skips the part that
needs something to stop, the way every suite here skips what its fixture cannot
provide. What is not tested is a URL that works: that needs a server, and a
test that needs a server does not belong here.
The device half of the same path needs no hardware either, because lavfi is
a device in this build and exists on every machine. input_test.cpp probes one
through startProbe and asserts two things about it: that the answer comes back
like any other input's, and that it reports stoppable: false — a stop would not
reach a libavdevice open, which is measured rather than assumed. It then puts a
watch that has already given up — once because its deadline is past, once because
somebody pressed Stop — through the same device's open and requires the probe to
fail both times, because avformat_find_stream_info answers success when the
callback cuts it short and a successful probe of a half-analysed file is worse
than the hang the deadline is there to end.
Already past, rather than expiring part-way through, and that is the whole of
what is deterministic here. The deadline is armed and then slept out before the
probe begins. Arming one for a microsecond and probing at once — which is what
this used to do — failed five runs in six, not because a device goes unpolled
(counted, find_stream_info polls the callback twice on every run of this
source) but because av_gettime_relative() is the system tick and steps in
0.5–1.5 ms on this platform, which is longer than the whole open: a deadline read
back inside the tick it was armed in has genuinely not passed, and the probe
genuinely succeeds. So the suite asserts the rule that exists — openInput asks
the watch as well as libav's return code — and not a claim about when a clock
ticks. The stop half of the pair carries no clock at all and is the same rule
proved twice. ui_capture.js asserts the route from the other side: activating a
device returns at once, the input is opening the instant the click does, no
preview session is opened over the top of it and Record is held while it is
outstanding. What no suite here can exercise is a device that genuinely hangs —
that wants a camera another program is holding, which is a machine and not a
fixture.
ui_capture.js follows a device the length of the stage: chosen out of
libavdevice's list, its option set from its own table and printed in front of its
-i, -t printed in front of it too (after the -i it would limit the output
instead), a recording started with no end that says so and offers no percentage,
every other stage refused while it runs, and then stopped — which is done and not
cancelled — leaving a file that probes and lands on the timeline. Then the same
with a -t, which does have a percentage and ends by itself. It also lays a device
on the timeline and requires the refusal, drags a region on the live screen and
checks the numbers it becomes are in the screen's pixels rather than the card's — and
that a drag off the edge is clamped, because an unclamped rectangle is one
libavdevice refuses at the open — and asserts that leaving the stage gives the device
back.
A file beside the device is checked in both suites and in three parts, because
the claim has three halves and only the third is hard to believe. capture_test.cpp
records testsrc with movie=still.png overlaid and reads the strip the card covers:
it is not the device's picture (so the file is in the frame and not merely in the
graph), and frame 45 reads the same as frame 5 (so a still is held, by overlay's
own eof_action=repeat rather than by a rule this application wrote). Then the same
with landscape.mp4, whose bar sweeps across it over ten seconds: frame 0 and frame
45 must differ, because a file raced to its end would be its last frame repeated
and both would read alike. That is the assertion that says the file is pulled one
frame per output frame. ui_capture.js does the same from the stage a person uses —
a movie node placed, wired into an overlay beside [0:v], recordGraph
accepting it, one -i in the command bar because a movie is not one, and a real
recording that comes out at the device's own size. Its file is the one the suite
recorded a section earlier, so that half still needs no fixture.
A device with a title on it, streamed out is the whole of what Not
yet used to call inexpressible, checked in one recording.
capture_test.cpp binds a UDP listener on the loopback in its own process
first — writing to a port nobody is on succeeds silently, whatever is wrong with
the plumbing, so a check without one proves nothing — then records testsrc with
movie=still.png overlaid to udp://127.0.0.1:45233 as MPEG-TS, and requires
that the job is Done, that it reports what it sent, and that the listener received
it beginning with a 0x47 sync byte. Nothing reaches a network. ui_capture.js
does the stage half: a URL in Save to draws the udp · linked in row, the
command bar prints the URL where a filename goes, and nothing about it is refused.
A device is not a clip, and Stop at does not change that, which is asserted
at both ends because the refusal used to be keyed on the wrong question.
ui_capture.js lays a lavfi device on the timeline and requires the refusal,
then sets -to 3, requires that the input now has a length of 3 — the same rule
a -loop 1 follows — and requires the refusal again. export_test.cpp asks
startExport for the same clip and requires a failure naming the device and
saying what a trim on one would cost, with and without a -t; and then renders
the same device through filterInputs and requires that one to succeed,
because a pad is pulled forward and never asked for an instant. The numbers behind
those two — 3040 ms for two seconds trimmed one second in against 2038 ms
untrimmed, and 2024 ms for a graph render paced by the device — are in deviceClip
in src/native/ffmpeg_export.h.
The session half adds a second input and follows what changes: the card and the column that say
which input they are about, the refusal at two inputs with no graph asserted twice — once
as the disabled button and once from record.start, so that a button which stopped asking
would still be caught — and then the graph, built the way a person builds one: two addSource
calls naming the devices' input ids, an hstack placed, three wires. Nothing in that sequence
knows what a recording is, which is the point of it. What is asserted off the other end is the
chain ([0:v][1:v]hstack=inputs=2[vout]), the pad it is mapped by, two -f/-i pairs and one
exact -filter_complex in the command bar, and then a real recording of two paced lavfi
devices whose file comes back 640 wide from two 320-wide pictures. That last number is the
assertion worth having: nothing but the graph could have produced it, so a session that quietly
recorded one device would fail rather than pass with a plausible file.
The composition preview is asserted the same way and before any file exists: the picture
below the cards has to be exactly twice the width of the picture on one of them. Measured
against the card rather than written down, because the two are made of the same devices and a
constant would only say whether somebody had edited the test. The session behind it is asked
directly too — three pads over two -is, two of them devices — which is the assertion that one
open per device is what is actually happening rather than what is intended.
Several destinations is driven through the picker and the rows and asserted off
the disk. tee has to be in the container list — it would be filtered out, being a
muxer that does not write the file it is named with — and picking it has to turn the
take already named into destination one rather than throwing the name away. The list
is then emptied through Remove, which is the one state that has nowhere to go and
must reach the button rather than the open; refilled through + Destination and the
two fields; and the argument the muxer is opened with is compared against
escapeTarget on both paths, because it is the render side's own function on the same
kind of argument. Then a real recording, and both files are probed: the same width
in each, which is what makes it one encode rather than two. Switching the container
back has to leave the single path where it was.
The meters are checked against arithmetic rather than against a file. aevalsrc puts the
amplitude in the expression, so a sine at 0.5 must read exactly -6.0 dBFS on the number, put
its bar at the RMS — which for a sine is peak over root two — and its mark 3 dB above that;
halving it must read -12.0, because a halving is 6.02 dB wherever on the scale it lands. The
bar is sampled at its ceiling over a moment rather than once, since it falls between blocks
of sound on purpose and one sample of a falling bar has a tick count in it — and it is bounded
tightly above and loosely below, which is the shape of that claim: a falling bar can never be
higher than the level it was last driven to, so anything over is a real disagreement, while
under it can only mean the sample landed on a tick that had not heard anything yet. Then 1.5,
which is a source past full scale: the light latches, the bar changes colour, the number goes
above zero rather than pinning to it, one click forgets both latches, and both fill again from
what is actually arriving.
Two claims about what kind of reading it is are checked the same way. Two expressions twelve
decibels apart is a bar per channel reading its own number, which a mono summary of the two
could not produce — and the two are named FL and FR out of libav's own layout. Then a
true peak: 12 kHz at 48 kHz with a quarter-cycle offset puts every sample on ±sin 45°, so
the loudest sample is 3 dB below the loudest point, and the meter reads -0.1 where a
sample-peak meter would read -3.1. The latch is cleared first, because a signal that starts
abruptly is a step and an oversampling filter rings on a step — what is being measured is the
steady state after it. Every sine source here carries arealtime for a related reason: a
lavfi device left to generate as fast as it can overflows the reader's sound queue, whose
oldest blocks are then dropped, and a meter handed a signal with cuts in it reads the cuts.
Monitoring is asserted at bro's mixer and not at the element. An element with a
src on it says nothing about whether anything is audible, so Listen is checked by
reading the master bus — new AudioContext().getBusPeakL(0), bro's own metering of
what the speakers are being handed — silent before the press, audible after it, and
silent again once the button is off and the half-second ring has drained. What that
proves is the part that could have been faked: the pad really is carrying blocks and
they really are reaching the mixer. The rest is asserted where it is decided —
monitoring off to begin with, the element removed rather than muted (because the
element is the listening), the feedback sentence naming the input it is warning
about, and the whole thing stopping with the session when the stage is left.
Then the states either side of a graph that works. Widening the hstack to three inputs leaves
a pad nothing arrives at, and that is a refusal — the button goes dead, the stage names the
empty pad in the Graph stage's own words, and the command bar prints no -filter_complex at
all rather than a line that cannot be run. Releasing an input takes the node reading it with
it, which leaves the same refusal from the other direction. Clearing the graph is the third
state and the commonest: recordGraph answers null, which is not a broken graph, and one
device is written as it comes.
Which pad the recording writes is followed through the whole gesture. With no output of its own
the graph offers no choice and there is no picker at all; placing one and moving the
hstack onto it puts a two-option picker on the stage, and until it is picked the recording
still writes the now-unfed video out and says so — the refusal naming the choice rather than
quietly following the wire. Picked, the same chain runs and comes out ending in [vout] with
the name it has on the stage appearing nowhere in what runs, which is the relabelling
asserted rather than assumed. Deleting the output from the Graph stage drops the recording back
to video out and takes the picker with it, because nothing on that stage knows a recording was
pointed at it.
ui_filtergraph.js needs no media at all: buildSpec()'s output is a plain object and
the translation into a filter graph is a pure function of it, so the graph is checked
against specs written out by hand — including the edits it must refuse rather than
approximate.
ui_graph.js needs one only for its last four sections, and watches the graph
from inside rather than
through the string it prints: the model, the printer's chain rule on shapes the
derivation does not produce, and the whole of what makes an edited graph
survive a rebuilt one. A filter lands on the wire it names and takes the pad
name with it; two at one point run in order; a lock outranks the timeline and
reports which control it took; a lock that happens to agree has outranked
nothing; a split copies both halves' filters and a delete takes them away; and
the run graph differs from the printed one by exactly one chain with the
inserted filter in both.
It also holds the checks on a value written as an expression, which need no media because
an expression is a string: the four states libav's evaluator puts one in, the round-trip
through the points printer and parser at two, three and four points, the refusals for an
expression it did not write and for a nest whose halves do not join up, and the sampled values
themselves — which are the one claim that could not be checked by reading the code, since they
come out of av_expr_eval rather than out of anything here. The last of it wants a file,
because the printed chain has to be read off a real node.
Given a file it goes on to the wiring gesture on the real stage, and then to
a filter in the viewer: inserting one points the clip's element at a
filtered view of its input and takes the fx badge off, the element decodes at
the size the chain produces, taking the filters off puts it back on the input,
a filter that resizes the picture on the way in is laid out in the shape it made
— on the screen and in the printed invocation, which is the same rectangle
arriving twice — and the two cases that keep the badge, a resize below the point
where the clip is placed and a chain libavfilter will not parse, do so with a
sentence on the picture saying which. Every one of them goes through overlay.insert, which is
what the palette calls, so the src moves because the application reacted and not
because the test asked it to.
Scrubbing a node is driven through the bar rather than through seekPlay,
because the arithmetic that turns a pointer into a fraction of the range belongs
to the bar and calling past it would not have exercised it. A press half way
along has to put the picture half way along and draw the marker there; the rate
has to be measured from the seek rather than crediting the jump as playback; and
a press back at the start has to land on a piece that is already in hand, which
is asserted as waiting being false — "instant" means no render outstanding, and
that is the whole reason a playback keeps its pieces.
The When lane is the last of it, and it is on the Compose stage rather than
the Graph one — the timeline lives in #st-compose, so behind a display:none
every rectangle on it is zero and a press computed from one would land nowhere.
What is asserted after each real drag is the stored expression, because the
claim is that the lane is a reading of it and writes through the same two
functions the strip does: an end moves and the other end does not, a body move
keeps the length, a press that dragged nothing writes nothing, and Ctrl+Z puts
a drag back. Then the two halves of the entry it closes — a second node with
spans is a second row, both cover the same second without either becoming
unreachable, a drag on one row edits that node and no other, and deleting a node
takes its row away. The lane's existence is checked in both directions (no
spans, no lane; the last span off, no lane), and a span dragged there is read back
out of the workspace and out of a document, which are the overlay's two reads.
One section is about the other clock and is the reason a span is clamped against a window
with a start rather than a bare length: on a clip cut from two seconds in and laid down at one,
a filter above the derivation's setpts written between(t,3,3.5) has to be drawn at the
edit's second 2 and a region dropped at the edit's second 4 has to be written back as the
file's second 5. Both directions, because that is what makes the two clocks one map rather than
two.
ui_measure.js is the half above that: a measurement started, run, read and
acted on. It clicks Crop and finds cropdetect on the graph and in the
command the bar prints; runs Measure now and finds the series on the render's
own clock; opens a plot and checks that taking a line off does not repaint the
one left; applies the crop and finds a crop node at the anchor the
measurement was taken at, carrying the characters cropdetect printed. Then it
puts the same filter at one clip's decode point instead and measures to that
node: the cut is two nodes of fourteen and one input pad, nothing after it is in
the printed graph, no scale is on the end — and the number that comes back is
the source's own width rather than the square composite's, which is the whole
claim being made about where a measurement is taken. A sound pad is cut at the
same way and renders to the end; and a press while the whole-graph measurement
still holds the one slot queues instead of failing, and starts when it comes
free. Three sections are written against hand-made channel records, the way
ui_filtergraph.js is written against hand-made specs — parsing what a filter
said is a pure function of what it said, so a cropdetect that has not settled,
an ebur128 with no summary and a blackdetect that found two stretches can be
stated exactly rather than hoped for out of a fixture. The cut those spans
produce is then made on the real timeline through the real split. Re-measure when stale is checked in both directions, and the interesting half is the things
that must not happen: the toggle is off to begin with and is written under a key
of its own with nothing of the encode side's blob beside it; a re-measure asked
for while something else holds the one job slot is refused and then does not
fire when the slot comes free; a second attempt for the same edit is refused and a
frame loop left running for a second and a half starts nothing. What it moves to
make the finding stale is a clip's level — the one thing in a render's subject
that cropdetect cannot see, since moving the clip would take it out of the range
and cropping it would change the answer rather than the question. Last, the A/B
comparison is rendered and measured, and a better setting has to measure better
— the one check that says the number is about the encoder rather than about the
plumbing, and beside it that the score was combined from more than one frame,
which is what says the channel had actually been drained when it was read.
ui_report.js drives a render the renderer has something to complain about —
a graph running at half the output rate, with cropdetect measuring on the way
past — and follows what it said from av_log inside libav to a line on screen:
that the drain runs off the frame loop without anyone asking, that the warning
is visible and attributed, that the whole of libav's chatter is kept and merely
filtered, and that what the filter measured arrives as a named series sampled
in order rather than as more log lines.
exporttest also covers where a render goes, which stopped being one file: a
segment render writing four .ts files and an m3u8 that names them, every name
in the playlist checked against the disk and the playlist opened back as one
piece of media of the whole render's length rather than one segment's; the same
through hls, where the playlist is the thing you name and the segments are the
pieces; a tee whose two destinations receive the same forty packets, one of
which decodes to the render in the rectangle the clip was given; and a real
network destination — a UDP socket bound on the loopback in the test process
before the render starts, because writing to a port nobody is on succeeds
silently whatever is wrong underneath. What arrives starts with an MPEG-TS sync
byte, and the render reports what it sent rather than a size. capturetest
records through a tee, since a recording is a device into the same Writer.
exporttest also covers the four things on the encode side that are claims
about bytes: a real two-pass encode at a bitrate target, which has to write its
statistics where -passlogfile said, come out a different size from one pass
and land closer to the target; a bitstream filter, checked by finding a level in
the written SPS that the encoder did not put there; forced keyframes, read back
out of the file's own packet flags rather than taken from the encoder, with a
GOP longer than the render so that every keyframe past the first is one that was
asked for; and -shortest stopping where the content does. Each refusal is
checked too — a pass 2 with no statistics, a bitstream filter this build lacks,
an option it does not have, an expression that will not parse, a decoder option
no decoder takes.
And -fps_mode vfr, which is about timestamps rather than about bytes and is checked by
reading them out of the container. The graph is deliberately unevenly spaced — a 50 fps
source with select keeping two frames in every four, so the gaps are one fiftieth then three
— because anything constant would pass under either mode and prove nothing: what is asserted is
that the first gap is a fiftieth and the second is three of them, which no frame number can
express, and that the same graph under cfr comes back on an even grid. The container's own
time base has to be fine enough to hold those, which is the second half of the same check. The
soundtrack is compared against the other walk rather than against a number, because the paced
walk covers up to each frame's own moment and owes whatever the last frame lasts — so the two
must come out the same length, and it must reach past the last picture to the end of the range.
All three refusals are checked by their sentences: a composited render, a render mapping a
graph pad, and a mode this renderer does not perform (passthrough), which is refused by name
rather than mapped onto one it does.
exporttest renders a timeline and then opens what it wrote, which is the only
way to check the things nobody can see until the render is over: that a clip
lands in the rectangle it was given and the rest of the canvas stays black,
that opacity is a blend and not a switch (half of a picture over black is half
as bright, whatever the picture is), that overlapping clips are summed rather
than picked between, and that stopping half way still leaves a playable file.
ui_export.js drives the Output workspace and checks the join above it — that the spec
it builds is the edit that is on screen, that every control turns into the
ffmpeg option it claims to be (and that a raw option beats the control setting
the same key), that the advanced editor's list is libavcodec's, that both
halves of the A/B preview render and land on identical pixels, and that the
file that comes out can be dropped straight back on the timeline. It also
drives the Write stage's stream list: a second audio track added, given a
language, a name and two flags at once, and then rendered and opened to find
both tracks in it — and every one of those printed by the command bar, because
anything reaching the muxer the bar does not print is a bug. And the muxer
picker: that the default group is only muxers avformat_query_codec said yes
to, that searching reaches the other hundred and forty by name, by libavformat's
description and by extension, that picking MPEG-TS sets -f mpegts rather than
a filename somebody hopes will be guessed, that a muxer which never answered
does not have the codec taken off it, and that the muxer's own options reach
the spec, the command and a file that opens as an MPEG-TS.
And where the render goes: that the shape of a destination is asked — a URL is a stream
because of its scheme, segment is a set of files because it says AVFMT_NOFILE, a frame
pattern is a set because the numbering is in the name, and C:/ is a path and not a protocol —
that a URL's own protocol options are offered beside the muxer's and reach the same bag, the
spec and the printed command; that the -f tee argument is built with tee's escaping (a | in
a target, a : and a ] in an option value) and then quoted for the shell, so what is printed
can be pasted and run; that picking tee makes the file already named the first destination
rather than throwing it away; that a two-destination render writes both and reports two; and
that the progress panel says something different and true for each shape — the count of files
for a set, "sent" and no offer to open anything for a stream.
Keep trying if it drops is asserted without streaming anywhere, which is most
of what there is to say about it: that a local file gets no fifo however the
setting is left and a URL does; that the printed command names fifo with the
real muxer as -fifo_format's argument and the wait in the microseconds libav
reads a bare duration as; that the wrapped muxer's own options come out as one
-format_opts bag and never as flags, because ffmpeg applies output options
to the muxer it was named with and -flvflags on a fifo is an exit; that a tee
is refused with a warning naming the per-destination form that does work; and
that a stored workspace with queueSize: "90" and waitSeconds: null comes back
as a number and as the sentinel, since zero is a real answer there and Number()
turns absence into it. Then the one half that can be rendered without a
network: a fifo around a local file, whose output has to come back the same codec,
the same size and the same length as the render that was not wrapped — which is
worth a file because the wrapping changes which muxer answers every question
about the container, and fifo answers AVERROR_PATCHWELCOME to every
query_codec, has no default codecs and no fourcc tables. What is not tested
is a destination that actually drops and comes back: that needs a server to take
away.
That section puts the whole stored blob back when it is done rather than the fields it
named. ui/.storage.json is one workspace shared by every suite and every run, and changing
the container rewrites the stream rows' codecs on the way past — which is remembered. Left
behind, it broke an assertion four thousand lines earlier on the next run of this suite, and
then a subtitle row in ui_subtitles.js, which had never heard of any of it. Restoring the
bytes is the only repair that covers what a setting quietly changed on its way past; restoring
the fields is what did not.
It also drives everything on the encode side that is not an encoder option: that
two-pass is a mode of the rate control and that choosing it makes the spec say
the range is walked twice with both passes naming one statistics file and the
command bar printing two invocations; that a forced keyframe at a cut point
follows the clip when the clip moves, which is the whole claim of deriving
it rather than copying it; that the field order prints as the two things it is;
that -fps_mode is a choice of two whose variable half is present and refused,
with its reason, while the render composites — and becomes available, reaches
the spec and prints without an -r the moment there is a filter on the graph; and
that a bitstream filter chain is offered only for the codec the stream is encoded
with, runs in the order shown, carries libavcodec's own option table and prints as
one -bsf:v the way av_bsf_list_parse_str takes it.
The UI actuation suite validates interactive user journeys through the application's DOM and canvas components using the Bro QuickJS runtime. Rather than calling internal functions directly, actuation tests dispatch real pointer, mouse, and keyboard input events to verify the interface in its true operating state.
Nine dedicated suites exercise complete workflows:
- suite_nav_topbar.js: Stage switching across the spine navigation bar, document actions (new, save, open), undo/redo.
- suite_compose_transport.js: Transport playback, playhead scrubbing, step navigation, loop, mute, volume, and playback rate.
- suite_compose_timeline.js: Tool modes (select, ripple, slip, rate), clip selection, dragging, edge trimming, splitting, and zooming.
- suite_compose_viewer.js: Interactive viewer crop tool actuation and handle adjustments.
- suite_sources.js: Input path entry, demuxer and decoder option tables, in/out trim controls, and timeline placement.
- suite_capture.js: Capture device selection, live preview decoding, device options, audio monitoring, and recording cycles.
- suite_graph.js: Graph canvas zoom and fit, fold toggling, node search filtering, palette additions, and wire deletion.
- suite_export.js: Intent presets, range handles, output path setting, stream management, and render execution.
- suite_shortcuts.js: Global keyboard shortcut dispatch, input focus suppression, and cross-stage shortcut isolation.
Run all actuation suites via CTest:
ctest --test-dir build -C Release -R actuate --output-on-failure
Or run them sequentially using the PowerShell runner, which automatically clears ui/.storage.json between runs to isolate state:
pwsh tests/actuate/run_all.ps1
Individual suites can also be executed directly against headless bro:
.\build\Release\ffmpeg-bro-headless.exe ui/ tests/actuate/suite_nav_topbar.js