Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 29 additions & 1 deletion apps/sandbox/app/shared/html/sandbox-state.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,12 @@
import { getInitialSkin, getInitialSource } from '@app/shared/sandbox-listener';
import {
getInitialAutoplay,
getInitialLoop,
getInitialMuted,
getInitialPreload,
getInitialSkin,
getInitialSource,
type PreloadValue,
} from '@app/shared/sandbox-listener';
import type { SourceId } from '@app/shared/sources';
import type { Skin, Styling } from '@app/types';

Expand All @@ -10,16 +18,36 @@ export type HtmlSandboxState = {
skin: Skin;
source: SourceId;
styling: Styling;
autoplay: boolean;
muted: boolean;
loop: boolean;
preload: PreloadValue;
};

export function createHtmlSandboxState(audioOnly?: boolean): HtmlSandboxState {
return {
skin: getInitialSkin(),
source: getInitialSource(audioOnly),
styling: getInitialStyling(),
autoplay: getInitialAutoplay(),
muted: getInitialMuted(),
loop: getInitialLoop(),
preload: getInitialPreload(),
};
}

/** Render the user-controlled media attributes (autoplay/muted/loop/preload) as HTML attributes. */
export function renderMediaAttrs(state: HtmlSandboxState): string {
return [
state.autoplay ? 'autoplay' : '',
state.muted ? 'muted' : '',
state.loop ? 'loop' : '',
`preload="${state.preload}"`,
]
.filter(Boolean)
.join(' ');
}

export function createLatestLoader() {
let loadVersion = 0;

Expand Down
14 changes: 12 additions & 2 deletions apps/sandbox/app/shared/react/providers.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { createPlayer } from '@videojs/react';
import { audioFeatures } from '@videojs/react/audio';
import { backgroundFeatures } from '@videojs/react/background';
import { liveAudioFeatures } from '@videojs/react/live-audio';
import { liveVideoFeatures } from '@videojs/react/live-video';
import { videoFeatures } from '@videojs/react/video';

// The `live-video` preset currently shares `videoFeatures`, so the same provider
// works for both live and VOD playback — only the skin swaps on the source.
export const { Provider: VideoProvider } = createPlayer({
features: videoFeatures,
});
Expand All @@ -16,3 +16,13 @@ export const { Provider: AudioProvider } = createPlayer({
export const { Provider: BackgroundVideoProvider } = createPlayer({
features: backgroundFeatures,
});

// Live providers register `liveFeature` so the LiveButton can read
// `liveEdgeStart` / `targetLiveWindow` and seek to the live edge.
export const { Provider: LiveVideoProvider } = createPlayer({
features: liveVideoFeatures,
});

export const { Provider: LiveAudioProvider } = createPlayer({
features: liveAudioFeatures,
});
8 changes: 8 additions & 0 deletions apps/sandbox/app/shared/react/use-autoplay.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { getInitialAutoplay, onAutoplayChange } from '@app/shared/sandbox-listener';
import { useEffect, useState } from 'react';

export function useAutoplay(): boolean {
const [autoplay, setAutoplay] = useState(getInitialAutoplay);
useEffect(() => onAutoplayChange(setAutoplay), []);
return autoplay;
}
8 changes: 8 additions & 0 deletions apps/sandbox/app/shared/react/use-loop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { getInitialLoop, onLoopChange } from '@app/shared/sandbox-listener';
import { useEffect, useState } from 'react';

export function useLoop(): boolean {
const [loop, setLoop] = useState(getInitialLoop);
useEffect(() => onLoopChange(setLoop), []);
return loop;
}
8 changes: 8 additions & 0 deletions apps/sandbox/app/shared/react/use-muted.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { getInitialMuted, onMutedChange } from '@app/shared/sandbox-listener';
import { useEffect, useState } from 'react';

export function useMuted(): boolean {
const [muted, setMuted] = useState(getInitialMuted);
useEffect(() => onMutedChange(setMuted), []);
return muted;
}
8 changes: 8 additions & 0 deletions apps/sandbox/app/shared/react/use-preload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { getInitialPreload, onPreloadChange, type PreloadValue } from '@app/shared/sandbox-listener';
import { useEffect, useState } from 'react';

export function usePreload(): PreloadValue {
const [preload, setPreload] = useState(getInitialPreload);
useEffect(() => onPreloadChange(setPreload), []);
return preload;
}
93 changes: 93 additions & 0 deletions apps/sandbox/app/shared/sandbox-listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ import { SKINS } from '@app/constants';
import type { Skin } from '@app/types';
import { DEFAULT_AUDIO_SOURCE, SOURCES, type SourceId } from './sources';

export const PRELOAD_VALUES = ['none', 'metadata', 'auto'] as const;
export type PreloadValue = (typeof PRELOAD_VALUES)[number];
export const DEFAULT_PRELOAD: PreloadValue = 'metadata';

const params = new URLSearchParams(window.location.search);

function readSkin(): Skin {
Expand All @@ -16,8 +20,21 @@ function readSource(): SourceId {
return source && source in SOURCES ? (source as SourceId) : 'hls-1';
}

function readBoolean(name: string): boolean {
return params.get(name) === '1';
}

function readPreload(): PreloadValue {
const value = params.get('preload');
return PRELOAD_VALUES.includes(value as PreloadValue) ? (value as PreloadValue) : DEFAULT_PRELOAD;
}

let currentSkin = readSkin();
let currentSource = readSource();
let currentAutoplay = readBoolean('autoplay');
let currentMuted = readBoolean('muted');
let currentLoop = readBoolean('loop');
let currentPreload = readPreload();

export function getInitialSkin(): Skin {
return currentSkin;
Expand Down Expand Up @@ -62,3 +79,79 @@ export function onSourceChange(callback: (source: SourceId) => void): () => void
window.removeEventListener('message', handler);
};
}

export function getInitialAutoplay(): boolean {
return currentAutoplay;
}

export function onAutoplayChange(callback: (autoplay: boolean) => void): () => void {
const handler = (event: MessageEvent) => {
if (event.data?.type !== 'autoplay-change' || typeof event.data.autoplay !== 'boolean') return;

currentAutoplay = event.data.autoplay;
callback(currentAutoplay);
};

window.addEventListener('message', handler);

return () => {
window.removeEventListener('message', handler);
};
}

export function getInitialMuted(): boolean {
return currentMuted;
}

export function onMutedChange(callback: (muted: boolean) => void): () => void {
const handler = (event: MessageEvent) => {
if (event.data?.type !== 'muted-change' || typeof event.data.muted !== 'boolean') return;

currentMuted = event.data.muted;
callback(currentMuted);
};

window.addEventListener('message', handler);

return () => {
window.removeEventListener('message', handler);
};
}

export function getInitialLoop(): boolean {
return currentLoop;
}

export function onLoopChange(callback: (loop: boolean) => void): () => void {
const handler = (event: MessageEvent) => {
if (event.data?.type !== 'loop-change' || typeof event.data.loop !== 'boolean') return;

currentLoop = event.data.loop;
callback(currentLoop);
};

window.addEventListener('message', handler);

return () => {
window.removeEventListener('message', handler);
};
}

export function getInitialPreload(): PreloadValue {
return currentPreload;
}

export function onPreloadChange(callback: (preload: PreloadValue) => void): () => void {
const handler = (event: MessageEvent) => {
if (event.data?.type !== 'preload-change' || !PRELOAD_VALUES.includes(event.data.preload)) return;

currentPreload = event.data.preload;
callback(currentPreload);
};

window.addEventListener('message', handler);

return () => {
window.removeEventListener('message', handler);
};
}
54 changes: 51 additions & 3 deletions apps/sandbox/app/shell/app.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { PLATFORMS, PRESETS, STYLINGS } from '@app/constants';
import { DEFAULT_PRELOAD, PRELOAD_VALUES, type PreloadValue } from '@app/shared/sandbox-listener';
import type { SourceId } from '@app/shared/sources';
import {
DASH_SOURCE_IDS,
Expand All @@ -22,12 +23,17 @@ function getPagePath(platform: Platform, preset: Preset): string {

function readParams() {
const params = new URLSearchParams(location.search);
const preload = params.get('preload');
return {
platform: (params.get('platform') ?? 'html') as Platform,
styling: (params.get('styling') ?? 'css') as Styling,
preset: (params.get('preset') ?? 'video') as Preset,
skin: (params.get('skin') ?? 'default') as 'default' | 'minimal',
source: (params.get('source') ?? 'hls-1') as SourceId,
autoplay: params.get('autoplay') === '1',
muted: params.get('muted') === '1',
loop: params.get('loop') === '1',
preload: PRELOAD_VALUES.includes(preload as PreloadValue) ? (preload as PreloadValue) : DEFAULT_PRELOAD,
};
}

Expand All @@ -38,15 +44,29 @@ export function App() {
const [preset, setPreset] = useState<Preset>(initial.preset);
const [skin, setSkin] = useState(initial.skin);
const [source, setSource] = useState(initial.source);
const [autoplay, setAutoplay] = useState(initial.autoplay);
const [muted, setMuted] = useState(initial.muted);
const [loop, setLoop] = useState(initial.loop);
const [preload, setPreload] = useState<PreloadValue>(initial.preload);
const iframeRef = useRef<HTMLIFrameElement>(null);

const pagePath = getPagePath(platform, preset);

// Keep the URL in sync with all state (including skin + source)
// Keep the URL in sync with all state.
useEffect(() => {
const params = new URLSearchParams({ platform, styling, preset, skin, source });
const params = new URLSearchParams({
platform,
styling,
preset,
skin,
source,
autoplay: autoplay ? '1' : '0',
muted: muted ? '1' : '0',
loop: loop ? '1' : '0',
preload,
});
history.replaceState(null, '', `/?${params}`);
}, [platform, styling, preset, skin, source]);
}, [platform, styling, preset, skin, source, autoplay, muted, loop, preload]);

useEffect(() => {
iframeRef.current?.contentWindow?.postMessage({ type: 'skin-change', skin }, '*');
Expand All @@ -56,6 +76,22 @@ export function App() {
iframeRef.current?.contentWindow?.postMessage({ type: 'source-change', source }, '*');
}, [source]);

useEffect(() => {
iframeRef.current?.contentWindow?.postMessage({ type: 'autoplay-change', autoplay }, '*');
}, [autoplay]);

useEffect(() => {
iframeRef.current?.contentWindow?.postMessage({ type: 'muted-change', muted }, '*');
}, [muted]);

useEffect(() => {
iframeRef.current?.contentWindow?.postMessage({ type: 'loop-change', loop }, '*');
}, [loop]);

useEffect(() => {
iframeRef.current?.contentWindow?.postMessage({ type: 'preload-change', preload }, '*');
}, [preload]);

// Constrain source to MP4 when switching to audio
useEffect(() => {
if (preset === 'audio' && SOURCES[source].type !== 'mp4') {
Expand Down Expand Up @@ -102,6 +138,14 @@ export function App() {
onSkinChange={setSkin}
source={source}
onSourceChange={handleSourceChange}
autoplay={autoplay}
onAutoplayChange={setAutoplay}
muted={muted}
onMutedChange={setMuted}
loop={loop}
onLoopChange={setLoop}
preload={preload}
onPreloadChange={setPreload}
availableSources={availableSources}
isBackgroundVideo={preset === 'background-video'}
isSimpleHlsVideo={preset === 'simple-hls-video'}
Expand All @@ -120,6 +164,10 @@ export function App() {
skin={skin}
styling={styling}
source={source}
autoplay={autoplay}
muted={muted}
loop={loop}
preload={preload}
/>
</div>
);
Expand Down
Loading
Loading