Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
15 changes: 15 additions & 0 deletions apps/api-demo/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# build output
dist/

# generated types
.astro/

# dependencies
node_modules/

# logs
npm-debug.log*
pnpm-debug.log*

# macOS-specific files
.DS_Store
47 changes: 47 additions & 0 deletions apps/api-demo/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# @videojs/api-demo

An Astro demo app that showcases the Video.js 10 **media API**.

The homepage renders the React `HlsJsVideo` player (with the default skin) and a
panel of controls wired directly to the media instance via `useMedia()`. It is a
hands-on playground for the media API:

- **Source** — load any HLS (`.m3u8`) URL for testing.
- **Setters / actions** — play/pause, seek (slider + exact time), playback rate,
volume, mute, and text/audio track selection, each calling the API directly
(`media.play()`, `media.currentTime = …`, `media.textTracks[i].mode = …`, …).
- **Getters** — a cloud of every readable property; click one to log its current
value.
- **Message log** — a live, color-coded console: media **events** (yellow),
**actions** (orange), and **getter** reads (magenta).
- **Shareable state** — every action is written to the URL as a query param
(booleans as `0`/`1`), so a configuration can be shared and is restored on
reload.

Styling, palette, typography (Instrument Sans / IBM Plex Mono / Eurostile), and
footer mirror videojs.org so the page shares the same look and feel.

## Develop

```bash
# from the repo root
pnpm --filter @videojs/api-demo dev

# or build the workspace packages first, then run directly
pnpm build:packages
pnpm --dir apps/api-demo dev
```

## Build

```bash
pnpm --filter @videojs/api-demo build
```

## Adding more API demos

`src/components/PlayerDemo.tsx` is the place to grow this. The media instance
returned by `useMedia()` exposes the full media API (seeking, volume, playback
rate, tracks, stream type, renditions, …) — add new controls alongside the
existing ones, log them as actions, and persist them to the URL to match the
rest of the demo.
23 changes: 23 additions & 0 deletions apps/api-demo/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
// @ts-check
import react from '@astrojs/react';
import tailwindcss from '@tailwindcss/vite';
import { defineConfig } from 'astro/config';

// https://astro.build/config
export default defineConfig({
integrations: [react()],
vite: {
plugins: [tailwindcss()],
optimizeDeps: {
// The workspace media packages ship ESM; let Vite resolve them directly
// instead of pre-bundling (mirrors the main site config).
exclude: ['@videojs/core', '@videojs/react'],
// react-dom (CJS) must be pre-bundled so its named exports are exposed as
// ESM bindings to the @astrojs/react client renderer.
include: ['react-dom', 'react-dom/client'],
},
resolve: {
dedupe: ['react', 'react-dom'],
},
},
});
2 changes: 2 additions & 0 deletions apps/api-demo/env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
30 changes: 30 additions & 0 deletions apps/api-demo/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
{
"name": "@videojs/api-demo",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"astro": "astro",
"check": "astro check"
},
"dependencies": {
"@videojs/core": "workspace:*",
"@videojs/react": "workspace:*",
"astro": "^6.3.1",
"react": "^19.2.4",
"react-dom": "^19.2.4"
},
"devDependencies": {
"@astrojs/check": "^0.9.8",
"@astrojs/react": "^5.0.2",
"@tailwindcss/vite": "^4.2.1",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"tailwindcss": "^4.2.1",
"typescript": "npm:@typescript/typescript6@^6.0.0",
"vite": "^7.0.0"
}
}
61 changes: 61 additions & 0 deletions apps/api-demo/src/components/Footer.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
// Footer adapted from videojs.org (site/src/components/Footer.astro): the mono
// logo on the left and the right-aligned descriptive text with links.
const year = new Date().getFullYear();
const VIDEOJS_URL = 'https://videojs.org';
const MUX_URL = 'https://www.mux.com';
---

<footer class="mt-16 md:mt-24">
<div
class="mx-auto flex w-full max-w-305 flex-col-reverse justify-between gap-10 border-t border-faded-black/30 px-5 pt-8 pb-12 lg:flex-row dark:border-manila-light/20"
>
<div class="relative w-72 text-faded-black lg:w-92 dark:text-manila-light">
<svg
class="w-full"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 381 68"
role="img"
aria-label="Video.js"
>
<path
fill="currentColor"
d="M88.705 0v14.958H71.204V0zm0 18.912v47.32H71.204v-47.32zM48.643 0v52.106L20.918 0H0l35.263 66.257h30.881V0zm101.694 0v66.257h-32.681c-13.328 0-23.917-13.475-23.917-25.754 0-13.969 9.755-25.519 23.578-25.519h11.502v14.958h-5.659c-6.078 0-11.085 4.812-11.085 10.718s4.929 10.64 11.006 10.64h9.755V0zm62.023 66.257h-29.577c-14.997 0-28.091-9.729-28.091-25.857 0-13.137 12.415-25.442 28.169-25.442h29.499v14.958l-11.659 17.039h-19.822l12.337-17.04h-8.999c-6.677 0-11.163 4.32-11.163 10.302 0 6.4 5.008 11.056 11.163 11.056h28.169v14.958zM245.069 68c-15.832 0-30.072-10.9-30.072-27.341 0-16.44 14.658-27.419 30.177-27.419s29.994 11.056 29.994 27.419-14.345 27.34-30.073 27.34zm-11.763-27.367c0 7.31 5.086 12.382 11.842 12.382 6.755 0 11.658-5.072 11.658-12.382s-4.825-12.46-11.58-12.46-11.92 5.306-11.92 12.46m124.57-15.503h-10.668c-2.921 0-5.268-1.405-5.268-4.293s2.269-4.032 5.112-4.084h29.134V0H346.4c-11.216 0-22.822 7.96-22.822 20.707s11.606 20.239 22.822 20.239h10.667c2.974 0 5.373 1.352 5.373 4.318s-2.425 4.266-5.373 4.266h-33.124v16.727h33.959c11.267 0 22.926-7.18 22.926-20.993 0-12.85-11.659-20.16-22.926-20.16zm-18.023 0h-12.337c-.365-1.38-.6-2.836-.6-4.423 0-1.405.183-2.706.496-3.954h12.337c-.783 1.145-1.226 2.523-1.226 4.084 0 1.639.495 3.096 1.33 4.292m6.521-21.748h26.395v9.963h-44.027c3.312-6.295 10.668-9.99 17.632-9.99zm-17.475 25.13h28.977c6.912 0 14.11 3.147 17.527 9.052h-29.029c-7.043 0-14.137-3.096-17.475-9.079zm48.512 16.778c0 1.509-.182 2.94-.495 4.266h-12.415c.834-1.17 1.304-2.627 1.304-4.266s-.47-3.122-1.304-4.318h12.311c.365 1.352.599 2.783.599 4.318M357.876 62.9h-30.568v-9.963h48.408c-3.234 6.555-10.563 9.963-17.84 9.963m-57.329-26.325c0 3.486-1.852 12.93-12.154 12.955h-11.294v16.753l13.485.13h3.338c15.415-.026 24.596-9.937 24.596-27.522V0h-17.971zm2.921 4.37h11.607c-.157 3.2-.626 6.062-1.435 8.611h-15.545c2.478-1.899 4.513-4.708 5.399-8.61zm.47-12.434h11.189v9.053h-11.241c0-.338.052-.65.052-1.015v-8.064zm0-3.382v-8.376h11.189v8.376zm-10.042 37.876h-3.26l-10.12-.078V52.912h31.716c-3.365 6.556-9.598 10.067-18.336 10.067zm21.231-59.623v9.99h-11.189v-9.99z"
></path>
</svg>
</div>
<p
class="text-base flex-1 text-left font-sans text-faded-black lg:text-right dark:text-manila-light"
>
Video.js is a free and open source HTML5 <a
href={VIDEOJS_URL}
class="underline hover:decoration-gold">video player.</a
>
<br />
Video hosting and streaming <a
href={MUX_URL}
class="underline hover:decoration-gold"
target="_blank"
rel="noopener noreferrer">sponsored by Mux</a
>
<br /><br />
The term VIDEO.JS is a registered trademark of Brightcove Inc.
<br />
&copy; 2010&ndash;{year} Video.js contributors
</p>
</div>

{/* Decorative colored bars, flush to the bottom edge. */}
<div
class="relative grid h-40 w-full md:h-52"
style="grid-template-rows: 80fr 60fr 45fr 20fr 10fr"
aria-hidden="true"
>
<div class="bg-bright-yellow"></div>
<div class="bg-gold"></div>
<div class="bg-orange"></div>
<div class="bg-red"></div>
<div class="bg-magenta"></div>
</div>
</footer>
15 changes: 15 additions & 0 deletions apps/api-demo/src/components/PlayerDemo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
'use client';

import { Demo } from './player-demo/demo';
import { MediaLogProvider } from './player-demo/media-log';
import { Player } from './player-demo/player';

export default function PlayerDemo() {
return (
<Player.Provider>
<MediaLogProvider>
<Demo />
</MediaLogProvider>
</Player.Provider>
);
}
48 changes: 48 additions & 0 deletions apps/api-demo/src/components/ThemeInit.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
---
import { THEME_COLOR, THEME_KEY } from '@/consts';
---

{/* Dark mode initialization script - runs before paint to prevent FOUC. */}
{
/* ts(2570) on define:vars-injected vars is expected — astro check can't see the injection (withastro/language-tools#711) */
}
<script
is:inline
define:vars={{ localStorageKey: THEME_KEY, themeColor: THEME_COLOR }}
>
if (
typeof localStorage !== "undefined" &&
typeof document !== "undefined" &&
typeof document.documentElement !== "undefined"
) {
let localStorageValue = localStorage[localStorageKey];
const isValidKey =
localStorageValue === "light" ||
localStorageValue === "dark" ||
localStorageValue === "system";
if (!localStorageValue || !isValidKey) {
localStorage[localStorageKey] = "system";
localStorageValue = "system";
}
let isDark = false;
if (localStorageValue === "dark") {
document.documentElement.classList.add("dark");
isDark = true;
} else if (localStorageValue === "system") {
if (
typeof window.matchMedia !== "undefined" &&
window.matchMedia("(prefers-color-scheme: dark)").matches
) {
document.documentElement.classList.add("dark");
isDark = true;
}
}
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
if (themeColorMeta) {
themeColorMeta.setAttribute(
"content",
isDark ? themeColor.dark : themeColor.light,
);
}
}
</script>
141 changes: 141 additions & 0 deletions apps/api-demo/src/components/ThemeToggle.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
'use client';

import { type ReactNode, useEffect, useState } from 'react';
import { THEME_COLOR, THEME_KEY } from '@/consts';

type Preference = 'system' | 'light' | 'dark';
type Theme = 'light' | 'dark';

function ComputerIcon() {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<rect x="7" y="17" width="10" height="4" stroke="currentColor" />
<rect x="2" y="3" width="20" height="14" rx="1" stroke="currentColor" />
<path d="M2 14H22V16C22 16.5523 21.5523 17 21 17H3C2.44772 17 2 16.5523 2 16V14Z" stroke="currentColor" />
</svg>
);
}

function SunIcon() {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M11.4941 17.9766C11.661 17.9905 11.8296 18 12 18C12.1664 18 12.3311 17.9908 12.4941 17.9775V23H11.4941V17.9766ZM17.9375 21.2734L17.0723 21.7744L14.5615 17.4258C14.8646 17.2824 15.1531 17.1139 15.4258 16.9238L17.9375 21.2734ZM8.57227 16.9238C8.84506 17.1141 9.1342 17.2823 9.4375 17.4258L6.92773 21.7734L6.0625 21.2734L8.57227 16.9238ZM6.56836 14.5508C6.71125 14.8545 6.87959 15.1437 7.06934 15.417L2.7207 17.9287L2.2207 17.0625L6.56836 14.5508ZM21.7793 17.0625L21.2793 17.9287L16.9297 15.417C17.1193 15.1439 17.2878 14.8552 17.4307 14.5518L21.7793 17.0625ZM23 11.4951V12.4951H17.9775C17.9909 12.3318 18 12.1668 18 12C18 11.8299 17.9914 11.6616 17.9775 11.4951H23ZM6.02246 11.4951C6.00859 11.6616 6 11.8299 6 12C6 12.1668 6.00911 12.3318 6.02246 12.4951H1V11.4951H6.02246ZM7.0752 8.57227C6.88482 8.84528 6.71678 9.13492 6.57324 9.43848L2.22656 6.92871L2.72656 6.0625L7.0752 8.57227ZM21.7734 6.92871L17.4258 9.4375C17.2823 9.1342 17.1141 8.84506 16.9238 8.57227L21.2734 6.0625L21.7734 6.92871ZM9.44727 6.56934C9.144 6.71214 8.85492 6.87981 8.58203 7.06934L6.07227 2.72168L6.9375 2.22168L9.44727 6.56934ZM17.9277 2.72168L15.416 7.06836C15.1434 6.87918 14.8547 6.71191 14.5518 6.56934L17.0625 2.22168L17.9277 2.72168ZM12.4941 6.02148C12.3311 6.00819 12.1664 6 12 6C11.8296 6 11.661 6.00853 11.4941 6.02246V1H12.4941V6.02148Z"
fill="currentColor"
/>
<circle cx="12" cy="12" r="4.5" stroke="currentColor" />
</svg>
);
}

function MoonIcon() {
return (
<svg width="20" height="20" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<path
d="M8.66602 7.83301L8.16602 7.83298V7.83301H8.66602ZM2 12H1.5V12L2 12ZM21.7831 12.8002L22.1575 13.1315L21.7831 12.8002ZM21.9605 12.8732L21.4624 12.8301L21.9605 12.8732ZM11.1969 2.217L11.5283 2.59137L11.1969 2.217ZM11.1969 2.217L10.8655 1.84263C9.21105 3.30733 8.16614 5.44808 8.16602 7.83298L8.66602 7.83301L9.16602 7.83303C9.16613 5.74663 10.079 3.87451 11.5283 2.59137L11.1969 2.217ZM8.66602 7.83301H8.16602C8.16602 12.2513 11.7477 15.833 16.166 15.833V15.333V14.833C12.3 14.833 9.16602 11.699 9.16602 7.83301H8.66602ZM16.166 15.333V15.833C18.552 15.833 20.6928 14.787 22.1575 13.1315L21.7831 12.8002L21.4086 12.4689C20.1254 13.9193 18.2532 14.833 16.166 14.833V15.333ZM21.9605 12.8732L21.4624 12.8301C21.0421 17.6879 16.9667 21.5 12 21.5V22V22.5C17.4904 22.5 21.9941 18.2863 22.4587 12.9163L21.9605 12.8732ZM12 22V21.5C6.75333 21.5 2.50005 17.2467 2.5 12L2 12L1.5 12C1.50005 17.7989 6.20104 22.5 12 22.5V22ZM2 12H2.5C2.5 7.03415 6.31064 2.95921 11.1671 2.53768L11.1238 2.03955L11.0806 1.54142C5.71207 2.0074 1.5 6.51065 1.5 12H2ZM21.7831 12.8002L22.1575 13.1315C22.0328 13.2725 21.846 13.2917 21.7101 13.2362C21.5715 13.1797 21.4453 13.0276 21.4624 12.8301L21.9605 12.8732L22.4587 12.9163C22.5118 12.3021 21.7669 12.0639 21.4086 12.4689L21.7831 12.8002ZM11.1969 2.217L11.5283 2.59137C11.9332 2.23292 11.6949 1.48811 11.0806 1.54142L11.1238 2.03955L11.1671 2.53768C10.9696 2.55482 10.8175 2.42865 10.7609 2.29011C10.7054 2.15421 10.7246 1.96738 10.8655 1.84263L11.1969 2.217Z"
fill="currentColor"
/>
</svg>
);
}

const themeOptions: { value: Preference; icon: ReactNode; label: string }[] = [
{ value: 'system', icon: <ComputerIcon />, label: 'System' },
{ value: 'light', icon: <SunIcon />, label: 'Light' },
{ value: 'dark', icon: <MoonIcon />, label: 'Dark' },
];

function initPreference(): Preference {
if (typeof localStorage === 'undefined') return 'system';
if (localStorage[THEME_KEY] === 'light') return 'light';
if (localStorage[THEME_KEY] === 'dark') return 'dark';
if (localStorage[THEME_KEY] === 'system') return 'system';
// Shouldn't be possible after the head script runs, but handle it.
localStorage[THEME_KEY] = 'system';
return 'system';
}

function getThemeFromPreference(preference: Preference): Theme {
if (preference === 'light') return 'light';
if (preference === 'dark') return 'dark';
if (typeof window !== 'undefined' && window.matchMedia?.('(prefers-color-scheme: dark)').matches) {
return 'dark';
}
return 'light';
}

export function ThemeToggle() {
const [preference, _setPreference] = useState<Preference | null>(null);
const [theme, setTheme] = useState<Theme | null>(null);

const setPreference = (newPreference: Preference) => {
_setPreference(newPreference);
if (typeof localStorage !== 'undefined') localStorage[THEME_KEY] = newPreference;
setTheme(getThemeFromPreference(newPreference));
};

// Initialize preference and theme on mount.
useEffect(() => {
const initialPreference = initPreference();
_setPreference(initialPreference);
setTheme(getThemeFromPreference(initialPreference));
}, []);

// Track the system preference while 'system' is selected.
useEffect(() => {
if (preference !== 'system') return;
if (typeof window === 'undefined' || typeof window.matchMedia === 'undefined') return;

const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
const onMediaChange = (event: MediaQueryListEvent) => setTheme(event.matches ? 'dark' : 'light');
mediaQuery.addEventListener('change', onMediaChange);

return () => mediaQuery.removeEventListener('change', onMediaChange);
}, [preference]);

// Keep document.documentElement and the theme-color meta in sync with theme.
useEffect(() => {
if (typeof document === 'undefined' || theme === null) return;
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
if (theme === 'dark') {
document.documentElement.classList.add('dark');
themeColorMeta?.setAttribute('content', THEME_COLOR.dark);
} else {
document.documentElement.classList.remove('dark');
themeColorMeta?.setAttribute('content', THEME_COLOR.light);
}
}, [theme]);

const disabled = preference === null;

return (
<fieldset
aria-label="Color theme"
className="m-0 flex min-w-0 items-center rounded-xs border border-faded-black bg-manila-light/80 p-0.75 backdrop-blur dark:border-manila-light dark:bg-faded-black/80"
>
{themeOptions.map((option) => {
const isPressed = preference === option.value;

return (
<button
key={option.value}
type="button"
aria-label={option.label}
aria-pressed={isPressed}
disabled={disabled}
onClick={() => setPreference(option.value)}
className={`flex items-center justify-center px-3 py-2 transition-colors ${
disabled ? 'cursor-wait opacity-50' : 'cursor-pointer'
} ${
isPressed
? 'bg-faded-black text-manila-light dark:bg-manila-light dark:text-faded-black'
: 'bg-transparent text-faded-black dark:text-manila-light'
}`}
>
{option.icon}
</button>
);
})}
</fieldset>
);
}
Loading
Loading