Add player poster
This commit is contained in:
parent
9bb37679c0
commit
ff6886575f
@ -10,6 +10,7 @@ import { getLocalStorage, setLocalStorage } from '../../utils/helpers';
|
|||||||
import {
|
import {
|
||||||
AppState,
|
AppState,
|
||||||
ChatState,
|
ChatState,
|
||||||
|
VideoState,
|
||||||
ChatVisibilityState,
|
ChatVisibilityState,
|
||||||
getChatState,
|
getChatState,
|
||||||
getChatVisibilityState,
|
getChatVisibilityState,
|
||||||
@ -39,6 +40,11 @@ export const chatStateAtom = atom<ChatState>({
|
|||||||
default: ChatState.Offline,
|
default: ChatState.Offline,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const videoStateAtom = atom<VideoState>({
|
||||||
|
key: 'videoStateAtom',
|
||||||
|
default: VideoState.Unavailable,
|
||||||
|
});
|
||||||
|
|
||||||
export const chatVisibilityAtom = atom<ChatVisibilityState>({
|
export const chatVisibilityAtom = atom<ChatVisibilityState>({
|
||||||
key: 'chatVisibility',
|
key: 'chatVisibility',
|
||||||
default: ChatVisibilityState.Visible,
|
default: ChatVisibilityState.Visible,
|
||||||
@ -71,6 +77,7 @@ export function ClientConfigStore() {
|
|||||||
const [chatMessages, setChatMessages] = useRecoilState<ChatMessage[]>(chatMessagesAtom);
|
const [chatMessages, setChatMessages] = useRecoilState<ChatMessage[]>(chatMessagesAtom);
|
||||||
const setChatDisplayName = useSetRecoilState<string>(chatDisplayNameAtom);
|
const setChatDisplayName = useSetRecoilState<string>(chatDisplayNameAtom);
|
||||||
const [appState, setAppState] = useRecoilState<AppState>(appStateAtom);
|
const [appState, setAppState] = useRecoilState<AppState>(appStateAtom);
|
||||||
|
const [videoState, setVideoState] = useRecoilState<VideoState>(videoStateAtom);
|
||||||
const [accessToken, setAccessToken] = useRecoilState<string>(accessTokenAtom);
|
const [accessToken, setAccessToken] = useRecoilState<string>(accessTokenAtom);
|
||||||
const [websocketService, setWebsocketService] =
|
const [websocketService, setWebsocketService] =
|
||||||
useRecoilState<WebsocketService>(websocketServiceAtom);
|
useRecoilState<WebsocketService>(websocketServiceAtom);
|
||||||
@ -81,7 +88,6 @@ export function ClientConfigStore() {
|
|||||||
try {
|
try {
|
||||||
const config = await ClientConfigService.getConfig();
|
const config = await ClientConfigService.getConfig();
|
||||||
setClientConfig(config);
|
setClientConfig(config);
|
||||||
setAppState(AppState.Online);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`ClientConfigService -> getConfig() ERROR: \n${error}`);
|
console.error(`ClientConfigService -> getConfig() ERROR: \n${error}`);
|
||||||
}
|
}
|
||||||
@ -100,7 +106,6 @@ export function ClientConfigStore() {
|
|||||||
setAccessToken(newAccessToken);
|
setAccessToken(newAccessToken);
|
||||||
// setLocalStorage('accessToken', newAccessToken);
|
// setLocalStorage('accessToken', newAccessToken);
|
||||||
setChatDisplayName(newDisplayName);
|
setChatDisplayName(newDisplayName);
|
||||||
setAppState(AppState.Online);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(`ChatService -> registerUser() ERROR: \n${e}`);
|
console.error(`ChatService -> registerUser() ERROR: \n${e}`);
|
||||||
}
|
}
|
||||||
|
@ -60,7 +60,7 @@ export default function ContentComponent() {
|
|||||||
return (
|
return (
|
||||||
<Content className={`${s.root}`} data-columns={chatOpen ? 2 : 1}>
|
<Content className={`${s.root}`} data-columns={chatOpen ? 2 : 1}>
|
||||||
<div className={`${s.leftCol}`}>
|
<div className={`${s.leftCol}`}>
|
||||||
<OwncastPlayer source="https://watch.owncast.online" />
|
<OwncastPlayer source="/hls/stream.m3u8" online={online} />
|
||||||
<Statusbar
|
<Statusbar
|
||||||
online={online}
|
online={online}
|
||||||
lastConnectTime={lastConnectTime}
|
lastConnectTime={lastConnectTime}
|
||||||
|
75
web/components/ui/CrossfadeImage/CrossfadeImage.tsx
Normal file
75
web/components/ui/CrossfadeImage/CrossfadeImage.tsx
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
import React, { useMemo, useState } from 'react';
|
||||||
|
|
||||||
|
type ObjectFit = React.CSSProperties['objectFit'];
|
||||||
|
|
||||||
|
interface CrossfadeImageProps {
|
||||||
|
src: string;
|
||||||
|
width: string;
|
||||||
|
height: string;
|
||||||
|
objectFit?: ObjectFit;
|
||||||
|
duration?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const imgStyle: React.CSSProperties = {
|
||||||
|
position: 'absolute',
|
||||||
|
width: `100%`,
|
||||||
|
height: `100%`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function CrossfadeImage({
|
||||||
|
src = '',
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
objectFit = 'fill',
|
||||||
|
duration = '1s',
|
||||||
|
}: CrossfadeImageProps) {
|
||||||
|
const spanStyle: React.CSSProperties = useMemo(
|
||||||
|
() => ({
|
||||||
|
display: 'inline-block',
|
||||||
|
position: 'relative',
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}),
|
||||||
|
[width, height],
|
||||||
|
);
|
||||||
|
|
||||||
|
const imgStyles = useMemo(
|
||||||
|
() => [
|
||||||
|
{ ...imgStyle, objectFit, opacity: 0, transition: `opacity ${duration}` },
|
||||||
|
{ ...imgStyle, objectFit, opacity: 1, transition: `opacity ${duration}` },
|
||||||
|
{ ...imgStyle, objectFit, opacity: 0 },
|
||||||
|
],
|
||||||
|
[objectFit, duration],
|
||||||
|
);
|
||||||
|
|
||||||
|
const [key, setKey] = useState(0);
|
||||||
|
const [srcs, setSrcs] = useState(['', '']);
|
||||||
|
const nextSrc = src !== srcs[1] ? src : '';
|
||||||
|
|
||||||
|
const onLoadImg = () => {
|
||||||
|
setKey((key + 1) % 3);
|
||||||
|
setSrcs([srcs[1], nextSrc]);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<span style={spanStyle}>
|
||||||
|
{[...srcs, nextSrc].map(
|
||||||
|
(src, index) =>
|
||||||
|
src !== '' && (
|
||||||
|
<img
|
||||||
|
key={(key + index) % 3}
|
||||||
|
src={src}
|
||||||
|
alt=""
|
||||||
|
style={imgStyles[index]}
|
||||||
|
onLoad={index === 2 ? onLoadImg : undefined}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
CrossfadeImage.defaultProps = {
|
||||||
|
objectFit: 'fill',
|
||||||
|
duration: '3s',
|
||||||
|
};
|
@ -45,7 +45,7 @@ export default function Statusbar(props: Props) {
|
|||||||
} else {
|
} else {
|
||||||
onlineMessage = 'Offline';
|
onlineMessage = 'Offline';
|
||||||
if (lastDisconnectTime) {
|
if (lastDisconnectTime) {
|
||||||
rightSideMessage = `Last live ${formatDistanceToNow(lastDisconnectTime)} ago.`;
|
rightSideMessage = `Last live ${formatDistanceToNow(new Date(lastDisconnectTime))} ago.`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,15 +1,26 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { useSetRecoilState } from 'recoil';
|
||||||
import VideoJS from './player';
|
import VideoJS from './player';
|
||||||
import ViewerPing from './viewer-ping';
|
import ViewerPing from './viewer-ping';
|
||||||
|
import VideoPoster from './VideoPoster';
|
||||||
import { getLocalStorage, setLocalStorage } from '../../utils/helpers';
|
import { getLocalStorage, setLocalStorage } from '../../utils/helpers';
|
||||||
|
import { videoStateAtom } from '../stores/ClientConfigStore';
|
||||||
|
import { VideoState } from '../../interfaces/application-state';
|
||||||
|
|
||||||
const PLAYER_VOLUME = 'owncast_volume';
|
const PLAYER_VOLUME = 'owncast_volume';
|
||||||
|
|
||||||
const ping = new ViewerPing();
|
const ping = new ViewerPing();
|
||||||
|
|
||||||
export default function OwncastPlayer(props) {
|
interface Props {
|
||||||
|
source: string;
|
||||||
|
online: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function OwncastPlayer(props: Props) {
|
||||||
const playerRef = React.useRef(null);
|
const playerRef = React.useRef(null);
|
||||||
const { source } = props;
|
const { source, online } = props;
|
||||||
|
|
||||||
|
const setVideoState = useSetRecoilState<VideoState>(videoStateAtom);
|
||||||
|
|
||||||
const setSavedVolume = () => {
|
const setSavedVolume = () => {
|
||||||
try {
|
try {
|
||||||
@ -51,7 +62,7 @@ export default function OwncastPlayer(props) {
|
|||||||
},
|
},
|
||||||
sources: [
|
sources: [
|
||||||
{
|
{
|
||||||
src: `${source}/hls/stream.m3u8`,
|
src: source,
|
||||||
type: 'application/x-mpegURL',
|
type: 'application/x-mpegURL',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
@ -75,6 +86,7 @@ export default function OwncastPlayer(props) {
|
|||||||
player.on('playing', () => {
|
player.on('playing', () => {
|
||||||
player.log('player is playing');
|
player.log('player is playing');
|
||||||
ping.start();
|
ping.start();
|
||||||
|
setVideoState(VideoState.Playing);
|
||||||
});
|
});
|
||||||
|
|
||||||
player.on('pause', () => {
|
player.on('pause', () => {
|
||||||
@ -85,10 +97,26 @@ export default function OwncastPlayer(props) {
|
|||||||
player.on('ended', () => {
|
player.on('ended', () => {
|
||||||
player.log('player is ended');
|
player.log('player is ended');
|
||||||
ping.stop();
|
ping.stop();
|
||||||
|
setVideoState(VideoState.Unavailable);
|
||||||
});
|
});
|
||||||
|
|
||||||
player.on('volumechange', handleVolume);
|
player.on('volumechange', handleVolume);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <VideoJS options={videoJsOptions} onReady={handlePlayerReady} />;
|
return (
|
||||||
|
<div style={{ display: 'grid' }}>
|
||||||
|
{online && (
|
||||||
|
<div style={{ gridColumn: 1, gridRow: 1 }}>
|
||||||
|
<VideoJS
|
||||||
|
style={{ gridColumn: 1, gridRow: 1 }}
|
||||||
|
options={videoJsOptions}
|
||||||
|
onReady={handlePlayerReady}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div style={{ gridColumn: 1, gridRow: 1 }}>
|
||||||
|
<VideoPoster online={online} initialSrc="/logo" src="/thumbnail.jpg" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
.player {
|
.player {
|
||||||
height: 80vh;
|
height: 80vh;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
background-color: green;
|
background-color: black;
|
||||||
|
|
||||||
|
.vjs-big-play-centered .vjs-big-play-button {
|
||||||
|
z-index: 99999 !important;
|
||||||
|
}
|
||||||
}
|
}
|
5
web/components/video/VideoPoster.module.scss
Normal file
5
web/components/video/VideoPoster.module.scss
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
.poster {
|
||||||
|
background-color: black;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
@ -1,109 +1,46 @@
|
|||||||
/*
|
import { useEffect, useState } from 'react';
|
||||||
VideoPoster is the image that covers up the video component and shows a
|
import CrossfadeImage from '../ui/CrossfadeImage/CrossfadeImage';
|
||||||
preview of the video, refreshing every N seconds.
|
import s from './VideoPoster.module.scss';
|
||||||
It's more complex than it needs to be, using the "double buffer" approach to
|
|
||||||
cross-fade the two images. Now that we've moved to React we may be able to
|
|
||||||
simply use some simple cross-fading component.
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { useEffect, useLayoutEffect, useState } from 'react';
|
const REFRESH_INTERVAL = 20_000;
|
||||||
import { ReactElement } from 'react-markdown/lib/react-markdown';
|
|
||||||
|
|
||||||
const REFRESH_INTERVAL = 15000;
|
interface Props {
|
||||||
const TEMP_IMAGE = 'http://localhost:8080/logo';
|
initialSrc: string;
|
||||||
const POSTER_BASE_URL = 'http://localhost:8080/';
|
src: string;
|
||||||
|
online: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export default function VideoPoster(props): ReactElement {
|
export default function VideoPoster(props: Props) {
|
||||||
const { active } = props;
|
const { online, initialSrc, src: base } = props;
|
||||||
const [flipped, setFlipped] = useState(false);
|
|
||||||
const [oldUrl, setOldUrl] = useState(TEMP_IMAGE);
|
|
||||||
const [url, setUrl] = useState(props.url);
|
|
||||||
const [currentUrl, setCurrentUrl] = useState(TEMP_IMAGE);
|
|
||||||
const [loadingImage, setLoadingImage] = useState(TEMP_IMAGE);
|
|
||||||
const [offlineImage, setOfflineImage] = useState(TEMP_IMAGE);
|
|
||||||
|
|
||||||
let refreshTimer = null;
|
let timer: ReturnType<typeof setInterval>;
|
||||||
|
const [src, setSrc] = useState(initialSrc);
|
||||||
const setLoaded = () => {
|
const [duration, setDuration] = useState('0s');
|
||||||
setFlipped(!flipped);
|
|
||||||
setUrl(loadingImage);
|
|
||||||
setOldUrl(currentUrl);
|
|
||||||
};
|
|
||||||
|
|
||||||
const fire = () => {
|
|
||||||
const cachebuster = Math.round(new Date().getTime() / 1000);
|
|
||||||
setLoadingImage(`${POSTER_BASE_URL}?cb=${cachebuster}`);
|
|
||||||
const img = new Image();
|
|
||||||
img.onload = setLoaded;
|
|
||||||
img.src = loadingImage;
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopRefreshTimer = () => {
|
|
||||||
clearInterval(refreshTimer);
|
|
||||||
refreshTimer = null;
|
|
||||||
};
|
|
||||||
|
|
||||||
const startRefreshTimer = () => {
|
|
||||||
stopRefreshTimer();
|
|
||||||
fire();
|
|
||||||
// Load a new copy of the image every n seconds
|
|
||||||
refreshTimer = setInterval(fire, REFRESH_INTERVAL);
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (active) {
|
clearInterval(timer);
|
||||||
fire();
|
timer = setInterval(() => {
|
||||||
startRefreshTimer();
|
if (duration === '0s') {
|
||||||
} else {
|
setDuration('3s');
|
||||||
stopRefreshTimer();
|
|
||||||
}
|
}
|
||||||
}, [active]);
|
|
||||||
|
|
||||||
// On component unmount.
|
setSrc(`${base}?${Date.now()}`);
|
||||||
useLayoutEffect(
|
}, REFRESH_INTERVAL);
|
||||||
() => () => {
|
}, []);
|
||||||
stopRefreshTimer();
|
|
||||||
},
|
|
||||||
[],
|
|
||||||
);
|
|
||||||
|
|
||||||
// TODO: Replace this with React memo logic.
|
|
||||||
// shouldComponentUpdate(prevProps, prevState) {
|
|
||||||
// return (
|
|
||||||
// this.props.active !== prevProps.active ||
|
|
||||||
// this.props.offlineImage !== prevProps.offlineImage ||
|
|
||||||
// this.state.url !== prevState.url ||
|
|
||||||
// this.state.oldUrl !== prevState.oldUrl
|
|
||||||
// );
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (!active) {
|
|
||||||
return (
|
|
||||||
<div id="oc-custom-poster">
|
|
||||||
<ThumbImage url={offlineImage} visible />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="oc-custom-poster">
|
<div className={s.poster}>
|
||||||
<ThumbImage url={!flipped ? oldUrl : url} visible />
|
{!online && <img src={initialSrc} alt="logo" height="500vh" />}
|
||||||
<ThumbImage url={flipped ? oldUrl : url} visible={!flipped} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ThumbImage({ url, visible }) {
|
{online && (
|
||||||
if (!url) {
|
<CrossfadeImage
|
||||||
return null;
|
src={src}
|
||||||
}
|
duration={duration}
|
||||||
return (
|
objectFit="contain"
|
||||||
<div
|
width="100%"
|
||||||
className="custom-thumbnail-image"
|
height="500px"
|
||||||
style={{
|
|
||||||
opacity: visible ? 1 : 0,
|
|
||||||
backgroundImage: `url(${url})`,
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -19,6 +19,12 @@ export enum ChatState {
|
|||||||
Offline, // Chat is offline/disconnected for some reason but is visible.
|
Offline, // Chat is offline/disconnected for some reason but is visible.
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export enum VideoState {
|
||||||
|
Available, // Play button should be visible and the user can begin playback.
|
||||||
|
Unavailable, // Play button not be visible and video is not available.
|
||||||
|
Playing, // Playback is taking place and the play button should not be shown.
|
||||||
|
}
|
||||||
|
|
||||||
export function getChatState(state: AppState): ChatState {
|
export function getChatState(state: AppState): ChatState {
|
||||||
switch (state) {
|
switch (state) {
|
||||||
case AppState.Loading:
|
case AppState.Loading:
|
||||||
|
@ -8,6 +8,18 @@ module.exports = withLess({
|
|||||||
source: '/api/:path*',
|
source: '/api/:path*',
|
||||||
destination: 'http://localhost:8080/api/:path*', // Proxy to Backend to work around CORS.
|
destination: 'http://localhost:8080/api/:path*', // Proxy to Backend to work around CORS.
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
source: '/hls/:path*',
|
||||||
|
destination: 'http://localhost:8080/hls/:path*', // Proxy to Backend to work around CORS.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/logo',
|
||||||
|
destination: 'http://localhost:8080/logo', // Proxy to Backend to work around CORS.
|
||||||
|
},
|
||||||
|
{
|
||||||
|
source: '/thumbnail.jpg',
|
||||||
|
destination: 'http://localhost:8080/thumbnail.jpg', // Proxy to Backend to work around CORS.
|
||||||
|
},
|
||||||
];
|
];
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
16
web/package-lock.json
generated
16
web/package-lock.json
generated
@ -29,6 +29,7 @@
|
|||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-chartkick": "0.5.2",
|
"react-chartkick": "0.5.2",
|
||||||
"react-contenteditable": "^3.3.6",
|
"react-contenteditable": "^3.3.6",
|
||||||
|
"react-crossfade-img": "^1.0.0",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-linkify": "1.0.0-alpha",
|
"react-linkify": "1.0.0-alpha",
|
||||||
"react-markdown": "8.0.0",
|
"react-markdown": "8.0.0",
|
||||||
@ -26610,6 +26611,15 @@
|
|||||||
"react": ">=16.3"
|
"react": ">=16.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-crossfade-img": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-crossfade-img/-/react-crossfade-img-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-7RG0rvTBA/K7EWcDPzIwEqUzJgoX3+YvrGghxxoqT3xG8onyzVUNenG2WCQ/hzdhT2mIU8e7UD5C0GKS/iyMww==",
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-docgen": {
|
"node_modules/react-docgen": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.0.tgz",
|
||||||
@ -52302,6 +52312,12 @@
|
|||||||
"prop-types": "^15.7.1"
|
"prop-types": "^15.7.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"react-crossfade-img": {
|
||||||
|
"version": "1.0.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-crossfade-img/-/react-crossfade-img-1.0.0.tgz",
|
||||||
|
"integrity": "sha512-7RG0rvTBA/K7EWcDPzIwEqUzJgoX3+YvrGghxxoqT3xG8onyzVUNenG2WCQ/hzdhT2mIU8e7UD5C0GKS/iyMww==",
|
||||||
|
"requires": {}
|
||||||
|
},
|
||||||
"react-docgen": {
|
"react-docgen": {
|
||||||
"version": "5.4.0",
|
"version": "5.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/react-docgen/-/react-docgen-5.4.0.tgz",
|
||||||
|
@ -33,6 +33,7 @@
|
|||||||
"react": "17.0.2",
|
"react": "17.0.2",
|
||||||
"react-chartkick": "0.5.2",
|
"react-chartkick": "0.5.2",
|
||||||
"react-contenteditable": "^3.3.6",
|
"react-contenteditable": "^3.3.6",
|
||||||
|
"react-crossfade-img": "^1.0.0",
|
||||||
"react-dom": "17.0.2",
|
"react-dom": "17.0.2",
|
||||||
"react-linkify": "1.0.0-alpha",
|
"react-linkify": "1.0.0-alpha",
|
||||||
"react-markdown": "8.0.0",
|
"react-markdown": "8.0.0",
|
||||||
|
@ -3,9 +3,9 @@ import { ComponentStory, ComponentMeta } from '@storybook/react';
|
|||||||
import OwncastPlayer from '../components/video/OwncastPlayer';
|
import OwncastPlayer from '../components/video/OwncastPlayer';
|
||||||
|
|
||||||
const streams = {
|
const streams = {
|
||||||
DemoServer: `https://watch.owncast.online`,
|
DemoServer: `https://watch.owncast.online/hls/stream.m3u8`,
|
||||||
RetroStrangeTV: `https://live.retrostrange.com`,
|
RetroStrangeTV: `https://live.retrostrange.com/hls/stream.m3u8`,
|
||||||
localhost: `http://localhost:8080`,
|
localhost: `http://localhost:8080/hls/stream.m3u8`,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
@ -28,5 +28,5 @@ const Template: ComponentStory<typeof OwncastPlayer> = args => <OwncastPlayer {.
|
|||||||
export const LiveDemo = Template.bind({});
|
export const LiveDemo = Template.bind({});
|
||||||
LiveDemo.args = {
|
LiveDemo.args = {
|
||||||
online: true,
|
online: true,
|
||||||
source: 'https://watch.owncast.online',
|
source: 'https://watch.owncast.online/hls/stream.m3u8',
|
||||||
};
|
};
|
||||||
|
34
web/stories/VideoPoster.stories.tsx
Normal file
34
web/stories/VideoPoster.stories.tsx
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { ComponentStory, ComponentMeta } from '@storybook/react';
|
||||||
|
import VideoPoster from '../components/video/VideoPoster';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'owncast/Video poster',
|
||||||
|
component: VideoPoster,
|
||||||
|
parameters: {},
|
||||||
|
} as ComponentMeta<typeof VideoPoster>;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
const Template: ComponentStory<typeof VideoPoster> = args => <VideoPoster {...args} />;
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
export const Example1 = Template.bind({});
|
||||||
|
Example1.args = {
|
||||||
|
initialSrc: 'https://watch.owncast.online/logo',
|
||||||
|
src: 'https://watch.owncast.online/thumbnail.jpg',
|
||||||
|
online: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Example2 = Template.bind({});
|
||||||
|
Example2.args = {
|
||||||
|
initialSrc: 'https://listen.batstationrad.io/logo',
|
||||||
|
src: 'https://listen.batstationrad.io//thumbnail.jpg',
|
||||||
|
online: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const Offline = Template.bind({});
|
||||||
|
Offline.args = {
|
||||||
|
initialSrc: 'https://watch.owncast.online/logo',
|
||||||
|
src: 'https://watch.owncast.online/thumbnail.jpg',
|
||||||
|
online: false,
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user