Osy#betaRun it on Osyrin: osyrin.com

Reference / UI

Playing a video on a page

Video(video: Videos.Harbour, poster: Textures.HarbourStill) · Video(video: Videos.Harbour, ambient: true, playing: moving) · Video(fileAsset: row.Clip.Id) · Video(src: "/intro.mp4", autoplay: true, muted: true)

`Video` plays one film, addressed one of three ways: a film the app SHIPS in `model/videos/` (`video: Videos.Harbour`, compile-checked), a clip someone uploaded (`fileAsset:` — a `FileAsset` row), or a URL (`src:`). `ambient: true` makes it a background film — muted, looping, inline, no browser controls, and still for a reader who asked for reduced motion — and `playing:` is bound both ways so the app draws its own pause. The HTML flags `autoplay`, `muted`, `loop` and `controls` are there too, and follow the browser's rules.

preview5 examples compiled by CIuiatomsassetsmedia

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 havewrite
a film the app ships, with the browser's controlsVideo(video: Videos.Harbour, poster: Textures.HarbourStill)
a background film behind a headingVideo(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 URLVideo(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 film

Everything 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.

flagdefaultwhat it does
controlstrue (false on an ambient film)the browser's own play button, scrubber and volume
autoplayfalsestarts the film by itself — and puts it inline on a phone, which iOS needs
mutedfalseno sound
loopfalsestarts 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 wrotethe 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 stoposy lint MUST ui-ambient-video-without-a-pause

See also#

Related

Showing a picture on a page

`Image` is the element that shows a picture, and it addresses one of two ways. `src` takes a URL — for a file in the…

textures

Drop `.png`, `.jpg` or `.webp` files into `model/textures/` and blit them onto a canvas with `Draw.Image(wall, …)`. The…

sound

Drop `.mp3`, `.wav`, `.ogg` or `.m4a` files into `model/sounds/` and play them with `Sound.Play(Sounds.Flap)`. The name…

upload

`Upload(onUploaded: Ingest) { … }` is a file picker wearing whatever you put inside it. When someone chooses a file its…

camera and microphone

Take a photograph, record a video, or record audio. `Camera.Start()` asks for the device and `Camera()` shows the…

Osysharp.Ui (the UI kit)

The bundled UI kit — ready-made styled controls like `Button`, the shared design-system vocabularies (`Tone`, `Size`)…

Files (addressing something the app stores)

The platform stores a file two ways, and which one you hold decides how you address it. A PATH-keyed file lives at an…