skills/showtime/references/html-export.mdHTML export: share a video as one interactive web page
Read this when the user wants to share, send, embed or publish a video as a web page instead of (or as well as) an MP4: "send me something I can open in the browser", "put it on the site", "make it shareable as an artifact", "an HTML version", or when the MP4 is not the point (a canvas film with a procedural score, an interactive demo someone scrubs through).
showtime export html <project> # -> showtime-out/<title>-<ts>/<title>.html
showtime export html <project> -o launch.html # one file, opens offline in any browser
showtime export html <project> --folder -o site/ # index.html + assets/ for web hosting
showtime export html <project> --target artifact -o launch.html # to publish as an HTML artifact
What you get#
One .html file (default) that holds the whole project: the stage runtime, the page, its
scripts and styles, the libraries it loads, fonts, images, emoji, JSON data, footage and the
mixed audio. Opened from disk, a chat attachment or a web server, it makes no network request
(a Content-Security-Policy in the file blocks any that a page might try).
It plays in a small player:
- a start screen until the viewer clicks, taps or presses a key (browsers only allow sound
after one): the poster frame drawn live under a soft scrim in the film's ground colour, and in one
corner the title, the subtitle and a small line above it (showtime.json
"subtitle","kicker", or--subtitle/--kicker, orFilm.start({subtitle, kicker})), a Play button with the length and "N chapters · Sound on". The title uses the film's own title font (the stage hands the player its font files) and the button the film's accent colour; every colour is checked for contrast against the ground and replaced when it would not read. The corner is the one where the poster frame has the least text (the stage reports where its text sits), so the block never covers the headline. The page's<title>is the project title; nothing else is written over the picture.--start posteralso packs the frame as an image, shown while the page loads. - fills the screen: the page around the picture is the film's background colour (never a black frame around it). On a phone held upright with a landscape film the picture runs full width at the top; the title, the Play button and a tappable chapter list sit under it, the controls are docked at the bottom within thumb reach and a line suggests turning the phone for full screen. A vertical film fills a phone's width; safe-area insets (notches, home bar) are respected.
- play/pause, a scrubber with chapter ticks, a time tooltip and the rendered part of a live score,
time, the current chapter (click it for the chapter menu), volume and mute, loop, fullscreen,
"copy a link to this moment", and
?for the key map; the controls hide while playing and come back on mouse move or tap (touch: 48 px targets, the time in 15 px type, chapter names in the scrubber tooltip) - deep links:
film.html#t=72.5,#t=1:12.5,#t=1m12sor#chapter=3/#chapter=data(the chapter's label) open the video there: the start screen says where, and playback starts there after the click. Changing the hash while it plays jumps there. - fits any window size or aspect, works with touch, respects reduced motion (no muted autoplay), labelled for screen readers; the controls use the system font
Keyboard (shown in the player with ?):
| Keys | Action |
|---|---|
| any key (before starting) | begin (a digit begins at that chapter) |
| Space, K | play / pause |
| ← / → | back / forward 1 s |
Shift + ← / →, , / . |
one frame back / forward |
| J / L | back / forward 5 s |
| 1-9 | jump to chapter 1-9 (a video without chapters: 10-90 %) |
[ / ], Page Up / Page Down |
previous / next chapter ([ goes to the start of the current chapter first when more than 1.5 s into it) |
| 0, Home / End | start / last frame |
| R | restart from the beginning and play |
| M, ↑ / ↓ | mute, volume |
| F | fullscreen |
| C | copy a link to this moment (#t=) |
| ? , Esc | show / hide the key map |
Frames are exactly the render's. The page runs in the same stage runtime showtime render
uses (virtual clock, seeded randomness, the same config), and the player seeks it to the
audio's time on every screen refresh, so a paused frame at t is the frame of the MP4 at
t (the test suite compares them with showtime snap). Picture follows the audio: there is
no drift, and a slow machine drops frames instead of falling behind the sound.
MP4 or HTML?#
| Share an MP4 when | Share HTML when |
|---|---|
| it goes to a social platform, a video host or an editor | it is opened by a person in a browser: a link, a chat, a docs page, an artifact |
| the page is heavy to draw (large blurs, many shaders, 4K) | it is a canvas film or motion graphics with a procedural score: a few hundred KB instead of MBs |
| it has long real footage (the HTML carries the clips as data) | people should scrub, pause on a frame, jump by chapter, or you want text to stay sharp at any size |
| it must play on anything, including smart TVs and mail clients | you want it embeddable (--controls none) or hosted as a page |
Both come from the same project, so offering both costs one command. After a render, offer the HTML version in one line when the destination is a browser.
Audio modes (--audio)#
| Mode | What is in the file | Use |
|---|---|---|
auto (default) |
score when the only sound is ST.score, else embed |
almost always |
score |
nothing: the browser plays the procedural score itself, streamed: it renders the score in short pieces ahead of the playhead (the first one while the start screen shows, usually well under a second) and the rest in the background, at the render's loudness (same gain and limiter the exporter measured) | canvas films and tutorials scored with Synth: the smallest file |
embed |
the audio exactly as the render builds it (score + audio mix, loudness to -14 LUFS) as AAC in .m4a at --bitrate (default 96k; about 12 KB per second) |
voice, music files, anything mixed; every narrated film (a voice is a mix: score would drop it, and footage layers make the file large anyway). auto already picks it |
none |
no sound | silent loops, embeds |
--codec opus makes the embedded track about a third smaller at the same quality; AAC is the
default because Chrome, Edge, Safari and Firefox decode it. Chromium builds without proprietary
codecs (and some Linux Firefox installs) cannot: the picture then plays silently and the player
says so on screen; --codec opus plays there. --lufs and --no-loudnorm work as in render.
With --audio score a project that also has an audio mix loses the mix (a warning says so): a
narrated film exported with --audio score has no voice. Leave --audio at auto (or say embed).
How the live score streams (and why seeking is exact)#
A Synth score sounds the same rendered from any time as the same stretch of a render from 0
(synth-score.md §5b): sustained sounds resume at their level and phase, short ones are not replayed,
noise is keyed to film time, and every event sits half-way between two samples so all renders round
it the same way. The player renders pieces of 1.5-12 s (sized to the machine's speed), each started
2.5 s early on a 128-sample boundary so reverb tails and compressors have settled, applies the
export's gain and limiter, and plays them through Web Audio back to back; the sound is the clock and
the picture follows it. A seek into a rendered stretch plays at once, elsewhere as soon as that piece
is ready (the picture holds with a spinner meanwhile). The test suite checks that the streamed sound
equals a whole render of the score to about -80 dB. A hand-written ST.score that does not honour
run.from is rendered whole before playing (mark it ST.score.seekable = true if it does).
window.showtimePlayer.audio is then {kind: 'score', currentTime, paused, duration, rendered(), level(t0, t1), verify(t0, t1), tap()}: verify compares the stream with a whole render, tap()
returns an AnalyserNode on the output.
Size budget#
The single file must stay under --max-mb (default 16 MB, the artifact size limit; 0 turns
it off). Base64 adds a third to binary files. When the export would be bigger, nothing is
written and the error lists the size by kind (video, audio, fonts, images, scripts) and the
largest files, with what to do. Typical sizes: a 12 s canvas film with a live score about 220 KB, a two-minute canvas tutorial about
170 KB, a 15 s DOM launch video with an embedded mix 0.7 MB. What keeps procedural films small:
- the runtime is trimmed to what the video uses:
film.jsandsynth.jsdrop sections the project never calls (charts, device frames, drum kit ...; the report lists them), the stage runtime drops its preview player; scripts and styles lose comments and spaces (--minify offkeeps them as written); - every text file (scripts, styles, data, the page) and the player itself travel gzip-compressed and are unpacked by the browser (DecompressionStream; every 2023+ browser has it);
- fonts: only WOFF2 (the older formats a stylesheet lists are left out), no faces for alphabets the video
never shows, and for canvas films no families no frame draws with (
--all-fontskeeps everything); - no poster image with the default start screen (the frame is drawn live).
Fonts are then usually the largest part: a variable font's Latin file is 30-65 KB.
Ways down: --bitrate 64k or --codec opus --bitrate 48k; --audio score for score-only
projects; footage recompressed (-c:v libvpx-vp9 -crf 36, the size it is shown at) or --folder;
images at the size they appear; fewer font families and weights.
Sharing and hosting#
- A file: send the
.html; it opens with a double-click. Nothing is uploaded by showtime. - An artifact or a docs page: export with
--target artifact(one file, held under 16 MB) and publish that. It plays when a host shows it in a sandboxed frame (even one withoutallow-same-origin; fullscreen then depends on the host). In such a frame the player hides what needs a file address or a download ("copy a link to this moment", the deep-link hint in the key map);--target artifactbakes that in, and the player also detects a sandboxed or claude.ai frame by itself. A host may also add its own Content-Security-Policy (inline styles only, no font URLs): the stage writes every stylesheet inline and builds fonts from their bytes (FontFace), so the theme, its fonts and its sizes survive that. A font that still fails is reported in the console; add#st-debugto the address to see the reports on screen. Publishing is the user's call; offer it, do not do it unasked. - A web site:
--folder -o site/writesindex.html,assets/vfs.js(scripts, styles, fonts, small images) andassets/media/(footage, the mixed audio and large images as real files, no base64, no size limit). Upload the folder anywhere static. It also opens from disk. The folder carries no Content-Security-Policy (it loads its own files), so the no-network guarantee is the single file's; set a CSP on the server if you need one. - Embedding:
--controls none --autoplay-muted --loopgives a bare looping picture (a click toggles pause); put the file in an<iframe>. Pages can drive it throughwindow.showtimePlayerinside that frame:ready(promise),play(),pause(),restart(),seek(t)(resolves when the frame is drawn),currentTime,duration,paused,started,muted,volume,loop,chapters,chapter(current),goToChapter(i),startTime(from a deep link),link(t)/copyLink()({url, hash, full}),audio,on('play'|'pause'|'seek'|'ended'|'loop'|'frame'|'ready'|'restart'|'link', fn); the player element also dispatchesshowtime:<event>DOM events.
Options#
| Option | Default | Notes |
|---|---|---|
-o FILE / --job JOB |
new job folder | never overwrites (-2, -3 ...); -o an existing folder (or dir/) writes <dir>/<title>.html; --job J -o embed.html writes that name inside the job folder |
--folder |
off | -o is then a folder |
--controls full|minimal|none |
full | minimal: play, scrubber, mute, fullscreen |
--autoplay-muted |
off | starts muted as soon as it loads, with a "Tap for sound" button |
--loop |
off | the viewer can toggle it |
--target file|artifact |
file | artifact: for a sandboxed host (one file, max 16 MB, no link/download features) |
--start card|poster |
card | what shows before playing (see above) |
--poster T / none |
showtime.json poster, else 40 % |
the frame behind the start screen (pick one with space in a corner for the title); with --start poster (or an explicit --poster) it is also packed as an image shown until the page is ready |
--lang CODE |
showtime.json lang, else the page's <html lang>, else the narration's lang:, else en |
sets <html lang> and the player's own words (Play, Chapters, Sound on, the key help) in en, es, fr, pt or de; other languages get English controls |
--audio-file FILE |
embed exactly this sound (a WAV, or the shipped MP4's audio) instead of rebuilding the score and the mix | |
--title, --subtitle, --kicker |
showtime.json title, subtitle, kicker |
page title and start screen |
showtime.json "startTitle": false |
title shown | the poster frame already says what the video is (a hook frame): only the Play row sits over the picture, on a light corner scrim; the title still heads the phone layout |
--minify auto|off |
auto | trim and compress the runtime, scripts and styles (off: as written, uncompressed; for debugging) |
--json |
report: output, bytes, audio, chapters, sizes by kind, largest files, warnings | |
--all-fonts, --no-csp, --page, --keep-work |
see showtime export --help |
Chapters come from showtime.json "chapters": [[0, "Intro"], [4.5, "Demo"]], else the film's
acts (or chapters) in Film.start, else the top-level clips (named by data-name or id). They
drive the scrubber ticks, the chapter menu, keys 1-9 and [/], and #chapter= links: give a video
with steps (a tutorial, a demo) one chapter per step.
A tutorial series (showtime new series) exports all at once: showtime series export <series> -o site/ writes every episode and the opener as single files plus an index.html listing them.
Limitations#
- Sound needs a click in every browser;
--autoplay-mutedstarts the picture muted instead (a live score then starts when the viewer taps for sound). - Deep links and copied links need the file to have an address: from disk, a web server or a
link to the file. Inside a host that shows it in a sandboxed frame (an artifact page) the hash of
the frame cannot be set from outside, so the player hides "copy link" there (the
ckey still copies#t=...with a note). iPhone: the volume slider is hidden (iOS only allows mute) and there is no fullscreen button (Safari has no element fullscreen on iPhone): the player suggests turning the phone instead. - Playing is drawing. The browser draws each frame live, so pages that take long to draw a frame (big blur filters, several shader layers, footage seeked every frame) drop frames on slower machines where the MP4 would not. Footage in pages is seeked per frame for exactness, which is smooth for short clips and heavy for long ones: use the MP4 for footage-led videos.
- Shader transitions are drawn live from a snapshot of the two scenes (the render captures them as screenshots); they look the same but are not bit-exact during the transition.
- Old browsers: needs a current Chrome, Edge, Firefox or Safari (2023+): import maps, blob
URLs,
srcdoc, DecompressionStream (older browsers get a one-line message instead of the video). A page's own import map ("three": "/_lib/three/build/three.module.js", prefix entries like"three/addons/") is folded into the player's single map, so bare imports work. - Codecs: footage plays only where the browser decodes it. H.264
.mp4clips and the AAC audio do not play in Chromium builds without proprietary codecs; VP9/WebM footage and--codec opusplay in every current browser engine. - --folder from disk (
file://): browsers refusefetch()of files there, so pages that read footage or large images as data (WebGL textures from those,fetchof a video) need the folder served over http; everything else works from disk. - Size: the embedded audio costs about 12 KB/s at 96k; embedded footage costs its size plus a third.
A score rendered live (
score) is rendered by the viewer's browser before playback can start: for a 12 s score that took from about 1 s to 10 s in testing, depending on how busy the machine was, and it grows with the score's length (a click in the meantime starts playback once ready). - Page code that builds URLs in unusual ways (reading
document.currentScript.src, string surgery onlocation.href, CSS@importadded at run time) may miss the packed files (new URL(path, location.href)does work: it resolves against the page's own address);showtime exportwarns about any file the page asked for that it could not pack, and the browser console names what is missing.