mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-02-25 18:55:25 -06:00
Cleanup video player and use consistently across recordings and events.
This commit is contained in:
parent
e8c342e162
commit
b70c11e7a7
15
web/package-lock.json
generated
15
web/package-lock.json
generated
@ -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"
|
||||||
|
@ -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",
|
||||||
|
@ -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,
|
||||||
|
@ -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>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,3 @@
|
|||||||
transform: rotate(360deg);
|
transform: rotate(360deg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.video-js.vjs-has-started .vjs-touch-overlay {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user