---
title: The video player
---

# The video player

## What it is

Omniscio has **one video player**, and every video plays in it. Whether you are watching a video in the app, opening a shared link in a browser, looking at a video embedded on another website, or watching a film an agent drew live with code, you get the same controls in the same places and the same keyboard shortcuts.

The controls are always the normal ones you expect from a video: play and pause, a bar you can drag to jump around, the time, volume, a speed menu, captions, picture-in-picture, download and full screen. If the player ever fails to start for some reason, the browser's own built-in controls stay in place instead, so a video can always be played.

## Where to find it

It appears wherever a video plays:

- **In the app** — a video in your daily drip feed, a video opened in a chat or gallery lightbox, a screen recording you are reviewing, a Telegram video message, and the video preview on an inbox card.
- **On a shared link** — the page a shared screen recording opens on, in any browser, on a computer or a phone.
- **On an embedded video** — the small player other websites (and the inbox card) show for a shared video.
- **On a live film** — a film an agent builds by drawing each frame with code rather than recording a file.

## How it behaves

### The controls

- A large **play button** sits in the middle of the video until it starts.
- The **bar along the bottom** holds every control: the Omniscio mark, play/pause, volume, the time (for example 0:12 / 1:30), captions, picture-in-picture, download, the speed, and full screen. Buttons only appear when they do something — captions only when the video has captions, download only when the video offers a file.
- The **speed control** is in that bar, beside full screen, with speeds from half speed to triple speed. Inside the app the speed you pick is remembered for the next video; see [Video playback speed](video-playback-speed.md).
- The **Omniscio mark** — the brand orb — begins the bar. It is decoration, not a button: it does nothing when clicked and is invisible to a screen reader, and on a very small player it steps aside so the controls keep their room.
- Clicking the picture plays or pauses; double-clicking it goes full screen.
- While the video plays and you are not touching it, the bar fades away after about two and a half seconds. Moving the mouse brings it back, and moving the mouse off the video hides it straight away. It never hides while the video is paused, while the mouse rests on the controls, while you are dragging the bar, or while the speed menu is open.
- **Tab** moves from control to control, and the one you are on is outlined.
- **Full screen** takes the whole player, so the bar and every control on it come along, and the video fills the screen. On a website that shows the video in a frame without allowing full screen, the browser refuses it there, so the button is left out rather than shown doing nothing. On Omniscio's own inbox cards and on a shared recording's page, full screen always works.

### Keyboard shortcuts

Click the video, press its play button, or tab to it first — the keys only act while the player has focus, and they never also trigger one of the app's own shortcuts.

| Key | What it does |
|---|---|
| Space or K | Play / pause |
| J / L | Back / forward 10 seconds |
| ← / → | Back / forward 5 seconds |
| ↑ / ↓ | Volume up / down |
| M | Mute / unmute |
| F | Full screen |
| C | Captions on / off |
| Shift + > / Shift + < | Faster / slower |
| 0 – 9 | Jump to that tenth of the video (0 is the start, 5 is halfway) |
| Home / End | Jump to the start / the end |
| Esc | Close the speed menu |

Each button's tooltip shows its key, and the keys listed there always work. When the volume slider is the control you are on, the arrow keys and Home / End change the volume instead; everywhere else in the player they move through the video.

### Phones

- Videos play **inside the page** on a phone instead of jumping to the phone's own full-screen player.
- A **tap** on the picture shows or hides the controls; it does not pause the video. Use the play button to pause.
- On an iPhone, full screen hands the video to Safari's own full-screen player (an iPhone has no full screen for anything but the video itself); an iPad keeps the whole player.

### Remaining gaps

- **Links shared before the one player existed keep their old page.** A shared recording page is built at the moment it is shared, so an older link still shows the player it was published with. Sharing it again gives it the new player.
- A shared link or embedded video always starts at normal speed, because a public page cannot read your Omniscio settings.

## For agents

### How it works (under the hood)

- The player is one framework-free script under `src/renderer/src/lib/video-player/` — plain DOM, no React — so the same code runs in the app, on a share page and on the embed page. `mountVideoPlayer(root, options)` builds it onto an element holding a `<video>`; the returned handle's `destroy()` removes everything it added and gives the browser's controls back.
- In the app, [`<VideoPlayer>`](../../src/renderer/src/components/ui/VideoPlayer.tsx) is the only way to play a video: it renders a `<video controls>` and mounts the player on it, with translated labels and the app's accent colour. The remembered speed comes from the `videoPlaybackRate` setting.
- Outside the app, `npm run video-player:build` bundles the player into one script string, committed as `src/shared/video-player-script.generated.ts` (and a copy for the embed page's cloud function). A page gets the player by marking its video's wrapper with `data-omniscio-player` (optionally `data-download-url` / `data-download-name`) and including that script — it mounts every marked player on load, and exposes `window.OmniscioVideoPlayer` (`mount`, `createTimelineMedia`, `autoMount`) for a page that mounts one itself, such as a live film. A test fails the build if the committed script is out of date.
- The **brand mark** is built by `brand.ts` from `ORB_PNG_BASE64` in [`src/shared/share-brand-assets.ts`](../../src/shared/share-brand-assets.ts) — the one copy of the orb bytes, shared with the public share pages and their thumbnail cards, so the player never keeps a second copy that can drift. It rides inline as a data URI because the CSP-locked embed page can fetch nothing external, and it is `aria-hidden`/`alt=""` on purpose: a maker's mark inside the controls is decoration, not a button, and announcing it would add noise to every video's keyboard order. The orb is never redrawn or recoloured (brand guide); it is the real image.
- A **live film** plays through the same player: `createTimelineMedia` turns a draw-one-frame function plus a duration (and, optionally, a soundtrack) into something the player can play, pause, seek and speed up like a video.
- **There is no second player.** A build guard fails on any new `<video>` element outside a short approved list — the player's own hosts, plus video that is not something a person watches, such as the live webcam self-view or an off-screen decoder — and its message names the one player to use instead. The rules the player keeps are in its contract, `.claude/memory/contracts/video-player-contract.md`.

### Publishing a video or a film

- **A video file** — an agent publishes an **H.264 MP4** through `POST /share/publish` by its `path`. The one video publisher (the same one screen recordings use) uploads it, and the link opens a share page that plays it in this player. Any other format is refused before upload, with a hint to convert it through `POST /convert` first. Details: [Share via CLI](share-cli.md#videos-and-live-films).
- **A film drawn in code** — an agent fetches this player's script from `GET /video-player/script`, puts it in its HTML page, mounts the film with `OmniscioVideoPlayer.mount(wrapper, { media: OmniscioVideoPlayer.createTimelineMedia({ duration, render }), surface: canvas })`, and publishes the page. It never builds its own controls.
- Every session Omniscio starts is told both routes in its standing publish instructions.

### Traps when changing it

- **A frame that shows the player must grant `allow="fullscreen *"`.** Chromium honours a plain `allow="fullscreen"` on a sandboxed frame, but WebKit refuses it, and it never reaches a page sandboxed by its own security policy (the share page's shell frames one). Without the grant the player leaves its full-screen button out.
- **A control that hides itself while it has keyboard focus must hand focus to the player first.** The big play button does; otherwise focus falls to the page and the player's keys stop working the moment it hides.
- **The app's own shortcuts hear a key before the player does.** Stopping the key inside the player cannot keep an app shortcut out; the `.ovp-root` row in `src/renderer/src/hooks/keyboard-shortcuts/surface-yields.ts` is what does.
- **Everything lives in the bar, and the speed menu opens UPWARD out of it.** Moving a control out of `.ovp-bar` changes what auto-hide treats as the control area (`CONTROL_AREA_SELECTOR` in `auto-hide.ts`) and what hides with the bar. The speed menu's own height is capped to the room above it, so a short player scrolls the list instead of pushing it off the bottom edge.
- **The full-screen size rule is `!important` on purpose.** The recording review and the lightbox cap the video's height inline, which would otherwise win in full screen and leave a small video in a black screen.
- **Only the volume slider owns the arrow keys.** On any other control, including the Mute button next to it, the arrows move through the video like everywhere else.

### CLI access

`GET /video-player/script` returns the player's standalone script (raw JavaScript) for a page an agent builds itself — see Publishing a video or a film above. Everywhere else the player is already part of the page that shows a video. The speed it starts at is the ordinary `videoPlaybackRate` setting.

## Related

[Video playback speed](video-playback-speed.md) covers the speed menu and how the chosen speed is remembered. [Shared recording rich previews](shared-recording-rich-preview.md) covers the preview card and embeddable player a shared recording gets when its link is pasted elsewhere. [Opening a media link](media-link-open.md) covers what happens when an agent's message links to a video file.
