mirror of
https://github.com/zoriya/v10.git
synced 2026-08-16 02:45:09 +00:00
fix(html): extended media not working over cdn (#1019)
This commit is contained in:
@@ -13,7 +13,8 @@
|
||||
"module": "dist/default/index.js",
|
||||
"types": "dist/dev/index.d.ts",
|
||||
"sideEffects": [
|
||||
"./dist/*/define/**/*.js"
|
||||
"./dist/*/define/**/*.js",
|
||||
"./cdn/**/*.js"
|
||||
],
|
||||
"files": [
|
||||
"dist",
|
||||
@@ -73,6 +74,16 @@
|
||||
"development": "./dist/dev/define/media/*.js",
|
||||
"default": "./dist/default/define/media/*.js"
|
||||
},
|
||||
"./cdn/media/*": {
|
||||
"types": "./cdn/media/*.dev.d.ts",
|
||||
"development": "./cdn/media/*.dev.js",
|
||||
"default": "./cdn/media/*.js"
|
||||
},
|
||||
"./cdn/*": {
|
||||
"types": "./cdn/*.dev.d.ts",
|
||||
"development": "./cdn/*.dev.js",
|
||||
"default": "./cdn/*.js"
|
||||
},
|
||||
"./*.css": "./dist/default/define/*.css",
|
||||
"./*": {
|
||||
"types": "./dist/dev/define/*.d.ts",
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
import '../define/background/player';
|
||||
import '../define/background/skin';
|
||||
import '../define/background/video';
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { readdirSync, writeFileSync } from 'node:fs';
|
||||
import { dirname, resolve } from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import type { UserConfig } from 'tsdown';
|
||||
@@ -12,7 +13,7 @@ const skinsDir = resolve(dirname(fileURLToPath(import.meta.url)), '../skins/src'
|
||||
const buildModes: BuildMode[] = ['dev', 'prod'];
|
||||
|
||||
const presets = ['video', 'video-minimal', 'audio', 'audio-minimal', 'background'];
|
||||
const media = ['hls-video', 'simple-hls-video'];
|
||||
const media = ['hls-video', 'simple-hls-video', 'dash-video'];
|
||||
|
||||
const entries = [
|
||||
...presets.map((name) => ({ src: `src/cdn/${name}.ts`, name })),
|
||||
@@ -20,50 +21,78 @@ const entries = [
|
||||
];
|
||||
|
||||
/**
|
||||
* One config per entry per mode → each output is fully self-contained.
|
||||
* Multiple entries in a single config causes rolldown to code-split shared
|
||||
* modules into chunks, which breaks the single-file CDN bundle requirement.
|
||||
* Rolldown plugin that generates empty `.d.ts` stubs for dev CDN entry points.
|
||||
* CDN entries are side-effect-only modules with no exports — the stubs let
|
||||
* TypeScript resolve `import '@videojs/html/cdn/...'` without errors.
|
||||
*/
|
||||
function dtsStubsPlugin(outDir: string) {
|
||||
function generate(dir: string) {
|
||||
for (const file of readdirSync(dir, { withFileTypes: true })) {
|
||||
if (file.isDirectory()) {
|
||||
generate(resolve(dir, file.name));
|
||||
} else if (file.name.endsWith('.dev.js') && !file.name.endsWith('.dev.js.map')) {
|
||||
writeFileSync(resolve(dir, file.name.replace('.dev.js', '.dev.d.ts')), 'export {};\n');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'cdn-dts-stubs',
|
||||
writeBundle() {
|
||||
generate(outDir);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* One config per mode with all entries grouped together.
|
||||
* This lets rolldown extract shared modules (store, element, core, hls.js, etc.)
|
||||
* into shared chunks instead of duplicating them across every bundle.
|
||||
* The ES module loader handles chunk deduplication transparently.
|
||||
*/
|
||||
const configs: UserConfig[] = [];
|
||||
|
||||
const outDir = 'cdn';
|
||||
|
||||
for (const mode of buildModes) {
|
||||
for (const { src, name } of entries) {
|
||||
const outName = mode === 'dev' ? `${name}.dev` : name;
|
||||
const isProd = mode === 'prod';
|
||||
|
||||
const isProd = mode === 'prod';
|
||||
const entryMap = Object.fromEntries(entries.map(({ src, name }) => [isProd ? name : `${name}.dev`, src]));
|
||||
|
||||
configs.push({
|
||||
entry: { [outName]: src },
|
||||
platform: 'browser',
|
||||
format: 'es',
|
||||
target: 'es2022',
|
||||
sourcemap: true,
|
||||
clean: false,
|
||||
dts: false,
|
||||
minify: isProd,
|
||||
noExternal: [/.*/],
|
||||
inlineOnly: false,
|
||||
treeshake: {
|
||||
moduleSideEffects: [{ test: /\/define\//, sideEffects: true }],
|
||||
},
|
||||
outDir: 'cdn',
|
||||
alias: {
|
||||
'@': new URL('./src', import.meta.url).pathname,
|
||||
},
|
||||
define: {
|
||||
__DEV__: isProd ? 'false' : 'true',
|
||||
},
|
||||
plugins: [inlineCssPlugin({ skinsDir, minify: isProd }), inlineTemplatePlugin({ minify: isProd })],
|
||||
inputOptions:
|
||||
mode === 'dev'
|
||||
? {
|
||||
resolve: {
|
||||
conditionNames: ['development', 'import', 'browser', 'default'],
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
configs.push({
|
||||
entry: entryMap,
|
||||
platform: 'browser',
|
||||
format: 'es',
|
||||
target: 'es2022',
|
||||
sourcemap: true,
|
||||
clean: mode === 'dev',
|
||||
dts: false,
|
||||
minify: isProd,
|
||||
noExternal: [/.*/],
|
||||
inlineOnly: false,
|
||||
treeshake: {
|
||||
moduleSideEffects: [{ test: /\/define\//, sideEffects: true }],
|
||||
},
|
||||
outDir,
|
||||
alias: {
|
||||
'@': new URL('./src', import.meta.url).pathname,
|
||||
},
|
||||
define: {
|
||||
__DEV__: isProd ? 'false' : 'true',
|
||||
},
|
||||
plugins: [
|
||||
inlineCssPlugin({ skinsDir, minify: isProd }),
|
||||
inlineTemplatePlugin({ minify: isProd }),
|
||||
...(!isProd ? [dtsStubsPlugin(outDir)] : []),
|
||||
],
|
||||
inputOptions: !isProd
|
||||
? {
|
||||
resolve: {
|
||||
conditionNames: ['development', 'import', 'browser', 'default'],
|
||||
},
|
||||
}
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export default defineConfig(configs);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export const SKINS = ['default', 'minimal'] as const;
|
||||
export const PLATFORMS = ['html', 'react'] as const;
|
||||
export const PLATFORMS = ['html', 'react', 'cdn'] as const;
|
||||
export const STYLINGS = ['css', 'tailwind'] as const;
|
||||
export const PRESETS = ['video', 'hls-video', 'simple-hls-video', 'dash-video', 'audio', 'background-video'] as const;
|
||||
|
||||
@@ -14,6 +14,7 @@ import { Navbar } from './navbar';
|
||||
import { Preview } from './preview';
|
||||
|
||||
function getPagePath(platform: Platform, preset: Preset): string {
|
||||
if (platform === 'cdn') return '/cdn/';
|
||||
if (preset === 'background-video') return `/${platform}-background-video/`;
|
||||
return `/${platform}-${preset}/`;
|
||||
}
|
||||
@@ -68,12 +69,12 @@ export function App() {
|
||||
}
|
||||
}, [preset, source, setSource]);
|
||||
|
||||
// Background video does not have a Tailwind skin variant.
|
||||
// CDN and background video do not have a Tailwind skin variant.
|
||||
useEffect(() => {
|
||||
if (preset === 'background-video' && styling === 'tailwind') {
|
||||
if ((platform === 'cdn' || preset === 'background-video') && styling === 'tailwind') {
|
||||
setStyling('css');
|
||||
}
|
||||
}, [preset, styling]);
|
||||
}, [platform, preset, styling]);
|
||||
|
||||
const availableSources = preset === 'audio' ? MP4_SOURCE_IDS : preset === 'dash-video' ? DASH_SOURCE_IDS : SOURCE_IDS;
|
||||
|
||||
@@ -101,9 +102,10 @@ export function App() {
|
||||
sources={SOURCES}
|
||||
/>
|
||||
<Preview
|
||||
key={`${pagePath}:${styling}`}
|
||||
key={`${pagePath}:${preset}:${styling}`}
|
||||
ref={iframeRef}
|
||||
pagePath={pagePath}
|
||||
preset={preset}
|
||||
skin={skin}
|
||||
styling={styling}
|
||||
source={source}
|
||||
|
||||
@@ -27,6 +27,7 @@ const SKIN_OPTIONS: readonly Skin[] = ['default', 'minimal'] satisfies readonly
|
||||
const PLATFORM_LABELS: Record<Platform, string> = {
|
||||
html: 'HTML',
|
||||
react: 'React',
|
||||
cdn: 'CDN',
|
||||
};
|
||||
|
||||
const PRESET_LABELS: Record<Preset, string> = {
|
||||
@@ -80,7 +81,7 @@ export function Navbar({
|
||||
options={stylings.map((s) => ({
|
||||
value: s,
|
||||
label: s === 'css' ? 'CSS' : 'Tailwind',
|
||||
disabled: s === 'tailwind' && isBackgroundVideo,
|
||||
disabled: s === 'tailwind' && (isBackgroundVideo || platform === 'cdn'),
|
||||
}))}
|
||||
/>
|
||||
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
import type { SourceId } from '@app/shared/sources';
|
||||
import type { Skin, Styling } from '@app/types';
|
||||
import type { Preset, Skin, Styling } from '@app/types';
|
||||
import { forwardRef, useState } from 'react';
|
||||
|
||||
type PreviewProps = {
|
||||
pagePath: string;
|
||||
preset: Preset;
|
||||
skin: Skin;
|
||||
styling: Styling;
|
||||
source: SourceId;
|
||||
};
|
||||
|
||||
export const Preview = forwardRef<HTMLIFrameElement, PreviewProps>(function Preview(
|
||||
{ pagePath, skin, styling, source },
|
||||
{ pagePath, preset, skin, styling, source },
|
||||
ref
|
||||
) {
|
||||
const [iframeUrl] = useState(
|
||||
() =>
|
||||
`${pagePath}?skin=${encodeURIComponent(skin)}&styling=${encodeURIComponent(styling)}&source=${encodeURIComponent(source)}`
|
||||
`${pagePath}?preset=${encodeURIComponent(preset)}&skin=${encodeURIComponent(skin)}&styling=${encodeURIComponent(styling)}&source=${encodeURIComponent(source)}`
|
||||
);
|
||||
const openUrl =
|
||||
`${pagePath}?skin=${encodeURIComponent(skin)}&styling=${encodeURIComponent(styling)}` +
|
||||
`${pagePath}?preset=${encodeURIComponent(preset)}&skin=${encodeURIComponent(skin)}&styling=${encodeURIComponent(styling)}` +
|
||||
`&source=${encodeURIComponent(source)}`;
|
||||
|
||||
return (
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Sandbox — CDN</title>
|
||||
<link rel="preconnect" href="https://rsms.me/" />
|
||||
<link rel="stylesheet" href="https://rsms.me/inter/inter.css" />
|
||||
</head>
|
||||
<body class="font-sans">
|
||||
<div id="root" class="flex justify-center items-center min-h-screen"></div>
|
||||
<script type="module" src="./main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,162 @@
|
||||
import '@app/styles.css';
|
||||
import { createHtmlSandboxState, createLatestLoader } from '@app/shared/html/sandbox-state';
|
||||
import { CSS_SKIN_TAGS } from '@app/shared/html/skin-tags';
|
||||
import { renderStoryboard } from '@app/shared/html/storyboard';
|
||||
import { loadAudioStylesheets, loadVideoStylesheets } from '@app/shared/html/stylesheets';
|
||||
import { onSkinChange, onSourceChange } from '@app/shared/sandbox-listener';
|
||||
import { BACKGROUND_VIDEO_SRC, getPosterSrc, getStoryboardSrc, SOURCES } from '@app/shared/sources';
|
||||
import type { Preset, Skin } from '@app/types';
|
||||
|
||||
const html = String.raw;
|
||||
|
||||
const params = new URLSearchParams(location.search);
|
||||
const preset = (params.get('preset') ?? 'video') as Preset;
|
||||
|
||||
const state = createHtmlSandboxState(preset === 'audio');
|
||||
const loadLatest = createLatestLoader();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CDN module loading — mirrors the exact import graph of each CDN bundle.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
async function loadCdnPreset(preset: Preset, skin: Skin) {
|
||||
switch (preset) {
|
||||
case 'video':
|
||||
case 'hls-video':
|
||||
case 'simple-hls-video':
|
||||
case 'dash-video':
|
||||
if (skin === 'minimal') await import('@videojs/html/cdn/video-minimal');
|
||||
else await import('@videojs/html/cdn/video');
|
||||
break;
|
||||
case 'audio':
|
||||
if (skin === 'minimal') await import('@videojs/html/cdn/audio-minimal');
|
||||
else await import('@videojs/html/cdn/audio');
|
||||
break;
|
||||
case 'background-video':
|
||||
await import('@videojs/html/background/skin.css');
|
||||
await import('@videojs/html/cdn/background');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async function loadCdnMedia(preset: Preset) {
|
||||
switch (preset) {
|
||||
case 'hls-video':
|
||||
await import('@videojs/html/cdn/media/hls-video');
|
||||
break;
|
||||
case 'simple-hls-video':
|
||||
await import('@videojs/html/cdn/media/simple-hls-video');
|
||||
break;
|
||||
case 'dash-video':
|
||||
await import('@videojs/html/cdn/media/dash-video');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Rendering — produces the exact HTML markup the installation builder generates.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function getPlayerTag(preset: Preset): string {
|
||||
if (preset === 'background-video') return 'background-video-player';
|
||||
if (preset === 'audio') return 'audio-player';
|
||||
return 'video-player';
|
||||
}
|
||||
|
||||
function getSkinTag(preset: Preset, skin: Skin): string {
|
||||
if (preset === 'background-video') return 'background-video-skin';
|
||||
if (preset === 'audio') return CSS_SKIN_TAGS[skin].audio;
|
||||
return CSS_SKIN_TAGS[skin].video;
|
||||
}
|
||||
|
||||
function getMediaTag(preset: Preset): string {
|
||||
const tags: Partial<Record<Preset, string>> = {
|
||||
'hls-video': 'hls-video',
|
||||
'simple-hls-video': 'simple-hls-video',
|
||||
'dash-video': 'dash-video',
|
||||
audio: 'audio',
|
||||
'background-video': 'background-video',
|
||||
};
|
||||
|
||||
return tags[preset] ?? 'video';
|
||||
}
|
||||
|
||||
function loadStylesheets(preset: Preset, skin: Skin) {
|
||||
if (preset === 'audio') loadAudioStylesheets(skin);
|
||||
else if (preset !== 'background-video') loadVideoStylesheets(skin);
|
||||
// Background CSS is loaded via dynamic import in loadCdnPreset.
|
||||
}
|
||||
|
||||
function isVideoPreset(preset: Preset): boolean {
|
||||
return preset === 'video' || preset === 'hls-video' || preset === 'simple-hls-video' || preset === 'dash-video';
|
||||
}
|
||||
|
||||
async function render() {
|
||||
await loadLatest(async () => {
|
||||
await loadCdnPreset(preset, state.skin);
|
||||
await loadCdnMedia(preset);
|
||||
});
|
||||
|
||||
loadStylesheets(preset, state.skin);
|
||||
|
||||
const root = document.getElementById('root')!;
|
||||
const playerTag = getPlayerTag(preset);
|
||||
const skinTag = getSkinTag(preset, state.skin);
|
||||
const mediaTag = getMediaTag(preset);
|
||||
const src = preset === 'background-video' ? BACKGROUND_VIDEO_SRC : SOURCES[state.source].url;
|
||||
const storyboard = isVideoPreset(preset) ? getStoryboardSrc(state.source) : undefined;
|
||||
const poster = isVideoPreset(preset) ? getPosterSrc(state.source) : undefined;
|
||||
|
||||
// Background video needs viewport dimensions instead of flex centering.
|
||||
if (preset === 'background-video') {
|
||||
root.className = '';
|
||||
root.style.cssText = 'width: 100vw; height: 100vh;';
|
||||
}
|
||||
|
||||
if (preset === 'background-video') {
|
||||
root.innerHTML = html`
|
||||
<${playerTag}>
|
||||
<${skinTag}>
|
||||
<${mediaTag} src="${src}"></${mediaTag}>
|
||||
</${skinTag}>
|
||||
</${playerTag}>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
if (preset === 'audio') {
|
||||
root.innerHTML = html`
|
||||
<div class="w-full max-w-xl mx-auto">
|
||||
<${playerTag}>
|
||||
<${skinTag}>
|
||||
<${mediaTag} src="${src}"></${mediaTag}>
|
||||
</${skinTag}>
|
||||
</${playerTag}>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
root.innerHTML = html`
|
||||
<${playerTag}>
|
||||
<${skinTag} class="w-full aspect-video max-w-4xl mx-auto">
|
||||
<${mediaTag} src="${src}" playsinline crossorigin="anonymous">
|
||||
${renderStoryboard(storyboard)}
|
||||
</${mediaTag}>
|
||||
${poster ? html`<img slot="poster" src="${poster}" alt="Video poster" />` : ''}
|
||||
</${skinTag}>
|
||||
</${playerTag}>
|
||||
`;
|
||||
}
|
||||
|
||||
render();
|
||||
|
||||
onSkinChange((skin) => {
|
||||
state.skin = skin;
|
||||
render();
|
||||
});
|
||||
|
||||
onSourceChange((source) => {
|
||||
state.source = source;
|
||||
render();
|
||||
});
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
"globalEnv": ["CONTEXT", "DEPLOY_PRIME_URL"],
|
||||
"tasks": {
|
||||
"build": {
|
||||
"dependsOn": ["^build"],
|
||||
"dependsOn": ["^build", "^build:cdn"],
|
||||
"outputs": ["dist/**", ".netlify/**"]
|
||||
},
|
||||
"build:cdn": {
|
||||
|
||||
Reference in New Issue
Block a user