Cleanup video player and use consistently across recordings and events.

This commit is contained in:
Jason Hunter 2021-06-10 15:02:43 -04:00 committed by Blake Blackshear
parent e8c342e162
commit b70c11e7a7
6 changed files with 121 additions and 83 deletions

15
web/package-lock.json generated
View File

@ -11947,15 +11947,6 @@
"resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-3.2.0.tgz", "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-3.2.0.tgz",
"integrity": "sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA==" "integrity": "sha512-g8vHMKK2/JGorSfqAZQUmYYNnXmfec4MLhwtEFS+mMs2IDY398GLysy6BH6K+aS1KMNu/xWZ8Sue/X/mdQPliA=="
}, },
"videojs-mobile-ui": {
"version": "0.5.3",
"resolved": "https://registry.npmjs.org/videojs-mobile-ui/-/videojs-mobile-ui-0.5.3.tgz",
"integrity": "sha512-rY+JFLUq2edqoWB4CHVxPLYQEYhSNdGylGe44MEdfxzqYaEgkf/qyDlmmpdN9BFIQ6vJ7eaQBxgTOHha8UpOGA==",
"requires": {
"global": "^4.3.2",
"video.js": "^5.19.2 || ^6.6.0 || ^7.0.0"
}
},
"videojs-playlist": { "videojs-playlist": {
"version": "4.3.1", "version": "4.3.1",
"resolved": "https://registry.npmjs.org/videojs-playlist/-/videojs-playlist-4.3.1.tgz", "resolved": "https://registry.npmjs.org/videojs-playlist/-/videojs-playlist-4.3.1.tgz",
@ -11966,9 +11957,9 @@
} }
}, },
"videojs-seek-buttons": { "videojs-seek-buttons": {
"version": "2.0.0", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/videojs-seek-buttons/-/videojs-seek-buttons-2.0.0.tgz", "resolved": "https://registry.npmjs.org/videojs-seek-buttons/-/videojs-seek-buttons-2.0.1.tgz",
"integrity": "sha512-fSq2COvwTT5OwD5urc3E+ktQRwdjptXNaeuv1Tld2yfoV1ep9Am9gE/O07ADgHJVedFatVUXnifTh6wlUWSyTA==", "integrity": "sha512-FIWIy0l1cy8zbJEcjBpL7m8t54219HNPRLfGcvs++CV3J7E6HbmF1bVwVMyh3Iev/Y95s0tnn0x5P6w/HTulfw==",
"requires": { "requires": {
"global": "^4.4.0", "global": "^4.4.0",
"video.js": "^6 || ^7" "video.js": "^6 || ^7"

View File

@ -18,9 +18,8 @@
"preact-async-route": "^2.2.1", "preact-async-route": "^2.2.1",
"preact-router": "^3.2.1", "preact-router": "^3.2.1",
"video.js": "^7.11.8", "video.js": "^7.11.8",
"videojs-mobile-ui": "^0.5.3",
"videojs-playlist": "^4.3.1", "videojs-playlist": "^4.3.1",
"videojs-seek-buttons": "^2.0.0" "videojs-seek-buttons": "^2.0.1"
}, },
"devDependencies": { "devDependencies": {
"@babel/eslint-parser": "^7.12.13", "@babel/eslint-parser": "^7.12.13",

View File

@ -4,7 +4,7 @@ module.exports = {
src: { url: '/dist' }, src: { url: '/dist' },
}, },
plugins: ['@snowpack/plugin-postcss', '@prefresh/snowpack', 'snowpack-plugin-hash'], plugins: ['@snowpack/plugin-postcss', '@prefresh/snowpack', 'snowpack-plugin-hash'],
routes: [{ match: 'routes', src: '.*', dest: '/index.html' }], routes: [{ match: 'all', src: '(?!.*(.svg|.gif|.json|.jpg|.jpeg|.png|.js)).*', dest: '/index.html' }],
optimize: { optimize: {
bundle: false, bundle: false,
minify: true, minify: true,

View File

@ -1,6 +1,6 @@
import { h, Component } from 'preact'; import { h, Component } from 'preact';
import { useRef, useEffect } from 'preact/hooks';
import videojs from 'video.js'; import videojs from 'video.js';
import 'videojs-mobile-ui';
import 'videojs-playlist'; import 'videojs-playlist';
import 'videojs-seek-buttons'; import 'videojs-seek-buttons';
import 'video.js/dist/video-js.css'; import 'video.js/dist/video-js.css';
@ -11,49 +11,85 @@ const defaultOptions = {
playbackRates: [0.5, 1, 2, 4, 8], playbackRates: [0.5, 1, 2, 4, 8],
fluid: true, fluid: true,
}; };
const defaultSeekOptions = {
forward: 30,
back: 10,
};
export default class VideoPlayer extends Component { export default function VideoPlayer({ children, options, seekOptions = {}, onReady = () => {}, onDispose = () => {} }) {
componentDidMount() { const playerRef = useRef();
const { options, onReady = () => {} } = this.props;
const videoJsOptions = {
...defaultOptions,
...options,
};
this.player = videojs(this.videoNode, videoJsOptions, function onPlayerReady() {
onReady(this);
});
this.player.seekButtons({
forward: 30,
back: 10,
});
this.player.mobileUi({
fullscreen: {
iOS: true,
},
});
}
componentWillUnmount() { useEffect(() => {
const { onDispose = () => {} } = this.props; const player = videojs(playerRef.current, { ...defaultOptions, ...options }, () => {
if (this.player) { onReady(player);
this.player.dispose(); });
onDispose(); player.seekButtons({
...defaultSeekOptions,
...seekOptions,
});
// Disable fullscreen on iOS if we have children
if (
children &&
videojs.browser.IS_IOS &&
videojs.browser.IOS_VERSION > 9 &&
!player.el_.ownerDocument.querySelector('.bc-iframe')
) {
player.tech_.el_.setAttribute('playsinline', 'playsinline');
player.tech_.supportsFullScreen = function () {
return false;
};
} }
}
// shouldComponentUpdate() { const screen = window.screen;
// return false;
// }
render() { const angle = () => {
const { style, children } = this.props; // iOS
return ( if (typeof window.orientation === 'number') {
<div style={style}> return window.orientation;
<div data-vjs-player> }
<video ref={(node) => (this.videoNode = node)} className="video-js vjs-default-skin" controls playsinline /> // Android
{children} if (screen && screen.orientation && screen.orientation.angle) {
</div> return window.orientation;
</div> }
); videojs.log('angle unknown');
} return 0;
};
const rotationHandler = () => {
const currentAngle = angle();
if (currentAngle === 90 || currentAngle === 270 || currentAngle === -90) {
if (player.paused() === false) {
player.requestFullscreen();
}
}
if ((currentAngle === 0 || currentAngle === 180) && player.isFullscreen()) {
player.exitFullscreen();
}
};
if (videojs.browser.IS_IOS) {
window.addEventListener('orientationchange', rotationHandler);
} else if (videojs.browser.IS_ANDROID && screen.orientation) {
// addEventListener('orientationchange') is not a user interaction on Android
screen.orientation.onchange = rotationHandler;
}
return () => {
if (videojs.browser.IS_IOS) {
window.removeEventListener('orientationchange', rotationHandler);
}
player.dispose();
onDispose();
};
}, []);
return (
<div data-vjs-player>
<video ref={playerRef} className="video-js vjs-default-skin" controls playsinline />
{children}
</div>
);
} }

View File

@ -25,7 +25,3 @@
transform: rotate(360deg); transform: rotate(360deg);
} }
} }
.video-js.vjs-has-started .vjs-touch-overlay {
display: none;
}

View File

@ -3,10 +3,13 @@ import { useCallback, useState } from 'preact/hooks';
import { route } from 'preact-router'; import { route } from 'preact-router';
import ActivityIndicator from '../components/ActivityIndicator'; import ActivityIndicator from '../components/ActivityIndicator';
import Button from '../components/Button'; import Button from '../components/Button';
import Delete from '../icons/Delete' import Clip from '../icons/Clip';
import Delete from '../icons/Delete';
import Snapshot from '../icons/Snapshot';
import Dialog from '../components/Dialog'; import Dialog from '../components/Dialog';
import Heading from '../components/Heading'; import Heading from '../components/Heading';
import Link from '../components/Link'; import Link from '../components/Link';
import VideoPlayer from '../components/VideoPlayer';
import { FetchStatus, useApiHost, useEvent } from '../api'; import { FetchStatus, useApiHost, useEvent } from '../api';
import { Table, Thead, Tbody, Th, Tr, Td } from '../components/Table'; import { Table, Thead, Tbody, Th, Tr, Td } from '../components/Table';
@ -24,9 +27,7 @@ export default function Event({ eventId }) {
setShowDialog(false); setShowDialog(false);
}; };
const handleClickDeleteDialog = useCallback(async () => { const handleClickDeleteDialog = useCallback(async () => {
let success; let success;
try { try {
const response = await fetch(`${apiHost}/api/events/${eventId}`, { method: 'DELETE' }); const response = await fetch(`${apiHost}/api/events/${eventId}`, { method: 'DELETE' });
@ -40,12 +41,11 @@ export default function Event({ eventId }) {
setDeleteStatus(FetchStatus.LOADED); setDeleteStatus(FetchStatus.LOADED);
setShowDialog(false); setShowDialog(false);
route('/events', true); route('/events', true);
} }
}, [apiHost, eventId, setShowDialog]); }, [apiHost, eventId, setShowDialog]);
if (status !== FetchStatus.LOADED) { if (status !== FetchStatus.LOADED) {
return <ActivityIndicator /> return <ActivityIndicator />;
} }
const startime = new Date(data.start_time * 1000); const startime = new Date(data.start_time * 1000);
@ -106,28 +106,44 @@ export default function Event({ eventId }) {
{data.has_clip ? ( {data.has_clip ? (
<Fragment> <Fragment>
<Heading size="sm">Clip</Heading> <Heading size="lg">Clip</Heading>
<video <VideoPlayer
aria-label={`Clip for event ${data.id}`} options={{
autoPlay sources: [
className="w-100" {
src={`${apiHost}/clips/${data.camera}-${eventId}.mp4`} src: `${apiHost}/clips/${data.camera}-${eventId}.mp4`,
controls type: 'video/mp4',
},
],
poster: data.has_snapshot
? `${apiHost}/clips/${data.camera}-${eventId}.jpg`
: `data:image/jpeg;base64,${data.thumbnail}`,
}}
seekOptions={{ forward: 10, back: 5 }}
onReady={(player) => {}}
/> />
<div className="text-center">
<Button className="mx-2" color="blue" href={`${apiHost}/clips/${data.camera}-${eventId}.mp4`} download>
<Clip className="w-6" /> Download Clip
</Button>
<Button className="mx-2" color="blue" href={`${apiHost}/clips/${data.camera}-${eventId}.jpg`} download>
<Snapshot className="w-6" /> Download Snapshot
</Button>
</div>
</Fragment> </Fragment>
) : ( ) : (
<p>No clip available</p> <Fragment>
<Heading size="sm">{data.has_snapshot ? 'Best Image' : 'Thumbnail'}</Heading>
<img
src={
data.has_snapshot
? `${apiHost}/clips/${data.camera}-${eventId}.jpg`
: `data:image/jpeg;base64,${data.thumbnail}`
}
alt={`${data.label} at ${(data.top_score * 100).toFixed(1)}% confidence`}
/>
</Fragment>
)} )}
<Heading size="sm">{data.has_snapshot ? 'Best image' : 'Thumbnail'}</Heading>
<img
src={
data.has_snapshot
? `${apiHost}/clips/${data.camera}-${eventId}.jpg`
: `data:image/jpeg;base64,${data.thumbnail}`
}
alt={`${data.label} at ${(data.top_score * 100).toFixed(1)}% confidence`}
/>
</div> </div>
); );
} }