> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cskn.dev/llms.txt
> Use this file to discover all available pages before exploring further.

# Video player

> Play a video in the shell's native full-screen player.

The shell plays the video in its own player: AVPlayer on iOS, ExoPlayer on Android, the shell's player on desktop, and a remote-controlled player on TV. Your page passes a URL and gets back playback state. The platform provides the controls, including scrubbing, picture-in-picture, AirPlay and full screen. When the player closes, `play` resolves with the position where the viewer stopped.

```tsx theme={null}
import { usePlayer } from '@cskn/sdk';

function Episode({ video, savedPosition }: { video: Video; savedPosition: number }) {
  const { play, availability, playing } = usePlayer();

  // In a browser or an older app version there's no player, so use your own.
  if (!availability?.available) return <video src={video.mp4Url} controls />;

  const watch = async () => {
    const result = await play(
      { src: video.hlsUrl, title: video.title, poster: video.posterUrl, startAt: savedPosition, progressInterval: 10 },
      (event) => {
        if (event.event === 'progress') saveResumePoint(video.id, event.position);
      },
    );
    if (!result.error) saveResumePoint(video.id, result.ended ? 0 : result.position);
  };

  return <button onClick={watch} disabled={playing}>Watch</button>;
}
```

## Options

<ParamField path="src" type="string" required>
  An `http(s)` URL: a progressive file (mp4, webm) or an HLS / DASH manifest.
</ParamField>

<ParamField path="format" type="'mp4' | 'webm' | 'hls' | 'dash'">
  Stream format. If you leave it out, the SDK infers it from the URL: `.m3u8` is `hls`, `.mpd` is `dash`, `.webm` is `webm`, and anything else is `mp4`. Set it when a manifest's URL doesn't end in the usual extension.
</ParamField>

<ParamField path="title" type="string">
  Shown above the video.
</ParamField>

<ParamField path="subtitle" type="string">
  Second line, such as a channel or show name.
</ParamField>

<ParamField path="poster" type="string">
  Image shown until the first frame arrives.
</ParamField>

<ParamField path="startAt" type="number">
  Position to start from, in seconds. Ignored for `live`.
</ParamField>

<ParamField path="autoplay" type="boolean" default="true">
  Start playing as soon as the video is ready.
</ParamField>

<ParamField path="loop" type="boolean" default="false">
  Start again from the beginning when the video ends.
</ParamField>

<ParamField path="muted" type="boolean" default="false">
  Start with the sound off.
</ParamField>

<ParamField path="live" type="boolean" default="false">
  A live stream. The player hides seeking and shows a LIVE badge.
</ParamField>

<ParamField path="closeOnEnd" type="boolean" default="false">
  Close the player when the video ends. Without it, the player stays open on the last frame. Ignored with `loop`.
</ParamField>

<ParamField path="progressInterval" type="number" default="0">
  While playing, send a `progress` event every this many seconds. With `0`, no `progress` events are sent. The final position always comes with the result.
</ParamField>

<ParamField path="id" type="string">
  Returned on every event and on the result.
</ParamField>

## Events

The optional second argument of `play` (or `openPlayer`) receives every state change while the player is open:

<ResponseField name="event" type="'ready' | 'play' | 'pause' | 'buffering' | 'progress' | 'ended' | 'error'">What happened.</ResponseField>
<ResponseField name="position" type="number">Current position, in seconds.</ResponseField>
<ResponseField name="duration" type="number">Length in seconds. It is `0` until the length is known, and stays `0` for a live stream.</ResponseField>

## Result

<ResponseField name="position" type="number">Where playback stopped, in seconds. Save it and pass it back as `startAt` to resume.</ResponseField>
<ResponseField name="duration" type="number">Length of the video in seconds.</ResponseField>
<ResponseField name="ended" type="boolean">Playback reached the end at least once.</ResponseField>

<ResponseField name="error" type="'not_available' | 'busy' | 'invalid_source' | 'playback_failed'">
  `not_available`: this surface has no player. `busy`: another video is already open. `invalid_source`: the URL is not http(s), or this platform can't play the format. `playback_failed`: the stream couldn't be loaded.
</ResponseField>

## Supported formats

| Surface | Formats | Notes |
| - | - | - |
| iOS | `mp4`, `hls` | Picture-in-picture and AirPlay. Full screen turns to match the video's shape. |
| Android | `mp4`, `webm`, `hls`, `dash` | |
| Desktop | `mp4`, `webm`, `hls` | Keyboard: Space / K, ← →, J / L, ↑ ↓, M, F, Esc. |
| TV | `mp4`, `hls` | Remote: OK plays and pauses, ◀ ▶ seek, media keys, Back closes. |
| Browser | — | Resolves `not_available` immediately. |

Check `availability.formats`, or call `getPlayerAvailability()`, before you choose which URL to pass.

The player doesn't support DRM or custom request headers. To protect a stream, use a signed URL that carries its token in the query string.

Only one video can be open at a time. To close it from your page, call `close()` (or `closePlayer()`). Without the hook, use `openPlayer(options, onEvent?)`, `closePlayer()` and `getPlayerAvailability()`.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.