mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
feat(cli): add @videojs/cli docs command for LLM-friendly installation (#1214)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
9681515446
commit
24b8b77c8a
@@ -0,0 +1,6 @@
|
||||
---
|
||||
---
|
||||
|
||||
<div class="contents" data-llms-ignore>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -0,0 +1,6 @@
|
||||
---
|
||||
---
|
||||
|
||||
<div hidden data-llms-only>
|
||||
<slot />
|
||||
</div>
|
||||
@@ -1,8 +1,8 @@
|
||||
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 type { InstallMethod } from '@/utils/installation/types';
|
||||
import HTMLCdnCodeBlock from './HTMLCdnCodeBlock';
|
||||
|
||||
export default function HTMLInstallTabs() {
|
||||
@@ -23,7 +23,6 @@ export default function HTMLInstallTabs() {
|
||||
}
|
||||
});
|
||||
|
||||
// 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'] });
|
||||
|
||||
@@ -1,134 +1,8 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import ClientCode from '@/components/Code/ClientCode';
|
||||
import { Tab, TabsList, TabsPanel, TabsRoot } from '@/components/Tabs';
|
||||
import { VJS10_DEMO_VIDEO } from '@/consts';
|
||||
import type { Renderer, Skin, UseCase } from '@/stores/installation';
|
||||
import { installMethod, renderer, skin, sourceUrl, 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-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',
|
||||
// youtube: 'youtube-video',
|
||||
};
|
||||
return map[renderer];
|
||||
}
|
||||
|
||||
function getProviderTag(useCase: UseCase): string {
|
||||
const map: Record<UseCase, string> = {
|
||||
'default-video': 'video-player',
|
||||
'default-audio': 'audio-player',
|
||||
'background-video': 'background-video-player',
|
||||
};
|
||||
return map[useCase];
|
||||
}
|
||||
|
||||
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> = {
|
||||
video: 'video-skin',
|
||||
audio: 'audio-skin',
|
||||
'minimal-video': 'video-minimal-skin',
|
||||
'minimal-audio': 'audio-minimal-skin',
|
||||
};
|
||||
return map[skin];
|
||||
}
|
||||
|
||||
function isVideoLikeRenderer(renderer: Renderer): boolean {
|
||||
return renderer === 'html5-video' || renderer === 'hls' || renderer === 'background-video';
|
||||
}
|
||||
|
||||
function getRendererElement(renderer: Renderer, url: string): string {
|
||||
const tag = getRendererTag(renderer);
|
||||
const src = url.trim() || getDefaultSourceUrl(renderer);
|
||||
const playsInline = isVideoLikeRenderer(renderer) ? ' playsinline' : '';
|
||||
return `<${tag} src="${src}"${playsInline}></${tag}>`;
|
||||
}
|
||||
|
||||
function getDefaultSourceUrl(renderer: Renderer): string {
|
||||
switch (renderer) {
|
||||
case 'hls':
|
||||
return VJS10_DEMO_VIDEO.hls;
|
||||
case 'background-video':
|
||||
case 'html5-audio':
|
||||
case 'html5-video':
|
||||
default:
|
||||
return VJS10_DEMO_VIDEO.mp4;
|
||||
}
|
||||
}
|
||||
|
||||
function generateHTMLCode(useCase: UseCase, skin: Skin, renderer: Renderer, url: string): string {
|
||||
const providerTag = getProviderTag(useCase);
|
||||
const skinTag = getSkinTag(useCase, skin);
|
||||
const rendererElement = getRendererElement(renderer, url);
|
||||
|
||||
return `<!--
|
||||
The PlayerProvider passes state between the UI components
|
||||
and Media, and makes fully custom UIs possible.
|
||||
It does not have layout by default (display:contents)
|
||||
-->
|
||||
<${providerTag}>
|
||||
<!--
|
||||
Skins contain the entire player UI and are easily swappable.
|
||||
They can each be "ejected" for full control and customization
|
||||
of UI components.
|
||||
-->
|
||||
<${skinTag}>
|
||||
<!--
|
||||
Media are players without UIs, handling networking
|
||||
and display of the media. They are easily swappable
|
||||
to handle different sources.
|
||||
-->
|
||||
${rendererElement}
|
||||
</${skinTag}>
|
||||
</${providerTag}>`;
|
||||
}
|
||||
|
||||
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 getMediaImportSubpath(renderer: Renderer): string | null {
|
||||
const map: Partial<Record<Renderer, string>> = {
|
||||
hls: 'hls-video',
|
||||
// 'mux-audio': 'mux-audio',
|
||||
// 'mux-background-video': 'mux-background-video',
|
||||
// 'mux-video': 'mux-video',
|
||||
};
|
||||
return map[renderer] ?? null;
|
||||
}
|
||||
|
||||
function generateJS(useCase: UseCase, skin: Skin, renderer: Renderer): string {
|
||||
if (useCase === 'background-video') {
|
||||
const mediaSubpath = getMediaImportSubpath(renderer);
|
||||
const mediaImport = mediaSubpath ? `\nimport '@videojs/html/media/${mediaSubpath}';` : '';
|
||||
return `import '@videojs/html/background/player';
|
||||
import '@videojs/html/background/skin';
|
||||
import '@videojs/html/background/video';${mediaImport}`;
|
||||
}
|
||||
const { group, skinFile } = getSkinImportParts(skin);
|
||||
const mediaSubpath = getMediaImportSubpath(renderer);
|
||||
const mediaImport = mediaSubpath ? `\nimport '@videojs/html/media/${mediaSubpath}';` : '';
|
||||
return `import '@videojs/html/${group}/player';
|
||||
import '@videojs/html/${group}/${skinFile}';${mediaImport}`;
|
||||
}
|
||||
import { generateHTMLUsageCode } from '@/utils/installation/codegen';
|
||||
|
||||
export default function HTMLUsageCodeBlock() {
|
||||
const $useCase = useStore(useCase);
|
||||
@@ -137,9 +11,17 @@ export default function HTMLUsageCodeBlock() {
|
||||
const $installMethod = useStore(installMethod);
|
||||
const $sourceUrl = useStore(sourceUrl);
|
||||
|
||||
const result = generateHTMLUsageCode({
|
||||
useCase: $useCase,
|
||||
skin: $skin,
|
||||
renderer: $renderer,
|
||||
sourceUrl: $sourceUrl,
|
||||
installMethod: $installMethod,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
{$installMethod !== 'cdn' && (
|
||||
{result.js && (
|
||||
<TabsRoot maxWidth={false}>
|
||||
<TabsList label="HTML implementation">
|
||||
<Tab value="javascript" initial>
|
||||
@@ -147,7 +29,7 @@ export default function HTMLUsageCodeBlock() {
|
||||
</Tab>
|
||||
</TabsList>
|
||||
<TabsPanel value="javascript" initial>
|
||||
<ClientCode code={generateJS($useCase, $skin, $renderer)} lang="javascript" />
|
||||
<ClientCode code={result.js} lang="javascript" />
|
||||
</TabsPanel>
|
||||
</TabsRoot>
|
||||
)}
|
||||
@@ -158,7 +40,7 @@ export default function HTMLUsageCodeBlock() {
|
||||
</Tab>
|
||||
</TabsList>
|
||||
<TabsPanel value="html" initial>
|
||||
<ClientCode code={generateHTMLCode($useCase, $skin, $renderer, $sourceUrl)} lang="html" />
|
||||
<ClientCode code={result.html} lang="html" />
|
||||
</TabsPanel>
|
||||
</TabsRoot>
|
||||
</>
|
||||
|
||||
@@ -1,143 +1,20 @@
|
||||
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 { 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-audio: 'MuxAudio',
|
||||
// mux-background-video: 'MuxBackgroundVideo',
|
||||
// mux-video: 'MuxVideo',
|
||||
// shaka: 'ShakaVideo',
|
||||
// spotify: 'SpotifyAudio',
|
||||
// vimeo: 'VimeoVideo',
|
||||
// wistia: 'WistiaVideo',
|
||||
// youtube: 'YoutubeVideo',
|
||||
};
|
||||
return map[renderer];
|
||||
}
|
||||
|
||||
function getSkinComponent(skin: Skin): string {
|
||||
const map: Record<Skin, string> = {
|
||||
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 getUseCaseFeatures(useCase: UseCase): string {
|
||||
const map: Record<UseCase, string> = {
|
||||
'default-video': 'videoFeatures',
|
||||
'default-audio': 'audioFeatures',
|
||||
'background-video': 'backgroundFeatures',
|
||||
};
|
||||
return map[useCase];
|
||||
}
|
||||
|
||||
function isPresetRenderer(renderer: Renderer): boolean {
|
||||
return renderer === 'html5-video' || renderer === 'html5-audio' || renderer === 'background-video';
|
||||
}
|
||||
|
||||
function isVideoLikeRenderer(renderer: Renderer): boolean {
|
||||
return renderer === 'html5-video' || renderer === 'hls' || renderer === 'background-video';
|
||||
}
|
||||
|
||||
function getRendererMediaSubpath(renderer: Renderer): string {
|
||||
const map: Partial<Record<Renderer, string>> = {
|
||||
// cloudflare: 'cloudflare-video',
|
||||
// dash: 'dash-video',
|
||||
hls: 'hls-video',
|
||||
// jwplayer: 'jwplayer-video',
|
||||
// 'mux-audio': 'mux-audio',
|
||||
// 'mux-background-video': 'mux-background-video',
|
||||
// 'mux-video': 'mux-video',
|
||||
// spotify: 'spotify-audio',
|
||||
// vimeo: 'vimeo-video',
|
||||
// wistia: 'wistia-video',
|
||||
// youtube: 'youtube-video',
|
||||
};
|
||||
return map[renderer] ?? renderer;
|
||||
}
|
||||
|
||||
function generateReactCode(useCase: UseCase, skin: Skin, renderer: Renderer): string {
|
||||
const rendererComponent = getRendererComponent(renderer);
|
||||
const featureType = getUseCaseFeatures(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`;
|
||||
|
||||
// Preset subpath where skin + default media components live
|
||||
const presetSubpath = isBackgroundVideo ? 'background' : group;
|
||||
|
||||
// Skin and media imports — preset renderers share a subpath with the skin
|
||||
let presetImport: string;
|
||||
let mediaImport: string | null = null;
|
||||
|
||||
if (isPresetRenderer(renderer)) {
|
||||
presetImport = `import { ${skinComponent}, ${rendererComponent} } from '@videojs/react/${presetSubpath}';`;
|
||||
} else {
|
||||
presetImport = `import { ${skinComponent} } from '@videojs/react/${presetSubpath}';`;
|
||||
mediaImport = `import { ${rendererComponent} } from '@videojs/react/media/${getRendererMediaSubpath(renderer)}';`;
|
||||
}
|
||||
|
||||
// Determine props — mux variants use src with stream.mux.com URL
|
||||
const propsInterface = 'interface MyPlayerProps {\n src: string;\n}';
|
||||
const destructuredProp = 'src';
|
||||
const rendererProps = isVideoLikeRenderer(renderer) ? 'src={src} playsInline' : 'src={src}';
|
||||
const rendererJsx = `<${rendererComponent} ${rendererProps} />`;
|
||||
|
||||
const imports = [
|
||||
`import '${skinCssImport}';`,
|
||||
`import { createPlayer, ${featureType} } from '@videojs/react';`,
|
||||
presetImport,
|
||||
...(mediaImport ? [mediaImport] : []),
|
||||
].join('\n');
|
||||
|
||||
return `'use client';
|
||||
|
||||
${imports}
|
||||
|
||||
const Player = createPlayer({ features: ${featureType} });
|
||||
|
||||
${propsInterface}
|
||||
|
||||
export const MyPlayer = ({ ${destructuredProp} }: MyPlayerProps) => {
|
||||
return (
|
||||
<Player.Provider>
|
||||
<${skinComponent}>
|
||||
${rendererJsx}
|
||||
</${skinComponent}>
|
||||
</Player.Provider>
|
||||
);
|
||||
};`;
|
||||
}
|
||||
import { generateReactCreateCode } from '@/utils/installation/codegen';
|
||||
|
||||
export default function ReactCreateCodeBlock() {
|
||||
const $useCase = useStore(useCase);
|
||||
const $skin = useStore(skin);
|
||||
const $renderer = useStore(renderer);
|
||||
|
||||
const result = generateReactCreateCode({
|
||||
useCase: $useCase,
|
||||
skin: $skin,
|
||||
renderer: $renderer,
|
||||
});
|
||||
|
||||
return (
|
||||
<TabsRoot maxWidth={false}>
|
||||
<TabsList label="React implementation">
|
||||
@@ -146,7 +23,7 @@ export default function ReactCreateCodeBlock() {
|
||||
</Tab>
|
||||
</TabsList>
|
||||
<TabsPanel value="react" initial>
|
||||
<ClientCode code={generateReactCode($useCase, $skin, $renderer)} lang="tsx" />
|
||||
<ClientCode code={result['MyPlayer.tsx']} lang="tsx" />
|
||||
</TabsPanel>
|
||||
</TabsRoot>
|
||||
);
|
||||
|
||||
@@ -1,34 +1,18 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import ClientCode from '@/components/Code/ClientCode';
|
||||
import { Tab, TabsList, TabsPanel, TabsRoot } from '@/components/Tabs';
|
||||
import { VJS10_DEMO_VIDEO } from '@/consts';
|
||||
import type { Renderer } from '@/stores/installation';
|
||||
import { renderer, sourceUrl } from '@/stores/installation';
|
||||
|
||||
function getDefaultSourceUrl(renderer: Renderer): string {
|
||||
return renderer === 'hls' ? VJS10_DEMO_VIDEO.hls : VJS10_DEMO_VIDEO.mp4;
|
||||
}
|
||||
|
||||
function generateUsageCode(url: string, renderer: Renderer): string {
|
||||
const source = url.trim() || getDefaultSourceUrl(renderer);
|
||||
const playerProp = `src="${source}"`;
|
||||
|
||||
return `import { MyPlayer } from '../components/player';
|
||||
|
||||
export const HomePage = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Welcome to My App</h1>
|
||||
<MyPlayer ${playerProp} />
|
||||
</div>
|
||||
);
|
||||
};`;
|
||||
}
|
||||
import { generateReactUsageCode } from '@/utils/installation/codegen';
|
||||
|
||||
export default function ReactUsageCodeBlock() {
|
||||
const $renderer = useStore(renderer);
|
||||
const $sourceUrl = useStore(sourceUrl);
|
||||
|
||||
const result = generateReactUsageCode({
|
||||
renderer: $renderer,
|
||||
sourceUrl: $sourceUrl,
|
||||
});
|
||||
|
||||
return (
|
||||
<TabsRoot maxWidth={false}>
|
||||
<TabsList label="React usage">
|
||||
@@ -37,7 +21,7 @@ export default function ReactUsageCodeBlock() {
|
||||
</Tab>
|
||||
</TabsList>
|
||||
<TabsPanel value="react" initial>
|
||||
<ClientCode code={generateUsageCode($sourceUrl, $renderer)} lang="tsx" />
|
||||
<ClientCode code={result['App.tsx']} lang="tsx" />
|
||||
</TabsPanel>
|
||||
</TabsRoot>
|
||||
);
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { useEffect } from 'react';
|
||||
import { Select, type SelectOption } from '@/components/Select';
|
||||
import type { Renderer, UseCase } from '@/stores/installation';
|
||||
import { renderer, sourceUrl, useCase, VALID_RENDERERS } from '@/stores/installation';
|
||||
import { renderer, sourceUrl, useCase } from '@/stores/installation';
|
||||
import { articleFor, detectRenderer } from '@/utils/installation/detect-renderer';
|
||||
import type { Renderer, UseCase } from '@/utils/installation/types';
|
||||
import { VALID_RENDERERS } from '@/utils/installation/types';
|
||||
|
||||
const RENDERER_LABELS: Record<Renderer, string> = {
|
||||
'background-video': 'Background Video',
|
||||
|
||||
@@ -3,8 +3,8 @@ 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, useCase } from '@/stores/installation';
|
||||
import type { Skin } from '@/utils/installation/types';
|
||||
|
||||
const VIDEO_SKINS: ImageRadioOption<Skin>[] = [
|
||||
{ value: 'video', label: 'Default', image: <Sparkles size={32} /> },
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { Globe, Image } from 'lucide-react';
|
||||
import ImageRadioGroup from '@/components/ImageRadioGroup';
|
||||
import type { UseCase } from '@/stores/installation';
|
||||
import { useCase } from '@/stores/installation';
|
||||
import type { UseCase } from '@/utils/installation/types';
|
||||
|
||||
export default function UseCasePicker() {
|
||||
const $useCase = useStore(useCase);
|
||||
|
||||
@@ -16,6 +16,8 @@ import Aside from '@/components/Aside.astro';
|
||||
import DocsLink from '@/components/docs/DocsLink.astro';
|
||||
import DocsLinkCard from '@/components/docs/DocsLinkCard.astro';
|
||||
import FrameworkCase from '@/components/docs/FrameworkCase.astro';
|
||||
import HumanCase from '@/components/docs/HumanCase.astro';
|
||||
import LLMCase from '@/components/docs/LLMCase.astro';
|
||||
import { TabsRoot, TabsList, TabsPanel, Tab } from '@/components/Tabs.tsx';
|
||||
|
||||
<Aside type="caution" title="Beta Software">
|
||||
@@ -29,6 +31,26 @@ Video.js is a **React video player component library** — composable primit
|
||||
Video.js is an **HTML video player built on custom elements** — lightweight, framework-free components for building accessible, customizable players with minimal bundle size.
|
||||
</FrameworkCase>
|
||||
|
||||
<LLMCase>
|
||||
<div data-cli-replace="installation">
|
||||
|
||||
Run `npx @videojs/cli docs how-to/installation` interactively, or pass all flags:
|
||||
|
||||
```bash
|
||||
npx @videojs/cli docs how-to/installation \
|
||||
--framework <html|react> \
|
||||
--preset <video|audio|background-video> \
|
||||
--skin <default|minimal> \
|
||||
--media <html5-video|html5-audio|hls|background-video> \
|
||||
--source-url <url> \
|
||||
--install-method <cdn|npm|pnpm|yarn|bun>
|
||||
```
|
||||
|
||||
</div>
|
||||
</LLMCase>
|
||||
|
||||
<HumanCase>
|
||||
|
||||
Answer the questions below to get started quickly with your first embed code.
|
||||
|
||||
## Choose your JS framework
|
||||
@@ -120,6 +142,8 @@ Add it to your components folder in a new file.
|
||||
</ContentWidth>
|
||||
</FrameworkCase>
|
||||
|
||||
</HumanCase>
|
||||
|
||||
## CSP
|
||||
|
||||
If your application uses a Content Security Policy, you may need to allow additional sources for player features to work correctly.
|
||||
|
||||
@@ -58,7 +58,11 @@ const fullTitle =
|
||||
/>
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
<meta name="algolia-site-verification" content="9CE2D1D6448256D3" />
|
||||
{Astro.site?.origin !== PRODUCTION_URL.origin && <meta name="robots" content="noindex" />}
|
||||
{
|
||||
Astro.site?.origin !== PRODUCTION_URL.origin && (
|
||||
<meta name="robots" content="noindex" />
|
||||
)
|
||||
}
|
||||
|
||||
{/* Analytics (production only) */}
|
||||
{import.meta.env.PROD && <Posthog />}
|
||||
|
||||
@@ -1,43 +1,12 @@
|
||||
import { atom } from 'nanostores';
|
||||
|
||||
export type Renderer =
|
||||
| 'background-video'
|
||||
// | 'cloudflare'
|
||||
// | 'dash'
|
||||
| 'hls'
|
||||
| 'html5-audio'
|
||||
| 'html5-video';
|
||||
// | 'jwplayer'
|
||||
// | 'mux-audio'
|
||||
// | 'mux-background-video'
|
||||
// | 'mux-video'
|
||||
// | 'shaka'
|
||||
// | 'spotify'
|
||||
// | 'vimeo'
|
||||
// | 'wistia'
|
||||
// | 'youtube'
|
||||
|
||||
export type Skin = 'video' | 'audio' | 'minimal-video' | 'minimal-audio';
|
||||
|
||||
export type UseCase = 'default-video' | 'default-audio' | 'background-video';
|
||||
import type { InstallMethod, Renderer, Skin, UseCase } from '@/utils/installation/types';
|
||||
|
||||
export const renderer = atom<Renderer>('html5-video');
|
||||
export const skin = atom<Skin>('video');
|
||||
export const useCase = atom<UseCase>('default-video');
|
||||
export const sourceUrl = atom<string>('');
|
||||
|
||||
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);
|
||||
|
||||
export const VALID_RENDERERS: Record<UseCase, Renderer[]> = {
|
||||
'default-video': [
|
||||
'html5-video',
|
||||
/* 'cloudflare', 'dash', */ 'hls' /* , 'jwplayer', 'mux-video', 'vimeo', 'wistia', 'youtube' */,
|
||||
],
|
||||
'default-audio': ['html5-audio' /* , 'mux-audio', 'spotify' */],
|
||||
'background-video': ['background-video' /* , 'mux-background-video' */],
|
||||
};
|
||||
|
||||
@@ -0,0 +1,231 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import {
|
||||
generateHTMLInstallCode,
|
||||
generateHTMLUsageCode,
|
||||
generateReactCreateCode,
|
||||
generateReactInstallCode,
|
||||
generateReactUsageCode,
|
||||
type InstallationOptions,
|
||||
validateInstallationOptions,
|
||||
} from '../codegen';
|
||||
|
||||
const baseHTML: InstallationOptions = {
|
||||
framework: 'html',
|
||||
useCase: 'default-video',
|
||||
skin: 'video',
|
||||
renderer: 'html5-video',
|
||||
sourceUrl: '',
|
||||
installMethod: 'npm',
|
||||
};
|
||||
|
||||
const baseReact: InstallationOptions = {
|
||||
framework: 'react',
|
||||
useCase: 'default-video',
|
||||
skin: 'video',
|
||||
renderer: 'html5-video',
|
||||
sourceUrl: '',
|
||||
installMethod: 'npm',
|
||||
};
|
||||
|
||||
describe('validateInstallationOptions', () => {
|
||||
it('accepts valid HTML + npm combo', () => {
|
||||
expect(validateInstallationOptions(baseHTML)).toEqual({ valid: true });
|
||||
});
|
||||
|
||||
it('accepts valid React + npm combo', () => {
|
||||
expect(validateInstallationOptions(baseReact)).toEqual({ valid: true });
|
||||
});
|
||||
|
||||
it('rejects React + CDN', () => {
|
||||
const result = validateInstallationOptions({ ...baseReact, installMethod: 'cdn' });
|
||||
expect(result.valid).toBe(false);
|
||||
if (!result.valid) {
|
||||
expect(result.reason).toContain('CDN');
|
||||
}
|
||||
});
|
||||
|
||||
it('accepts any renderer regardless of use case', () => {
|
||||
expect(validateInstallationOptions({ ...baseHTML, useCase: 'default-audio', renderer: 'hls' })).toEqual({
|
||||
valid: true,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateHTMLInstallCode', () => {
|
||||
it('returns install commands for all methods', () => {
|
||||
const result = generateHTMLInstallCode(baseHTML);
|
||||
expect(result.npm).toBe('npm install @videojs/html');
|
||||
expect(result.pnpm).toBe('pnpm add @videojs/html');
|
||||
expect(result.yarn).toBe('yarn add @videojs/html');
|
||||
expect(result.bun).toBe('bun add @videojs/html');
|
||||
});
|
||||
|
||||
it('returns CDN script tags', () => {
|
||||
const result = generateHTMLInstallCode(baseHTML);
|
||||
expect(result.cdn).toContain('<script');
|
||||
expect(result.cdn).toContain('cdn.jsdelivr.net');
|
||||
});
|
||||
|
||||
it('includes HLS media script in CDN output', () => {
|
||||
const result = generateHTMLInstallCode({ ...baseHTML, renderer: 'hls' });
|
||||
expect(result.cdn).toContain('media/hls-video.js');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateReactInstallCode', () => {
|
||||
it('returns install commands for all methods', () => {
|
||||
const result = generateReactInstallCode();
|
||||
expect(result.npm).toBe('npm install @videojs/react');
|
||||
expect(result.pnpm).toBe('pnpm add @videojs/react');
|
||||
expect(result.yarn).toBe('yarn add @videojs/react');
|
||||
expect(result.bun).toBe('bun add @videojs/react');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateHTMLUsageCode', () => {
|
||||
it('generates HTML with video-player and video-skin for default video', () => {
|
||||
const result = generateHTMLUsageCode(baseHTML);
|
||||
expect(result.html).toContain('<video-player>');
|
||||
expect(result.html).toContain('<video-skin>');
|
||||
expect(result.html).toContain('<video src=');
|
||||
expect(result.html).toContain('playsinline');
|
||||
});
|
||||
|
||||
it('includes JS imports when not CDN', () => {
|
||||
const result = generateHTMLUsageCode(baseHTML);
|
||||
expect(result.js).toBeDefined();
|
||||
expect(result.js).toContain("import '@videojs/html/video/player'");
|
||||
expect(result.js).toContain("import '@videojs/html/video/skin'");
|
||||
});
|
||||
|
||||
it('omits JS imports when CDN', () => {
|
||||
const result = generateHTMLUsageCode({ ...baseHTML, installMethod: 'cdn' });
|
||||
expect(result.js).toBeUndefined();
|
||||
});
|
||||
|
||||
it('uses audio tags for audio use case', () => {
|
||||
const opts: InstallationOptions = {
|
||||
...baseHTML,
|
||||
useCase: 'default-audio',
|
||||
skin: 'audio',
|
||||
renderer: 'html5-audio',
|
||||
};
|
||||
const result = generateHTMLUsageCode(opts);
|
||||
expect(result.html).toContain('<audio-player>');
|
||||
expect(result.html).toContain('<audio-skin>');
|
||||
expect(result.html).toContain('<audio src=');
|
||||
expect(result.html).not.toContain('playsinline');
|
||||
});
|
||||
|
||||
it('uses background-video tags', () => {
|
||||
const opts: InstallationOptions = {
|
||||
...baseHTML,
|
||||
useCase: 'background-video',
|
||||
renderer: 'background-video',
|
||||
};
|
||||
const result = generateHTMLUsageCode(opts);
|
||||
expect(result.html).toContain('<background-video-player>');
|
||||
expect(result.html).toContain('<background-video-skin>');
|
||||
});
|
||||
|
||||
it('includes HLS media import in JS', () => {
|
||||
const result = generateHTMLUsageCode({ ...baseHTML, renderer: 'hls' });
|
||||
expect(result.js).toContain("import '@videojs/html/media/hls-video'");
|
||||
});
|
||||
|
||||
it('uses minimal skin tag', () => {
|
||||
const result = generateHTMLUsageCode({ ...baseHTML, skin: 'minimal-video' });
|
||||
expect(result.html).toContain('<video-minimal-skin>');
|
||||
expect(result.js).toContain("import '@videojs/html/video/minimal-skin'");
|
||||
});
|
||||
|
||||
it('uses custom source URL when provided', () => {
|
||||
const result = generateHTMLUsageCode({ ...baseHTML, sourceUrl: 'https://example.com/video.mp4' });
|
||||
expect(result.html).toContain('https://example.com/video.mp4');
|
||||
});
|
||||
|
||||
it('uses default demo URL when source URL is empty', () => {
|
||||
const result = generateHTMLUsageCode(baseHTML);
|
||||
expect(result.html).toContain('stream.mux.com');
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateReactCreateCode', () => {
|
||||
it('generates a React player component for default video', () => {
|
||||
const result = generateReactCreateCode(baseReact);
|
||||
const code = result['MyPlayer.tsx'];
|
||||
expect(code).toContain("'use client'");
|
||||
expect(code).toContain('createPlayer');
|
||||
expect(code).toContain('videoFeatures');
|
||||
expect(code).toContain('<VideoSkin>');
|
||||
expect(code).toContain('<Video src={src} playsInline />');
|
||||
expect(code).toContain("from '@videojs/react/video'");
|
||||
expect(code).toContain("import '@videojs/react/video/skin.css'");
|
||||
});
|
||||
|
||||
it('uses separate media import for HLS', () => {
|
||||
const result = generateReactCreateCode({ ...baseReact, renderer: 'hls' });
|
||||
const code = result['MyPlayer.tsx'];
|
||||
expect(code).toContain("import { VideoSkin } from '@videojs/react/video'");
|
||||
expect(code).toContain("import { HlsVideo } from '@videojs/react/media/hls-video'");
|
||||
expect(code).toContain('<HlsVideo src={src} playsInline />');
|
||||
});
|
||||
|
||||
it('uses audio features and components', () => {
|
||||
const opts: InstallationOptions = {
|
||||
...baseReact,
|
||||
useCase: 'default-audio',
|
||||
skin: 'audio',
|
||||
renderer: 'html5-audio',
|
||||
};
|
||||
const result = generateReactCreateCode(opts);
|
||||
const code = result['MyPlayer.tsx'];
|
||||
expect(code).toContain('audioFeatures');
|
||||
expect(code).toContain('<AudioSkin>');
|
||||
expect(code).toContain('<Audio src={src} />');
|
||||
expect(code).not.toContain('playsInline');
|
||||
});
|
||||
|
||||
it('uses minimal skin component', () => {
|
||||
const result = generateReactCreateCode({ ...baseReact, skin: 'minimal-video' });
|
||||
const code = result['MyPlayer.tsx'];
|
||||
expect(code).toContain('<MinimalVideoSkin>');
|
||||
expect(code).toContain("import '@videojs/react/video/minimal-skin.css'");
|
||||
});
|
||||
|
||||
it('uses background video components', () => {
|
||||
const opts: InstallationOptions = {
|
||||
...baseReact,
|
||||
useCase: 'background-video',
|
||||
renderer: 'background-video',
|
||||
};
|
||||
const result = generateReactCreateCode(opts);
|
||||
const code = result['MyPlayer.tsx'];
|
||||
expect(code).toContain('backgroundFeatures');
|
||||
expect(code).toContain('<BackgroundVideoSkin>');
|
||||
expect(code).toContain('<BackgroundVideo');
|
||||
expect(code).toContain("import '@videojs/react/background/skin.css'");
|
||||
});
|
||||
});
|
||||
|
||||
describe('generateReactUsageCode', () => {
|
||||
it('generates usage code with default URL', () => {
|
||||
const result = generateReactUsageCode(baseReact);
|
||||
const code = result['App.tsx'];
|
||||
expect(code).toContain("import { MyPlayer } from '../components/player'");
|
||||
expect(code).toContain('<MyPlayer src=');
|
||||
expect(code).toContain('stream.mux.com');
|
||||
});
|
||||
|
||||
it('uses HLS URL for HLS renderer', () => {
|
||||
const result = generateReactUsageCode({ ...baseReact, renderer: 'hls' });
|
||||
const code = result['App.tsx'];
|
||||
expect(code).toContain('.m3u8');
|
||||
});
|
||||
|
||||
it('uses custom source URL', () => {
|
||||
const result = generateReactUsageCode({ ...baseReact, sourceUrl: 'https://example.com/stream.m3u8' });
|
||||
const code = result['App.tsx'];
|
||||
expect(code).toContain('https://example.com/stream.m3u8');
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { Renderer, Skin, UseCase } from '@/stores/installation';
|
||||
import type { Renderer, Skin, UseCase } from '@/utils/installation/types';
|
||||
|
||||
const CDN_BASE = 'https://cdn.jsdelivr.net/npm/@videojs/html/cdn';
|
||||
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
import { VJS10_DEMO_VIDEO } from '@/consts';
|
||||
import { generateCdnCode } from '@/utils/installation/cdn-code';
|
||||
import type { InstallMethod, Renderer, Skin, UseCase } from '@/utils/installation/types';
|
||||
|
||||
export interface InstallationOptions {
|
||||
framework: 'html' | 'react';
|
||||
useCase: UseCase;
|
||||
skin: Skin;
|
||||
renderer: Renderer;
|
||||
sourceUrl: string;
|
||||
installMethod: InstallMethod;
|
||||
}
|
||||
|
||||
type ValidationResult = { valid: true } | { valid: false; reason: string };
|
||||
|
||||
export function validateInstallationOptions(opts: InstallationOptions): ValidationResult {
|
||||
if (opts.framework === 'react' && opts.installMethod === 'cdn') {
|
||||
return { valid: false, reason: 'CDN installation is not supported for React. Use npm, pnpm, yarn, or bun.' };
|
||||
}
|
||||
|
||||
return { valid: true };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Shared helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function getDefaultSourceUrl(renderer: Renderer): string {
|
||||
return renderer === 'hls' ? VJS10_DEMO_VIDEO.hls : VJS10_DEMO_VIDEO.mp4;
|
||||
}
|
||||
|
||||
function resolveSourceUrl(sourceUrl: string, renderer: Renderer): string {
|
||||
return sourceUrl.trim() || getDefaultSourceUrl(renderer);
|
||||
}
|
||||
|
||||
function isVideoLikeRenderer(renderer: Renderer): boolean {
|
||||
return renderer === 'html5-video' || renderer === 'hls' || renderer === 'background-video';
|
||||
}
|
||||
|
||||
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 getMediaImportSubpath(renderer: Renderer): string | null {
|
||||
const map: Partial<Record<Renderer, string>> = {
|
||||
hls: 'hls-video',
|
||||
};
|
||||
return map[renderer] ?? null;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HTML Install
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function generateHTMLInstallCode(
|
||||
opts: Pick<InstallationOptions, 'useCase' | 'skin' | 'renderer'>
|
||||
): Record<'cdn' | 'npm' | 'pnpm' | 'yarn' | 'bun', string> {
|
||||
return {
|
||||
cdn: generateCdnCode(opts.useCase, opts.skin, opts.renderer),
|
||||
npm: 'npm install @videojs/html',
|
||||
pnpm: 'pnpm add @videojs/html',
|
||||
yarn: 'yarn add @videojs/html',
|
||||
bun: 'bun add @videojs/html',
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// React Install
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function generateReactInstallCode(): Record<'npm' | 'pnpm' | 'yarn' | 'bun', string> {
|
||||
return {
|
||||
npm: 'npm install @videojs/react',
|
||||
pnpm: 'pnpm add @videojs/react',
|
||||
yarn: 'yarn add @videojs/react',
|
||||
bun: 'bun add @videojs/react',
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// HTML Usage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function getRendererTag(renderer: Renderer): string {
|
||||
const map: Record<Renderer, string> = {
|
||||
'background-video': 'background-video',
|
||||
hls: 'hls-video',
|
||||
'html5-audio': 'audio',
|
||||
'html5-video': 'video',
|
||||
};
|
||||
return map[renderer];
|
||||
}
|
||||
|
||||
function getProviderTag(useCase: UseCase): string {
|
||||
const map: Record<UseCase, string> = {
|
||||
'default-video': 'video-player',
|
||||
'default-audio': 'audio-player',
|
||||
'background-video': 'background-video-player',
|
||||
};
|
||||
return map[useCase];
|
||||
}
|
||||
|
||||
function getSkinTag(useCase: UseCase, skin: Skin): string {
|
||||
if (useCase === 'background-video') {
|
||||
return 'background-video-skin';
|
||||
}
|
||||
const map: Record<Skin, string> = {
|
||||
video: 'video-skin',
|
||||
audio: 'audio-skin',
|
||||
'minimal-video': 'video-minimal-skin',
|
||||
'minimal-audio': 'audio-minimal-skin',
|
||||
};
|
||||
return map[skin];
|
||||
}
|
||||
|
||||
function generateHTMLMarkup(useCase: UseCase, skin: Skin, renderer: Renderer, url: string): string {
|
||||
const providerTag = getProviderTag(useCase);
|
||||
const skinTag = getSkinTag(useCase, skin);
|
||||
const tag = getRendererTag(renderer);
|
||||
const src = resolveSourceUrl(url, renderer);
|
||||
const playsInline = isVideoLikeRenderer(renderer) ? ' playsinline' : '';
|
||||
|
||||
return `<!--
|
||||
The PlayerProvider passes state between the UI components
|
||||
and Media, and makes fully custom UIs possible.
|
||||
It does not have layout by default (display:contents)
|
||||
-->
|
||||
<${providerTag}>
|
||||
<!--
|
||||
Skins contain the entire player UI and are easily swappable.
|
||||
They can each be "ejected" for full control and customization
|
||||
of UI components.
|
||||
-->
|
||||
<${skinTag}>
|
||||
<!--
|
||||
Media are players without UIs, handling networking
|
||||
and display of the media. They are easily swappable
|
||||
to handle different sources.
|
||||
-->
|
||||
<${tag} src="${src}"${playsInline}></${tag}>
|
||||
</${skinTag}>
|
||||
</${providerTag}>`;
|
||||
}
|
||||
|
||||
function generateHTMLJSImports(useCase: UseCase, skin: Skin, renderer: Renderer): string {
|
||||
if (useCase === 'background-video') {
|
||||
const mediaSubpath = getMediaImportSubpath(renderer);
|
||||
const mediaImport = mediaSubpath ? `\nimport '@videojs/html/media/${mediaSubpath}';` : '';
|
||||
return `import '@videojs/html/background/player';
|
||||
import '@videojs/html/background/skin';
|
||||
import '@videojs/html/background/video';${mediaImport}`;
|
||||
}
|
||||
const { group, skinFile } = getSkinImportParts(skin);
|
||||
const mediaSubpath = getMediaImportSubpath(renderer);
|
||||
const mediaImport = mediaSubpath ? `\nimport '@videojs/html/media/${mediaSubpath}';` : '';
|
||||
return `import '@videojs/html/${group}/player';
|
||||
import '@videojs/html/${group}/${skinFile}';${mediaImport}`;
|
||||
}
|
||||
|
||||
export function generateHTMLUsageCode(
|
||||
opts: Pick<InstallationOptions, 'useCase' | 'skin' | 'renderer' | 'sourceUrl' | 'installMethod'>
|
||||
): { html: string; js?: string } {
|
||||
const html = generateHTMLMarkup(opts.useCase, opts.skin, opts.renderer, opts.sourceUrl);
|
||||
const js = opts.installMethod !== 'cdn' ? generateHTMLJSImports(opts.useCase, opts.skin, opts.renderer) : undefined;
|
||||
return { html, js };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// React Create
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function getRendererComponent(renderer: Renderer): string {
|
||||
const map: Record<Renderer, string> = {
|
||||
'background-video': 'BackgroundVideo',
|
||||
hls: 'HlsVideo',
|
||||
'html5-audio': 'Audio',
|
||||
'html5-video': 'Video',
|
||||
};
|
||||
return map[renderer];
|
||||
}
|
||||
|
||||
function getSkinComponent(skin: Skin): string {
|
||||
const map: Record<Skin, string> = {
|
||||
video: 'VideoSkin',
|
||||
audio: 'AudioSkin',
|
||||
'minimal-video': 'MinimalVideoSkin',
|
||||
'minimal-audio': 'MinimalAudioSkin',
|
||||
};
|
||||
return map[skin];
|
||||
}
|
||||
|
||||
function getUseCaseFeatures(useCase: UseCase): string {
|
||||
const map: Record<UseCase, string> = {
|
||||
'default-video': 'videoFeatures',
|
||||
'default-audio': 'audioFeatures',
|
||||
'background-video': 'backgroundFeatures',
|
||||
};
|
||||
return map[useCase];
|
||||
}
|
||||
|
||||
function isPresetRenderer(renderer: Renderer): boolean {
|
||||
return renderer === 'html5-video' || renderer === 'html5-audio' || renderer === 'background-video';
|
||||
}
|
||||
|
||||
function getRendererMediaSubpath(renderer: Renderer): string {
|
||||
const map: Partial<Record<Renderer, string>> = {
|
||||
hls: 'hls-video',
|
||||
};
|
||||
return map[renderer] ?? renderer;
|
||||
}
|
||||
|
||||
export function generateReactCreateCode(
|
||||
opts: Pick<InstallationOptions, 'useCase' | 'skin' | 'renderer'>
|
||||
): Record<'MyPlayer.tsx', string> {
|
||||
const { useCase, skin, renderer } = opts;
|
||||
const rendererComponent = getRendererComponent(renderer);
|
||||
const featureType = getUseCaseFeatures(useCase);
|
||||
|
||||
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 presetSubpath = isBackgroundVideo ? 'background' : group;
|
||||
|
||||
let presetImport: string;
|
||||
let mediaImport: string | null = null;
|
||||
|
||||
if (isPresetRenderer(renderer)) {
|
||||
presetImport = `import { ${skinComponent}, ${rendererComponent} } from '@videojs/react/${presetSubpath}';`;
|
||||
} else {
|
||||
presetImport = `import { ${skinComponent} } from '@videojs/react/${presetSubpath}';`;
|
||||
mediaImport = `import { ${rendererComponent} } from '@videojs/react/media/${getRendererMediaSubpath(renderer)}';`;
|
||||
}
|
||||
|
||||
const rendererProps = isVideoLikeRenderer(renderer) ? 'src={src} playsInline' : 'src={src}';
|
||||
const rendererJsx = `<${rendererComponent} ${rendererProps} />`;
|
||||
|
||||
const imports = [
|
||||
`import '${skinCssImport}';`,
|
||||
`import { createPlayer, ${featureType} } from '@videojs/react';`,
|
||||
presetImport,
|
||||
...(mediaImport ? [mediaImport] : []),
|
||||
].join('\n');
|
||||
|
||||
return {
|
||||
'MyPlayer.tsx': `'use client';
|
||||
|
||||
${imports}
|
||||
|
||||
const Player = createPlayer({ features: ${featureType} });
|
||||
|
||||
interface MyPlayerProps {
|
||||
src: string;
|
||||
}
|
||||
|
||||
export const MyPlayer = ({ src }: MyPlayerProps) => {
|
||||
return (
|
||||
<Player.Provider>
|
||||
<${skinComponent}>
|
||||
${rendererJsx}
|
||||
</${skinComponent}>
|
||||
</Player.Provider>
|
||||
);
|
||||
};`,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// React Usage
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export function generateReactUsageCode(
|
||||
opts: Pick<InstallationOptions, 'renderer' | 'sourceUrl'>
|
||||
): Record<'App.tsx', string> {
|
||||
const source = resolveSourceUrl(opts.sourceUrl, opts.renderer);
|
||||
|
||||
return {
|
||||
'App.tsx': `import { MyPlayer } from '../components/player';
|
||||
|
||||
export const HomePage = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Welcome to My App</h1>
|
||||
<MyPlayer src="${source}" />
|
||||
</div>
|
||||
);
|
||||
};`,
|
||||
};
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { Renderer, UseCase } from '@/stores/installation';
|
||||
import { VALID_RENDERERS } from '@/stores/installation';
|
||||
import type { Renderer, UseCase } from '@/utils/installation/types';
|
||||
import { VALID_RENDERERS } from '@/utils/installation/types';
|
||||
|
||||
export interface DetectionResult {
|
||||
renderer: Renderer;
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
export type Renderer = 'background-video' | 'hls' | 'html5-audio' | 'html5-video';
|
||||
|
||||
export type Skin = 'video' | 'audio' | 'minimal-video' | 'minimal-audio';
|
||||
|
||||
export type UseCase = 'default-video' | 'default-audio' | 'background-video';
|
||||
|
||||
export type InstallMethod = 'cdn' | 'npm' | 'pnpm' | 'yarn' | 'bun';
|
||||
|
||||
export const VALID_RENDERERS: Record<UseCase, Renderer[]> = {
|
||||
'default-video': ['html5-video', 'hls'],
|
||||
'default-audio': ['html5-audio'],
|
||||
'background-video': ['background-video'],
|
||||
};
|
||||
Reference in New Issue
Block a user