Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
000473e
Add Video fit example and show Open Video UI usage in change-video-fi…
devin-ai-integration[bot] Oct 12, 2026
bcdb0df
Add Debug display example
devin-ai-integration[bot] Oct 12, 2026
7663a80
Add Yospace SSAI example
devin-ai-integration[bot] Oct 12, 2026
8c51c11
Add Clip API with playlist example
devin-ai-integration[bot] Oct 12, 2026
177decf
Add Chromeless UI example
devin-ai-integration[bot] Oct 12, 2026
fef713f
Add Web Audio API example
devin-ai-integration[bot] Oct 12, 2026
deca30e
Add Up Next example
devin-ai-integration[bot] Oct 12, 2026
6070390
Add Peak limiter example
devin-ai-integration[bot] Oct 12, 2026
a5247dd
Store the lookahead limiter next to the Peak limiter example
devin-ai-integration[bot] Oct 12, 2026
2797a43
Center the Up Next play button
devin-ai-integration[bot] Oct 12, 2026
3586d9e
Chromeless UI: slower center icon animation, fullscreen the whole con…
devin-ai-integration[bot] Oct 12, 2026
e0ca109
Up Next: open a Default UI page from the Default UI example
devin-ai-integration[bot] Oct 13, 2026
771e50c
Chromeless UI: subtitle selection menu, slower center icon animation
devin-ai-integration[bot] Oct 13, 2026
082c7e7
Use MP4 for Web Audio and Peak limiter demos, add Chromeless audio tr…
devin-ai-integration[bot] Oct 13, 2026
a33b7c5
Remove MP4/Safari note from Web Audio and Peak limiter examples
devin-ai-integration[bot] Oct 13, 2026
28dee63
Load @synthlet/lookahead-limiter from unpkg through an import map
devin-ai-integration[bot] Oct 13, 2026
887ffb5
Use docs theme controls in new Web SDK demos, fix Chromeless outline …
devin-ai-integration[bot] Oct 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ millicast/api/
theolive/api/
theolive_versioned_docs/*/api/

# Third-party code
theoplayer/static/theoplayer/*/examples/peak-limiter/lookahead-limiter-*.mjs

# Submodules
theoplayer/external/
theoplayer_versioned_docs/*/external/
2 changes: 2 additions & 0 deletions spellcheck/dictionary.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1024,3 +1024,5 @@ Zettabyte
HLG
HLG-compatible
UIs
bookmarklet
lookahead
140 changes: 140 additions & 0 deletions theoplayer/examples/_components/ClipApiWithPlaylistDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import React, { type JSX, useRef, useState } from 'react';
import useBaseUrl from '@docusaurus/useBaseUrl';
import Example, { type ExampleController } from '@site/src/components/Example';
import styles from '../shared.module.css';

interface PlaylistItem {
title: string;
poster: string;
clip: { startTime: number; endTime: number };
}

interface PlaylistState {
active: number;
items: PlaylistItem[];
}

function formatTime(seconds: number): string {
const minutes = Math.floor(seconds / 60);
return `${minutes}:${String(Math.floor(seconds % 60)).padStart(2, '0')}`;
}

export default function ClipApiWithPlaylistDemo(): JSX.Element {
const exampleRef = useRef<ExampleController>(null);
const [state, setState] = useState<PlaylistState>({ active: 0, items: [] });
const [form, setForm] = useState({ src: '', title: 'My clip', startTime: '0', endTime: '30' });

const startTime = Number(form.startTime);
const endTime = Number(form.endTime);
const canAdd = form.src.trim() !== '' && Number.isFinite(startTime) && Number.isFinite(endTime) && startTime >= 0 && endTime > startTime;

function updateForm(key: keyof typeof form, value: string) {
setForm((previous) => ({ ...previous, [key]: value }));
}

return (
<>
<Example
ref={exampleRef}
src={useBaseUrl('/theoplayer/v11/examples/clip-api-with-playlist/demo.html')}
onMessage={(message) => {
if (message.type === 'playlist-state') {
setState({
active: typeof message.active === 'number' ? message.active : 0,
items: Array.isArray(message.items) ? (message.items as PlaylistItem[]) : [],
});
}
}}
/>
<div className={styles.panel}>
<div className={styles.controls}>
<h3 className={styles.panelTitle}>Playlist</h3>
<button className="button button--secondary button--sm" onClick={() => exampleRef.current?.postMessage({ type: 'playlist-previous' })}>
Previous
</button>
<button className="button button--secondary button--sm" onClick={() => exampleRef.current?.postMessage({ type: 'playlist-next' })}>
Next
</button>
</div>
<ul className={styles.bookmarkList}>
{state.items.map((item, index) => (
<li key={index}>
<button
className={`${styles.bookmarkButton} ${index === state.active ? styles.bookmarkButtonActive : ''}`}
onClick={() => exampleRef.current?.postMessage({ type: 'playlist-load', index })}
>
<span className={styles.playlistRow}>
<span className={styles.playlistNumber}>{index + 1}</span>
{item.poster ? <img className={styles.playlistPoster} src={item.poster} alt="" /> : <span className={styles.playlistPoster} />}
<span className={styles.playlistTitle}>
{item.title}
{index === state.active && ' (playing)'}
</span>
<span className={styles.playlistStatus}>
{formatTime(item.clip.startTime)} – {formatTime(item.clip.endTime)}
</span>
</span>
</button>
</li>
))}
</ul>
</div>
<div className={styles.panel}>
<h3 className={styles.panelTitle}>Add a clip</h3>
<div className={styles.formGrid}>
<label className={styles.formField}>
Stream URL
<input
className={styles.input}
type="url"
value={form.src}
placeholder="https://example.com/stream.m3u8"
onChange={(event) => updateForm('src', event.target.value)}
/>
</label>
<label className={styles.formField}>
Title
<input className={styles.input} value={form.title} onChange={(event) => updateForm('title', event.target.value)} />
</label>
<label className={styles.formField}>
Start (seconds)
<input
className={styles.input}
type="number"
min="0"
value={form.startTime}
onChange={(event) => updateForm('startTime', event.target.value)}
/>
</label>
<label className={styles.formField}>
End (seconds)
<input
className={styles.input}
type="number"
min="0"
value={form.endTime}
onChange={(event) => updateForm('endTime', event.target.value)}
/>
</label>
</div>
<div className={styles.formActions}>
<button
className="button button--primary button--sm"
disabled={!canAdd}
onClick={() =>
exampleRef.current?.postMessage({
type: 'playlist-add',
src: form.src.trim(),
title: form.title.trim() || form.src.trim(),
startTime,
endTime,
})
}
>
Add and play
</button>
</div>
</div>
</>
);
}
99 changes: 99 additions & 0 deletions theoplayer/examples/_components/PeakLimiterDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import React, { type JSX, useRef, useState } from 'react';
import useBaseUrl from '@docusaurus/useBaseUrl';
import Example, { type ExampleController } from '@site/src/components/Example';
import styles from '../shared.module.css';

export default function PeakLimiterDemo(): JSX.Element {
const exampleRef = useRef<ExampleController>(null);
const [boost, setBoost] = useState(18);
const [ceiling, setCeiling] = useState(-1);
const [enabled, setEnabled] = useState(true);
const [peak, setPeak] = useState<number | null>(null);
const level = peak === null ? 0 : Math.min(100, Math.max(0, (peak + 48) / 54) * 100);

return (
<>
<Example
ref={exampleRef}
src={useBaseUrl('/theoplayer/v11/examples/peak-limiter/demo.html')}
onMessage={(message) => {
if (message.type === 'limiter-peak') setPeak(message.decibels as number | null);
}}
/>
<div className={styles.panel}>
<table className={`${styles.readout} ${styles.parameterTable}`}>
<tbody>
<tr>
<th>
<label htmlFor="limiter-boost">Boost</label>
</th>
<td>
<input
id="limiter-boost"
type="range"
min={0}
max={24}
step={1}
value={boost}
onChange={(event) => {
const decibels = Number(event.target.value);
setBoost(decibels);
exampleRef.current?.postMessage({ type: 'limiter-boost', decibels });
}}
/>
</td>
<td>+{boost} dB</td>
</tr>
<tr>
<th>
<label htmlFor="limiter-ceiling">Ceiling</label>
</th>
<td>
<input
id="limiter-ceiling"
type="range"
min={-24}
max={0}
step={1}
value={ceiling}
disabled={!enabled}
onChange={(event) => {
const decibels = Number(event.target.value);
setCeiling(decibels);
exampleRef.current?.postMessage({ type: 'limiter-ceiling', decibels });
}}
/>
</td>
<td>{ceiling} dB</td>
</tr>
</tbody>
</table>
<div className={styles.controls}>
<label>
<input
type="checkbox"
checked={enabled}
onChange={(event) => {
setEnabled(event.target.checked);
exampleRef.current?.postMessage({ type: 'limiter-enabled', enabled: event.target.checked });
}}
/>
Limiter on
</label>
</div>
<div className={styles.rangeRow}>
<div className={styles.rangeLabel}>
<span>Output peak</span>
<code>{peak === null ? '-∞ dB' : `${peak.toFixed(1)} dB`}</code>
</div>
<div className={styles.rangeBar}>
<div
className={styles.rangeSegment}
style={{ width: `${level}%`, background: peak !== null && peak > 0 ? 'var(--ifm-color-danger)' : 'var(--ifm-color-success)' }}
/>
</div>
</div>
</div>
</>
);
}
42 changes: 42 additions & 0 deletions theoplayer/examples/_components/VideoFitDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React, { type CSSProperties, type JSX, useRef, useState } from 'react';
import clsx from 'clsx';
import Example, { type ExampleController } from '@site/src/components/Example';
import styles from '../shared.module.css';

const fits = ['contain', 'cover', 'fill'] as const;

interface VideoFitDemoProps {
src: string;
style?: CSSProperties;
exposeGlobals?: boolean;
}

export default function VideoFitDemo({ src, style, exposeGlobals }: VideoFitDemoProps): JSX.Element {
const exampleRef = useRef<ExampleController>(null);
const [fit, setFit] = useState<(typeof fits)[number]>('contain');
return (
<>
<Example ref={exampleRef} src={src} style={style} exposeGlobals={exposeGlobals} />
<div className={styles.panel}>
<div className={styles.controls}>
<div className="button-group" role="group" aria-label="Video fit">
{fits.map((value) => (
<button
key={value}
type="button"
aria-pressed={value === fit}
className={clsx('button button--sm', value === fit ? 'button--primary' : 'button--secondary')}
onClick={() => {
setFit(value);
exampleRef.current?.postMessage({ type: 'video-fit', fit: value });
}}
>
{value}
</button>
))}
</div>
</div>
</div>
</>
);
}
79 changes: 79 additions & 0 deletions theoplayer/examples/_components/WebAudioDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React, { type JSX, useRef, useState } from 'react';
import useBaseUrl from '@docusaurus/useBaseUrl';
import Example, { type ExampleController } from '@site/src/components/Example';
import styles from '../shared.module.css';

const params = [
{ name: 'pan', label: 'Pan', min: -1, max: 1, step: 0.05, value: 0, format: (value: number) => value.toFixed(2) },
{ name: 'bass', label: 'Bass', min: -12, max: 12, step: 1, value: 0, format: (value: number) => `${value} dB` },
{ name: 'mid', label: 'Mid', min: -12, max: 12, step: 1, value: 0, format: (value: number) => `${value} dB` },
{ name: 'treble', label: 'Treble', min: -12, max: 12, step: 1, value: 0, format: (value: number) => `${value} dB` },
{ name: 'volume', label: 'Volume', min: 0, max: 1, step: 0.01, value: 1, format: (value: number) => `${Math.round(value * 100)}%` },
] as const;

type ParamName = (typeof params)[number]['name'];

const defaults = Object.fromEntries(params.map(({ name, value }) => [name, value])) as Record<ParamName, number>;

export default function WebAudioDemo(): JSX.Element {
const exampleRef = useRef<ExampleController>(null);
const [values, setValues] = useState(defaults);
const [bypass, setBypass] = useState(false);

function setParam(name: ParamName, value: number) {
setValues((prev) => ({ ...prev, [name]: value }));
exampleRef.current?.postMessage({ type: 'web-audio-param', name, value });
}

return (
<>
<Example ref={exampleRef} src={useBaseUrl('/theoplayer/v11/examples/web-audio/demo.html')} style={{ aspectRatio: '16 / 10.9' }} />
<div className={styles.panel}>
<p className={styles.hint}>Press play, then move the sliders. The audio context starts after you interact with the player.</p>
<table className={`${styles.readout} ${styles.parameterTable}`}>
<tbody>
{params.map(({ name, label, min, max, step, format }) => (
<tr key={name}>
<th>
<label htmlFor={`web-audio-${name}`}>{label}</label>
</th>
<td>
<input
id={`web-audio-${name}`}
type="range"
min={min}
max={max}
step={step}
value={values[name]}
onChange={(event) => setParam(name, Number(event.target.value))}
/>
</td>
<td>{format(values[name])}</td>
</tr>
))}
</tbody>
</table>
<div className={styles.controls}>
<label>
<input
type="checkbox"
checked={bypass}
onChange={(event) => {
setBypass(event.target.checked);
exampleRef.current?.postMessage({ type: 'web-audio-bypass', bypass: event.target.checked });
}}
/>
Bypass effects
</label>
<button
type="button"
className="button button--sm button--secondary"
onClick={() => params.forEach(({ name, value }) => setParam(name, value))}
>
Reset
</button>
</div>
</div>
</>
);
}
Loading
Loading