Limits
Each limit is named once here, with a link to the page that explains the feature around it.
Stream copy
Section titled “Stream copy”An export that can copy its streams does, with no re-encode. ExportPlan.copyBlockedBy lists every reason one didn’t,
so you can see what to drop to get it back. See Export.
| Source of the block | CopyBlocker |
|---|---|
| The spec | FrameRateSet, VideoCodecNamed, AudioCodecNamed, HeightSet, BitrateSet |
| The edit’s shape | MultipleTracks, MultipleClips, TrackStartsLate, TrackDropsAStream, FrameResized |
| Effects | CompositionHasEffects, TrackHasEffects, ClipHasEffects |
| Audio | AudioSpecChanged, CompositionGainChanged, TrackGainChanged, ClipGainChanged |
| Timing | ClipSpeedChanged |
| The source | TrimNotOnSyncSample, SourceUntimed, MuxerRefusesSource, SourceCodecUnnameable, GradeMustToneMap |
| The backend | BackendCannotCopy, NonSquarePixels |
Every built-in backend can copy, so none of them reports BackendCannotCopy. A source with non-square pixels always
transcodes on Android, and copyBlockedBy names NonSquarePixels. quality blocks as BitrateSet, and
MatchSource doesn’t block at all.
Upload, Share and Archive each name a codec, so those presets never copy. Original
keeps the copy open.
A copy writes the frames on every slot of the plan’s duration, the count its preview shows, and stops in decode
order on the packet that holds the last of them. When that frame is a B-frame, the references it decodes from are
presented after the end, and the copy writes them too. That’s the reorder residue, and it is never more than the
source’s reorder depth. A 30 fps H.264 source with two B-frames copied from 0 to 1.010 s writes the grid’s 30 frames and
nothing past them. Copied from 0 to 1.040 s it writes the grid’s 31 and a residue of 2. Every backend writes those same
frames, Android on API 30 and 36 alike. The audio runs to the trim’s end either way, on every backend.
The residue is presented past the grid, so playing the file shows it after the grid’s last frame. A preview of the copy
ends on the grid at the plan’s duration, holding its last frame there, and never shows the residue.
On ffmpeg, Apple and the browser the packet list a copy counts by is read when the export is planned, at up to 1000 fps
over the window and one second past it. A source listing more packets than that, or one whose packets can’t be listed,
is refused at plan() with SourceUnreadable, naming why. So is a listing where no packet is presented on the copy’s
grid, since that copy would write no video.
A copy counts its frames by the packets’ presentation times, so a source that lists packets without them can’t stop a
copy that ends before the source does. An AVI holding B-frames reads that way. Such a clip transcodes and
copyBlockedBy names SourceUntimed. Running the clip to the source’s end keeps the copy, since that copy counts
nothing.
On Apple a copy’s preview shows the frames its export writes, and holds the last of them where the audio runs past it.
Probe deadline
Section titled “Probe deadline”On ffmpeg every short call to ffmpeg or ffprobe has a deadline of 30 s, plus 10 ms for each second of source it
reads, so the packet read for a two hour copy gets 102 s. That covers the toolchain lookup, reading a source and its color, the sync sample a trimmed copy opens on, the
packets a copy counts by, the encoder test encodes and decoding an overlay picture. A preview’s opening reads are the
same calls, so they share it.
A call still running at its deadline is stopped and the plan refused as TimedOut, which names the read in operation
and the deadline it had in deadline. Reading the written file back at the end of an export fails it the same way. An
encoder test encode that runs out of time fails capabilities() as TimedOut naming the encoder and the size, and so
does a plan that needed them. None of these is kept, so the next call reads or measures again. The toolchain lookup is
the exception, and is ToolchainMissing naming the option ffmpeg didn’t answer. An export and a preview’s frames run
as long as their media does and have no deadline.
Encoder color
Section titled “Encoder color”Some Android encoders write a different color matrix than they were asked for. filmstrip tags the file with what the
encoder writes, so its colors decode as authored, and reports a ColorSpaceChanged adjustment. Android names primaries
and matrix together, so a file tagged BT.601 records BT.470 BG primaries too, and a color-managed player can show greens
a shade apart from the preview.
That adjustment only arrives on ExportStatus.Success. A plan can’t see it, so a plan can say Capable and a strict
spec can’t refuse it. See Export.
Placement
Section titled “Placement”A clip’s placement is the same fraction of the frame on every backend, but not every backend can draw a fraction of a pixel. See Fit.
- media3 draws a frame’s edge on a pixel edge, and
scale,cropandpadin ffmpeg take whole pixels. Both round the placement the same way, so they land on the same pixels. - On a 4:2:0 chain, ffmpeg’s
padandoverlayland an odd offset on the even pixel below it, one pixel up and left of the other backends. On an 8-bit export that covers a picture overlay, a cropped effect region and an animated overlay’s position on every frame. A canvas built by acropwithFit.ContainorFit.Stretchmoves to 4:4:4 where it has an odd figure, so it lands exact. - Apple and the browser place a clip at its exact fraction.
Animated overlay cost
Section titled “Animated overlay cost”ffmpeg draws an animated overlay’s opacity with geq, which reads every texel of the picture on every frame the
opacity moves. Over a 1080p picture that measured 27 ms a frame on ffmpeg 9.0.2 on a 16 core Mac. Where the opacity
holds, geq is switched off and a colorchannelmixer holds the alpha, and those frames measured 1.8 ms. A size that
moves runs the fade on the picture at its own size, so a large picture that grows and fades pays for its full
resolution on every frame of the ramp.
Preview against the file
Section titled “Preview against the file”An ffmpeg preview ends its graph on the format the export writes and converts from there for the screen, so it shows the frame the encoder is handed, and encoder loss is all that’s left between the two. That holds on a caption, a picture, a grade and an HDR grade, at every speed. A graph that doesn’t already end on that format pays for one more conversion. At 1080p on ffmpeg 9.0.2 a caption read 478 frames per second against 543 without it, and a composition grade 469 against 516.
An overlay runs on every frame of its plan, so a frame outside its window goes through the same conversions as one inside it, in the file and in the preview.
A media3 preview and its file part in chroma around a caption, because the encoder reduces chroma to 4:2:0 and a preview
keeps every pixel’s. On an API 36 emulator the software c2.android.avc.encoder takes each 2x2 block’s chroma from its
top left pixel, which puts a Cb about 1 code from the preview’s top left where a box average sits about 5 away. Past a
file’s first 7 frames luma reads 45 to 51 dB against the file on API 30 and API 36, at 1/32, 0.7, 1.5 and 32, and the
bitrate doesn’t move the chroma. Each block’s chroma there stays within 5 codes of the preview’s range over that block
and the pixels around it, against a measured worst of 4, where a wrong frame reads 11 or more.
The encoder’s first 7 frames of any file, 0 to 6, sit further off at every speed, speed 1 included. They read up to 13 codes of chroma and 45 to 49 dB of luma on API 30 and API 36, and no encoder setting or bitrate media3 offers moves them. filmstrip holds them within 16 codes, which a wrong colour breaks but a wrong frame doesn’t, so those frames are told apart by luma. A clip at 1/32 runs 32 times its source, so it writes mostly those frames only when it holds under about 8.3 ms of a 30 fps source. Hardware encoders aren’t measured.
Relaxed seek
Section titled “Relaxed seek”A SeekAccuracy.Nearest seek lands within each backend’s own tolerance, and PlaybackEvent.SeekCompleted reports the
slot of the frame it shows.
- Apple lands on the nearest sync sample within ±250 ms of the target, either side, from
NEAREST_SEEK_TOLERANCE. - The browser lands on the sync sample at or before the target, read by
readSyncSampleinBrowserSeeks. - ffmpeg lands exactly. It waits
SCRUB_DEBOUNCEfor the finger to settle instead of landing early. - Android lands exactly. media3 takes no seek tolerance, so
clampedAccuracyturns a relaxed seek into an exact one.
HDR text and pictures
Section titled “HDR text and pictures”On an HDR grade, an opaque overlay texel lands at the light a fill of that color lands at, and soft edges, fades and opacity blend in linear display light. That holds on every backend, ffmpeg included. The one exception is media3 on an HLG grade, which holds the frame as scene light and blends there, and the plan reports it as an approximated effect. See Overlays and HDR.
ffmpeg splits a looping clip into one branch per pass, and up to one pass of decoded media sits in memory for each branch still waiting. A long clip looped many times costs that much memory. See Extra tracks.
Clip speed
Section titled “Clip speed”Every backend plays 1/32 to 32 with the pitch kept and decodes its sources in full, so the cost scales with the speed. Each file runs the shared output length, give or take its last frame and its audio tail. What’s left is each backend’s own. See Speed.
A clip shorter than an output frame behaves the same on every backend. A primary track whose video covers no output
frame is refused with InvalidComposition. A clip that covers none inside a longer run lays no picture, its neighbours
keep their frames, and its audio still plays. Each frame a clip covers shows the source frame nearest its source time,
and a trim holding no source frame shows the nearest one outside it. The file holds a sample on every one of those
frames, except where media3 shows one source frame on several, as its sparse slow motion below does.
| Backend | Limit |
|---|---|
| AVFoundation | An export renders sped audio to temporary disk, 16-bit PCM for the output length. |
| media3 | The AAC encoder runs at most 32 ms past the output length. |
| media3 | On API 30 the AAC encoder ends up to 55 ms before the output length, at any speed. |
| media3 | Slow motion is sparse. Nothing duplicates a frame, so a clip’s frames sit apart by speed. |
| ffmpeg | A preview has no audio, at any speed. |
| ffmpeg | Picks frames off the probed rate’s grid from zero, timed to 1/1000 of an output frame. |
Sparse slow motion means a clip at 1/32 from a 30 fps source holds each frame for about a second. The file plays and its video ends on the output length, though its audio was measured running up to 457 ms past it at 1/32. The other backends write every output slot. A media3 preview reads whole milliseconds, so a read within 1 ms of a frame’s own time can land on either side of it. The AAC tail is what the encoder adds at the end of the stream, and a clip at speed 1 has it too.
ffmpeg picks the frame a slot shows from the source’s probed frame rate, as if every frame sat on that rate’s grid from zero, and reads each frame’s time to a thousandth of an output frame, 33 µs at 30 fps. A variable rate source, or one whose frames don’t sit on that grid, can show the frame either side of a slot whose source time lands near half way between two of them. So can any source within that thousandth of half way.
AVFoundation lays frames on a composition track in ticks of 1/3600 of a second, about 0.28 ms. Two source frames inside
one tick can’t both be shown, since AVFoundation shows the later one there. An output frame whose nearest source frame
is the earlier of the two shows the later instead, which sits less than a tick from it. Each clip reads the times of at
most 10,000,000 source frames, more than an eight hour clip at 120 fps holds. A clip past that, a source whose frame
times AVFoundation won’t give, or a frame it won’t lay is refused by name. An export fails with InvalidComposition
and a preview with UnsupportedFormat, both with the same reason, and nothing of the clip is left on the track.
On API 30 c2.android.aac.encoder flushes nothing at the end of the stream and keeps only the whole access units it was
fed, so the last of the audio never reaches the file. On an emulator that measured 33 to 53 ms, and the same PCM through
the same encoder on API 36 keeps all of it. The levels between aren’t measured. An export that copies its audio isn’t
affected.
A media3 clip whose source frames come faster than the output’s, from its speed or its source rate, leaves out the ones between the frames it shows through media3’s frame rate cap. The cap keeps a frame only at or past its next tick, a whole number of microseconds per output frame past the first frame it kept. An output frame isn’t a whole number of microseconds, so a long clip’s frames run ahead of those ticks, a third of a microsecond per frame at 30 fps. About 55 minutes into one clip at 30 fps, or 7 at 60, the cap keeps a frame it should leave out, and from there the file’s frames no longer sit on their slots. That holds for a 60 fps source at speed 1 into 30 as much as for a sped up clip. A clip whose source rate times its speed is at or below the output rate leaves nothing out and isn’t affected.
Filters
Section titled “Filters”Filters draw one shared signal on every backend, and these are where a backend lands apart from it or refuses.
- An Apple device that can’t compile a Core Image Metal kernel, which takes an Apple6 GPU on iOS or on macOS 12, refuses
vhsby name. - The browser draws a shader pass on a clip’s whole frame, so a clip whose own frame is longer on a side than the page’s
MAX_TEXTURE_SIZEis refused by name. A fit that crops makes that frame bigger than the output. - ffmpeg blurs with
gblur, a recursive approximation of a Gaussian that lands narrower than the sigma it’s given. A hard edge reads up to about a fifth of its step off, the frame’s own edge against the black past it included. Anywhere else it’s within a few codes. - Below about 0.005 pixels
gblur’s own filter darkens the frame, so a blur smaller than 0.01 pixels runs at 0.01, which changes no pixel. - An Apple device that can’t compile a Core Image Metal kernel refuses
crtby name too. - ffmpeg carries a frame in 4:2:0, which can’t hold the stripes of colour in
crt’s phosphor mask. Luma keeps them, within 3 codes of the other backends, and chroma holds their mean over each sample, within 3 codes. An encoded 4:2:0 file is the same on every backend. - ffmpeg builds
crt’s map once, as eight frames read from scratch files, and draws from them in sixteen-bit red, green and blue. With an x264 encode that ran at about 10 fps at 1080p and 2.6 fps at 4K on an x86 server, wherefilmran at about 2 fps at both. It peaks near 1.5 GB at 1080p and 5.4 GB at 4K, and acrton a clip or a track adds about 550 MB per 1080p clip, since ffmpeg holds each clip’s working frames until the export ends. Clips of one size share the eight frames. - An Apple device that can’t compile a Core Image Metal kernel refuses
lutby name too. luton media3 and the browser reads its table from a texture 2 timesceil(sqrt(N))tiles wide, so a grid of 65 points takes 1170x520 and a grid of 129 takes 3096x1419. A GL context whoseMAX_TEXTURE_SIZEis smaller fails by name, and media3 and the browser need OpenGL ES 3.- ffmpeg reads
filmgrain, andvhsgrain and dropouts, from planes filmstrip draws in Kotlin and hands over a loopback TCP socket, at the same samples ageqwould write. Measured on ffmpeg 8.0.1 on a 12 thread x86 server with x264 medium,filmruns about 17.5 fps at 1080p and 7.9 fps at 4K against 2.05 and 1.96 ongeq, andvhsabout 9.1 and 2.8 against 6.75 and 2.3. Peak ffmpeg memory forfilmwas about 1 GB at 1080p and 3.4 GB at 4K, and forvhsabout 1 GB and 3.3 GB. - A plan falls back to the
geqgraph by name in the diagnostics, as the same samples and slower, when the JVM cannot bind a loopback listener, when the ffmpeg build has notcpprotocol, when a graph would open more than 16 live inputs, and forvhswhen a source is not tagged BT.709. ffmpeg offers no way to copy one pad’s matrix onto another, and a VHS map carries chroma that a conversion would move. - A plan where every clip runs one equal
crtat the output’s size, decoded as plain 4:2:0 and joined with no gap, runs the look once after the join. That took 12 clips at 1080p from 5.5 GB to about 1.9 GB.filmandvhsstay on each clip, since they read a frame’s own timestamp and the join moves it. - Changing a
crtin a preview restarts ffmpeg like any graph change, measured at 0.6 to 1.7 s to the first frame on the same server. luton ffmpeg passes through RGB and 4:2:0 on its way back to the encoder. Measured on ffmpeg 8.0.1 x86_64, luma lands within 1.72 codes ofCubeLut.at, chroma within 1.17, and a channel read back as RGB within 2.59.- ffmpeg draws
lutthroughlut3dongbrap. With an x264 veryfast encode and a 65 point table that ran at about 70 fps at 1080p and 21 fps at 4K on a 12 thread x86 server, against 300 and 85 fps with no table. - A build missing a filter a look runs on refuses it by name, like any other effect.
Preview on iOS 27
Section titled “Preview on iOS 27”On the iOS 27.0 simulator runtime, a preview shown through an AVPlayerLayer in a window fails before play, a seek or a
first frame, and its status can read Ready for a moment first. An item with a video composition fails this way, and
every preview with video has one. A plain item with no video composition reached ready. In the demo app a 720p H.264
clip, the same clip trimmed and a 1080p H.265 clip all failed. So did a composition from Apple’s
videoCompositionWithPropertiesOfAsset with no filmstrip code in it, tried once in the demo app.
On the iOS 27.2 simulator runtime the 720p H.264 clip played in the demo app. The trim and the H.265 clip reached ready there and weren’t played through. On the iOS 26.5 simulator runtime a test read a video composition item in a window reaching ready, without playing it or reading a picture.
- The player’s status is
PlaybackStatus.Errorwith aPlaybackError.Underlying. ItsplatformCodeis -11800 and its message ends with(NSOSStatusErrorDomain -12784). OnePlaybackEvent.Failedreports it for each failed item. play()on that item starts nothing and the status staysError. Setting a new composition is what tries again.- An
AVPlayerItemVideoOutputon an item with a video composition fails the same way on that runtime, with no layer. - Export isn’t stopped there. In the demo app the 720p H.264 clip whose preview had just failed exported and re-probed at its full length, and filmstrip’s tone map export tests and one other Apple export test pass on fixtures on that runtime. Other clips and edits weren’t exported.
A physical iPhone on iOS 27 is unmeasured, so whether a device does this too isn’t known yet.
MediaSink.Uri is file:// only on Apple and ffmpeg, and anything else is refused as SinkUnwritable. ffmpeg also
takes a bare path there. Android writes to any other scheme through its content resolver. The browser ignores the URI
and hands back an object URL. See Sinks.
In-memory sources
Section titled “In-memory sources”On Android, Apple and ffmpeg a MediaSource.Bytes is written to a file before it is read, and the file stays until a
later process first uses a buffer. Disk use grows with the distinct buffers a process reads, with no bound. The browser
keeps the buffer in memory. See Sources.
ffmpeg writes under a directory named for the user in the shared temporary directory. If another user already holds that name, the process writes to a private directory of its own instead, removed when the JVM shuts down.
AVFoundation goes by a file’s name, so filmstrip names a buffer’s file from its leading bytes, the same names a path
would carry. A buffer of a container AVFoundation doesn’t open reads as SourceUnreadable, the same as a path does:
| Container | macOS | iOS Simulator |
|---|---|---|
| Matroska, WebM | No | No |
| Transport stream | Yes | No |
Android’s platform extractor refuses a QuickTime file with no ftyp box, so such a buffer reads as SourceUnreadable,
the same as a path to that file does. Apple and ffmpeg read it.
Browsers without WebCodecs
Section titled “Browsers without WebCodecs”WebCodecs is the only way filmstrip decodes or encodes in a browser, and there’s no fallback. Firefox for Android has neither half, per caniuse as checked on 2026-11-06.
- With no
VideoDecoder, a preview of anything with video fails before it reads a source. Its status isPlaybackStatus.Errorwith aPlaybackError.UnsupportedFormatsaying “This browser has no WebCodecs video decoder, so it can’t preview video.” AnAudioSpec.AudioOnlycomposition draws nothing, so it needs anAudioDecoderinstead. - With no
AudioDecoder, a preview of anything with video plays silently and doesn’t reportPlayerFeature.AudioMonitoring. AnAudioSpec.AudioOnlycomposition has nothing to play, so it fails before it reads a source, with aPlaybackError.UnsupportedFormatsaying “This browser has no WebCodecs audio decoder, so it can’t preview audio.” - With no
VideoEncoder, a preview runs as normal and carries its audio.Filmstrip.capabilities()lists no video encoder, and a plan refuses withFilmstripError.NoEncoder, so checkvideoon the capabilities to know an export can’t run. - With a
VideoEncoderand noAudioEncoder, an export writes no audio and its preview matches it, so both are silent.
Thumbnails need a VideoDecoder too. With none, each request fails with a FilmstripError.InvalidComposition carrying
the same words as the preview.
The demo on this site reads the same two globals. With no VideoDecoder it keeps the colour bars and says so where LOAD
would be, and nothing downloads. See Player.
Named in the guides
Section titled “Named in the guides”The rest are next to the feature they limit.
- Player: seeking, scrubbing and redraw cost per backend.
- Overlays: file paths in the browser, texture sizes, off-frame animation, the ffmpeg even pixel snap and line breaking.
- Effects:
kenBurns,rotate,Fill.Blurand color effects on a kept grade. - Audio: the media3 tail and the browser preview’s fades.
- HDR: what each backend can tone map and read.
- Stills: which backends put a still on the timeline.