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:
Michael David Kuckuk 2023-01-01 01:08:54 +01:00 committed by GitHub
parent db3e20b480
commit 2f2300db8d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 117 additions and 9 deletions

View file

@ -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<OwncastPlayerProps> = ({ source, online }) => {
export const OwncastPlayer: FC<OwncastPlayerProps> = ({
source,
online,
initiallyMuted = false,
}) => {
const playerRef = React.useRef(null);
const [videoPlaying, setVideoPlaying] = useRecoilState<boolean>(isVideoPlayingAtom);
const clockSkew = useRecoilValue<Number>(clockSkewAtom);
@ -215,6 +221,7 @@ export const OwncastPlayer: FC<OwncastPlayerProps> = ({ source, online }) => {
playsinline: true,
liveui: true,
preload: 'auto',
muted: initiallyMuted,
controlBar: {
progressControl: {
seekBar: false,
@ -239,7 +246,7 @@ export const OwncastPlayer: FC<OwncastPlayerProps> = ({ source, online }) => {
type: 'application/x-mpegURL',
},
],
};
} satisfies VideoJsPlayerOptions;
const handlePlayerReady = (player, videojs) => {
playerRef.current = player;

View file

@ -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<VideoJSProps> = ({ options, onReady }) => {
const videoRef = React.useRef(null);
const playerRef = React.useRef(null);
const videoRef = React.useRef<HTMLVideoElement | null>(null);
const playerRef = React.useRef<VideoJsPlayer | null>(null);
React.useEffect(() => {
// Make sure Video.js player is only initialized once
@ -19,7 +19,7 @@ export const VideoJS: FC<VideoJSProps> = ({ 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);
}));

7
web/package-lock.json generated
View file

@ -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",

View file

@ -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",

View file

@ -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<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 (
<>
<ClientConfigStore />
<div className="video-embed">
{online && <OwncastPlayer source="/hls/stream.m3u8" online={online} />}
{online && (
<OwncastPlayer
source="/hls/stream.m3u8"
online={online}
initiallyMuted={initiallyMuted}
/>
)}
{!online && (
<OfflineBanner
streamName={name}

View 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',
};