mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
fix(site): reset installation guide to implemented features (#707)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
4b282ff074
commit
2db6be75be
@@ -9,22 +9,25 @@ function getSkinImportParts(skin: Skin): { group: string; skinFile: string } {
|
||||
return { group: skin, skinFile: 'skin' };
|
||||
}
|
||||
|
||||
// jsdelivr doesn't resolve Node package exports, so we use the full file path
|
||||
const CDN_BASE = 'https://cdn.jsdelivr.net/npm/@videojs/html@next/dist/default/define';
|
||||
|
||||
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';
|
||||
import '${CDN_BASE}/background/player.js';
|
||||
import '${CDN_BASE}/background/skin.js';
|
||||
</script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/videojs/html/background/skin.css" />`;
|
||||
<link rel="stylesheet" href="${CDN_BASE}/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';
|
||||
import '${CDN_BASE}/${group}/player.js';
|
||||
import '${CDN_BASE}/${group}/${skinFile}.js';
|
||||
</script>
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/videojs/html/${group}/${skinFile}.css" />`;
|
||||
<link rel="stylesheet" href="${CDN_BASE}/${group}/${skinFile}.css" />`;
|
||||
}
|
||||
|
||||
export default function HTMLCdnCodeBlock() {
|
||||
|
||||
@@ -48,16 +48,16 @@ export default function HTMLInstallTabs() {
|
||||
<HTMLCdnCodeBlock />
|
||||
</TabsPanel>
|
||||
<TabsPanel value="npm">
|
||||
<ClientCode code="npm install @videojs/html" lang="bash" />
|
||||
<ClientCode code="npm install @videojs/html@next" lang="bash" />
|
||||
</TabsPanel>
|
||||
<TabsPanel value="pnpm">
|
||||
<ClientCode code="pnpm add @videojs/html" lang="bash" />
|
||||
<ClientCode code="pnpm add @videojs/html@next" lang="bash" />
|
||||
</TabsPanel>
|
||||
<TabsPanel value="yarn">
|
||||
<ClientCode code="yarn add @videojs/html" lang="bash" />
|
||||
<ClientCode code="yarn add @videojs/html@next" lang="bash" />
|
||||
</TabsPanel>
|
||||
<TabsPanel value="bun">
|
||||
<ClientCode code="bun add @videojs/html" lang="bash" />
|
||||
<ClientCode code="bun add @videojs/html@next" lang="bash" />
|
||||
</TabsPanel>
|
||||
</TabsRoot>
|
||||
</div>
|
||||
|
||||
@@ -2,25 +2,25 @@ 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 { installMethod, muxPlaybackId, renderer, skin, sourceUrl, 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',
|
||||
// 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',
|
||||
// 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',
|
||||
// spotify: 'spotify-audio',
|
||||
// vimeo: 'vimeo-video',
|
||||
// wistia: 'wistia-video',
|
||||
// youtube: 'youtube-video',
|
||||
};
|
||||
return map[renderer];
|
||||
}
|
||||
@@ -42,34 +42,22 @@ function getSkinTag(useCase: UseCase, skin: Skin): string {
|
||||
const map: Record<Skin, string> = {
|
||||
video: 'video-skin',
|
||||
audio: 'audio-skin',
|
||||
'minimal-video': 'minimal-video-skin',
|
||||
'minimal-audio': 'minimal-audio-skin',
|
||||
'minimal-video': 'video-minimal-skin',
|
||||
'minimal-audio': 'audio-minimal-skin',
|
||||
};
|
||||
return map[skin];
|
||||
}
|
||||
|
||||
function getRendererElement(renderer: Renderer, playbackId: string | null, url: string): string {
|
||||
function getRendererElement(renderer: Renderer, url: string): string {
|
||||
const tag = getRendererTag(renderer);
|
||||
|
||||
// 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}>`;
|
||||
}
|
||||
|
||||
const src = url.trim() || '...';
|
||||
return `<${tag} src="${src}"></${tag}>`;
|
||||
}
|
||||
|
||||
function generateHTMLCode(
|
||||
useCase: UseCase,
|
||||
skin: Skin,
|
||||
renderer: Renderer,
|
||||
playbackId: string | null,
|
||||
url: string
|
||||
): string {
|
||||
function generateHTMLCode(useCase: UseCase, skin: Skin, renderer: Renderer, url: string): string {
|
||||
const providerTag = getProviderTag(useCase);
|
||||
const skinTag = getSkinTag(useCase, skin);
|
||||
const rendererElement = getRendererElement(renderer, playbackId, url);
|
||||
const rendererElement = getRendererElement(renderer, url);
|
||||
|
||||
return `<!--
|
||||
The PlayerProvider passes state between the UI components
|
||||
@@ -99,24 +87,37 @@ function getSkinImportParts(skin: Skin): { group: string; skinFile: string } {
|
||||
return { group: skin, skinFile: 'skin' };
|
||||
}
|
||||
|
||||
function generateJS(useCase: UseCase, skin: Skin): string {
|
||||
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/skin.css';
|
||||
import '@videojs/html/background/video';`;
|
||||
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}';
|
||||
import '@videojs/html/${group}/${skinFile}.css';`;
|
||||
import '@videojs/html/${group}/${skinFile}.css';${mediaImport}`;
|
||||
}
|
||||
|
||||
export default function HTMLUsageCodeBlock() {
|
||||
const $useCase = useStore(useCase);
|
||||
const $skin = useStore(skin);
|
||||
const $renderer = useStore(renderer);
|
||||
const $muxPlaybackId = useStore(muxPlaybackId);
|
||||
const $installMethod = useStore(installMethod);
|
||||
const $sourceUrl = useStore(sourceUrl);
|
||||
|
||||
@@ -130,7 +131,7 @@ export default function HTMLUsageCodeBlock() {
|
||||
</Tab>
|
||||
</TabsList>
|
||||
<TabsPanel value="javascript" initial>
|
||||
<ClientCode code={generateJS($useCase, $skin)} lang="javascript" />
|
||||
<ClientCode code={generateJS($useCase, $skin, $renderer)} lang="javascript" />
|
||||
</TabsPanel>
|
||||
</TabsRoot>
|
||||
)}
|
||||
@@ -141,7 +142,7 @@ export default function HTMLUsageCodeBlock() {
|
||||
</Tab>
|
||||
</TabsList>
|
||||
<TabsPanel value="html" initial>
|
||||
<ClientCode code={generateHTMLCode($useCase, $skin, $renderer, $muxPlaybackId, $sourceUrl)} lang="html" />
|
||||
<ClientCode code={generateHTMLCode($useCase, $skin, $renderer, $sourceUrl)} lang="html" />
|
||||
</TabsPanel>
|
||||
</TabsRoot>
|
||||
</>
|
||||
|
||||
@@ -2,25 +2,25 @@ 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 { renderer, skin, useCase } from '@/stores/installation';
|
||||
|
||||
function getRendererComponent(renderer: Renderer): string {
|
||||
const map: Record<Renderer, string> = {
|
||||
'background-video': 'BackgroundVideo',
|
||||
cloudflare: 'CloudflareVideo',
|
||||
dash: 'DashVideo',
|
||||
// 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', this one probably adds more confusion than help at this level
|
||||
spotify: 'SpotifyAudio',
|
||||
vimeo: 'VimeoVideo',
|
||||
wistia: 'WistiaVideo',
|
||||
youtube: 'YoutubeVideo',
|
||||
// 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];
|
||||
}
|
||||
@@ -43,9 +43,9 @@ function getSkinImportParts(skin: Skin): { group: string; skinFile: string } {
|
||||
|
||||
function getUseCaseFeatures(useCase: UseCase): string {
|
||||
const map: Record<UseCase, string> = {
|
||||
'default-video': 'video',
|
||||
'default-audio': 'audio',
|
||||
'background-video': 'background',
|
||||
'default-video': 'videoFeatures',
|
||||
'default-audio': 'audioFeatures',
|
||||
'background-video': 'backgroundFeatures',
|
||||
};
|
||||
return map[useCase];
|
||||
}
|
||||
@@ -56,22 +56,22 @@ function isPresetRenderer(renderer: Renderer): boolean {
|
||||
|
||||
function getRendererMediaSubpath(renderer: Renderer): string {
|
||||
const map: Partial<Record<Renderer, string>> = {
|
||||
cloudflare: 'cloudflare-video',
|
||||
dash: 'dash-video',
|
||||
// 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',
|
||||
// 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, playbackId: string | null): string {
|
||||
function generateReactCode(useCase: UseCase, skin: Skin, renderer: Renderer): string {
|
||||
const rendererComponent = getRendererComponent(renderer);
|
||||
const featureType = getUseCaseFeatures(useCase);
|
||||
|
||||
@@ -97,27 +97,21 @@ function generateReactCode(useCase: UseCase, skin: Skin, renderer: Renderer, pla
|
||||
mediaImport = `import { ${rendererComponent} } from '@videojs/react/media/${getRendererMediaSubpath(renderer)}';`;
|
||||
}
|
||||
|
||||
// Determine props based on renderer + 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}';
|
||||
const destructuredProp = isMuxWithPlaybackId ? 'playbackId' : 'src';
|
||||
const rendererJsx = isMuxWithPlaybackId
|
||||
? `<${rendererComponent} playbackId={playbackId} />`
|
||||
: `<${rendererComponent} src={src} />`;
|
||||
// Determine props — mux variants use src with stream.mux.com URL
|
||||
const propsInterface = 'interface MyPlayerProps {\n src: string;\n}';
|
||||
const destructuredProp = 'src';
|
||||
const rendererJsx = `<${rendererComponent} src={src} />`;
|
||||
|
||||
const imports = [
|
||||
`import '${skinCssImport}';`,
|
||||
`import { createPlayer, features } from '@videojs/react';`,
|
||||
`import { createPlayer, ${featureType} } from '@videojs/react';`,
|
||||
presetImport,
|
||||
...(mediaImport ? [mediaImport] : []),
|
||||
].join('\n');
|
||||
|
||||
return `${imports}
|
||||
|
||||
const Player = createPlayer({ features: features.${featureType} });
|
||||
const Player = createPlayer({ features: ${featureType} });
|
||||
|
||||
${propsInterface}
|
||||
|
||||
@@ -136,7 +130,6 @@ export default function ReactCreateCodeBlock() {
|
||||
const $useCase = useStore(useCase);
|
||||
const $skin = useStore(skin);
|
||||
const $renderer = useStore(renderer);
|
||||
const $muxPlaybackId = useStore(muxPlaybackId);
|
||||
|
||||
return (
|
||||
<TabsRoot maxWidth={false}>
|
||||
@@ -146,7 +139,7 @@ export default function ReactCreateCodeBlock() {
|
||||
</Tab>
|
||||
</TabsList>
|
||||
<TabsPanel value="react" initial>
|
||||
<ClientCode code={generateReactCode($useCase, $skin, $renderer, $muxPlaybackId)} lang="tsx" />
|
||||
<ClientCode code={generateReactCode($useCase, $skin, $renderer)} lang="tsx" />
|
||||
</TabsPanel>
|
||||
</TabsRoot>
|
||||
);
|
||||
|
||||
@@ -1,21 +1,10 @@
|
||||
import { useStore } from '@nanostores/react';
|
||||
import ClientCode from '@/components/Code/ClientCode';
|
||||
import { Tab, TabsList, TabsPanel, TabsRoot } from '@/components/Tabs';
|
||||
import type { Renderer } from '@/stores/installation';
|
||||
import { muxPlaybackId, renderer, sourceUrl } from '@/stores/installation';
|
||||
import { renderer, sourceUrl } from '@/stores/installation';
|
||||
|
||||
function generateUsageCode(renderer: Renderer, playbackId: string | null, url: string): string {
|
||||
const isMuxWithPlaybackId =
|
||||
(renderer === 'mux-video' || renderer === 'mux-audio' || renderer === 'mux-background-video') && playbackId;
|
||||
|
||||
let playerProp: string;
|
||||
if (isMuxWithPlaybackId) {
|
||||
playerProp = `playbackId="${playbackId}"`;
|
||||
} else if (url.trim()) {
|
||||
playerProp = `src="${url.trim()}"`;
|
||||
} else {
|
||||
playerProp = 'src="https://example.com/video.mp4"';
|
||||
}
|
||||
function generateUsageCode(url: string): string {
|
||||
const playerProp = url.trim() ? `src="${url.trim()}"` : 'src="https://example.com/video.mp4"';
|
||||
|
||||
return `import { MyPlayer } from '../components/player';
|
||||
|
||||
@@ -30,8 +19,7 @@ export const HomePage = () => {
|
||||
}
|
||||
|
||||
export default function ReactUsageCodeBlock() {
|
||||
const $renderer = useStore(renderer);
|
||||
const $muxPlaybackId = useStore(muxPlaybackId);
|
||||
useStore(renderer);
|
||||
const $sourceUrl = useStore(sourceUrl);
|
||||
|
||||
return (
|
||||
@@ -42,7 +30,7 @@ export default function ReactUsageCodeBlock() {
|
||||
</Tab>
|
||||
</TabsList>
|
||||
<TabsPanel value="react" initial>
|
||||
<ClientCode code={generateUsageCode($renderer, $muxPlaybackId, $sourceUrl)} lang="tsx" />
|
||||
<ClientCode code={generateUsageCode($sourceUrl)} lang="tsx" />
|
||||
</TabsPanel>
|
||||
</TabsRoot>
|
||||
);
|
||||
|
||||
@@ -2,24 +2,24 @@ import { useStore } from '@nanostores/react';
|
||||
import { useEffect } from 'react';
|
||||
import { Select, type SelectOption } from '@/components/Select';
|
||||
import type { Renderer, UseCase } from '@/stores/installation';
|
||||
import { muxPlaybackId, renderer, sourceUrl, useCase, VALID_RENDERERS } from '@/stores/installation';
|
||||
import { articleFor, detectRenderer, extractMuxPlaybackId } from '@/utils/installation/detect-renderer';
|
||||
import { renderer, sourceUrl, useCase, VALID_RENDERERS } from '@/stores/installation';
|
||||
import { articleFor, detectRenderer } from '@/utils/installation/detect-renderer';
|
||||
|
||||
const RENDERER_LABELS: Record<Renderer, string> = {
|
||||
'background-video': 'Background Video',
|
||||
cloudflare: 'Cloudflare',
|
||||
dash: 'DASH',
|
||||
// cloudflare: 'Cloudflare',
|
||||
// dash: 'DASH',
|
||||
hls: 'HLS',
|
||||
'html5-audio': 'HTML5 Audio',
|
||||
'html5-video': 'HTML5 Video',
|
||||
jwplayer: 'JW Player',
|
||||
'mux-audio': 'Mux',
|
||||
'mux-background-video': 'Mux Background Video',
|
||||
'mux-video': 'Mux',
|
||||
spotify: 'Spotify',
|
||||
vimeo: 'Vimeo',
|
||||
wistia: 'Wistia',
|
||||
youtube: 'YouTube',
|
||||
// jwplayer: 'JW Player',
|
||||
// 'mux-audio': 'Mux',
|
||||
// 'mux-background-video': 'Mux Background Video',
|
||||
// 'mux-video': 'Mux',
|
||||
// spotify: 'Spotify',
|
||||
// vimeo: 'Vimeo',
|
||||
// wistia: 'Wistia',
|
||||
// youtube: 'YouTube',
|
||||
};
|
||||
|
||||
function buildOptions(useCase: UseCase): SelectOption<Renderer>[] {
|
||||
@@ -36,16 +36,15 @@ export default function RendererSelect() {
|
||||
|
||||
const options = buildOptions($useCase);
|
||||
const detection = detectRenderer($sourceUrl, $useCase);
|
||||
const detectedRenderer = detection?.renderer ?? null;
|
||||
|
||||
// Auto-select renderer when detection or use case changes
|
||||
// Auto-select renderer when the detected renderer or use case changes.
|
||||
// Uses the primitive `detectedRenderer` string instead of the `detection`
|
||||
// object to avoid re-firing on every render (new object reference each time),
|
||||
// which would override manual dropdown selection.
|
||||
useEffect(() => {
|
||||
if (detection) {
|
||||
renderer.set(detection.renderer);
|
||||
|
||||
const playbackId = extractMuxPlaybackId($sourceUrl);
|
||||
if (playbackId) {
|
||||
muxPlaybackId.set(playbackId);
|
||||
}
|
||||
if (detectedRenderer) {
|
||||
renderer.set(detectedRenderer);
|
||||
} else {
|
||||
// No valid detection — ensure current renderer is valid for use case
|
||||
const current = renderer.get();
|
||||
@@ -54,7 +53,7 @@ export default function RendererSelect() {
|
||||
renderer.set(validRenderers[0]!);
|
||||
}
|
||||
}
|
||||
}, [detection, $sourceUrl, $useCase]);
|
||||
}, [detectedRenderer, $useCase]);
|
||||
|
||||
const showDetectionMatch = $sourceUrl.trim() && detection && detection.renderer === $renderer;
|
||||
const showDetectionSuggestion = $sourceUrl.trim() && detection && detection.renderer !== $renderer;
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
---
|
||||
title: 'Under Construction'
|
||||
description: 'Video.js 10 docs are coming soon!'
|
||||
---
|
||||
|
||||
Please pardon our dust as we build this all out.
|
||||
@@ -18,29 +18,18 @@ Video.js v10 is a complete rebuild from the ground up, designed to be more modul
|
||||
- Reserved only for urgent API-breaking changes that cannot be avoided
|
||||
- Acts as a stopgap between v8 and the new v10 rebuild
|
||||
|
||||
## October 30, 2025: Technical preview
|
||||
|
||||
|
||||
**First public look** at Video.js v10, rebuilt from the ground up.
|
||||
|
||||
**Goal:** Get feedback from early adopters and plugin authors.
|
||||
|
||||
**What to expect:**
|
||||
- Limited feature set
|
||||
- Not production-ready
|
||||
- Existing plugins and integrations will not work with v10
|
||||
- Developers encouraged to check it out and give feedback
|
||||
|
||||
## February 2026: v10 Beta Release
|
||||
## March 2026: v10 Beta Release
|
||||
|
||||
Wider testing release to the community.
|
||||
|
||||
**Core feature parity target:**
|
||||
|
||||
- Basic control set (play, pause, seek, volume, fullscreen)
|
||||
- Adaptive streaming support via existing community engines
|
||||
- No plugin migrations yet, without plugin author contribution
|
||||
|
||||
**What to expect:**
|
||||
|
||||
- Close to stable, but API changes are still possible
|
||||
- Intended for experimental adoption in real-world projects
|
||||
- Developers encouraged to try it out and report issues
|
||||
@@ -50,18 +39,21 @@ Wider testing release to the community.
|
||||
**Stable release** of Video.js v10
|
||||
|
||||
**Feature targets:**
|
||||
|
||||
- Core feature parity with v8 in terms of fundamentals
|
||||
- Feature parity with Media Chrome, Vidstack, Plyr, and Mux Player
|
||||
- Community encouraged to begin full reintegration projects
|
||||
- Plugin ecosystem migration begins in earnest
|
||||
|
||||
**Engine development focus:**
|
||||
|
||||
- Lightweight adaptive streaming
|
||||
- MoQ (Media over QUIC) support
|
||||
|
||||
## Late-2026: Feature parity with v8 Contrib
|
||||
|
||||
**Extended feature parity** with Video.js "contrib" plugins:
|
||||
|
||||
- Ads integration
|
||||
- Analytics support
|
||||
- Further community plugin migration dependent on contributions
|
||||
@@ -69,10 +61,12 @@ Wider testing release to the community.
|
||||
## End-of-life plan for Video.js v8
|
||||
|
||||
### Mid-2026
|
||||
|
||||
- v8/v9 moved to "video.js-legacy" repository and package
|
||||
- Hosted CDN copies will remain available
|
||||
|
||||
### 2027 and beyond
|
||||
|
||||
- Security patches only
|
||||
- Community contributions de-prioritized
|
||||
|
||||
|
||||
@@ -74,22 +74,22 @@ Video.js supports a wide range of file types and hosting services. It's easy to
|
||||
</TabsList>
|
||||
<TabsPanel client:idle value="npm" initial>
|
||||
```bash
|
||||
npm install @videojs/react
|
||||
npm install @videojs/react@next
|
||||
```
|
||||
</TabsPanel>
|
||||
<TabsPanel client:idle value="pnpm">
|
||||
```bash
|
||||
pnpm add @videojs/react
|
||||
pnpm add @videojs/react@next
|
||||
```
|
||||
</TabsPanel>
|
||||
<TabsPanel client:idle value="yarn">
|
||||
```bash
|
||||
yarn add @videojs/react
|
||||
yarn add @videojs/react@next
|
||||
```
|
||||
</TabsPanel>
|
||||
<TabsPanel client:idle value="bun">
|
||||
```bash
|
||||
bun add @videojs/react
|
||||
bun add @videojs/react@next
|
||||
```
|
||||
</TabsPanel>
|
||||
</TabsRoot>
|
||||
|
||||
+12
-8
@@ -18,14 +18,18 @@ export const sidebar: Sidebar = [
|
||||
{ slug: 'concepts/v10-roadmap', sidebarLabel: 'Roadmap' },
|
||||
],
|
||||
},
|
||||
{
|
||||
sidebarLabel: 'Concepts',
|
||||
contents: [{ slug: 'concepts/architecture' }, { slug: 'concepts/skins' }, { slug: 'concepts/ui-components' }],
|
||||
},
|
||||
{
|
||||
sidebarLabel: 'How to',
|
||||
contents: [{ slug: 'how-to/customize-skins' }],
|
||||
},
|
||||
// {
|
||||
// sidebarLabel: 'Concepts',
|
||||
// contents: [
|
||||
// { slug: 'concepts/architecture' },
|
||||
// { slug: 'concepts/skins' },
|
||||
// { slug: 'concepts/ui-components' }
|
||||
// ],
|
||||
// },
|
||||
// {
|
||||
// sidebarLabel: 'How to',
|
||||
// contents: [{ slug: 'how-to/customize-skins' }],
|
||||
// },
|
||||
{
|
||||
sidebarLabel: 'Components',
|
||||
contents: [
|
||||
|
||||
@@ -2,20 +2,20 @@ import { atom } from 'nanostores';
|
||||
|
||||
export type Renderer =
|
||||
| 'background-video'
|
||||
| 'cloudflare'
|
||||
| 'dash'
|
||||
// | 'cloudflare'
|
||||
// | 'dash'
|
||||
| 'hls'
|
||||
| 'html5-audio'
|
||||
| 'html5-video'
|
||||
| 'jwplayer'
|
||||
| 'mux-audio'
|
||||
| 'mux-background-video'
|
||||
| 'mux-video'
|
||||
// | 'shaka'
|
||||
| 'spotify'
|
||||
| 'vimeo'
|
||||
| 'wistia'
|
||||
| 'youtube';
|
||||
| 'html5-video';
|
||||
// | 'jwplayer'
|
||||
// | 'mux-audio'
|
||||
// | 'mux-background-video'
|
||||
// | 'mux-video'
|
||||
// | 'shaka'
|
||||
// | 'spotify'
|
||||
// | 'vimeo'
|
||||
// | 'wistia'
|
||||
// | 'youtube'
|
||||
|
||||
export type Skin = 'video' | 'audio' | 'minimal-video' | 'minimal-audio';
|
||||
|
||||
@@ -34,7 +34,10 @@ export const installMethod = atom<InstallMethod>('cdn');
|
||||
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'],
|
||||
'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' */],
|
||||
};
|
||||
|
||||
@@ -1,124 +1,16 @@
|
||||
import { describe, expect, it } from 'vitest';
|
||||
import { articleFor, detectRenderer, extractMuxPlaybackId, isRendererValidForUseCase } from '../detect-renderer';
|
||||
import { articleFor, detectRenderer, isRendererValidForUseCase } from '../detect-renderer';
|
||||
|
||||
describe('detectRenderer', () => {
|
||||
describe('domain rules', () => {
|
||||
it('detects youtube.com', () => {
|
||||
expect(detectRenderer('https://www.youtube.com/watch?v=abc123', 'default-video')).toEqual({
|
||||
renderer: 'youtube',
|
||||
label: 'YouTube',
|
||||
});
|
||||
});
|
||||
// Domain-specific rules (YouTube, Vimeo, Mux, Spotify, Cloudflare, JW Player, Wistia)
|
||||
// are commented out until their renderer elements are implemented.
|
||||
// Mux stream.mux.com URLs with .m3u8 extension are detected as HLS via extension rules.
|
||||
|
||||
it('detects youtu.be', () => {
|
||||
expect(detectRenderer('https://youtu.be/abc123', 'default-video')).toEqual({
|
||||
renderer: 'youtube',
|
||||
label: 'YouTube',
|
||||
});
|
||||
});
|
||||
|
||||
it('detects m.youtube.com', () => {
|
||||
expect(detectRenderer('https://m.youtube.com/watch?v=abc123', 'default-video')).toEqual({
|
||||
renderer: 'youtube',
|
||||
label: 'YouTube',
|
||||
});
|
||||
});
|
||||
|
||||
it('detects vimeo.com', () => {
|
||||
expect(detectRenderer('https://vimeo.com/123456', 'default-video')).toEqual({
|
||||
renderer: 'vimeo',
|
||||
label: 'Vimeo',
|
||||
});
|
||||
});
|
||||
|
||||
it('detects player.vimeo.com', () => {
|
||||
expect(detectRenderer('https://player.vimeo.com/video/123456', 'default-video')).toEqual({
|
||||
renderer: 'vimeo',
|
||||
label: 'Vimeo',
|
||||
});
|
||||
});
|
||||
|
||||
it('detects stream.mux.com for default-video', () => {
|
||||
it('detects stream.mux.com .m3u8 as HLS', () => {
|
||||
expect(detectRenderer('https://stream.mux.com/abc123.m3u8', 'default-video')).toEqual({
|
||||
renderer: 'mux-video',
|
||||
label: 'Mux',
|
||||
});
|
||||
});
|
||||
|
||||
it('detects stream.mux.com for default-audio', () => {
|
||||
expect(detectRenderer('https://stream.mux.com/abc123.m3u8', 'default-audio')).toEqual({
|
||||
renderer: 'mux-audio',
|
||||
label: 'Mux',
|
||||
});
|
||||
});
|
||||
|
||||
it('detects stream.mux.com for background-video', () => {
|
||||
expect(detectRenderer('https://stream.mux.com/abc123.m3u8', 'background-video')).toEqual({
|
||||
renderer: 'mux-background-video',
|
||||
label: 'Mux',
|
||||
});
|
||||
});
|
||||
|
||||
it('detects open.spotify.com', () => {
|
||||
expect(detectRenderer('https://open.spotify.com/track/abc123', 'default-audio')).toEqual({
|
||||
renderer: 'spotify',
|
||||
label: 'Spotify',
|
||||
});
|
||||
});
|
||||
|
||||
it('detects watch.videodelivery.net', () => {
|
||||
expect(detectRenderer('https://watch.videodelivery.net/abc123', 'default-video')).toEqual({
|
||||
renderer: 'cloudflare',
|
||||
label: 'Cloudflare',
|
||||
});
|
||||
});
|
||||
|
||||
it('detects videodelivery.net', () => {
|
||||
expect(detectRenderer('https://videodelivery.net/abc123', 'default-video')).toEqual({
|
||||
renderer: 'cloudflare',
|
||||
label: 'Cloudflare',
|
||||
});
|
||||
});
|
||||
|
||||
it('detects cloudflarestream.com', () => {
|
||||
expect(detectRenderer('https://cloudflarestream.com/abc123/manifest/video.m3u8', 'default-video')).toEqual({
|
||||
renderer: 'cloudflare',
|
||||
label: 'Cloudflare',
|
||||
});
|
||||
});
|
||||
|
||||
it('detects cdn.jwplayer.com', () => {
|
||||
expect(detectRenderer('https://cdn.jwplayer.com/players/abc123.html', 'default-video')).toEqual({
|
||||
renderer: 'jwplayer',
|
||||
label: 'JW Player',
|
||||
});
|
||||
});
|
||||
|
||||
it('detects content.jwplatform.com', () => {
|
||||
expect(detectRenderer('https://content.jwplatform.com/videos/abc123.mp4', 'default-video')).toEqual({
|
||||
renderer: 'jwplayer',
|
||||
label: 'JW Player',
|
||||
});
|
||||
});
|
||||
|
||||
it('detects fast.wistia.com', () => {
|
||||
expect(detectRenderer('https://fast.wistia.com/medias/abc123', 'default-video')).toEqual({
|
||||
renderer: 'wistia',
|
||||
label: 'Wistia',
|
||||
});
|
||||
});
|
||||
|
||||
it('detects fast.wistia.net', () => {
|
||||
expect(detectRenderer('https://fast.wistia.net/medias/abc123', 'default-video')).toEqual({
|
||||
renderer: 'wistia',
|
||||
label: 'Wistia',
|
||||
});
|
||||
});
|
||||
|
||||
it('detects *.wistia.com', () => {
|
||||
expect(detectRenderer('https://mycompany.wistia.com/medias/abc123', 'default-video')).toEqual({
|
||||
renderer: 'wistia',
|
||||
label: 'Wistia',
|
||||
renderer: 'hls',
|
||||
label: 'HLS',
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -131,12 +23,7 @@ describe('detectRenderer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('detects .mpd as DASH', () => {
|
||||
expect(detectRenderer('https://example.com/video.mpd', 'default-video')).toEqual({
|
||||
renderer: 'dash',
|
||||
label: 'DASH',
|
||||
});
|
||||
});
|
||||
// .mpd detection commented out until DASH renderer is implemented
|
||||
|
||||
it('detects .mp4 as HTML5 Video', () => {
|
||||
expect(detectRenderer('https://example.com/video.mp4', 'default-video')).toEqual({
|
||||
@@ -209,30 +96,7 @@ describe('detectRenderer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('domain priority over extension', () => {
|
||||
it('stream.mux.com with .m3u8 detects as Mux, not HLS', () => {
|
||||
expect(detectRenderer('https://stream.mux.com/abc123.m3u8', 'default-video')).toEqual({
|
||||
renderer: 'mux-video',
|
||||
label: 'Mux',
|
||||
});
|
||||
});
|
||||
|
||||
it('content.jwplatform.com with .mp4 detects as JW Player, not HTML5 Video', () => {
|
||||
expect(detectRenderer('https://content.jwplatform.com/videos/abc.mp4', 'default-video')).toEqual({
|
||||
renderer: 'jwplayer',
|
||||
label: 'JW Player',
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('URL without protocol', () => {
|
||||
it('auto-prepends https://', () => {
|
||||
expect(detectRenderer('youtube.com/watch?v=abc', 'default-video')).toEqual({
|
||||
renderer: 'youtube',
|
||||
label: 'YouTube',
|
||||
});
|
||||
});
|
||||
|
||||
it('auto-prepends https:// for extension-based detection', () => {
|
||||
expect(detectRenderer('example.com/video.mp4', 'default-video')).toEqual({
|
||||
renderer: 'html5-video',
|
||||
@@ -260,18 +124,6 @@ describe('detectRenderer', () => {
|
||||
});
|
||||
|
||||
describe('use-case filtering', () => {
|
||||
it('returns null for YouTube with audio use case', () => {
|
||||
expect(detectRenderer('https://www.youtube.com/watch?v=abc', 'default-audio')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for YouTube with background-video use case', () => {
|
||||
expect(detectRenderer('https://www.youtube.com/watch?v=abc', 'background-video')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for Spotify with default-video use case', () => {
|
||||
expect(detectRenderer('https://open.spotify.com/track/abc', 'default-video')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for .mp3 with default-video use case', () => {
|
||||
expect(detectRenderer('https://example.com/audio.mp3', 'default-video')).toBeNull();
|
||||
});
|
||||
@@ -282,36 +134,6 @@ describe('detectRenderer', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('extractMuxPlaybackId', () => {
|
||||
it('extracts playback ID from stream.mux.com URL with .m3u8', () => {
|
||||
expect(extractMuxPlaybackId('https://stream.mux.com/abc123.m3u8')).toBe('abc123');
|
||||
});
|
||||
|
||||
it('extracts playback ID from stream.mux.com URL without extension', () => {
|
||||
expect(extractMuxPlaybackId('https://stream.mux.com/abc123')).toBe('abc123');
|
||||
});
|
||||
|
||||
it('returns null for non-Mux URL', () => {
|
||||
expect(extractMuxPlaybackId('https://example.com/abc123.m3u8')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for mux.com (not stream.mux.com)', () => {
|
||||
expect(extractMuxPlaybackId('https://mux.com/abc123')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for empty URL', () => {
|
||||
expect(extractMuxPlaybackId('')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for invalid URL', () => {
|
||||
expect(extractMuxPlaybackId('not a url')).toBeNull();
|
||||
});
|
||||
|
||||
it('returns null for stream.mux.com with no path segment', () => {
|
||||
expect(extractMuxPlaybackId('https://stream.mux.com/')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('articleFor', () => {
|
||||
it('returns "an" for hls', () => {
|
||||
expect(articleFor('hls')).toBe('an');
|
||||
@@ -324,26 +146,6 @@ describe('articleFor', () => {
|
||||
it('returns "an" for html5-audio', () => {
|
||||
expect(articleFor('html5-audio')).toBe('an');
|
||||
});
|
||||
|
||||
it('returns "a" for youtube', () => {
|
||||
expect(articleFor('youtube')).toBe('a');
|
||||
});
|
||||
|
||||
it('returns "a" for vimeo', () => {
|
||||
expect(articleFor('vimeo')).toBe('a');
|
||||
});
|
||||
|
||||
it('returns "a" for mux-video', () => {
|
||||
expect(articleFor('mux-video')).toBe('a');
|
||||
});
|
||||
|
||||
it('returns "a" for dash', () => {
|
||||
expect(articleFor('dash')).toBe('a');
|
||||
});
|
||||
|
||||
it('returns "a" for jwplayer', () => {
|
||||
expect(articleFor('jwplayer')).toBe('a');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isRendererValidForUseCase', () => {
|
||||
@@ -351,22 +153,10 @@ describe('isRendererValidForUseCase', () => {
|
||||
expect(isRendererValidForUseCase('html5-video', 'default-video')).toBe(true);
|
||||
});
|
||||
|
||||
it('youtube is valid for default-video', () => {
|
||||
expect(isRendererValidForUseCase('youtube', 'default-video')).toBe(true);
|
||||
});
|
||||
|
||||
it('youtube is not valid for default-audio', () => {
|
||||
expect(isRendererValidForUseCase('youtube', 'default-audio')).toBe(false);
|
||||
});
|
||||
|
||||
it('html5-audio is valid for default-audio', () => {
|
||||
expect(isRendererValidForUseCase('html5-audio', 'default-audio')).toBe(true);
|
||||
});
|
||||
|
||||
it('spotify is valid for default-audio', () => {
|
||||
expect(isRendererValidForUseCase('spotify', 'default-audio')).toBe(true);
|
||||
});
|
||||
|
||||
it('background-video is valid for background-video', () => {
|
||||
expect(isRendererValidForUseCase('background-video', 'background-video')).toBe(true);
|
||||
});
|
||||
|
||||
@@ -7,45 +7,45 @@ export interface DetectionResult {
|
||||
}
|
||||
|
||||
const DOMAIN_RULES: Array<{ match: (hostname: string) => boolean; renderer: Renderer; label: string }> = [
|
||||
{
|
||||
match: (h) => h === 'youtube.com' || h === 'www.youtube.com' || h === 'youtu.be' || h === 'm.youtube.com',
|
||||
renderer: 'youtube',
|
||||
label: 'YouTube',
|
||||
},
|
||||
{
|
||||
match: (h) => h === 'vimeo.com' || h === 'www.vimeo.com' || h === 'player.vimeo.com',
|
||||
renderer: 'vimeo',
|
||||
label: 'Vimeo',
|
||||
},
|
||||
{
|
||||
match: (h) => h === 'stream.mux.com' || h === 'mux.com' || h === 'www.mux.com',
|
||||
renderer: 'mux-video',
|
||||
label: 'Mux',
|
||||
},
|
||||
{
|
||||
match: (h) => h === 'open.spotify.com',
|
||||
renderer: 'spotify',
|
||||
label: 'Spotify',
|
||||
},
|
||||
{
|
||||
match: (h) =>
|
||||
h === 'watch.videodelivery.net' ||
|
||||
h === 'videodelivery.net' ||
|
||||
h === 'cloudflarestream.com' ||
|
||||
h === 'www.cloudflarestream.com',
|
||||
renderer: 'cloudflare',
|
||||
label: 'Cloudflare',
|
||||
},
|
||||
{
|
||||
match: (h) => h === 'cdn.jwplayer.com' || h === 'content.jwplatform.com',
|
||||
renderer: 'jwplayer',
|
||||
label: 'JW Player',
|
||||
},
|
||||
{
|
||||
match: (h) => h === 'fast.wistia.com' || h === 'fast.wistia.net' || h.endsWith('.wistia.com'),
|
||||
renderer: 'wistia',
|
||||
label: 'Wistia',
|
||||
},
|
||||
// {
|
||||
// match: (h) => h === 'youtube.com' || h === 'www.youtube.com' || h === 'youtu.be' || h === 'm.youtube.com',
|
||||
// renderer: 'youtube',
|
||||
// label: 'YouTube',
|
||||
// },
|
||||
// {
|
||||
// match: (h) => h === 'vimeo.com' || h === 'www.vimeo.com' || h === 'player.vimeo.com',
|
||||
// renderer: 'vimeo',
|
||||
// label: 'Vimeo',
|
||||
// },
|
||||
// {
|
||||
// match: (h) => h === 'stream.mux.com' || h === 'mux.com' || h === 'www.mux.com',
|
||||
// renderer: 'mux-video',
|
||||
// label: 'Mux',
|
||||
// },
|
||||
// {
|
||||
// match: (h) => h === 'open.spotify.com',
|
||||
// renderer: 'spotify',
|
||||
// label: 'Spotify',
|
||||
// },
|
||||
// {
|
||||
// match: (h) =>
|
||||
// h === 'watch.videodelivery.net' ||
|
||||
// h === 'videodelivery.net' ||
|
||||
// h === 'cloudflarestream.com' ||
|
||||
// h === 'www.cloudflarestream.com',
|
||||
// renderer: 'cloudflare',
|
||||
// label: 'Cloudflare',
|
||||
// },
|
||||
// {
|
||||
// match: (h) => h === 'cdn.jwplayer.com' || h === 'content.jwplatform.com',
|
||||
// renderer: 'jwplayer',
|
||||
// label: 'JW Player',
|
||||
// },
|
||||
// {
|
||||
// match: (h) => h === 'fast.wistia.com' || h === 'fast.wistia.net' || h.endsWith('.wistia.com'),
|
||||
// renderer: 'wistia',
|
||||
// label: 'Wistia',
|
||||
// },
|
||||
];
|
||||
|
||||
const VIDEO_EXTENSIONS = new Set(['.mp4', '.webm', '.mov', '.ogv']);
|
||||
@@ -70,13 +70,6 @@ function getExtension(pathname: string): string {
|
||||
return clean.slice(dot).toLowerCase();
|
||||
}
|
||||
|
||||
function resolveRendererForUseCase(renderer: Renderer, useCase: UseCase): Renderer {
|
||||
if (renderer !== 'mux-video') return renderer;
|
||||
if (useCase === 'default-audio') return 'mux-audio';
|
||||
if (useCase === 'background-video') return 'mux-background-video';
|
||||
return 'mux-video';
|
||||
}
|
||||
|
||||
export function detectRenderer(url: string, useCase: UseCase): DetectionResult | null {
|
||||
const trimmed = url.trim();
|
||||
if (!trimmed) return null;
|
||||
@@ -87,9 +80,8 @@ export function detectRenderer(url: string, useCase: UseCase): DetectionResult |
|
||||
// Check domain rules first
|
||||
for (const rule of DOMAIN_RULES) {
|
||||
if (rule.match(parsed.hostname)) {
|
||||
const resolved = resolveRendererForUseCase(rule.renderer, useCase);
|
||||
if (!isRendererValidForUseCase(resolved, useCase)) return null;
|
||||
return { renderer: resolved, label: rule.label };
|
||||
if (!isRendererValidForUseCase(rule.renderer, useCase)) return null;
|
||||
return { renderer: rule.renderer, label: rule.label };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,10 +93,10 @@ export function detectRenderer(url: string, useCase: UseCase): DetectionResult |
|
||||
return { renderer: 'hls', label: 'HLS' };
|
||||
}
|
||||
|
||||
if (ext === '.mpd') {
|
||||
if (!isRendererValidForUseCase('dash', useCase)) return null;
|
||||
return { renderer: 'dash', label: 'DASH' };
|
||||
}
|
||||
// if (ext === '.mpd') {
|
||||
// if (!isRendererValidForUseCase('dash', useCase)) return null;
|
||||
// return { renderer: 'dash', label: 'DASH' };
|
||||
// }
|
||||
|
||||
if (VIDEO_EXTENSIONS.has(ext)) {
|
||||
if (!isRendererValidForUseCase('html5-video', useCase)) return null;
|
||||
@@ -119,38 +111,25 @@ export function detectRenderer(url: string, useCase: UseCase): DetectionResult |
|
||||
return null;
|
||||
}
|
||||
|
||||
export function extractMuxPlaybackId(url: string): string | null {
|
||||
const parsed = parseUrl(url);
|
||||
if (!parsed) return null;
|
||||
|
||||
if (parsed.hostname !== 'stream.mux.com') return null;
|
||||
|
||||
// pathname is /{PLAYBACK_ID} or /{PLAYBACK_ID}.m3u8
|
||||
const segment = parsed.pathname.slice(1); // remove leading /
|
||||
if (!segment) return null;
|
||||
|
||||
return segment.replace(/\.m3u8$/, '') || null;
|
||||
}
|
||||
|
||||
export function isRendererValidForUseCase(renderer: Renderer, useCase: UseCase): boolean {
|
||||
return VALID_RENDERERS[useCase].includes(renderer);
|
||||
}
|
||||
|
||||
const RENDERER_ARTICLES: Record<Renderer, 'a' | 'an'> = {
|
||||
'background-video': 'a',
|
||||
cloudflare: 'a',
|
||||
dash: 'a',
|
||||
// cloudflare: 'a',
|
||||
// dash: 'a',
|
||||
hls: 'an',
|
||||
'html5-audio': 'an',
|
||||
'html5-video': 'an',
|
||||
jwplayer: 'a',
|
||||
'mux-audio': 'a',
|
||||
'mux-background-video': 'a',
|
||||
'mux-video': 'a',
|
||||
spotify: 'a',
|
||||
vimeo: 'a',
|
||||
wistia: 'a',
|
||||
youtube: 'a',
|
||||
// jwplayer: 'a',
|
||||
// 'mux-audio': 'a',
|
||||
// 'mux-background-video': 'a',
|
||||
// 'mux-video': 'a',
|
||||
// spotify: 'a',
|
||||
// vimeo: 'a',
|
||||
// wistia: 'a',
|
||||
// youtube: 'a',
|
||||
};
|
||||
|
||||
export function articleFor(renderer: Renderer): 'a' | 'an' {
|
||||
|
||||
Reference in New Issue
Block a user