A video player for looping video art. Its one job is the thing ordinary players get wrong: crossing the loop point without a gap in the sound, a stutter in the picture, or a black frame.
Made for gallery walls, projections and screens that have to run unattended for hours.
Platforms. The native app is macOS only (macOS 12 or newer) — it is built on Apple's AVFoundation. For Windows and Linux there is the browser player in
web/, which uses the same seamless technique and runs fullscreen throughtools/kiosk.py.
QuickTime Player, VLC and IINA all restart their playback pipeline when a file loops: they seek back to the start, flush the audio buffers, and refill them. You hear a short silence and often see a held frame. For a film that is invisible; for a one-minute loop that repeats sixty times an hour it is the thing your audience notices most.
Two smaller causes pile on top of it. Lossy audio codecs such as AAC begin with a few milliseconds of priming samples that get decoded again on every restart. And a loop whose last frame is not the visual neighbour of its first frame will jump no matter how good the player is — see docs/seamless-looping.md for how to author one that does not.
Uses AVPlayerLooper, which enqueues and prerolls the next cycle while the
current one is still playing. Nothing is torn down at the loop point, so audio
and video simply continue.
./macos/build.sh # writes build/Video Art Player.appRequires the Xcode command line tools (xcode-select --install). There are no
other dependencies and nothing to install — the result is a self-contained
140 KB app bundle.
Put the app next to your video and double-click it: it picks up the video beside the bundle and plays it fullscreen, looping forever. Or drive it from a terminal:
"build/Video Art Player.app/Contents/MacOS/VideoArtPlayer" loop.mov --screen 1| Option | Effect |
|---|---|
--screen N |
Which display to use, 0 is the main one |
--fill |
Crop to fill the display instead of letterboxing |
--window, -w |
Windowed instead of fullscreen |
--volume V |
Volume, 0.0 to 1.0 |
--mute |
Start muted |
--test |
Measure the gap at the loop point, print it, exit |
--help |
Full usage |
Keys while playing: Esc/Q quit · Space pause · M mute · F windowed/fullscreen · C cursor.
The app keeps the display and the system awake for as long as it runs, and hides
the cursor and the menu bar in fullscreen. Given several files it prefers .mov
over .mp4, because uncompressed audio has no priming delay to stumble over.
Because the app is not notarised, macOS blocks it on first launch: right-click →
Open, or xattr -dr com.apple.quarantine "Video Art Player.app".
--test seeks to just before the end, then compares wall-clock time against
media time across the loop point. A player that flushes its pipeline shows up as
a positive gap.
$ VideoArtPlayer loop.mov --test
test: loop point crossed, gap -9 ms (wall clock 1.85 s vs media 1.86 s), rate=1.0
Anything under a few tens of milliseconds is measurement noise. It exits 0
when the gap is within tolerance, so you can put it in CI or a pre-show check.
web/index.html is a single self-contained file. Open it,
drop your video in, and it plays fullscreen and loops.
If you also drop in a separate audio file, the video plays muted and the
soundtrack loops through Web Audio as an AudioBufferSourceNode, which
continues sample-exactly across the loop point instead of restarting. The video
is then softly locked to the audio clock by nudging its playback rate a fraction
of a percent — never by seeking, which would be visible.
For an installation, let the launcher do it:
python3 tools/kiosk.py loop.mp4 soundtrack.wavThat serves the player and your media on localhost (with byte-range support, so
4K files seek properly), then opens Chrome, Edge, Brave or Firefox in kiosk
mode with autoplay allowed. Python 3 is the only requirement. --browser,
--port, --fill and --no-browser are there when you need them.
Keys: Space pause · F fullscreen · M mute · C cursor · D diagnostics (loop count and audio/video drift).
- macOS — System Settings → General → Login Items, or a
launchdagent. - Windows — put a shortcut to
pythonw tools\kiosk.py …inshell:startup, or use Task Scheduler with "At log on". - Linux — a
~/.config/autostart/*.desktopentry, or asystemd --userservice.
| macOS app | Browser player | |
|---|---|---|
| Platforms | macOS 12+ | macOS, Windows, Linux |
| Dependencies | Xcode CLT to build, none to run | a browser; Python 3 for kiosk mode |
| Gapless audio | always, from the file itself | with a separate audio file |
| Multiple displays | --screen N, one instance per display |
one browser window per display |
| Best for | a Mac driving a gallery screen | anything that is not a Mac |
Verified here on macOS 26 (Apple silicon) for the app, and in Chromium for the browser player. The browser player uses only standard web APIs, so Chrome, Edge and Firefox on Windows and Linux should behave the same — but that has not been tested on those platforms.
A seamless player cannot rescue a video that does not loop. The short version: build all motion from components whose period divides the loop exactly, so the last frame lands back on the first, and quantise every pitch in the soundtrack to a whole number of cycles per loop. No crossfades, no fade to black.
The full method, with the arithmetic and the encoder settings that preserve it, is in docs/seamless-looping.md.
examples/silkstream/ contains a complete generative
work built this way — a one-minute 4K loop with an ambient soundtrack, where the
last frame is bit-identical to the first and the audio seam is smaller than a
single sample step. Both generators are seeded, so rendering reproduces exactly
the same artwork.
MIT — see LICENSE.
Created by Serge de Beer. The Silkstream example is his artwork; the code that generates it is MIT-licensed along with the rest of the repository.
