owncast/web/components/video/OwncastPlayer/OwncastPlayer.tsx

318 lines
8.5 KiB
TypeScript
Raw Normal View History

import React, { FC, useContext, useEffect } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import { useHotkeys } from 'react-hotkeys-hook';
import { VideoJsPlayerOptions } from 'video.js';
import classNames from 'classnames';
reafctor: normalize component formatting (#2082) * refactor: move/rename BanUserButton file * refactor: move/rename Chart file * refactor: update generic component filenames to PascalCase * refactor: update config component filenames to PascalCase * refactor: update AdminLayout component filename to PascalCase * refactor: update/move VideoJS component * chore(eslint): disable bad react/require-default-props rule * refactor: normalize ActionButton component * refactor: normalize ActionButtonRow component * refactor: normalize FollowButton component * refactor: normalize NotifyButton component * refactor: normalize ChatActionMessage component * refactor: normalize ChatContainer component * refactor: normalize ChatJoinMessage component * refactor: normalize ChatModerationActionMenu component * refactor: normalize ChatModerationDetailsModal component * refactor: normalize ChatModeratorNotification component * refactor: normalize ChatSocialMessage component * refactor: normalize ChatSystemMessage component * refactor: normalize ChatTextField component * refactor: normalize ChatUserBadge component * refactor: normalize ChatUserMessage component * refactor: normalize ContentHeader component * refactor: normalize OwncastLogo component * refactor: normalize UserDropdown component * chore(eslint): modify react/function-component-definition rule * refactor: normalize CodecSelector component * refactor: update a bunch of functional components using eslint * refactor: update a bunch of functional components using eslint, pt2 * refactor: update a bunch of functional components using eslint, pt3 * refactor: replace all component->component default imports with named imports * refactor: replace all component-stories->component default imports with named imports * refactor: remove default exports from most components * chore(eslint): add eslint config files for the components and pages dirs * fix: use-before-define error in ChatContainer * Fix ChatContainer import * Only process .tsx files in Next builds Co-authored-by: Gabe Kangas <gabek@real-ity.com>
2022-09-07 10:00:28 +03:00
import { VideoJS } from '../VideoJS/VideoJS';
refactor(stories): co-locate stories with components (#2078) * refactor: move ActionButton component * refactor: move BanUserButton component * refactor: move ChatActionMessage component * refactor: move ChatContainer component * refactor: move AuthModal component * refactor: move BrowserNotifyModal component * refactor: move ChatUserMessage component * refactor: move ChatJoinMessage component * refactor: move ChatTextField component * refactor: move ChatUserBadge component * refactor: move FollowerCollection and SingleFollower components * fix: bad import path * refactor: move FollowModal component * refactor: move Modal component * refactor: move ContentHeader component * refactor: move ChatSystemMessage component * refactor: move Header component * refactor: move Footer component * refactor: move StatusBar component * refactor: move OfflineBanner component * refactor: move OwncastPlayer component * refactor: move IndieAuthModal component * refactor: move SocialLinks component * refactor: move VideoPoster component * refactor: move FollowModal component * refactor: move FediAuthModal.tsx component * refactor: move UserDropdown component * refactor: move ChatSocialMessage component * refactor: move Logo component * refactor: move NotifyReminderPopup component * refactor: move NameChangeModal component * refactor: move FatalErrorStateModal component * refactor: move ChatModeratorNotification component * refactor: move ChatModerationActionMenu and ChatModerationDetailsModal components * refactor: move CustomPageContent component * refactor: move storybook Introduction file * refactor: update storybook story import path * refactor: move storybook preview styles * refactor: move storybook doc pages * refactor: move Color and ImageAsset components * fix: bad import path * fix: bad import path in story file
2022-09-03 21:38:52 +03:00
import ViewerPing from '../viewer-ping';
reafctor: normalize component formatting (#2082) * refactor: move/rename BanUserButton file * refactor: move/rename Chart file * refactor: update generic component filenames to PascalCase * refactor: update config component filenames to PascalCase * refactor: update AdminLayout component filename to PascalCase * refactor: update/move VideoJS component * chore(eslint): disable bad react/require-default-props rule * refactor: normalize ActionButton component * refactor: normalize ActionButtonRow component * refactor: normalize FollowButton component * refactor: normalize NotifyButton component * refactor: normalize ChatActionMessage component * refactor: normalize ChatContainer component * refactor: normalize ChatJoinMessage component * refactor: normalize ChatModerationActionMenu component * refactor: normalize ChatModerationDetailsModal component * refactor: normalize ChatModeratorNotification component * refactor: normalize ChatSocialMessage component * refactor: normalize ChatSystemMessage component * refactor: normalize ChatTextField component * refactor: normalize ChatUserBadge component * refactor: normalize ChatUserMessage component * refactor: normalize ContentHeader component * refactor: normalize OwncastLogo component * refactor: normalize UserDropdown component * chore(eslint): modify react/function-component-definition rule * refactor: normalize CodecSelector component * refactor: update a bunch of functional components using eslint * refactor: update a bunch of functional components using eslint, pt2 * refactor: update a bunch of functional components using eslint, pt3 * refactor: replace all component->component default imports with named imports * refactor: replace all component-stories->component default imports with named imports * refactor: remove default exports from most components * chore(eslint): add eslint config files for the components and pages dirs * fix: use-before-define error in ChatContainer * Fix ChatContainer import * Only process .tsx files in Next builds Co-authored-by: Gabe Kangas <gabek@real-ity.com>
2022-09-07 10:00:28 +03:00
import { VideoPoster } from '../VideoPoster/VideoPoster';
refactor(stories): co-locate stories with components (#2078) * refactor: move ActionButton component * refactor: move BanUserButton component * refactor: move ChatActionMessage component * refactor: move ChatContainer component * refactor: move AuthModal component * refactor: move BrowserNotifyModal component * refactor: move ChatUserMessage component * refactor: move ChatJoinMessage component * refactor: move ChatTextField component * refactor: move ChatUserBadge component * refactor: move FollowerCollection and SingleFollower components * fix: bad import path * refactor: move FollowModal component * refactor: move Modal component * refactor: move ContentHeader component * refactor: move ChatSystemMessage component * refactor: move Header component * refactor: move Footer component * refactor: move StatusBar component * refactor: move OfflineBanner component * refactor: move OwncastPlayer component * refactor: move IndieAuthModal component * refactor: move SocialLinks component * refactor: move VideoPoster component * refactor: move FollowModal component * refactor: move FediAuthModal.tsx component * refactor: move UserDropdown component * refactor: move ChatSocialMessage component * refactor: move Logo component * refactor: move NotifyReminderPopup component * refactor: move NameChangeModal component * refactor: move FatalErrorStateModal component * refactor: move ChatModeratorNotification component * refactor: move ChatModerationActionMenu and ChatModerationDetailsModal components * refactor: move CustomPageContent component * refactor: move storybook Introduction file * refactor: update storybook story import path * refactor: move storybook preview styles * refactor: move storybook doc pages * refactor: move Color and ImageAsset components * fix: bad import path * fix: bad import path in story file
2022-09-03 21:38:52 +03:00
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';
const PLAYER_VOLUME = 'owncast_volume';
const LATENCY_COMPENSATION_ENABLED = 'latencyCompensatorEnabled';
const ping = new ViewerPing();
let playbackMetrics = null;
let latencyCompensator = null;
let latencyCompensatorEnabled = false;
reafctor: normalize component formatting (#2082) * refactor: move/rename BanUserButton file * refactor: move/rename Chart file * refactor: update generic component filenames to PascalCase * refactor: update config component filenames to PascalCase * refactor: update AdminLayout component filename to PascalCase * refactor: update/move VideoJS component * chore(eslint): disable bad react/require-default-props rule * refactor: normalize ActionButton component * refactor: normalize ActionButtonRow component * refactor: normalize FollowButton component * refactor: normalize NotifyButton component * refactor: normalize ChatActionMessage component * refactor: normalize ChatContainer component * refactor: normalize ChatJoinMessage component * refactor: normalize ChatModerationActionMenu component * refactor: normalize ChatModerationDetailsModal component * refactor: normalize ChatModeratorNotification component * refactor: normalize ChatSocialMessage component * refactor: normalize ChatSystemMessage component * refactor: normalize ChatTextField component * refactor: normalize ChatUserBadge component * refactor: normalize ChatUserMessage component * refactor: normalize ContentHeader component * refactor: normalize OwncastLogo component * refactor: normalize UserDropdown component * chore(eslint): modify react/function-component-definition rule * refactor: normalize CodecSelector component * refactor: update a bunch of functional components using eslint * refactor: update a bunch of functional components using eslint, pt2 * refactor: update a bunch of functional components using eslint, pt3 * refactor: replace all component->component default imports with named imports * refactor: replace all component-stories->component default imports with named imports * refactor: remove default exports from most components * chore(eslint): add eslint config files for the components and pages dirs * fix: use-before-define error in ChatContainer * Fix ChatContainer import * Only process .tsx files in Next builds Co-authored-by: Gabe Kangas <gabek@real-ity.com>
2022-09-07 10:00:28 +03:00
export type OwncastPlayerProps = {
2022-05-11 01:36:09 +03:00
source: string;
online: boolean;
initiallyMuted?: boolean;
title: string;
className?: string;
reafctor: normalize component formatting (#2082) * refactor: move/rename BanUserButton file * refactor: move/rename Chart file * refactor: update generic component filenames to PascalCase * refactor: update config component filenames to PascalCase * refactor: update AdminLayout component filename to PascalCase * refactor: update/move VideoJS component * chore(eslint): disable bad react/require-default-props rule * refactor: normalize ActionButton component * refactor: normalize ActionButtonRow component * refactor: normalize FollowButton component * refactor: normalize NotifyButton component * refactor: normalize ChatActionMessage component * refactor: normalize ChatContainer component * refactor: normalize ChatJoinMessage component * refactor: normalize ChatModerationActionMenu component * refactor: normalize ChatModerationDetailsModal component * refactor: normalize ChatModeratorNotification component * refactor: normalize ChatSocialMessage component * refactor: normalize ChatSystemMessage component * refactor: normalize ChatTextField component * refactor: normalize ChatUserBadge component * refactor: normalize ChatUserMessage component * refactor: normalize ContentHeader component * refactor: normalize OwncastLogo component * refactor: normalize UserDropdown component * chore(eslint): modify react/function-component-definition rule * refactor: normalize CodecSelector component * refactor: update a bunch of functional components using eslint * refactor: update a bunch of functional components using eslint, pt2 * refactor: update a bunch of functional components using eslint, pt3 * refactor: replace all component->component default imports with named imports * refactor: replace all component-stories->component default imports with named imports * refactor: remove default exports from most components * chore(eslint): add eslint config files for the components and pages dirs * fix: use-before-define error in ChatContainer * Fix ChatContainer import * Only process .tsx files in Next builds Co-authored-by: Gabe Kangas <gabek@real-ity.com>
2022-09-07 10:00:28 +03:00
};
2022-05-11 01:36:09 +03:00
export const OwncastPlayer: FC<OwncastPlayerProps> = ({
source,
online,
initiallyMuted = false,
title,
className,
}) => {
const VideoSettingsService = useContext(VideoSettingsServiceContext);
2022-04-27 05:29:13 +03:00
const playerRef = React.useRef(null);
const [videoPlaying, setVideoPlaying] = useRecoilState<boolean>(isVideoPlayingAtom);
const clockSkew = useRecoilValue<Number>(clockSkewAtom);
2022-04-27 05:29:13 +03:00
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 setLatencyCompensatorItemTitle = t => {
const item = document.querySelector('.latency-toggle-item > .vjs-menu-item-text');
if (!item) {
return;
}
item.innerHTML = t;
};
const startLatencyCompensator = () => {
if (latencyCompensator) {
latencyCompensator.stop();
}
latencyCompensatorEnabled = true;
latencyCompensator = new LatencyCompensator(playerRef.current);
latencyCompensator.setClockSkew(clockSkew);
latencyCompensator.enable();
setLocalStorage(LATENCY_COMPENSATION_ENABLED, true);
setLatencyCompensatorItemTitle('disable minimized latency');
};
const stopLatencyCompensator = () => {
if (latencyCompensator) {
latencyCompensator.disable();
}
latencyCompensator = null;
latencyCompensatorEnabled = false;
setLocalStorage(LATENCY_COMPENSATION_ENABLED, false);
setLatencyCompensatorItemTitle(
'<span style="font-size: 0.8em">enable minimized latency (experimental)</span>',
);
};
const toggleLatencyCompensator = () => {
if (latencyCompensatorEnabled) {
stopLatencyCompensator();
} else {
startLatencyCompensator();
}
};
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,
});
2022-04-27 05:29:13 +03:00
const videoJsOptions = {
autoplay: false,
controls: true,
responsive: true,
fluid: false,
fill: true,
playsinline: true,
2022-04-27 05:29:13 +03:00
liveui: true,
preload: 'auto',
muted: initiallyMuted,
2022-04-27 05:29:13 +03:00
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: [
{
2022-05-11 01:36:09 +03:00
src: source,
2022-04-27 05:29:13 +03:00
type: 'application/x-mpegURL',
},
],
} satisfies VideoJsPlayerOptions;
2022-04-27 05:29:13 +03:00
const handlePlayerReady = (player, videojs) => {
2022-04-27 05:29:13 +03:00
playerRef.current = player;
setSavedVolume();
setupAirplay(player, videojs);
2022-04-27 05:29:13 +03:00
// You can handle player events here, for example:
player.on('waiting', () => {
console.debug('player is waiting');
2022-04-27 05:29:13 +03:00
});
player.on('dispose', () => {
console.debug('player will dispose');
ping.stop();
2022-04-27 05:29:13 +03:00
});
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);
});
2022-06-19 23:49:42 +03:00
videojs.hookOnce();
player.on('volumechange', handleVolume);
playbackMetrics = new PlaybackMetrics(player, videojs);
playbackMetrics.setClockSkew(clockSkew);
2022-06-19 23:49:42 +03:00
createSettings(player, videojs);
2022-04-27 05:29:13 +03:00
};
useEffect(() => {
if (playbackMetrics) {
playbackMetrics.setClockSkew(clockSkew);
}
}, [clockSkew]);
useEffect(
() => () => {
stopLatencyCompensator();
playbackMetrics?.stop();
},
[],
);
2022-05-11 01:36:09 +03:00
return (
<div className={classNames(styles.container, className)} id="player">
2022-05-11 01:36:09 +03:00
{online && (
<div className={styles.player}>
<VideoJS options={videoJsOptions} onReady={handlePlayerReady} aria-label={title} />
2022-05-11 01:36:09 +03:00
</div>
)}
<div className={styles.poster}>
{!videoPlaying && (
<VideoPoster online={online} initialSrc="/thumbnail.jpg" src="/thumbnail.jpg" />
)}
2022-05-11 01:36:09 +03:00
</div>
</div>
);
reafctor: normalize component formatting (#2082) * refactor: move/rename BanUserButton file * refactor: move/rename Chart file * refactor: update generic component filenames to PascalCase * refactor: update config component filenames to PascalCase * refactor: update AdminLayout component filename to PascalCase * refactor: update/move VideoJS component * chore(eslint): disable bad react/require-default-props rule * refactor: normalize ActionButton component * refactor: normalize ActionButtonRow component * refactor: normalize FollowButton component * refactor: normalize NotifyButton component * refactor: normalize ChatActionMessage component * refactor: normalize ChatContainer component * refactor: normalize ChatJoinMessage component * refactor: normalize ChatModerationActionMenu component * refactor: normalize ChatModerationDetailsModal component * refactor: normalize ChatModeratorNotification component * refactor: normalize ChatSocialMessage component * refactor: normalize ChatSystemMessage component * refactor: normalize ChatTextField component * refactor: normalize ChatUserBadge component * refactor: normalize ChatUserMessage component * refactor: normalize ContentHeader component * refactor: normalize OwncastLogo component * refactor: normalize UserDropdown component * chore(eslint): modify react/function-component-definition rule * refactor: normalize CodecSelector component * refactor: update a bunch of functional components using eslint * refactor: update a bunch of functional components using eslint, pt2 * refactor: update a bunch of functional components using eslint, pt3 * refactor: replace all component->component default imports with named imports * refactor: replace all component-stories->component default imports with named imports * refactor: remove default exports from most components * chore(eslint): add eslint config files for the components and pages dirs * fix: use-before-define error in ChatContainer * Fix ChatContainer import * Only process .tsx files in Next builds Co-authored-by: Gabe Kangas <gabek@real-ity.com>
2022-09-07 10:00:28 +03:00
};
export default OwncastPlayer;