From 2f2300db8dec53dcb464ffb89c9819aa49a450bb Mon Sep 17 00:00:00 2001 From: Michael David Kuckuk <8076094+LBBO@users.noreply.github.com> Date: Sun, 1 Jan 2023 01:08:54 +0100 Subject: [PATCH] Add `initiallyMuted` query parameter to embed player (#2539) * Add query param to initially mute embed player * Add stories for embed player * Improve VideoJS typing --- .../video/OwncastPlayer/OwncastPlayer.tsx | 11 ++- web/components/video/VideoJS/VideoJS.tsx | 12 ++-- web/package-lock.json | 7 ++ web/package.json | 1 + web/pages/embed/video/index.tsx | 26 ++++++- web/stories/VideoEmbed.stories.tsx | 69 +++++++++++++++++++ 6 files changed, 117 insertions(+), 9 deletions(-) create mode 100644 web/stories/VideoEmbed.stories.tsx diff --git a/web/components/video/OwncastPlayer/OwncastPlayer.tsx b/web/components/video/OwncastPlayer/OwncastPlayer.tsx index 4d5733fbf..884e9cfaf 100644 --- a/web/components/video/OwncastPlayer/OwncastPlayer.tsx +++ b/web/components/video/OwncastPlayer/OwncastPlayer.tsx @@ -1,6 +1,7 @@ import React, { FC, useEffect } from 'react'; import { useRecoilState, useRecoilValue } from 'recoil'; import { useHotkeys } from 'react-hotkeys-hook'; +import { VideoJsPlayerOptions } from 'video.js'; import { VideoJS } from '../VideoJS/VideoJS'; import ViewerPing from '../viewer-ping'; import { VideoPoster } from '../VideoPoster/VideoPoster'; @@ -24,6 +25,7 @@ let latencyCompensatorEnabled = false; export type OwncastPlayerProps = { source: string; online: boolean; + initiallyMuted?: boolean; }; async function getVideoSettings() { @@ -38,7 +40,11 @@ async function getVideoSettings() { return qualities; } -export const OwncastPlayer: FC = ({ source, online }) => { +export const OwncastPlayer: FC = ({ + source, + online, + initiallyMuted = false, +}) => { const playerRef = React.useRef(null); const [videoPlaying, setVideoPlaying] = useRecoilState(isVideoPlayingAtom); const clockSkew = useRecoilValue(clockSkewAtom); @@ -215,6 +221,7 @@ export const OwncastPlayer: FC = ({ source, online }) => { playsinline: true, liveui: true, preload: 'auto', + muted: initiallyMuted, controlBar: { progressControl: { seekBar: false, @@ -239,7 +246,7 @@ export const OwncastPlayer: FC = ({ source, online }) => { type: 'application/x-mpegURL', }, ], - }; + } satisfies VideoJsPlayerOptions; const handlePlayerReady = (player, videojs) => { playerRef.current = player; diff --git a/web/components/video/VideoJS/VideoJS.tsx b/web/components/video/VideoJS/VideoJS.tsx index 101a2e8d1..81e2276ac 100644 --- a/web/components/video/VideoJS/VideoJS.tsx +++ b/web/components/video/VideoJS/VideoJS.tsx @@ -1,17 +1,17 @@ import React, { FC } from 'react'; -import videojs from 'video.js'; +import videojs, { VideoJsPlayer, VideoJsPlayerOptions } from 'video.js'; import styles from './VideoJS.module.scss'; require('video.js/dist/video-js.css'); export type VideoJSProps = { - options: any; - onReady: (player: videojs.Player, vjsInstance: videojs) => void; + options: VideoJsPlayerOptions; + onReady: (player: videojs.Player, vjsInstance: typeof videojs) => void; }; export const VideoJS: FC = ({ options, onReady }) => { - const videoRef = React.useRef(null); - const playerRef = React.useRef(null); + const videoRef = React.useRef(null); + const playerRef = React.useRef(null); React.useEffect(() => { // Make sure Video.js player is only initialized once @@ -19,7 +19,7 @@ export const VideoJS: FC = ({ options, onReady }) => { const videoElement = videoRef.current; // 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'); return onReady && onReady(player, videojs); })); diff --git a/web/package-lock.json b/web/package-lock.json index 739856b96..e40d59c0e 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -79,6 +79,7 @@ "@types/react": "18.0.26", "@types/react-linkify": "1.0.1", "@types/ua-parser-js": "0.7.36", + "@types/video.js": "^7.3.50", "@typescript-eslint/eslint-plugin": "5.47.1", "@typescript-eslint/parser": "5.47.1", "babel-loader": "9.1.0", @@ -11973,6 +11974,12 @@ "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", "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": { "version": "4.41.33", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.33.tgz", diff --git a/web/package.json b/web/package.json index 20c1e983d..dad76412e 100644 --- a/web/package.json +++ b/web/package.json @@ -83,6 +83,7 @@ "@types/react": "18.0.26", "@types/react-linkify": "1.0.1", "@types/ua-parser-js": "0.7.36", + "@types/video.js": "^7.3.50", "@typescript-eslint/eslint-plugin": "5.47.1", "@typescript-eslint/parser": "5.47.1", "babel-loader": "9.1.0", diff --git a/web/pages/embed/video/index.tsx b/web/pages/embed/video/index.tsx index 0855cc601..9f3289483 100644 --- a/web/pages/embed/video/index.tsx +++ b/web/pages/embed/video/index.tsx @@ -1,5 +1,6 @@ import React from 'react'; import { useRecoilValue } from 'recoil'; +import { useRouter } from 'next/router'; import { clientConfigStateAtom, ClientConfigStore, @@ -21,11 +22,34 @@ export default function VideoEmbed() { const { offlineMessage } = clientConfig; const { viewerCount, lastConnectTime, lastDisconnectTime } = status; const online = useRecoilValue(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); + + const initiallyMuted = query.initiallyMuted === 'true'; + return ( <>
- {online && } + {online && ( + + )} {!online && ( ( +