The video player
The one video player every Omniscio video plays in — in the app, on a shared link, on the page other websites embed, and for films drawn live by code. Covers its controls, keyboard shortcuts, full screen, phones, and what happens to links shared before it existed.
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.
- 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.
When a video's link runs out
- A shared video streams from a link that only stays valid for a short while, so a page left open a long time — a paused tab, or a share the app quietly preloaded for you — can outlive it.
- When that happens the player fetches a fresh link by itself and carries on from the same moment, so the video never freezes a few seconds in. A paused video comes back paused.
- If the video truly cannot be reached any more (the share was revoked or deleted), it stops after a few tries and shows the error instead of retrying forever.
- This needs the new player, so a link shared before this change still freezes once its link runs out; reloading the page, or sharing the video again, fixes it.
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'sdestroy()removes everything it added and gives the browser's controls back. - In the app,
<VideoPlayer>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 thevideoPlaybackRatesetting. - Outside the app,
npm run video-player:buildbundles the player into one script string, committed assrc/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 withdata-omniscio-player(optionallydata-download-url/data-download-name) and including that script — it mounts every marked player on load, and exposeswindow.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.tsfromORB_PNG_BASE64insrc/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 isaria-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. - Expired-link recovery lives in
network-recovery.ts. A share's/s/<token>/videoanswers a 302 to a signed storage URL that lives 30 minutes, and Chromium fetches every later byte range from that redirect target, so an old page stalls at the edge of its buffer withMEDIA_ERR_NETWORK. On that error (http(s) sources only) the player callsload()— which goes back through/videofor a fresh link — restorescurrentTimeonloadedmetadata, and resumes only if the last play/pause event was a play. At most 3 reloads in a row without a second of progress, then the error stays. The in-app share prewarm keeps share pages alive for hours, which is how this surfaced. - A live film plays through the same player:
createTimelineMediaturns 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/publishby itspath. 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 throughPOST /convertfirst. Details: Share via CLI. - 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 withOmniscioVideoPlayer.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 plainallow="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-rootrow insrc/renderer/src/hooks/keyboard-shortcuts/surface-yields.tsis what does. - Everything lives in the bar, and the speed menu opens UPWARD out of it. Moving a control out of
.ovp-barchanges what auto-hide treats as the control area (CONTROL_AREA_SELECTORinauto-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
!importanton 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 covers the speed menu and how the chosen speed is remembered. Shared recording rich previews covers the preview card and embeddable player a shared recording gets when its link is pasted elsewhere. Opening a media link covers what happens when an agent's message links to a video file.
Last verified 2026-10-05