Summary#
A video is a file in your app, like a picture or a sound. Drop an .mp4 or a .webm into model/videos/ and it
becomes a member of your app's Videos vocabulary — model/videos/harbour.mp4 is Videos.Harbour:
model/
videos/
harbour.mp4 → Videos.Harbour
textures/
harbour_still.jpg → Textures.HarbourStill (its still, the `poster`)| you have | write |
|---|---|
| a film the app ships, with the browser's controls | Video(video: Videos.Harbour, poster: Textures.HarbourStill) |
| a background film behind a heading | Video(video: Videos.Harbour, ambient: true, playing: moving) — or the UI kit's AmbientVideo |
a clip somebody uploaded (a FileAsset) | Video(fileAsset: row.Clip.Id) |
| a URL | Video(src: "/intro.mp4") |
Video is a renderer primitive: no using, nothing to install. It is never a picture — Image(texture:) takes a
Textures member and a film is a Videos member, so one cannot be handed to the other.
Signature#
Video(video: Videos.Harbour, poster: Textures.HarbourStill) // a film the app ships
Video(video: Videos.Harbour, ambient: true, playing: moving) // a background film, its pause yours to draw
Video(fileAsset: row.Clip.Id, poster: "/stills/clip.jpg") // a clip somebody uploaded
Video(src: "/intro.mp4", autoplay: true, muted: true, loop: true) // the HTML flags, each present when true
Video(video: Videos.Harbour, controls: false) // no browser controls on an ordinary filmEverything else a Video takes is the ambient vocabulary every element has — the style props (w, h, rounded,
objectFit, objectPosition), the accessibility props, the event props. osy kit atoms Video prints the lot.
Description#
How do I play a film the app ships?#
Put it in model/videos/ and name it by its member. Its poster: is the picture shown before it plays — a picture
the app ships in model/textures/, or a URL:
using Osysharp.Ui;
[Route("/tour")]
[Title("The harbour tour")]
component HarbourTour() {
render {
Stack(gap: 4, p: 6) {
Text("The harbour, from the water", fontWeight: FontWeight.Bold);
Video(video: Videos.Harbour, poster: Textures.HarbourStill, w: "100%", rounded: Radius.Lg);
}
}
}The film is served from your app's own origin, under the type its bytes say it is (video/mp4 or video/webm,
never what its name claims), and the server answers the byte ranges a browser asks for as it buffers and seeks —
Safari plays nothing from a server that will not.
How do I put a film behind a heading?#
ambient: true is the one switch for a background film. It means, all at once: muted (a browser will not start
sound on its own), looping, inline on a phone, no browser controls, and started by the platform — but only
for a reader who has not asked their device for reduced motion. For that reader the film never moves; they see its
poster.
The UI kit's AmbientVideo is the whole thing — the film filling its box, your words on top as its children, and a
pause button in the corner that keeps one name ("Pause the film") and says whether it is pressed:
using Osysharp.Ui;
[Route("/welcome")]
[Title("Welcome aboard")]
component Welcome() {
render {
Box(h: "420px") {
AmbientVideo(Videos.Harbour, Textures.HarbourStill) {
Stack(p: 8, h: "100%", justify: Justify.End) {
Text("Sail with us", fontSize: FontSize.Title, fontWeight: FontWeight.Bold, color: "#ffffff");
}
}
}
}
}Why does a background film need a pause?#
Anything that moves on a page for more than five seconds needs a way to stop it (WCAG 2.2.2) — for a reader with a
vestibular condition, for one who is trying to read the words over it, for one on a slow phone. An ambient film has no
browser controls by design, so the pause is yours to draw, and osy lint holds you to it: an ambient film with no
reachable pause is the MUST rule ui-ambient-video-without-a-pause.
How do I draw my own pause?#
Bind playing: to a field. It goes both ways: write false and the film pauses, write true and it plays — and
the film writes back when it starts, pauses or ends by itself. So the pause is any control that holds that one boolean:
a Switch bound to the same field is the whole of it. Under reduced motion the field reads false from the start, so
the switch shows the film as off, and turning it on plays it: the platform withholds only its own start, never the
reader's.
⚠ Not a Button whose label swaps between "Pause" and "Play" — a control that changes its own name cannot be found by
it once pressed, and a screen reader hears one button vanish and another appear (ui-toggle-written-as-a-button).
using Osysharp.Ui;
[Route("/sea")]
[Title("The sea")]
component Sea() {
bool moving = true;
render {
Stack(gap: 3, p: 6) {
Video(video: Videos.Harbour, poster: Textures.HarbourStill, ambient: true, playing: moving,
w: "100%", h: "360px", objectFit: ObjectFit.Cover, rounded: Radius.Lg);
Switch("Play the film", value: moving);
}
}
}What about autoplay, muted, loop and controls?#
They are HTML's own flags, and they are here for a film that is not a background one. Each is present when true and
absent when false — loop: false does not loop.
| flag | default | what it does |
|---|---|---|
controls | true (false on an ambient film) | the browser's own play button, scrubber and volume |
autoplay | false | starts the film by itself — and puts it inline on a phone, which iOS needs |
muted | false | no sound |
loop | false | starts again at the end |
⚠ A browser refuses to start a film with sound on its own. autoplay: true without muted: true is legal, and the
film then sits still until the reader interacts — so the compiler WARNS (VIDEO_AUTOPLAY_WITH_SOUND). Restating a flag
beside ambient: true is refused: ambient already says it.
using Osysharp.Ui;
[Route("/loop")]
[Title("A short loop")]
component ShortLoop() {
render {
Video(video: Videos.Harbour, autoplay: true, muted: true, loop: true, controls: false, w: "320px");
}
}How do I play a clip somebody uploaded?#
fileAsset: takes the id of a FileAsset — the row an upload or File.Create answers with — exactly as
Image(fileAsset:) does. The renderer asks for a short-lived signed address when it draws, so the reader's own read of
the row decides whether they may watch it, and the file route answers byte ranges too:
using Osysharp.Storage;
using Osysharp.Ui;
entity Reel {
[Required, MaxLength(80)] string Title;
FileAsset? Clip;
security { allow read when IsAuthenticated; }
}
[Route("/reels")]
[Title("Reels")]
component Reels() {
live var reels = Reel.OrderBy(r => r.Title);
render {
Stack(gap: 4, p: 6) {
foreach (var r in reels) {
Stack(gap: 1) {
Text(r.Title, fontWeight: FontWeight.Bold);
Video(fileAsset: r.Clip.Id, w: "480px");
}
}
}
}
}Which files does model/videos/ take?#
An .mp4 (.m4v) or a .webm — the two containers every current browser plays — of at most 24 MB each, and
256 MB for all of an app's films together. The type is read from the file's own first bytes: a picture or a song named
.mp4 is refused at compile time, naming what it really is (an audio-only MP4 belongs in model/sounds/). A 15-second
background loop encoded for the web at 1080p is 3–15 MB; footage people upload belongs in the file store, played with
fileAsset:.
Errors#
| you wrote | the compiler says |
|---|---|
Image(texture: Videos.Harbour) | Image shows a picture; a film plays in Video(video: Videos.Harbour) |
Video(video: "harbour") | video: takes a Videos member — and lists every film this app ships |
Video(src: …, video: …) | Video plays one film — keep one of src:, video:, fileAsset: |
Video(ambient: true, muted: true) | ambient: true already makes the film muted — drop muted: |
Video(video: Videos.Harbour, autoplay: true) | WARNING VIDEO_AUTOPLAY_WITH_SOUND — add muted: true, or write ambient: true |
Video(…, playing: count) (an int) | Video.playing binds a Boolean |
| an ambient film nobody can stop | osy lint MUST ui-ambient-video-without-a-pause |
See also#
- Showing a picture on a page — a picture, addressed the same three ways
- textures — the pictures an app ships, which a
poster:names - sound — sounds the app ships, played by name
- upload — how a clip arrives as a file
- Osysharp.Ui (the UI kit) —
AmbientVideoand the rest of the kit