docs(site): update getting started code examples to match new api (#473)

Co-authored-by: Darius Cepulis <dcepulis@mux.com>
This commit is contained in:
Steve Heffernan
2026-02-09 18:49:43 -03:00
committed by GitHub
co-authored by Darius Cepulis
parent 222d90f675
commit 5f27a6bc94
12 changed files with 306 additions and 124 deletions
@@ -0,0 +1,35 @@
import { useStore } from '@nanostores/react';
import ClientCode from '@/components/Code/ClientCode';
import type { Skin, UseCase } from '@/stores/installation';
import { skin, useCase } from '@/stores/installation';
function getSkinImportParts(skin: Skin): { group: string; skinFile: string } {
if (skin === 'minimal-video') return { group: 'video', skinFile: 'minimal-skin' };
if (skin === 'minimal-audio') return { group: 'audio', skinFile: 'minimal-skin' };
return { group: skin, skinFile: 'skin' };
}
function generateCdnCode(useCase: UseCase, skin: Skin): string {
if (useCase === 'background-video') {
return `<script>
import 'https://cdn.jsdelivr.net/npm/videojs/html/background/player.js';
import 'https://cdn.jsdelivr.net/npm/videojs/html/background/skin.js';
</script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/videojs/html/background/skin.css" />`;
}
const { group, skinFile } = getSkinImportParts(skin);
return `<script>
import 'https://cdn.jsdelivr.net/npm/videojs/html/${group}/player.js';
import 'https://cdn.jsdelivr.net/npm/videojs/html/${group}/${skinFile}.js';
</script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/videojs/html/${group}/${skinFile}.css" />`;
}
export default function HTMLCdnCodeBlock() {
const $useCase = useStore(useCase);
const $skin = useStore(skin);
return <ClientCode code={generateCdnCode($useCase, $skin)} lang="html" />;
}
@@ -0,0 +1,65 @@
import { useEffect, useRef } from 'react';
import ClientCode from '@/components/Code/ClientCode';
import { Tab, TabsList, TabsPanel, TabsRoot } from '@/components/Tabs';
import type { InstallMethod } from '@/stores/installation';
import { installMethod } from '@/stores/installation';
import HTMLCdnCodeBlock from './HTMLCdnCodeBlock';
export default function HTMLInstallTabs() {
const ref = useRef<HTMLDivElement>(null);
// Observe tab changes and sync to installMethod store
useEffect(() => {
const root = ref.current?.querySelector('[data-tabs-root]');
if (!root) return;
const observer = new MutationObserver(() => {
const activeTab = root.querySelector('[role="tab"][data-tab-active="true"]');
if (activeTab) {
const value = activeTab.getAttribute('data-value');
if (value) {
installMethod.set(value as InstallMethod);
}
}
});
// Observe all tab elements for attribute changes
const tabs = root.querySelectorAll('[role="tab"]');
tabs.forEach((tab) => {
observer.observe(tab, { attributes: true, attributeFilter: ['data-tab-active'] });
});
return () => observer.disconnect();
}, []);
return (
<div ref={ref}>
<TabsRoot>
<TabsList label="Installation">
<Tab value="cdn" initial>
cdn
</Tab>
<Tab value="npm">npm</Tab>
<Tab value="pnpm">pnpm</Tab>
<Tab value="yarn">yarn</Tab>
<Tab value="bun">bun</Tab>
</TabsList>
<TabsPanel value="cdn" initial>
<HTMLCdnCodeBlock />
</TabsPanel>
<TabsPanel value="npm">
<ClientCode code="npm install @videojs/html" lang="bash" />
</TabsPanel>
<TabsPanel value="pnpm">
<ClientCode code="pnpm add @videojs/html" lang="bash" />
</TabsPanel>
<TabsPanel value="yarn">
<ClientCode code="yarn add @videojs/html" lang="bash" />
</TabsPanel>
<TabsPanel value="bun">
<ClientCode code="bun add @videojs/html" lang="bash" />
</TabsPanel>
</TabsRoot>
</div>
);
}
@@ -2,18 +2,21 @@ import { useStore } from '@nanostores/react';
import ClientCode from '@/components/Code/ClientCode';
import { Tab, TabsList, TabsPanel, TabsRoot } from '@/components/Tabs';
import type { Renderer, Skin, UseCase } from '@/stores/installation';
import { muxPlaybackId, renderer, skin, useCase } from '@/stores/installation';
import { installMethod, muxPlaybackId, renderer, skin, useCase } from '@/stores/installation';
function getRendererTag(renderer: Renderer): string {
const map: Record<Renderer, string> = {
'background-video': 'background-video',
cloudflare: 'cloudflare-video',
dash: 'dash-video',
hls: 'hls-video',
'html5-audio': 'audio',
'html5-video': 'video',
jwplayer: 'jwplayer-video',
mux: 'mux-video',
shaka: 'shaka-video',
'mux-audio': 'mux-audio',
'mux-background-video': 'mux-background-video',
'mux-video': 'mux-video',
// shaka: 'shaka-video',
spotify: 'spotify-audio',
vimeo: 'vimeo-video',
wistia: 'wistia-video',
@@ -31,11 +34,16 @@ function getProviderTag(useCase: UseCase): string {
return map[useCase];
}
function getSkinTag(skin: Skin): string {
function getSkinTag(useCase: UseCase, skin: Skin): string {
// Background video has fixed skin
if (useCase === 'background-video') {
return 'background-video-skin';
}
const map: Record<Skin, string> = {
'default-video': 'video-skin',
'default-audio': 'audio-skin',
minimal: 'minimal-video-skin',
video: 'video-skin',
audio: 'audio-skin',
'minimal-video': 'minimal-video-skin',
'minimal-audio': 'minimal-audio-skin',
};
return map[skin];
}
@@ -43,8 +51,8 @@ function getSkinTag(skin: Skin): string {
function getRendererElement(renderer: Renderer, playbackId: string | null): string {
const tag = getRendererTag(renderer);
// When renderer is 'mux' and we have a playback ID, use playback-id attribute
if (renderer === 'mux' && playbackId) {
// When renderer is a mux variant and we have a playback ID, use playback-id attribute
if ((renderer === 'mux-video' || renderer === 'mux-audio' || renderer === 'mux-background-video') && playbackId) {
return `<${tag} playback-id="${playbackId}"></${tag}>`;
}
@@ -54,7 +62,7 @@ function getRendererElement(renderer: Renderer, playbackId: string | null): stri
function generateHTMLCode(useCase: UseCase, skin: Skin, renderer: Renderer, playbackId: string | null): string {
const providerTag = getProviderTag(useCase);
const skinTag = getSkinTag(skin);
const skinTag = getSkinTag(useCase, skin);
const rendererElement = getRendererElement(renderer, playbackId);
return `<!--
@@ -79,17 +87,22 @@ function generateHTMLCode(useCase: UseCase, skin: Skin, renderer: Renderer, play
</${providerTag}>`;
}
function getSkinImportPath(skin: Skin): string {
const map: Record<Skin, string> = {
'default-video': '@videojs/html/video/skin',
'default-audio': '@videojs/html/audio/skin',
minimal: '@videojs/html/video/minimal-skin',
};
return map[skin];
function getSkinImportParts(skin: Skin): { group: string; skinFile: string } {
if (skin === 'minimal-video') return { group: 'video', skinFile: 'minimal-skin' };
if (skin === 'minimal-audio') return { group: 'audio', skinFile: 'minimal-skin' };
return { group: skin, skinFile: 'skin' };
}
function generateJS(skin: Skin): string {
return `import '${getSkinImportPath(skin)}';`;
function generateJS(useCase: UseCase, skin: Skin): string {
if (useCase === 'background-video') {
return `import '@videojs/html/background/player';
import '@videojs/html/background/skin';
import '@videojs/html/background/skin.css';`;
}
const { group, skinFile } = getSkinImportParts(skin);
return `import '@videojs/html/${group}/player';
import '@videojs/html/${group}/${skinFile}';
import '@videojs/html/${group}/${skinFile}.css';`;
}
export default function HTMLUsageCodeBlock() {
@@ -97,19 +110,22 @@ export default function HTMLUsageCodeBlock() {
const $skin = useStore(skin);
const $renderer = useStore(renderer);
const $muxPlaybackId = useStore(muxPlaybackId);
const $installMethod = useStore(installMethod);
return (
<>
<TabsRoot maxWidth={false}>
<TabsList label="HTML implementation">
<Tab value="javascript" initial>
JavaScript
</Tab>
</TabsList>
<TabsPanel value="javascript" initial>
<ClientCode code={generateJS($skin)} lang="javascript" />
</TabsPanel>
</TabsRoot>
{$installMethod !== 'cdn' && (
<TabsRoot maxWidth={false}>
<TabsList label="HTML implementation">
<Tab value="javascript" initial>
JavaScript
</Tab>
</TabsList>
<TabsPanel value="javascript" initial>
<ClientCode code={generateJS($useCase, $skin)} lang="javascript" />
</TabsPanel>
</TabsRoot>
)}
<TabsRoot maxWidth={false}>
<TabsList label="HTML implementation">
<Tab value="html" initial>
@@ -27,7 +27,7 @@ import UploaderOverlay from './UploaderOverlay';
* 3. If 401: show login overlay, wait for auth, retry
* 4. Upload begins with returned URL
* 5. On success: poll for playback ID
* 6. When ready: update renderer to 'mux', store playback ID in nanostore
* 6. When ready: update renderer to 'mux-video', store playback ID in nanostore
*/
export default function MuxUploaderPanel() {
// Local state for upload flow (not shared across islands)
@@ -158,7 +158,7 @@ export default function MuxUploaderPanel() {
// Success! Update local state and nanostores (for cross-island use)
setPlaybackId(result.playbackId);
setState('ready');
renderer.set('mux');
renderer.set('mux-video');
muxPlaybackId.set(result.playbackId);
}, [uploadId]);
@@ -6,14 +6,17 @@ import { muxPlaybackId, renderer, skin, useCase } from '@/stores/installation';
function getRendererComponent(renderer: Renderer): string {
const map: Record<Renderer, string> = {
'background-video': 'BackgroundVideo',
cloudflare: 'CloudflareVideo',
dash: 'DashVideo',
hls: 'HlsVideo',
'html5-audio': 'Audio',
'html5-video': 'Video',
jwplayer: 'JwplayerVideo',
mux: 'MuxVideo',
shaka: 'ShakaVideo',
'mux-audio': 'MuxAudio',
'mux-background-video': 'MuxBackgroundVideo',
'mux-video': 'MuxVideo',
// shaka: 'ShakaVideo', this one probably adds more confusion than help at this level
spotify: 'SpotifyAudio',
vimeo: 'VimeoVideo',
wistia: 'WistiaVideo',
@@ -24,13 +27,20 @@ function getRendererComponent(renderer: Renderer): string {
function getSkinComponent(skin: Skin): string {
const map: Record<Skin, string> = {
'default-video': 'VideoSkin',
'default-audio': 'AudioSkin',
minimal: 'MinimalVideoSkin',
video: 'VideoSkin',
audio: 'AudioSkin',
'minimal-video': 'MinimalVideoSkin',
'minimal-audio': 'MinimalAudioSkin',
};
return map[skin];
}
function getSkinImportParts(skin: Skin): { group: string; skinFile: string } {
if (skin === 'minimal-video') return { group: 'video', skinFile: 'minimal-skin' };
if (skin === 'minimal-audio') return { group: 'audio', skinFile: 'minimal-skin' };
return { group: skin, skinFile: 'skin' };
}
function getPresetAccess(useCase: UseCase): string {
if (useCase === 'default-video') {
return 'presets.website';
@@ -38,13 +48,32 @@ function getPresetAccess(useCase: UseCase): string {
return `presets['${useCase}']`;
}
function getProviderComponent(useCase: UseCase): string {
const map: Record<UseCase, string> = {
'default-video': 'VideoProvider',
'default-audio': 'AudioProvider',
'background-video': 'BackgroundVideoProvider',
};
return map[useCase];
}
function generateReactCode(useCase: UseCase, skin: Skin, renderer: Renderer, playbackId: string | null): string {
const skinComponent = getSkinComponent(skin);
const providerComponent = getProviderComponent(useCase);
const rendererComponent = getRendererComponent(renderer);
const presetAccess = getPresetAccess(useCase);
// Background video has fixed skin and subpath imports, others use skin picker value
const isBackgroundVideo = useCase === 'background-video';
const skinComponent = isBackgroundVideo ? 'BackgroundVideoSkin' : getSkinComponent(skin);
const { group, skinFile } = getSkinImportParts(skin);
const skinCssImport = isBackgroundVideo
? '@videojs/react/background/skin.css'
: `@videojs/react/${group}/${skinFile}.css`;
const componentImportPath = isBackgroundVideo ? '@videojs/react/background' : '@videojs/react';
// Determine props based on renderer + playbackId
const isMuxWithPlaybackId = renderer === 'mux' && playbackId;
const isMuxWithPlaybackId =
(renderer === 'mux-video' || renderer === 'mux-audio' || renderer === 'mux-background-video') && playbackId;
const propsInterface = isMuxWithPlaybackId
? 'interface MyPlayerProps {\n playbackId: string;\n}'
: 'interface MyPlayerProps {\n src: string;\n}';
@@ -53,21 +82,17 @@ function generateReactCode(useCase: UseCase, skin: Skin, renderer: Renderer, pla
? `<${rendererComponent} playbackId={playbackId} />`
: `<${rendererComponent} src={src} />`;
return `import { createPlayer, presets, ${rendererComponent} } from '@videojs/react';
import { ${skinComponent} } from '@videojs/react/presets/${useCase}';
import '@videojs/react/presets/${useCase}/skins/${skin}.css';
return `import '${skinCssImport}';
import { ${providerComponent}, ${skinComponent}, ${rendererComponent} } from '${componentImportPath}';
${propsInterface}
// Set up the player state features
const { PlayerProvider } = createPlayer(${presetAccess});
export const MyPlayer = ({ ${destructuredProp} }: MyPlayerProps) => {
return (
{/* The Provider passes state between the UI components
and the Media, and makes fully custom UIs possible.
Does not render its own HTML element. */}
<PlayerProvider>
<${providerComponent}>
{/* Skins contain the entire player UI and are easily swappable.
They can each be "ejected" for full control and customization
of UI components. */}
@@ -77,7 +102,7 @@ export const MyPlayer = ({ ${destructuredProp} }: MyPlayerProps) => {
They are easily swappable to handle different sources. */}
${rendererJsx}
</${skinComponent}>
</PlayerProvider>
</${providerComponent}>
);
};`;
}
@@ -5,7 +5,8 @@ import type { Renderer } from '@/stores/installation';
import { muxPlaybackId, renderer } from '@/stores/installation';
function generateUsageCode(renderer: Renderer, playbackId: string | null): string {
const isMuxWithPlaybackId = renderer === 'mux' && playbackId;
const isMuxWithPlaybackId =
(renderer === 'mux-video' || renderer === 'mux-audio' || renderer === 'mux-background-video') && playbackId;
const playerProp = isMuxWithPlaybackId ? `playbackId="${playbackId}"` : 'src="https://example.com/video.mp4"';
return `import { MyPlayer } from '../components/player';
@@ -1,41 +1,64 @@
import { useStore } from '@nanostores/react';
import { Box } from 'lucide-react';
import { useEffect } from 'react';
import type { ImageRadioOption } from '@/components/ImageRadioGroup';
import ImageRadioGroup from '@/components/ImageRadioGroup';
import type { Renderer } from '@/stores/installation';
import { renderer } from '@/stores/installation';
import { renderer, useCase } from '@/stores/installation';
const PRIORITY_RENDERER_OPTIONS: ImageRadioOption<Renderer>[] = [
const VIDEO_RENDERERS: ImageRadioOption<Renderer>[] = [
{ value: 'html5-video', label: 'HTML5 Video', image: <Box size={16} /> },
{ value: 'html5-audio', label: 'HTML5 Audio', image: <Box size={16} /> },
...(
[
{ value: 'cloudflare', label: 'Cloudflare', image: <Box size={16} /> },
{ value: 'dash', label: 'DASH', image: <Box size={16} /> },
{ value: 'hls', label: 'HLS', image: <Box size={16} /> },
{ value: 'jwplayer', label: 'JW Player', image: <Box size={16} /> },
{ value: 'mux-video', label: 'Mux', image: <Box size={16} /> },
// { value: 'shaka', label: 'Shaka', image: <Box size={16} /> },
{ value: 'vimeo', label: 'Vimeo', image: <Box size={16} /> },
{ value: 'wistia', label: 'Wistia', image: <Box size={16} /> },
{ value: 'youtube', label: 'YouTube', image: <Box size={16} /> },
] satisfies ImageRadioOption<Renderer>[]
).sort((a, b) => a.label.localeCompare(b.label)),
];
const RENDERER_OPTIONS: ImageRadioOption<Renderer>[] = (
[
{ value: 'cloudflare', label: 'Cloudflare', image: <Box size={16} /> },
{ value: 'dash', label: 'DASH', image: <Box size={16} /> },
{ value: 'hls', label: 'HLS', image: <Box size={16} /> },
{ value: 'jwplayer', label: 'JW Player', image: <Box size={16} /> },
{ value: 'mux', label: 'Mux', image: <Box size={16} /> },
{ value: 'shaka', label: 'Shaka', image: <Box size={16} /> },
{ value: 'spotify', label: 'Spotify', image: <Box size={16} /> },
{ value: 'vimeo', label: 'Vimeo', image: <Box size={16} /> },
{ value: 'wistia', label: 'Wistia', image: <Box size={16} /> },
{ value: 'youtube', label: 'YouTube', image: <Box size={16} /> },
] satisfies ImageRadioOption<Renderer>[]
).sort((a, b) => a.label.localeCompare(b.label));
const AUDIO_RENDERERS: ImageRadioOption<Renderer>[] = [
{ value: 'html5-audio', label: 'HTML5 Audio', image: <Box size={16} /> },
{ value: 'mux-audio', label: 'Mux', image: <Box size={16} /> },
{ value: 'spotify', label: 'Spotify', image: <Box size={16} /> },
];
const ALL_RENDERER_OPTIONS = [...PRIORITY_RENDERER_OPTIONS, ...RENDERER_OPTIONS];
const BACKGROUND_VIDEO_RENDERERS: ImageRadioOption<Renderer>[] = [
{ value: 'background-video', label: 'Background Video', image: <Box size={16} /> },
{ value: 'mux-background-video', label: 'Mux Background Video', image: <Box size={16} /> },
];
/** URL input and renderer dropdown for manual selection */
export default function RendererSelect() {
const $renderer = useStore(renderer);
const $useCase = useStore(useCase);
const options =
$useCase === 'default-audio'
? AUDIO_RENDERERS
: $useCase === 'background-video'
? BACKGROUND_VIDEO_RENDERERS
: VIDEO_RENDERERS;
// Auto-switch renderer when use case changes and current renderer is invalid
useEffect(() => {
const validValues = options.map((o) => o.value);
if (!validValues.includes($renderer)) {
renderer.set(options[0].value);
}
}, [$useCase]);
return (
<ImageRadioGroup
value={$renderer}
onChange={(value) => renderer.set(value)}
options={ALL_RENDERER_OPTIONS}
options={options}
aria-label="Select renderer"
size="sm"
labelPosition="inline"
+25 -11
View File
@@ -1,22 +1,36 @@
import { useStore } from '@nanostores/react';
import { Minus, Sparkles } from 'lucide-react';
import { useEffect } from 'react';
import type { ImageRadioOption } from '@/components/ImageRadioGroup';
import ImageRadioGroup from '@/components/ImageRadioGroup';
import type { Skin } from '@/stores/installation';
import { skin } from '@/stores/installation';
import { skin, useCase } from '@/stores/installation';
const VIDEO_SKINS: ImageRadioOption<Skin>[] = [
{ value: 'video', label: 'Default Video', image: <Sparkles size={32} /> },
{ value: 'minimal-video', label: 'Minimal', image: <Minus size={32} /> },
];
const AUDIO_SKINS: ImageRadioOption<Skin>[] = [
{ value: 'audio', label: 'Default Audio', image: <Sparkles size={32} /> },
{ value: 'minimal-audio', label: 'Minimal', image: <Minus size={32} /> },
];
export default function SkinPicker() {
const $skin = useStore(skin);
const $useCase = useStore(useCase);
const options = $useCase === 'default-audio' ? AUDIO_SKINS : VIDEO_SKINS;
// Auto-switch skin when use case changes and current skin is invalid
useEffect(() => {
const validValues = options.map((o) => o.value);
if (!validValues.includes($skin)) {
skin.set(options[0].value);
}
}, [$useCase]);
return (
<ImageRadioGroup
value={$skin}
onChange={(value) => skin.set(value as Skin)}
options={[
{ value: 'default-video' satisfies Skin, label: 'Default Video', image: <Sparkles size={32} /> },
{ value: 'default-audio' satisfies Skin, label: 'Default Audio', image: <Sparkles size={32} /> },
{ value: 'minimal' satisfies Skin, label: 'Minimal', image: <Minus size={32} /> },
]}
aria-label="Select skin"
/>
<ImageRadioGroup value={$skin} onChange={(value) => skin.set(value)} options={options} aria-label="Select skin" />
);
}
@@ -0,0 +1,12 @@
---
import H2Markdown from '../typography/H2Markdown.astro';
import P from '../typography/P.astro';
import SkinPickerSectionClient from './SkinPickerSectionClient.tsx';
---
<SkinPickerSectionClient client:idle>
<H2Markdown>
Choose skin
</H2Markdown>
<P>Choose how your player looks.</P>
</SkinPickerSectionClient>
@@ -0,0 +1,19 @@
import { useStore } from '@nanostores/react';
import { useCase } from '@/stores/installation';
import SkinPicker from './SkinPicker';
export default function SkinPickerSection({ children }: { children: React.ReactNode }) {
const $useCase = useStore(useCase);
// Hide for background-video use case
if ($useCase === 'background-video') {
return null;
}
return (
<>
{children}
<SkinPicker />
</>
);
}
+8 -43
View File
@@ -5,8 +5,9 @@ description: Get started quickly with Video.js by building your first embed code
import JSPicker from '@/components/installation/JSPicker.astro';
import RendererPicker from '@/components/installation/RendererPicker';
import SkinPicker from '@/components/installation/SkinPicker';
import SkinPickerSection from '@/components/installation/SkinPickerSection.astro';
import UseCasePicker from '@/components/installation/UseCasePicker';
import HTMLInstallTabs from '@/components/installation/HTMLInstallTabs';
import HTMLUsageCodeBlock from '@/components/installation/HTMLUsageCodeBlock';
import ReactCreateCodeBlock from '@/components/installation/ReactCreateCodeBlock';
import ReactUsageCodeBlock from '@/components/installation/ReactUsageCodeBlock';
@@ -25,7 +26,7 @@ Answer the questions below to get started quickly with your first embed code.
## Choose your JS framework
Video.js aims to provide idiomatic development experiences in your favorite JS and CSS frameworks. More to come ([Vote on what's next](https://github.com/videojs/v10/discussions/categories/ideas))\[todo\].
Video.js aims to provide idiomatic development experiences in your favorite JS and CSS frameworks. More to come. ([vote](https://github.com/videojs/v10/discussions/categories/ideas))\[todo\].
<ContentWidth margins="lg">
<JSPicker />
@@ -34,12 +35,15 @@ Video.js aims to provide idiomatic development experiences in your favorite JS a
## Choose your use case
The defaults work well for general website playback. More use case player presets to come.
The defaults work well for general website playback. More prebuilt players to come. ([vote](https://github.com/videojs/v10/discussions/categories/ideas))\[todo\]
<ContentWidth margins="lg">
<UseCasePicker client:idle />
</ContentWidth>
<ContentWidth margins="lg">
<SkinPickerSection />
</ContentWidth>
## Choose your media source type
@@ -52,46 +56,7 @@ Video.js supports a wide range of file types and hosting services. It's easy to
## Install Video.js
<FrameworkCase frameworks={["html"]}>
<TabsRoot client:idle>
<TabsList client:idle label="Installation">
<Tab client:idle value="cdn" initial>cdn</Tab>
<Tab client:idle value="npm">npm</Tab>
<Tab client:idle value="pnpm">pnpm</Tab>
<Tab client:idle value="yarn">yarn</Tab>
<Tab client:idle value="bun">bun</Tab>
</TabsList>
<TabsPanel client:idle value="cdn">
```html
<script>
// don't need this, will be included in frosted
// import 'https://cdn.jsdelivr.net/npm/videojs/html/presets/website.js';
import 'https://cdn.jsdelivr.net/npm/videojs/html/presets/website/skins/frosted.js';
</script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/videojs/html/presets/website/skins/frosted.css" />
```
</TabsPanel>
<TabsPanel client:idle value="npm" initial>
```bash
npm install @videojs/html
```
</TabsPanel>
<TabsPanel client:idle value="pnpm">
```bash
pnpm add @videojs/html
```
</TabsPanel>
<TabsPanel client:idle value="yarn">
```bash
yarn add @videojs/html
```
</TabsPanel>
<TabsPanel client:idle value="bun">
```bash
bun add @videojs/html
```
</TabsPanel>
</TabsRoot>
<HTMLInstallTabs client:idle />
</FrameworkCase>
<FrameworkCase frameworks={["react"]}>
<TabsRoot client:idle>
+11 -4
View File
@@ -1,26 +1,33 @@
import { atom } from 'nanostores';
export type Renderer =
| 'background-video'
| 'cloudflare'
| 'dash'
| 'hls'
| 'html5-audio'
| 'html5-video'
| 'jwplayer'
| 'mux'
| 'shaka'
| 'mux-audio'
| 'mux-background-video'
| 'mux-video'
// | 'shaka'
| 'spotify'
| 'vimeo'
| 'wistia'
| 'youtube';
export type Skin = 'default-video' | 'default-audio' | 'minimal';
export type Skin = 'video' | 'audio' | 'minimal-video' | 'minimal-audio';
export type UseCase = 'default-video' | 'default-audio' | 'background-video';
export const renderer = atom<Renderer>('html5-video');
export const skin = atom<Skin>('default-video');
export const skin = atom<Skin>('video');
export const useCase = atom<UseCase>('default-video');
export type InstallMethod = 'cdn' | 'npm' | 'pnpm' | 'yarn' | 'bun';
export const installMethod = atom<InstallMethod>('cdn');
/** Mux playback ID from successful upload (used by code generation) */
export const muxPlaybackId = atom<string | null>(null);