Add initiallyMuted
query parameter to embed player (#2539)
* Add query param to initially mute embed player * Add stories for embed player * Improve VideoJS typing
This commit is contained in:
parent
db3e20b480
commit
2f2300db8d
@ -1,6 +1,7 @@
|
|||||||
import React, { FC, useEffect } from 'react';
|
import React, { FC, useEffect } from 'react';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import { VideoJsPlayerOptions } from 'video.js';
|
||||||
import { VideoJS } from '../VideoJS/VideoJS';
|
import { VideoJS } from '../VideoJS/VideoJS';
|
||||||
import ViewerPing from '../viewer-ping';
|
import ViewerPing from '../viewer-ping';
|
||||||
import { VideoPoster } from '../VideoPoster/VideoPoster';
|
import { VideoPoster } from '../VideoPoster/VideoPoster';
|
||||||
@ -24,6 +25,7 @@ let latencyCompensatorEnabled = false;
|
|||||||
export type OwncastPlayerProps = {
|
export type OwncastPlayerProps = {
|
||||||
source: string;
|
source: string;
|
||||||
online: boolean;
|
online: boolean;
|
||||||
|
initiallyMuted?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
async function getVideoSettings() {
|
async function getVideoSettings() {
|
||||||
@ -38,7 +40,11 @@ async function getVideoSettings() {
|
|||||||
return qualities;
|
return qualities;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const OwncastPlayer: FC<OwncastPlayerProps> = ({ source, online }) => {
|
export const OwncastPlayer: FC<OwncastPlayerProps> = ({
|
||||||
|
source,
|
||||||
|
online,
|
||||||
|
initiallyMuted = false,
|
||||||
|
}) => {
|
||||||
const playerRef = React.useRef(null);
|
const playerRef = React.useRef(null);
|
||||||
const [videoPlaying, setVideoPlaying] = useRecoilState<boolean>(isVideoPlayingAtom);
|
const [videoPlaying, setVideoPlaying] = useRecoilState<boolean>(isVideoPlayingAtom);
|
||||||
const clockSkew = useRecoilValue<Number>(clockSkewAtom);
|
const clockSkew = useRecoilValue<Number>(clockSkewAtom);
|
||||||
@ -215,6 +221,7 @@ export const OwncastPlayer: FC<OwncastPlayerProps> = ({ source, online }) => {
|
|||||||
playsinline: true,
|
playsinline: true,
|
||||||
liveui: true,
|
liveui: true,
|
||||||
preload: 'auto',
|
preload: 'auto',
|
||||||
|
muted: initiallyMuted,
|
||||||
controlBar: {
|
controlBar: {
|
||||||
progressControl: {
|
progressControl: {
|
||||||
seekBar: false,
|
seekBar: false,
|
||||||
@ -239,7 +246,7 @@ export const OwncastPlayer: FC<OwncastPlayerProps> = ({ source, online }) => {
|
|||||||
type: 'application/x-mpegURL',
|
type: 'application/x-mpegURL',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
} satisfies VideoJsPlayerOptions;
|
||||||
|
|
||||||
const handlePlayerReady = (player, videojs) => {
|
const handlePlayerReady = (player, videojs) => {
|
||||||
playerRef.current = player;
|
playerRef.current = player;
|
||||||
|
@ -1,17 +1,17 @@
|
|||||||
import React, { FC } from 'react';
|
import React, { FC } from 'react';
|
||||||
import videojs from 'video.js';
|
import videojs, { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js';
|
||||||
import styles from './VideoJS.module.scss';
|
import styles from './VideoJS.module.scss';
|
||||||
|
|
||||||
require('video.js/dist/video-js.css');
|
require('video.js/dist/video-js.css');
|
||||||
|
|
||||||
export type VideoJSProps = {
|
export type VideoJSProps = {
|
||||||
options: any;
|
options: VideoJsPlayerOptions;
|
||||||
onReady: (player: videojs.Player, vjsInstance: videojs) => void;
|
onReady: (player: videojs.Player, vjsInstance: typeof videojs) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const VideoJS: FC<VideoJSProps> = ({ options, onReady }) => {
|
export const VideoJS: FC<VideoJSProps> = ({ options, onReady }) => {
|
||||||
const videoRef = React.useRef(null);
|
const videoRef = React.useRef<HTMLVideoElement | null>(null);
|
||||||
const playerRef = React.useRef(null);
|
const playerRef = React.useRef<VideoJsPlayer | null>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
// Make sure Video.js player is only initialized once
|
// Make sure Video.js player is only initialized once
|
||||||
@ -19,7 +19,7 @@ export const VideoJS: FC<VideoJSProps> = ({ options, onReady }) => {
|
|||||||
const videoElement = videoRef.current;
|
const videoElement = videoRef.current;
|
||||||
|
|
||||||
// eslint-disable-next-line no-multi-assign
|
// eslint-disable-next-line no-multi-assign
|
||||||
const player = (playerRef.current = videojs(videoElement, options, () => {
|
const player: VideoJsPlayer = (playerRef.current = videojs(videoElement, options, () => {
|
||||||
console.debug('player is ready');
|
console.debug('player is ready');
|
||||||
return onReady && onReady(player, videojs);
|
return onReady && onReady(player, videojs);
|
||||||
}));
|
}));
|
||||||
|
7
web/package-lock.json
generated
7
web/package-lock.json
generated
@ -79,6 +79,7 @@
|
|||||||
"@types/react": "18.0.26",
|
"@types/react": "18.0.26",
|
||||||
"@types/react-linkify": "1.0.1",
|
"@types/react-linkify": "1.0.1",
|
||||||
"@types/ua-parser-js": "0.7.36",
|
"@types/ua-parser-js": "0.7.36",
|
||||||
|
"@types/video.js": "^7.3.50",
|
||||||
"@typescript-eslint/eslint-plugin": "5.47.1",
|
"@typescript-eslint/eslint-plugin": "5.47.1",
|
||||||
"@typescript-eslint/parser": "5.47.1",
|
"@typescript-eslint/parser": "5.47.1",
|
||||||
"babel-loader": "9.1.0",
|
"babel-loader": "9.1.0",
|
||||||
@ -11973,6 +11974,12 @@
|
|||||||
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
|
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz",
|
||||||
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
|
"integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ=="
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/video.js": {
|
||||||
|
"version": "7.3.50",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/video.js/-/video.js-7.3.50.tgz",
|
||||||
|
"integrity": "sha512-xG0xoeyLGuWhtWMBBLRVhTEOfT2n6AjhNoWhFWVbpa6A8hSMi4eNvttuHYXsn6NslITu7IUdKPDRQ2bAWgXKDA==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/webpack": {
|
"node_modules/@types/webpack": {
|
||||||
"version": "4.41.33",
|
"version": "4.41.33",
|
||||||
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.33.tgz",
|
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.33.tgz",
|
||||||
|
@ -83,6 +83,7 @@
|
|||||||
"@types/react": "18.0.26",
|
"@types/react": "18.0.26",
|
||||||
"@types/react-linkify": "1.0.1",
|
"@types/react-linkify": "1.0.1",
|
||||||
"@types/ua-parser-js": "0.7.36",
|
"@types/ua-parser-js": "0.7.36",
|
||||||
|
"@types/video.js": "^7.3.50",
|
||||||
"@typescript-eslint/eslint-plugin": "5.47.1",
|
"@typescript-eslint/eslint-plugin": "5.47.1",
|
||||||
"@typescript-eslint/parser": "5.47.1",
|
"@typescript-eslint/parser": "5.47.1",
|
||||||
"babel-loader": "9.1.0",
|
"babel-loader": "9.1.0",
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
import {
|
import {
|
||||||
clientConfigStateAtom,
|
clientConfigStateAtom,
|
||||||
ClientConfigStore,
|
ClientConfigStore,
|
||||||
@ -21,11 +22,34 @@ export default function VideoEmbed() {
|
|||||||
const { offlineMessage } = clientConfig;
|
const { offlineMessage } = clientConfig;
|
||||||
const { viewerCount, lastConnectTime, lastDisconnectTime } = status;
|
const { viewerCount, lastConnectTime, lastDisconnectTime } = status;
|
||||||
const online = useRecoilValue<boolean>(isOnlineSelector);
|
const online = useRecoilValue<boolean>(isOnlineSelector);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* router.query isn't initialized until hydration
|
||||||
|
* (see https://github.com/vercel/next.js/discussions/11484)
|
||||||
|
* but router.asPath is initialized earlier, so we parse the
|
||||||
|
* query parameters ourselves
|
||||||
|
*/
|
||||||
|
const path = router.asPath.split('?')[1] ?? '';
|
||||||
|
const query = path.split('&').reduce((currQuery, part) => {
|
||||||
|
const [key, value] = part.split('=');
|
||||||
|
return { ...currQuery, [key]: value };
|
||||||
|
}, {} as Record<string, string>);
|
||||||
|
|
||||||
|
const initiallyMuted = query.initiallyMuted === 'true';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<ClientConfigStore />
|
<ClientConfigStore />
|
||||||
<div className="video-embed">
|
<div className="video-embed">
|
||||||
{online && <OwncastPlayer source="/hls/stream.m3u8" online={online} />}
|
{online && (
|
||||||
|
<OwncastPlayer
|
||||||
|
source="/hls/stream.m3u8"
|
||||||
|
online={online}
|
||||||
|
initiallyMuted={initiallyMuted}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{!online && (
|
{!online && (
|
||||||
<OfflineBanner
|
<OfflineBanner
|
||||||
streamName={name}
|
streamName={name}
|
||||||
|
69
web/stories/VideoEmbed.stories.tsx
Normal file
69
web/stories/VideoEmbed.stories.tsx
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
import { ComponentMeta, ComponentStory } from '@storybook/react';
|
||||||
|
|
||||||
|
const Template = ({
|
||||||
|
origin,
|
||||||
|
query,
|
||||||
|
title,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
}: {
|
||||||
|
origin: string;
|
||||||
|
query: string;
|
||||||
|
title: string;
|
||||||
|
width: number;
|
||||||
|
height: number;
|
||||||
|
}) => (
|
||||||
|
<iframe
|
||||||
|
src={`${origin}/embed/video?${query}`}
|
||||||
|
title={title}
|
||||||
|
height={`${height}px`}
|
||||||
|
width={`${width}px`}
|
||||||
|
referrerPolicy="origin"
|
||||||
|
scrolling="no"
|
||||||
|
allowFullScreen
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const origins = {
|
||||||
|
DemoServer: `https://watch.owncast.online`,
|
||||||
|
RetroStrangeTV: `https://live.retrostrange.com`,
|
||||||
|
localhost: `http://localhost:3000`,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: 'owncast/Player/Embeds',
|
||||||
|
component: Template,
|
||||||
|
argTypes: {
|
||||||
|
origin: {
|
||||||
|
options: Object.keys(origins),
|
||||||
|
mapping: origins,
|
||||||
|
control: {
|
||||||
|
type: 'select',
|
||||||
|
},
|
||||||
|
defaultValue: origins.DemoServer,
|
||||||
|
},
|
||||||
|
query: {
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
defaultValue: 'My Title',
|
||||||
|
type: 'string',
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
defaultValue: 350,
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
defaultValue: 550,
|
||||||
|
type: 'number',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies ComponentMeta<typeof Template>;
|
||||||
|
|
||||||
|
export const Default: ComponentStory<typeof Template> = Template.bind({});
|
||||||
|
Default.args = {};
|
||||||
|
|
||||||
|
export const InitiallyMuted: ComponentStory<typeof Template> = Template.bind({});
|
||||||
|
InitiallyMuted.args = {
|
||||||
|
query: 'initiallyMuted=true',
|
||||||
|
};
|
Loading…
x
Reference in New Issue
Block a user