# Playing a video on a page

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

<!-- id: ui-video · area: ui · stability: preview · html: https://osysharp.com/reference/ui/video/ -->

## Summary        {#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`:

```text
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      {#signature}
```osy syntax
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    {#description}

### How do I play a film the app ships?   {#shipped}
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:

```osy title="a film with the browser's own controls" test app=media-demo
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}
`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:

```osy title="a background film with its own pause, from the kit" test app=media-demo
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?   {#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?   {#playing}
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`).

```osy title="a background film and a pause you draw" test app=media-demo
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?   {#flags}
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.

```osy title="a short loop that starts by itself, without sound" test app=media-demo
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?   {#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:

```osy title="a list of uploaded clips, each playable" test app=media-demo
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?   {#files}
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         {#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       {#see-also}
- [Showing a picture on a page](https://osysharp.com/reference/ui/image/) — a picture, addressed the same three ways
- [textures](https://osysharp.com/reference/ui/textures/) — the pictures an app ships, which a `poster:` names
- [sound](https://osysharp.com/reference/ui/sound/) — sounds the app ships, played by name
- [upload](https://osysharp.com/reference/ui/upload/) — how a clip arrives as a file
- [Osysharp.Ui (the UI kit)](https://osysharp.com/reference/ui/kit/) — `AmbientVideo` and the rest of the kit
