Episodes
A shelf for browsing a playlist by season, with thumbnails, running times, and progress.
import { episodes } from "react-video-kit/episodes";Adds 3.2 KB to a player4.5 KB on its ownSource
Adds a button beside the settings menu, which opens a shelf of everything in the queue. Press play, press the stack button, and pick another episode. In this demo the last four seconds of each one also offer Next Episode, through the skip plugin, and the i button opens the info panel.
"use client";import { episodes } from "react-video-kit/episodes";import { info } from "react-video-kit/info";import { defaultPlugins, VideoPlayer } from "react-video-kit/player";import { playlist } from "react-video-kit/playlist";import { skip } from "react-video-kit/skip";const items = [ { src: "/media/episodes/1.mp4", thumbnail: "/media/episodes/1.jpg", poster: "/media/episodes/1.jpg", season: "Season 1", eyebrow: "Season 1, Episode 1", title: "The Green Coast", description: "The swell arrives after a thousand miles of open ocean, and meets the first land it has seen in days.", duration: 14, }, { src: "/media/episodes/2.mp4", thumbnail: "/media/episodes/2.jpg", poster: "/media/episodes/2.jpg", season: "Season 1", eyebrow: "Season 1, Episode 2", title: "Open Water", description: "Far from the reef, a turtle crosses a stretch of deep blue that takes it most of a week.", duration: 14, }, { src: "/media/episodes/3.mp4", thumbnail: "/media/episodes/3.jpg", poster: "/media/episodes/3.jpg", season: "Season 1", eyebrow: "Season 1, Episode 3", title: "Breakwater", description: "When the wind turns offshore, every wave along the point stands up and holds its shape.", duration: 14, }, { src: "/media/episodes/4.mp4", thumbnail: "/media/episodes/4.jpg", poster: "/media/episodes/4.jpg", season: "Season 1", eyebrow: "Season 1, Episode 4", title: "Over the Strait", description: "From a thousand feet up, the currents that shape the islands finally come into view.", duration: 14, }, { src: "/media/episodes/5.mp4", thumbnail: "/media/episodes/5.jpg", poster: "/media/episodes/5.jpg", season: "Season 2", eyebrow: "Season 2, Episode 1", title: "Heavy Water", description: "Winter storms send the biggest swells of the year towards a single stretch of reef.", duration: 14, }, { src: "/media/episodes/6.mp4", thumbnail: "/media/episodes/6.jpg", poster: "/media/episodes/6.jpg", season: "Season 2", eyebrow: "Season 2, Episode 2", title: "Low Sun", description: "The last hour of light turns the whole bay to gold, and the water finally goes quiet.", duration: 14, },];const plugins = [ playlist({ items }), episodes({ progress: (_, index) => (index === 0 ? 1 : index === 1 ? 0.45 : null) }), // The last few seconds of each one stand in for its credits. skip({ segments: [{ kind: "credits", start: 10, end: 14 }] }), info({ facts: ["2024", "TV-PG", "6 episodes"] }), ...defaultPlugins(),];export default function Episodes() { return <VideoPlayer plugins={plugins} load="visible" />;}Usage
Episodes come from the playlist plugin, so install that as well. Each item describes itself:
import { episodes } from "react-video-kit/episodes";import { VideoPlayer } from "react-video-kit/player";import { playlist } from "react-video-kit/playlist";const plugins = [ playlist({ items: [ { src: "/s1e1.mp4", thumbnail: "/s1e1.jpg", season: "Season 1", eyebrow: "Season 1, Episode 1", title: "The Green Coast", description: "The swell meets the first land it has seen in days.", duration: 2520, }, // And so on, in play order. ], }), episodes(),];<VideoPlayer plugins={plugins} />;| Field | On the shelf |
|---|---|
thumbnail | The image, or poster if there's no thumbnail |
title | The name, on one line |
duration | The running time, given in seconds and shown in the player's language, such as 42 min |
eyebrow | Shown in place of the running time when there's no duration |
description | Two lines under the name, on large players |
season | Which season it's listed under |
The same fields fill the title above the timeline as each episode plays.
Seasons
Items with the same season are listed together, and a button for each season appears when there are two or more. The shelf always opens on the season that's playing. Give every item a season, or none of them. With none, they all share one row.
Show progress
progress returns how far through an episode the viewer is, from 0 to 1, and the shelf draws a bar on its thumbnail. Return nothing for no bar.
episodes({ progress: (item) => watched.get(item.title),});The episode that's playing always shows its real position instead. progress only runs as the shelf renders, which happens when it opens or something on it changes, not during playback.
Open it yourself
"use client";import { useCommands } from "react-video-kit";import { episodes } from "react-video-kit/episodes";function BrowseSeason2() { const { open, setSeason } = useCommands(episodes); return ( <button onClick={() => { open(); setSeason("Season 2"); }} > Season 2 </button> );}Place the parts yourself
VideoPlayer places the button and the shelf through slots. When you compose your own layout, put them wherever you like:
import * as Video from "react-video-kit";import { EpisodesButton, EpisodesShelf } from "react-video-kit/episodes";<Video.Root plugins={plugins}> <Video.Media /> <EpisodesShelf /> <Video.Controls> <Video.PlayPause /> <EpisodesButton /> </Video.Controls></Video.Root>;Behaviour
- Playback carries on behind the shelf, and the other controls step aside until it closes.
- Choosing an episode closes the shelf and plays it. Choosing the one that's already playing only closes the shelf.
- Focus moves to the episode that's playing. The left and right arrows move between episodes, in reading order for the language, and Home and End go to the ends. Tab reaches the season buttons and the close button.
- Esc, the close button, and a click above the shelf all close it, and focus goes back to the button that opened it.
- The hotkeys plugin ignores keys pressed inside the shelf, and the idle plugin doesn't hide anything while it's open.
- With a mouse, buttons at the ends of the row page through it when there's more to see.
- With a single item in the queue, the button doesn't render.
- Nothing renders until the shelf first opens, and thumbnails load as they scroll into view.
- The root has
data-episodes-openwhile it's open.
API
Options
| Option | Type | Description |
|---|---|---|
| progress? | ((item: PlaylistItem, index: number) => number | null | undefined) | How far through an episode the viewer is, from 0 to 1, for the bar on its thumbnail. The episode that's playing always shows its real position instead. |
State
Read it with useVideoState(episodes, (state) => …).
| Field | Type | Description |
|---|---|---|
| open | boolean | True while the shelf is showing. |
| season | string | null | The season on the shelf, or null when episodes have no seasons. |
| progress | ((item: PlaylistItem, index: number) => number | null | undefined) | null | The function that reads an episode's progress. |
Commands
Call them with useCommands(episodes).
| Command | Type | Description |
|---|---|---|
| open | () => void | Opens the shelf on the season that's playing. |
| close | () => void | Closes the shelf, and returns focus to its button. |
| toggle | () => void | Opens the shelf if closed, and closes it if open. |
| setSeason | (season: string | null) => void | Shows this season's episodes on the shelf. |
Parts
| Part | Description |
|---|---|
| EpisodesButton | Opens the shelf. Hidden with a single item, since there's nothing to browse. |
| EpisodesShelf | The shelf of episodes. It renders nothing until it first opens. |