owncast/web/components/video/OwncastPlayer/OwncastPlayer.tsx
Patrick Bollinger 9a7925444f
Fix embedded status bar being cut off (#3352)
* Fix embedded status bar being cut off

This should resolve https://github.com/owncast/owncast/issues/3210.

As recommended in the discussion, flexbox was the way to solve this issue.
The main thing that needed to be addressed though was not applying too many constraints on the OwncastPlayer component just because it was embedded.
By removing the embedded constraints, styling appears to be working as expected.
I'll attach screenshots to the pull request.

* Fix eslint error
2023-10-21 21:46:10 -07:00

316 lines
8.6 KiB
TypeScript

import React, { FC, useContext, useEffect } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useHotkeys } from 'react-hotkeys-hook';
import classNames from 'classnames';
import { ErrorBoundary } from 'react-error-boundary';
import { VideoJS } from '../VideoJS/VideoJS';
import ViewerPing from '../viewer-ping';
import { VideoPoster } from '../VideoPoster/VideoPoster';
import { getLocalStorage, setLocalStorage } from '../../../utils/localStorage';
import { isVideoPlayingAtom, clockSkewAtom } from '../../stores/ClientConfigStore';
import PlaybackMetrics from '../metrics/playback';
import { createVideoSettingsMenuButton } from '../settings-menu';
import LatencyCompensator from '../latencyCompensator';
import styles from './OwncastPlayer.module.scss';
import { VideoSettingsServiceContext } from '../../../services/video-settings-service';
import { ComponentError } from '../../ui/ComponentError/ComponentError';
const PLAYER_VOLUME = 'owncast_volume';
const LATENCY_COMPENSATION_ENABLED = 'latencyCompensatorEnabled';
const ping = new ViewerPing();
let playbackMetrics = null;
let latencyCompensator = null;
let latencyCompensatorEnabled = false;
export type OwncastPlayerProps = {
source: string;
online: boolean;
initiallyMuted?: boolean;
title: string;
className?: string;
};
export const OwncastPlayer: FC<OwncastPlayerProps> = ({
source,
online,
initiallyMuted = false,
title,
className,
}) => {
const VideoSettingsService = useContext(VideoSettingsServiceContext);
const playerRef = React.useRef(null);
const [videoPlaying, setVideoPlaying] = useRecoilState<boolean>(isVideoPlayingAtom);
const clockSkew = useRecoilValue<Number>(clockSkewAtom);
const setSavedVolume = () => {
try {
playerRef.current.volume(getLocalStorage(PLAYER_VOLUME) || 1);
} catch (err) {
console.warn(err);
}
};
const handleVolume = () => {
setLocalStorage(PLAYER_VOLUME, playerRef.current.muted() ? 0 : playerRef.current.volume());
};
const togglePlayback = () => {
if (playerRef.current.paused()) {
playerRef.current.play();
} else {
playerRef.current.pause();
}
};
const toggleMute = () => {
if (playerRef.current.muted() || playerRef.current.volume() === 0) {
playerRef.current.volume(0.7);
} else {
playerRef.current.volume(0);
}
};
const toggleFullScreen = () => {
if (playerRef.current.isFullscreen()) {
playerRef.current.exitFullscreen();
} else {
playerRef.current.requestFullscreen();
}
};
const startLatencyCompensator = () => {
if (latencyCompensator) {
latencyCompensator.stop();
}
latencyCompensatorEnabled = true;
latencyCompensator = new LatencyCompensator(playerRef.current);
latencyCompensator.setClockSkew(clockSkew);
latencyCompensator.enable();
setLocalStorage(LATENCY_COMPENSATION_ENABLED, true);
};
const stopLatencyCompensator = () => {
if (latencyCompensator) {
latencyCompensator.disable();
}
latencyCompensator = null;
latencyCompensatorEnabled = false;
setLocalStorage(LATENCY_COMPENSATION_ENABLED, false);
};
// Toggle minimized latency mode. Return the new state.
const toggleLatencyCompensator = () => {
if (latencyCompensatorEnabled) {
stopLatencyCompensator();
} else {
startLatencyCompensator();
}
return latencyCompensatorEnabled;
};
const setupLatencyCompensator = player => {
const tech = player.tech({ IWillNotUseThisInPlugins: true });
// VHS is required.
if (!tech || !tech.vhs) {
return;
}
const latencyCompensatorEnabledSaved = getLocalStorage(LATENCY_COMPENSATION_ENABLED);
if (latencyCompensatorEnabledSaved === 'true' && tech && tech.vhs) {
startLatencyCompensator();
} else {
stopLatencyCompensator();
}
};
const createSettings = async (player, videojs) => {
const videoQualities = await VideoSettingsService.getVideoQualities();
const menuButton = createVideoSettingsMenuButton(
player,
videojs,
videoQualities,
toggleLatencyCompensator,
);
player.controlBar.addChild(
menuButton,
{},
// eslint-disable-next-line no-underscore-dangle
player.controlBar.children_.length - 2,
);
setupLatencyCompensator(player);
};
const setupAirplay = (player, videojs) => {
// eslint-disable-next-line no-prototype-builtins
if (window.hasOwnProperty('WebKitPlaybackTargetAvailabilityEvent')) {
const VJSButtonClass = videojs.getComponent('Button');
class ConcreteButtonClass extends VJSButtonClass {
constructor() {
super(player);
}
// eslint-disable-next-line class-methods-use-this
handleClick() {
try {
const videoElement = document.getElementsByTagName('video')[0];
(videoElement as any).webkitShowPlaybackTargetPicker();
} catch (e) {
console.error(e);
}
}
}
const ccbc = new ConcreteButtonClass();
const concreteButtonInstance = player.controlBar.addChild(ccbc);
concreteButtonInstance.addClass('vjs-airplay');
}
};
// Register keyboard shortcut for the space bar to toggle playback
useHotkeys('space', e => {
e.preventDefault();
togglePlayback();
});
// Register keyboard shortcut for f to toggle full screen
useHotkeys('f', toggleFullScreen, {
enableOnContentEditable: false,
});
// Register keyboard shortcut for the "m" key to toggle mute
useHotkeys('m', toggleMute, {
enableOnContentEditable: false,
});
useHotkeys('0', () => playerRef.current.volume(playerRef.current.volume() + 0.1), {
enableOnContentEditable: false,
});
useHotkeys('9', () => playerRef.current.volume(playerRef.current.volume() - 0.1), {
enableOnContentEditable: false,
});
const videoJsOptions = {
autoplay: false,
controls: true,
responsive: true,
fluid: false,
fill: true,
playsinline: true,
liveui: true,
preload: 'auto',
muted: initiallyMuted,
controlBar: {
progressControl: {
seekBar: false,
},
},
html5: {
vhs: {
// used to select the lowest bitrate playlist initially. This helps to decrease playback start time. This setting is false by default.
enableLowInitialPlaylist: true,
experimentalBufferBasedABR: true,
useNetworkInformationApi: true,
maxPlaylistRetries: 30,
},
},
liveTracker: {
trackingThreshold: 0,
liveTolerance: 15,
},
sources: [
{
src: source,
type: 'application/x-mpegURL',
},
],
};
const handlePlayerReady = (player, videojs) => {
playerRef.current = player;
setSavedVolume();
setupAirplay(player, videojs);
// You can handle player events here, for example:
player.on('waiting', () => {
console.debug('player is waiting');
});
player.on('dispose', () => {
console.debug('player will dispose');
ping.stop();
});
player.on('playing', () => {
console.debug('player is playing');
ping.start();
setVideoPlaying(true);
});
player.on('pause', () => {
console.debug('player is paused');
ping.stop();
setVideoPlaying(false);
});
player.on('ended', () => {
console.debug('player is ended');
ping.stop();
setVideoPlaying(false);
});
videojs.hookOnce();
player.on('volumechange', handleVolume);
playbackMetrics = new PlaybackMetrics(player, videojs);
playbackMetrics.setClockSkew(clockSkew);
createSettings(player, videojs);
};
useEffect(() => {
if (playbackMetrics) {
playbackMetrics.setClockSkew(clockSkew);
}
}, [clockSkew]);
useEffect(
() => () => {
stopLatencyCompensator();
playbackMetrics?.stop();
},
[],
);
return (
<ErrorBoundary
// eslint-disable-next-line react/no-unstable-nested-components
fallbackRender={({ error, resetErrorBoundary }) => (
<ComponentError
componentName="OwncastPlayer"
message={error.message}
retryFunction={resetErrorBoundary}
/>
)}
>
<div className={classNames(styles.container, className)} id="player">
{online && (
<div className={styles.player}>
<VideoJS options={videoJsOptions} onReady={handlePlayerReady} aria-label={title} />
</div>
)}
<div className={styles.poster}>
{!videoPlaying && (
<VideoPoster online={online} initialSrc="/thumbnail.jpg" src="/thumbnail.jpg" />
)}
</div>
</div>
</ErrorBoundary>
);
};