Add basic player component

This commit is contained in:
Gabe Kangas
2022-04-26 19:29:13 -07:00
parent 35546c0c6d
commit 07c6faad60
10 changed files with 886 additions and 14 deletions

View File

@@ -0,0 +1,57 @@
import React from 'react';
// This imports the functional component from the previous sample.
import VideoJS from './player';
export default function OwncastPlayer(props) {
const playerRef = React.useRef(null);
const { source } = props;
const videoJsOptions = {
autoplay: false,
controls: true,
responsive: true,
fluid: true,
liveui: true,
preload: 'auto',
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}/hls/stream.m3u8`,
type: 'application/x-mpegURL',
},
],
};
const handlePlayerReady = player => {
playerRef.current = player;
// You can handle player events here, for example:
player.on('waiting', () => {
player.log('player is waiting');
});
player.on('dispose', () => {
player.log('player will dispose');
});
};
return <VideoJS options={videoJsOptions} onReady={handlePlayerReady} />;
}

View File

@@ -0,0 +1,438 @@
import React from 'react';
import videojs from 'video.js';
require('video.js/dist/video-js.css');
// import { getLocalStorage, setLocalStorage } from '../../utils/helpers.js';
// import { PLAYER_VOLUME, URL_STREAM } from '../../utils/constants.js';
export function VideoJS(props) {
const videoRef = React.useRef(null);
const playerRef = React.useRef(null);
const { options, onReady } = props;
React.useEffect(() => {
// Make sure Video.js player is only initialized once
if (!playerRef.current) {
const videoElement = videoRef.current;
// if (!videoElement) return;
const player = (playerRef.current = videojs(videoElement, options, () => {
player.log('player is ready');
onReady && onReady(player);
}));
// You can update player in the `else` block here, for example:
// } else {
player.autoplay(options.autoplay);
player.src(options.sources);
}
}, [options, videoRef]);
// Dispose the Video.js player when the functional component unmounts
React.useEffect(() => {
const player = playerRef.current;
return () => {
if (player) {
player.dispose();
playerRef.current = null;
}
};
}, [playerRef]);
return (
<div data-vjs-player>
<video ref={videoRef} className="video-js vjs-big-play-centered" />
</div>
);
}
export default VideoJS;
// import PlaybackMetrics from '../metrics/playback.js';
// import LatencyCompensator from './latencyCompensator.js';
// const VIDEO_ID = 'video';
// const LATENCY_COMPENSATION_ENABLED = 'latencyCompensatorEnabled';
// // Video setup
// const VIDEO_SRC = {
// src: URL_STREAM,
// type: 'application/x-mpegURL',
// };
// const VIDEO_OPTIONS = {
// autoplay: false,
// liveui: true,
// preload: 'auto',
// 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: [VIDEO_SRC],
// };
// export const POSTER_DEFAULT = `/img/logo.png`;
// export const POSTER_THUMB = `/thumbnail.jpg`;
// export default class MenuSeparator extends VjsMenuItem {
// constructor(player, options) {
// super(player, options);
// }
// createEl(tag = 'button', props = {}, attributes = {}) {
// const el = super.createEl(tag, props, attributes);
// el.innerHTML = '<hr style="opacity: 0.3; margin-left: 10px; margin-right: 10px;" />';
// return el;
// }
// }
// class OwncastPlayer {
// constructor() {
// window.VIDEOJS_NO_DYNAMIC_STYLE = true; // style override
// this.vjsPlayer = null;
// this.latencyCompensator = null;
// this.playbackMetrics = null;
// this.appPlayerReadyCallback = null;
// this.appPlayerPlayingCallback = null;
// this.appPlayerEndedCallback = null;
// this.hasStartedPlayback = false;
// this.latencyCompensatorEnabled = false;
// // bind all the things because safari
// this.startPlayer = this.startPlayer.bind(this);
// this.handleReady = this.handleReady.bind(this);
// this.handlePlaying = this.handlePlaying.bind(this);
// this.handleVolume = this.handleVolume.bind(this);
// this.handleEnded = this.handleEnded.bind(this);
// this.handleError = this.handleError.bind(this);
// this.addQualitySelector = this.addVideoSettingsMenu.bind(this);
// this.addQualitySelector = this.addVideoSettingsMenu.bind(this);
// this.toggleLatencyCompensator = this.toggleLatencyCompensator.bind(this);
// this.startLatencyCompensator = this.startLatencyCompensator.bind(this);
// this.stopLatencyCompensator = this.stopLatencyCompensator.bind(this);
// this.qualitySelectionMenu = null;
// this.latencyCompensatorToggleButton = null;
// }
// init() {
// // this.addAirplay();
// // this.addVideoSettingsMenu();
// // Add a cachebuster param to playlist URLs.
// videojs.Vhs.xhr.beforeRequest = options => {
// if (options.uri.match('m3u8')) {
// const cachebuster = Math.random().toString(16).substr(2, 8);
// options.uri = `${options.uri}?cachebust=${cachebuster}`;
// }
// return options;
// };
// this.vjsPlayer = videojs(VIDEO_ID, VIDEO_OPTIONS);
// this.vjsPlayer.ready(this.handleReady);
// }
// setupPlayerCallbacks(callbacks) {
// const { onReady, onPlaying, onEnded, onError } = callbacks;
// this.appPlayerReadyCallback = onReady;
// this.appPlayerPlayingCallback = onPlaying;
// this.appPlayerEndedCallback = onEnded;
// this.appPlayerErrorCallback = onError;
// }
// // play
// startPlayer() {
// this.log('Start playing');
// const source = { ...VIDEO_SRC };
// try {
// this.vjsPlayer.volume(getLocalStorage(PLAYER_VOLUME) || 1);
// } catch (err) {
// console.warn(err);
// }
// this.vjsPlayer.src(source);
// }
// setupPlaybackMetrics() {
// // this.playbackMetrics = new PlaybackMetrics(this.vjsPlayer, videojs);
// }
// setupLatencyCompensator() {
// const tech = this.vjsPlayer.tech({ IWillNotUseThisInPlugins: true });
// // VHS is required.
// if (!tech || !tech.vhs) {
// }
// // const latencyCompensatorEnabledSaved = getLocalStorage(LATENCY_COMPENSATION_ENABLED);
// // if (latencyCompensatorEnabledSaved === 'true' && tech && tech.vhs) {
// // this.startLatencyCompensator();
// // } else {
// // this.stopLatencyCompensator();
// // }
// }
// startLatencyCompensator() {
// // this.latencyCompensator = new LatencyCompensator(this.vjsPlayer);
// // this.latencyCompensator.enable();
// // this.latencyCompensatorEnabled = true;
// // this.setLatencyCompensatorItemTitle('disable minimized latency');
// }
// stopLatencyCompensator() {
// // if (this.latencyCompensator) {
// // this.latencyCompensator.disable();
// // }
// // this.LatencyCompensator = null;
// // this.latencyCompensatorEnabled = false;
// // this.setLatencyCompensatorItemTitle(
// // '<span style="font-size: 0.8em">enable minimized latency (experimental)</span>',
// // );
// }
// handleReady() {
// console.log('handleReady');
// this.vjsPlayer.on('error', this.handleError);
// this.vjsPlayer.on('playing', this.handlePlaying);
// this.vjsPlayer.on('volumechange', this.handleVolume);
// this.vjsPlayer.on('ended', this.handleEnded);
// this.vjsPlayer.on('loadeddata', () => {
// this.setupPlaybackMetrics();
// this.setupLatencyCompensator();
// });
// if (this.appPlayerReadyCallback) {
// // start polling
// this.appPlayerReadyCallback();
// }
// this.vjsPlayer.log.level('debug');
// }
// handleVolume() {
// setLocalStorage(PLAYER_VOLUME, this.vjsPlayer.muted() ? 0 : this.vjsPlayer.volume());
// }
// handlePlaying() {
// if (this.appPlayerPlayingCallback) {
// // start polling
// this.appPlayerPlayingCallback();
// }
// if (this.latencyCompensator && !this.hasStartedPlayback) {
// this.latencyCompensator.enable();
// }
// this.hasStartedPlayback = true;
// }
// handleEnded() {
// if (this.appPlayerEndedCallback) {
// this.appPlayerEndedCallback();
// }
// this.stopLatencyCompensator();
// }
// handleError(e) {
// this.log(`on Error: ${JSON.stringify(e)}`);
// if (this.appPlayerEndedCallback) {
// this.appPlayerEndedCallback();
// }
// }
// toggleLatencyCompensator() {
// if (this.latencyCompensatorEnabled) {
// this.stopLatencyCompensator();
// setLocalStorage(LATENCY_COMPENSATION_ENABLED, false);
// } else {
// this.startLatencyCompensator();
// setLocalStorage(LATENCY_COMPENSATION_ENABLED, true);
// }
// }
// setLatencyCompensatorItemTitle(title) {
// const item = document.querySelector('.latency-toggle-item > .vjs-menu-item-text');
// if (!item) {
// return;
// }
// item.innerHTML = title;
// }
// log(message) {
// // console.log(`>>> Player: ${message}`);
// }
// render(): JSX.Element {
// return (
// <div data-vjs-player>
// <video ref={videoRef} className='video-js vjs-big-play-centered' />
// </div>
// );
// }
// async addVideoSettingsMenu() {
// if (this.qualityMenuButton) {
// player.controlBar.removeChild(this.qualityMenuButton);
// }
// videojs.hookOnce('setup', async player => {
// let qualities = [];
// try {
// const response = await fetch('http://localhost:8080/api/video/variants');
// qualities = await response.json();
// } catch (e) {
// console.error(e);
// }
// const MenuItem = videojs.getComponent('MenuItem');
// const MenuButtonClass = videojs.getComponent('MenuButton');
// const lowLatencyItem = new MenuItem(player, {
// selectable: true,
// });
// lowLatencyItem.setAttribute('class', 'latency-toggle-item');
// lowLatencyItem.on('click', () => {
// this.toggleLatencyCompensator();
// });
// this.latencyCompensatorToggleButton = lowLatencyItem;
// const separator = new MenuSeparator(player, {
// selectable: false,
// });
// const MenuButton = videojs.extend(MenuButtonClass, {
// // The `init()` method will also work for constructor logic here, but it is
// // deprecated. If you provide an `init()` method, it will override the
// // `constructor()` method!
// constructor() {
// MenuButtonClass.call(this, player);
// },
// createItems() {
// const tech = this.player_.tech({ IWillNotUseThisInPlugins: true });
// const defaultAutoItem = new MenuItem(player, {
// selectable: true,
// label: 'Auto',
// });
// const items = qualities.map(item => {
// const newMenuItem = new MenuItem(player, {
// selectable: true,
// label: item.name,
// });
// // Quality selected
// newMenuItem.on('click', () => {
// // If for some reason tech doesn't exist, then don't do anything
// if (!tech) {
// console.warn('Invalid attempt to access null player tech');
// return;
// }
// // Only enable this single, selected representation.
// tech.vhs.representations().forEach((rep, index) => {
// rep.enabled(index === item.index);
// });
// newMenuItem.selected(false);
// });
// return newMenuItem;
// });
// defaultAutoItem.on('click', () => {
// // Re-enable all representations.
// tech.vhs.representations().forEach((rep, index) => {
// rep.enabled(true);
// });
// defaultAutoItem.selected(false);
// });
// const supportsLatencyCompensator = !!tech && !!tech.vhs;
// // Only show the quality selector if there is more than one option.
// if (qualities.length < 2 && supportsLatencyCompensator) {
// return [lowLatencyItem];
// }
// if (qualities.length > 1 && supportsLatencyCompensator) {
// return [defaultAutoItem, ...items, separator, lowLatencyItem];
// }
// if (!supportsLatencyCompensator && qualities.length == 1) {
// return [];
// }
// return [defaultAutoItem, ...items];
// },
// });
// // If none of the settings in this menu are applicable then don't show it.
// const tech = player.tech({ IWillNotUseThisInPlugins: true });
// if (qualities.length < 2 && (!tech || !tech.vhs)) {
// return;
// }
// const menuButton = new MenuButton();
// menuButton.addClass('vjs-quality-selector');
// player.controlBar.addChild(menuButton, {}, player.controlBar.children_.length - 2);
// this.qualityMenuButton = menuButton;
// this.latencyCompensatorToggleButton = lowLatencyItem;
// });
// }
// addAirplay() {
// videojs.hookOnce('setup', player => {
// if (window.WebKitPlaybackTargetAvailabilityEvent) {
// const videoJsButtonClass = videojs.getComponent('Button');
// const concreteButtonClass = videojs.extend(videoJsButtonClass, {
// // The `init()` method will also work for constructor logic here, but it is
// // deprecated. If you provide an `init()` method, it will override the
// // `constructor()` method!
// constructor() {
// videoJsButtonClass.call(this, player);
// },
// handleClick() {
// const videoElement = document.getElementsByTagName('video')[0];
// videoElement.webkitShowPlaybackTargetPicker();
// },
// });
// const concreteButtonInstance = player.controlBar.addChild(new concreteButtonClass());
// concreteButtonInstance.addClass('vjs-airplay');
// }
// });
// }
// }
// export { OwncastPlayer };
// const VjsMenuItem = videojs.getComponent('MenuItem');
// VjsMenuItem.registerComponent('MenuSeparator', MenuSeparator);

362
web/package-lock.json generated
View File

@@ -31,7 +31,8 @@
"react-markdown": "8.0.0",
"react-markdown-editor-lite": "1.3.2",
"recoil": "^0.7.2",
"ua-parser-js": "1.0.2"
"ua-parser-js": "1.0.2",
"video.js": "^7.18.1"
},
"devDependencies": {
"@babel/core": "^7.17.9",
@@ -11392,6 +11393,66 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@videojs/http-streaming": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.13.1.tgz",
"integrity": "sha512-1x3fkGSPyL0+iaS3/lTvfnPTtfqzfgG+ELQtPPtTvDwqGol9Mx3TNyZwtSTdIufBrqYRn7XybB/3QNMsyjq13A==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "3.0.4",
"aes-decrypter": "3.1.2",
"global": "^4.4.0",
"m3u8-parser": "4.7.0",
"mpd-parser": "0.21.0",
"mux.js": "6.0.1",
"video.js": "^6 || ^7"
},
"engines": {
"node": ">=8",
"npm": ">=5"
},
"peerDependencies": {
"video.js": "^6 || ^7"
}
},
"node_modules/@videojs/http-streaming/node_modules/@videojs/vhs-utils": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.4.tgz",
"integrity": "sha512-hui4zOj2I1kLzDgf8QDVxD3IzrwjS/43KiS8IHQO0OeeSsb4pB/lgNt1NG7Dv0wMQfCccUpMVLGcK618s890Yg==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"global": "^4.4.0",
"url-toolkit": "^2.2.1"
},
"engines": {
"node": ">=8",
"npm": ">=5"
}
},
"node_modules/@videojs/vhs-utils": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz",
"integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"global": "^4.4.0",
"url-toolkit": "^2.2.1"
},
"engines": {
"node": ">=8",
"npm": ">=5"
}
},
"node_modules/@videojs/xhr": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.6.0.tgz",
"integrity": "sha512-7J361GiN1tXpm+gd0xz2QWr3xNWBE+rytvo8J3KuggFaLg+U37gZQ2BuPLcnkfGffy2e+ozY70RHC8jt7zjA6Q==",
"dependencies": {
"@babel/runtime": "^7.5.5",
"global": "~4.4.0",
"is-function": "^1.0.1"
}
},
"node_modules/@webassemblyjs/ast": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
@@ -11622,6 +11683,14 @@
"@xtuc/long": "4.2.2"
}
},
"node_modules/@xmldom/xmldom": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.5.tgz",
"integrity": "sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A==",
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/@xobotyi/scrollbar-width": {
"version": "1.9.5",
"resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz",
@@ -11815,6 +11884,17 @@
"node": ">= 0.12.0"
}
},
"node_modules/aes-decrypter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.2.tgz",
"integrity": "sha512-42nRwfQuPRj9R1zqZBdoxnaAmnIFyDi0MNyTVhjdFOd8fifXKKRfwIHIZ6AMn1or4x5WONzjwRTbTWcsIQ0O4A==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.0",
"global": "^4.4.0",
"pkcs7": "^1.0.4"
}
},
"node_modules/agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -19119,6 +19199,11 @@
"node": ">=8"
}
},
"node_modules/individual": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/individual/-/individual-2.0.0.tgz",
"integrity": "sha1-gzsJfa0jKU52EXqY+zjg2a1hu5c="
},
"node_modules/infer-owner": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
@@ -22302,6 +22387,11 @@
"node": ">=8"
}
},
"node_modules/keycode": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.1.tgz",
"integrity": "sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg=="
},
"node_modules/keyv": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
@@ -22612,6 +22702,16 @@
"lz-string": "bin/bin.js"
}
},
"node_modules/m3u8-parser": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.7.0.tgz",
"integrity": "sha512-48l/OwRyjBm+QhNNigEEcRcgbRvnUjL7rxs597HmW9QSNbyNvt+RcZ9T/d9vxi9A9z7EZrB1POtZYhdRlwYQkQ==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.0",
"global": "^4.4.0"
}
},
"node_modules/make-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
@@ -23641,6 +23741,20 @@
"rimraf": "bin.js"
}
},
"node_modules/mpd-parser": {
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.21.0.tgz",
"integrity": "sha512-NbpMJ57qQzFmfCiP1pbL7cGMbVTD0X1hqNgL0VYP1wLlZXLf/HtmvQpNkOA1AHkPVeGQng+7/jEtSvNUzV7Gdg==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.2",
"@xmldom/xmldom": "^0.7.2",
"global": "^4.4.0"
},
"bin": {
"mpd-to-m3u8-json": "bin/parse.js"
}
},
"node_modules/mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@@ -23654,6 +23768,22 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/mux.js": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.0.1.tgz",
"integrity": "sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w==",
"dependencies": {
"@babel/runtime": "^7.11.2",
"global": "^4.4.0"
},
"bin": {
"muxjs-transmux": "bin/transmux.js"
},
"engines": {
"node": ">=8",
"npm": ">=5"
}
},
"node_modules/nan": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
@@ -24805,6 +24935,17 @@
"node": ">= 6"
}
},
"node_modules/pkcs7": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz",
"integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==",
"dependencies": {
"@babel/runtime": "^7.5.5"
},
"bin": {
"pkcs7": "bin/cli.js"
}
},
"node_modules/pkg-dir": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz",
@@ -27598,6 +27739,14 @@
"resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz",
"integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw=="
},
"node_modules/rust-result": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/rust-result/-/rust-result-1.0.0.tgz",
"integrity": "sha1-NMdbLm3Dn+WHXlveyFteD5FTb3I=",
"dependencies": {
"individual": "^2.0.0"
}
},
"node_modules/sade": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
@@ -27614,6 +27763,14 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/safe-json-parse": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-4.0.0.tgz",
"integrity": "sha1-fA9XjPzNEtM6ccDgVBPi7KFx6qw=",
"dependencies": {
"rust-result": "^1.0.0"
}
},
"node_modules/safe-regex": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
@@ -30377,6 +30534,11 @@
"node": ">=4"
}
},
"node_modules/url-toolkit": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz",
"integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg=="
},
"node_modules/url/node_modules/punycode": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz",
@@ -30580,6 +30742,39 @@
"url": "https://opencollective.com/unified"
}
},
"node_modules/video.js": {
"version": "7.18.1",
"resolved": "https://registry.npmjs.org/video.js/-/video.js-7.18.1.tgz",
"integrity": "sha512-mnXdmkVcD5qQdKMZafDjqdhrnKGettZaGSVkExjACiylSB4r2Yt5W1bchsKmjFpfuNfszsMjTUnnoIWSSqoe/Q==",
"dependencies": {
"@babel/runtime": "^7.12.5",
"@videojs/http-streaming": "2.13.1",
"@videojs/vhs-utils": "^3.0.4",
"@videojs/xhr": "2.6.0",
"aes-decrypter": "3.1.2",
"global": "^4.4.0",
"keycode": "^2.2.0",
"m3u8-parser": "4.7.0",
"mpd-parser": "0.21.0",
"mux.js": "6.0.1",
"safe-json-parse": "4.0.0",
"videojs-font": "3.2.0",
"videojs-vtt.js": "^0.15.3"
}
},
"node_modules/videojs-font": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-3.2.0.tgz",
"integrity": "sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA=="
},
"node_modules/videojs-vtt.js": {
"version": "0.15.3",
"resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.3.tgz",
"integrity": "sha512-5FvVsICuMRx6Hd7H/Y9s9GDeEtYcXQWzGMS+sl4UX3t/zoHp3y+isSfIPRochnTH7h+Bh1ILyC639xy9Z6kPag==",
"dependencies": {
"global": "^4.3.1"
}
},
"node_modules/vm-browserify": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
@@ -39893,6 +40088,53 @@
"eslint-visitor-keys": "^3.0.0"
}
},
"@videojs/http-streaming": {
"version": "2.13.1",
"resolved": "https://registry.npmjs.org/@videojs/http-streaming/-/http-streaming-2.13.1.tgz",
"integrity": "sha512-1x3fkGSPyL0+iaS3/lTvfnPTtfqzfgG+ELQtPPtTvDwqGol9Mx3TNyZwtSTdIufBrqYRn7XybB/3QNMsyjq13A==",
"requires": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "3.0.4",
"aes-decrypter": "3.1.2",
"global": "^4.4.0",
"m3u8-parser": "4.7.0",
"mpd-parser": "0.21.0",
"mux.js": "6.0.1",
"video.js": "^6 || ^7"
},
"dependencies": {
"@videojs/vhs-utils": {
"version": "3.0.4",
"resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.4.tgz",
"integrity": "sha512-hui4zOj2I1kLzDgf8QDVxD3IzrwjS/43KiS8IHQO0OeeSsb4pB/lgNt1NG7Dv0wMQfCccUpMVLGcK618s890Yg==",
"requires": {
"@babel/runtime": "^7.12.5",
"global": "^4.4.0",
"url-toolkit": "^2.2.1"
}
}
}
},
"@videojs/vhs-utils": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/@videojs/vhs-utils/-/vhs-utils-3.0.5.tgz",
"integrity": "sha512-PKVgdo8/GReqdx512F+ombhS+Bzogiofy1LgAj4tN8PfdBx3HSS7V5WfJotKTqtOWGwVfSWsrYN/t09/DSryrw==",
"requires": {
"@babel/runtime": "^7.12.5",
"global": "^4.4.0",
"url-toolkit": "^2.2.1"
}
},
"@videojs/xhr": {
"version": "2.6.0",
"resolved": "https://registry.npmjs.org/@videojs/xhr/-/xhr-2.6.0.tgz",
"integrity": "sha512-7J361GiN1tXpm+gd0xz2QWr3xNWBE+rytvo8J3KuggFaLg+U37gZQ2BuPLcnkfGffy2e+ozY70RHC8jt7zjA6Q==",
"requires": {
"@babel/runtime": "^7.5.5",
"global": "~4.4.0",
"is-function": "^1.0.1"
}
},
"@webassemblyjs/ast": {
"version": "1.11.1",
"resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz",
@@ -40129,6 +40371,11 @@
"@xtuc/long": "4.2.2"
}
},
"@xmldom/xmldom": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/@xmldom/xmldom/-/xmldom-0.7.5.tgz",
"integrity": "sha512-V3BIhmY36fXZ1OtVcI9W+FxQqxVLsPKcNjWigIaa81dLC9IolJl5Mt4Cvhmr0flUnjSpTdrbMTSbXqYqV5dT6A=="
},
"@xobotyi/scrollbar-width": {
"version": "1.9.5",
"resolved": "https://registry.npmjs.org/@xobotyi/scrollbar-width/-/scrollbar-width-1.9.5.tgz",
@@ -40282,6 +40529,17 @@
"resolved": "https://registry.npmjs.org/address/-/address-1.1.2.tgz",
"integrity": "sha512-aT6camzM4xEA54YVJYSqxz1kv4IHnQZRtThJJHhUMRExaU5spC7jX5ugSwTaTgJliIgs4VhZOk7htClvQ/LmRA=="
},
"aes-decrypter": {
"version": "3.1.2",
"resolved": "https://registry.npmjs.org/aes-decrypter/-/aes-decrypter-3.1.2.tgz",
"integrity": "sha512-42nRwfQuPRj9R1zqZBdoxnaAmnIFyDi0MNyTVhjdFOd8fifXKKRfwIHIZ6AMn1or4x5WONzjwRTbTWcsIQ0O4A==",
"requires": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.0",
"global": "^4.4.0",
"pkcs7": "^1.0.4"
}
},
"agent-base": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@@ -45949,6 +46207,11 @@
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="
},
"individual": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/individual/-/individual-2.0.0.tgz",
"integrity": "sha1-gzsJfa0jKU52EXqY+zjg2a1hu5c="
},
"infer-owner": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz",
@@ -48473,6 +48736,11 @@
"resolved": "https://registry.npmjs.org/junk/-/junk-3.1.0.tgz",
"integrity": "sha512-pBxcB3LFc8QVgdggvZWyeys+hnrNWg4OcZIU/1X59k5jQdLBlCsYGRQaz234SqoRLTCgMH00fY0xRJH+F9METQ=="
},
"keycode": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/keycode/-/keycode-2.2.1.tgz",
"integrity": "sha512-Rdgz9Hl9Iv4QKi8b0OlCRQEzp4AgVxyCtz5S/+VIHezDmrDhkp2N2TqBWOLz0/gbeREXOOiI9/4b8BY9uw2vFg=="
},
"keyv": {
"version": "3.1.0",
"resolved": "https://registry.npmjs.org/keyv/-/keyv-3.1.0.tgz",
@@ -48713,6 +48981,16 @@
"integrity": "sha1-wNjq82BZ9wV5bh40SBHPTEmNOiY=",
"dev": true
},
"m3u8-parser": {
"version": "4.7.0",
"resolved": "https://registry.npmjs.org/m3u8-parser/-/m3u8-parser-4.7.0.tgz",
"integrity": "sha512-48l/OwRyjBm+QhNNigEEcRcgbRvnUjL7rxs597HmW9QSNbyNvt+RcZ9T/d9vxi9A9z7EZrB1POtZYhdRlwYQkQ==",
"requires": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.0",
"global": "^4.4.0"
}
},
"make-dir": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz",
@@ -49409,6 +49687,17 @@
}
}
},
"mpd-parser": {
"version": "0.21.0",
"resolved": "https://registry.npmjs.org/mpd-parser/-/mpd-parser-0.21.0.tgz",
"integrity": "sha512-NbpMJ57qQzFmfCiP1pbL7cGMbVTD0X1hqNgL0VYP1wLlZXLf/HtmvQpNkOA1AHkPVeGQng+7/jEtSvNUzV7Gdg==",
"requires": {
"@babel/runtime": "^7.12.5",
"@videojs/vhs-utils": "^3.0.2",
"@xmldom/xmldom": "^0.7.2",
"global": "^4.4.0"
}
},
"mri": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz",
@@ -49419,6 +49708,15 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"mux.js": {
"version": "6.0.1",
"resolved": "https://registry.npmjs.org/mux.js/-/mux.js-6.0.1.tgz",
"integrity": "sha512-22CHb59rH8pWGcPGW5Og7JngJ9s+z4XuSlYvnxhLuc58cA1WqGDQPzuG8I+sPm1/p0CdgpzVTaKW408k5DNn8w==",
"requires": {
"@babel/runtime": "^7.11.2",
"global": "^4.4.0"
}
},
"nan": {
"version": "2.15.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz",
@@ -50323,6 +50621,14 @@
"resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.5.tgz",
"integrity": "sha512-8V9+HQPupnaXMA23c5hvl69zXvTwTzyAYasnkb0Tts4XvO4CliqONMOnvlq26rkhLC3nWDFBJf73LU1e1VZLaQ=="
},
"pkcs7": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/pkcs7/-/pkcs7-1.0.4.tgz",
"integrity": "sha512-afRERtHn54AlwaF2/+LFszyAANTCggGilmcmILUzEjvs3XgFZT+xE6+QWQcAGmu4xajy+Xtj7acLOPdx5/eXWQ==",
"requires": {
"@babel/runtime": "^7.5.5"
}
},
"pkg-dir": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-5.0.0.tgz",
@@ -52383,6 +52689,14 @@
}
}
},
"rust-result": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/rust-result/-/rust-result-1.0.0.tgz",
"integrity": "sha1-NMdbLm3Dn+WHXlveyFteD5FTb3I=",
"requires": {
"individual": "^2.0.0"
}
},
"sade": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz",
@@ -52396,6 +52710,14 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"safe-json-parse": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-4.0.0.tgz",
"integrity": "sha1-fA9XjPzNEtM6ccDgVBPi7KFx6qw=",
"requires": {
"rust-result": "^1.0.0"
}
},
"safe-regex": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz",
@@ -54539,6 +54861,11 @@
"prepend-http": "^2.0.0"
}
},
"url-toolkit": {
"version": "2.2.5",
"resolved": "https://registry.npmjs.org/url-toolkit/-/url-toolkit-2.2.5.tgz",
"integrity": "sha512-mtN6xk+Nac+oyJ/PrI7tzfmomRVNFIWKUbG8jdYFt52hxbiReFAXIjYskvu64/dvuW71IcB7lV8l0HvZMac6Jg=="
},
"use": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz",
@@ -54680,6 +55007,39 @@
"unist-util-stringify-position": "^3.0.0"
}
},
"video.js": {
"version": "7.18.1",
"resolved": "https://registry.npmjs.org/video.js/-/video.js-7.18.1.tgz",
"integrity": "sha512-mnXdmkVcD5qQdKMZafDjqdhrnKGettZaGSVkExjACiylSB4r2Yt5W1bchsKmjFpfuNfszsMjTUnnoIWSSqoe/Q==",
"requires": {
"@babel/runtime": "^7.12.5",
"@videojs/http-streaming": "2.13.1",
"@videojs/vhs-utils": "^3.0.4",
"@videojs/xhr": "2.6.0",
"aes-decrypter": "3.1.2",
"global": "^4.4.0",
"keycode": "^2.2.0",
"m3u8-parser": "4.7.0",
"mpd-parser": "0.21.0",
"mux.js": "6.0.1",
"safe-json-parse": "4.0.0",
"videojs-font": "3.2.0",
"videojs-vtt.js": "^0.15.3"
}
},
"videojs-font": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-3.2.0.tgz",
"integrity": "sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA=="
},
"videojs-vtt.js": {
"version": "0.15.3",
"resolved": "https://registry.npmjs.org/videojs-vtt.js/-/videojs-vtt.js-0.15.3.tgz",
"integrity": "sha512-5FvVsICuMRx6Hd7H/Y9s9GDeEtYcXQWzGMS+sl4UX3t/zoHp3y+isSfIPRochnTH7h+Bh1ILyC639xy9Z6kPag==",
"requires": {
"global": "^4.3.1"
}
},
"vm-browserify": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",

View File

@@ -34,7 +34,8 @@
"react-markdown": "8.0.0",
"react-markdown-editor-lite": "1.3.2",
"recoil": "^0.7.2",
"ua-parser-js": "1.0.2"
"ua-parser-js": "1.0.2",
"video.js": "^7.18.1"
},
"devDependencies": {
"@babel/core": "^7.17.9",

View File

@@ -25,7 +25,7 @@ const DropdownExample = () => (
);
export default {
title: 'owncast/Dropdown',
title: 'example/Dropdown',
component: Dropdown,
parameters: {},
} as ComponentMeta<typeof Dropdown>;

View File

@@ -8,7 +8,7 @@ import Plugin from './assets/plugin.svg';
import Repo from './assets/repo.svg';
import StackAlt from './assets/stackalt.svg';
<Meta title="Example/Introduction" />
<Meta title="Owncast/Readme" />
<style>{`
.subheading {

View File

@@ -45,7 +45,7 @@ class TabsExample extends React.Component {
}
export default {
title: 'owncast/Tabs',
title: 'example/Tabs',
component: Tabs,
} as ComponentMeta<typeof Tabs>;

View File

@@ -1,16 +1,32 @@
import React from 'react';
import { ComponentStory, ComponentMeta } from '@storybook/react';
import VideoPoster from '../components/video/VideoPoster';
import OwncastPlayer from '../components/video/owncastplayer';
const streams = {
DemoServer: `https://watch.owncast.online`,
RetroStrangeTV: `https://live.retrostrange.com`,
localhost: `http://localhost:8080`,
};
export default {
title: 'owncast/VideoPoster',
component: VideoPoster,
title: 'owncast/Player',
component: OwncastPlayer,
argTypes: {
source: {
options: Object.keys(streams),
mapping: streams,
control: {
type: 'select',
},
},
},
parameters: {},
} as ComponentMeta<typeof VideoPoster>;
} as ComponentMeta<typeof OwncastPlayer>;
const VideoPosterExample = () => <VideoPoster />;
const Template: ComponentStory<typeof OwncastPlayer> = args => <OwncastPlayer {...args} />;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const Template: ComponentStory<typeof VideoPoster> = args => <VideoPosterExample />;
export const Basic = Template.bind({});
export const LiveDemo = Template.bind({});
LiveDemo.args = {
online: true,
source: 'https://watch.owncast.online',
};

78
web/utils/constants.js Normal file
View File

@@ -0,0 +1,78 @@
// misc constants used throughout the app
export const URL_STATUS = `/api/status`;
export const URL_CHAT_HISTORY = `/api/chat`;
export const URL_CUSTOM_EMOJIS = `/api/emoji`;
export const URL_CONFIG = `/api/config`;
export const URL_VIEWER_PING = `/api/ping`;
// inline moderation actions
export const URL_HIDE_MESSAGE = `/api/chat/updatemessagevisibility`;
export const URL_BAN_USER = `/api/chat/users/setenabled`;
// TODO: This directory is customizable in the config. So we should expose this via the config API.
export const URL_STREAM = `/hls/stream.m3u8`;
export const URL_WEBSOCKET = `${
location.protocol === 'https:' ? 'wss' : 'ws'
}://${location.host}/ws`;
export const URL_CHAT_REGISTRATION = `/api/chat/register`;
export const URL_FOLLOWERS = `/api/followers`;
export const URL_PLAYBACK_METRICS = `/api/metrics/playback`;
export const URL_REGISTER_NOTIFICATION = `/api/notifications/register`;
export const URL_REGISTER_EMAIL_NOTIFICATION = `/api/notifications/register/email`;
export const URL_CHAT_INDIEAUTH_BEGIN = `/api/auth/indieauth`;
export const TIMER_STATUS_UPDATE = 5000; // ms
export const TIMER_DISABLE_CHAT_AFTER_OFFLINE = 5 * 60 * 1000; // 5 mins
export const TIMER_STREAM_DURATION_COUNTER = 1000;
export const TEMP_IMAGE =
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7';
export const OWNCAST_LOGO_LOCAL = '/img/logo.svg';
export const MESSAGE_OFFLINE = 'Stream is offline.';
export const MESSAGE_ONLINE = 'Stream is online.';
export const URL_OWNCAST = 'https://owncast.online'; // used in footer
export const PLAYER_VOLUME = 'owncast_volume';
export const KEY_ACCESS_TOKEN = 'owncast_access_token';
export const KEY_EMBED_CHAT_ACCESS_TOKEN = 'owncast_embed_chat_access_token';
export const KEY_USERNAME = 'owncast_username';
export const KEY_CUSTOM_USERNAME_SET = 'owncast_custom_username_set';
export const KEY_CHAT_DISPLAYED = 'owncast_chat';
export const KEY_CHAT_FIRST_MESSAGE_SENT = 'owncast_first_message_sent';
export const CHAT_INITIAL_PLACEHOLDER_TEXT =
'Type here to chat, no account necessary.';
export const CHAT_PLACEHOLDER_TEXT = 'Message';
export const CHAT_PLACEHOLDER_OFFLINE = 'Chat is offline.';
export const CHAT_MAX_MESSAGE_LENGTH = 500;
export const EST_SOCKET_PAYLOAD_BUFFER = 512;
export const CHAT_CHAR_COUNT_BUFFER = 20;
export const CHAT_OK_KEYCODES = [
'ArrowLeft',
'ArrowUp',
'ArrowRight',
'ArrowDown',
'Shift',
'Meta',
'Alt',
'Delete',
'Backspace',
];
export const CHAT_KEY_MODIFIERS = ['Control', 'Shift', 'Meta', 'Alt'];
export const MESSAGE_JUMPTOBOTTOM_BUFFER = 500;
// app styling
export const WIDTH_SINGLE_COL = 780;
export const HEIGHT_SHORT_WIDE = 500;
export const ORIENTATION_PORTRAIT = 'portrait';
export const ORIENTATION_LANDSCAPE = 'landscape';
// localstorage keys
export const HAS_DISPLAYED_NOTIFICATION_MODAL_KEY =
'HAS_DISPLAYED_NOTIFICATION_MODAL';
export const USER_VISIT_COUNT_KEY = 'USER_VISIT_COUNT';
export const USER_DISMISSED_ANNOYING_NOTIFICATION_POPUP_KEY =
'USER_DISMISSED_ANNOYING_NOTIFICATION_POPUP_KEY';

214
web/utils/helpers.js Normal file
View File

@@ -0,0 +1,214 @@
import { ORIENTATION_LANDSCAPE, ORIENTATION_PORTRAIT } from './constants.js';
export function getLocalStorage(key) {
try {
return localStorage.getItem(key);
} catch (e) {}
return null;
}
export function setLocalStorage(key, value) {
try {
if (value !== '' && value !== null) {
localStorage.setItem(key, value);
} else {
localStorage.removeItem(key);
}
return true;
} catch (e) {}
return false;
}
export function clearLocalStorage(key) {
localStorage.removeItem(key);
}
// jump down to the max height of a div, with a slight delay
export function jumpToBottom(element, behavior) {
if (!element) return;
if (!behavior) {
behavior = document.visibilityState === 'visible' ? 'smooth' : 'instant';
}
setTimeout(
() => {
element.scrollTo({
top: element.scrollHeight,
left: 0,
behavior: behavior,
});
},
50,
element
);
}
// convert newlines to <br>s
export function addNewlines(str) {
return str.replace(/(?:\r\n|\r|\n)/g, '<br />');
}
export function pluralize(string, count) {
if (count === 1) {
return string;
} else {
return string + 's';
}
}
// Trying to determine if browser is mobile/tablet.
// Source: https://developer.mozilla.org/en-US/docs/Web/HTTP/Browser_detection_using_the_user_agent
export function hasTouchScreen() {
let hasTouch = false;
if ('maxTouchPoints' in navigator) {
hasTouch = navigator.maxTouchPoints > 0;
} else if ('msMaxTouchPoints' in navigator) {
hasTouch = navigator.msMaxTouchPoints > 0;
} else {
var mQ = window.matchMedia && matchMedia('(pointer:coarse)');
if (mQ && mQ.media === '(pointer:coarse)') {
hasTouch = !!mQ.matches;
} else if ('orientation' in window) {
hasTouch = true; // deprecated, but good fallback
} else {
// Only as a last resort, fall back to user agent sniffing
hasTouch = navigator.userAgentData.mobile;
}
}
return hasTouch;
}
export function getOrientation(forTouch = false) {
// chrome mobile gives misleading matchMedia result when keyboard is up
if (forTouch && window.screen && window.screen.orientation) {
return window.screen.orientation.type.match('portrait')
? ORIENTATION_PORTRAIT
: ORIENTATION_LANDSCAPE;
} else {
// all other cases
return window.matchMedia('(orientation: portrait)').matches
? ORIENTATION_PORTRAIT
: ORIENTATION_LANDSCAPE;
}
}
export function padLeft(text, pad, size) {
return String(pad.repeat(size) + text).slice(-size);
}
export function parseSecondsToDurationString(seconds = 0) {
const finiteSeconds = Number.isFinite(+seconds) ? Math.abs(seconds) : 0;
const days = Math.floor(finiteSeconds / 86400);
const daysString = days > 0 ? `${days} day${days > 1 ? 's' : ''} ` : '';
const hours = Math.floor((finiteSeconds / 3600) % 24);
const hoursString = hours || days ? padLeft(`${hours}:`, '0', 3) : '';
const mins = Math.floor((finiteSeconds / 60) % 60);
const minString = padLeft(`${mins}:`, '0', 3);
const secs = Math.floor(finiteSeconds % 60);
const secsString = padLeft(`${secs}`, '0', 2);
return daysString + hoursString + minString + secsString;
}
export function setVHvar() {
var vh = window.innerHeight * 0.01;
// Then we set the value in the --vh custom property to the root of the document
document.documentElement.style.setProperty('--vh', `${vh}px`);
}
export function doesObjectSupportFunction(object, functionName) {
return typeof object[functionName] === 'function';
}
// return a string of css classes
export function classNames(json) {
const classes = [];
Object.entries(json).map(function (item) {
const [key, value] = item;
if (value) {
classes.push(key);
}
return null;
});
return classes.join(' ');
}
// taken from
// https://medium.com/@TCAS3/debounce-deep-dive-javascript-es6-e6f8d983b7a1
export function debounce(fn, time) {
let timeout;
return function () {
const functionCall = () => fn.apply(this, arguments);
clearTimeout(timeout);
timeout = setTimeout(functionCall, time);
};
}
export function getDiffInDaysFromNow(timestamp) {
const time = typeof timestamp === 'string' ? new Date(timestamp) : timestamp;
return (new Date() - time) / (24 * 3600 * 1000);
}
// "Last live today at [time]" or "last live [date]"
export function makeLastOnlineString(timestamp) {
if (!timestamp) {
return '';
}
let string = '';
const time = new Date(timestamp);
const comparisonDate = new Date(time).setHours(0, 0, 0, 0);
if (comparisonDate == new Date().setHours(0, 0, 0, 0)) {
const atTime = time.toLocaleTimeString([], {
hour: '2-digit',
minute: '2-digit',
});
string = `Today ${atTime}`;
} else {
string = time.toLocaleDateString();
}
return `Last live: ${string}`;
}
// Routing & Tabs
export const ROUTE_RECORDINGS = 'recordings';
export const ROUTE_SCHEDULE = 'schedule';
// looks for `/recording|schedule/id` pattern to determine what to display from the tab view
export function checkUrlPathForDisplay() {
const pathTest = [ROUTE_RECORDINGS, ROUTE_SCHEDULE];
const pathParts = window.location.pathname.split('/');
if (pathParts.length >= 2) {
const part = pathParts[1].toLowerCase();
if (pathTest.includes(part)) {
return {
section: part,
sectionId: pathParts[2] || '',
};
}
}
return null;
}
export function paginateArray(items, page, perPage) {
const offset = perPage * (page - 1);
const totalPages = Math.ceil(items.length / perPage);
const paginatedItems = items.slice(offset, perPage * page);
return {
previousPage: page - 1 ? page - 1 : null,
nextPage: totalPages > page ? page + 1 : null,
total: items.length,
totalPages: totalPages,
items: paginatedItems,
};
}