diff --git a/web/package-lock.json b/web/package-lock.json index aaf2087287..39a8812992 100644 --- a/web/package-lock.json +++ b/web/package-lock.json @@ -11947,15 +11947,6 @@ "resolved": "https://registry.npmjs.org/videojs-font/-/videojs-font-3.2.0.tgz", "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": { "version": "4.3.1", "resolved": "https://registry.npmjs.org/videojs-playlist/-/videojs-playlist-4.3.1.tgz", @@ -11966,9 +11957,9 @@ } }, "videojs-seek-buttons": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/videojs-seek-buttons/-/videojs-seek-buttons-2.0.0.tgz", - "integrity": "sha512-fSq2COvwTT5OwD5urc3E+ktQRwdjptXNaeuv1Tld2yfoV1ep9Am9gE/O07ADgHJVedFatVUXnifTh6wlUWSyTA==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/videojs-seek-buttons/-/videojs-seek-buttons-2.0.1.tgz", + "integrity": "sha512-FIWIy0l1cy8zbJEcjBpL7m8t54219HNPRLfGcvs++CV3J7E6HbmF1bVwVMyh3Iev/Y95s0tnn0x5P6w/HTulfw==", "requires": { "global": "^4.4.0", "video.js": "^6 || ^7" diff --git a/web/package.json b/web/package.json index a750294313..052d98234d 100644 --- a/web/package.json +++ b/web/package.json @@ -18,9 +18,8 @@ "preact-async-route": "^2.2.1", "preact-router": "^3.2.1", "video.js": "^7.11.8", - "videojs-mobile-ui": "^0.5.3", "videojs-playlist": "^4.3.1", - "videojs-seek-buttons": "^2.0.0" + "videojs-seek-buttons": "^2.0.1" }, "devDependencies": { "@babel/eslint-parser": "^7.12.13", diff --git a/web/snowpack.config.js b/web/snowpack.config.js index 085042a560..7258eaebac 100644 --- a/web/snowpack.config.js +++ b/web/snowpack.config.js @@ -4,7 +4,7 @@ module.exports = { src: { url: '/dist' }, }, 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: { bundle: false, minify: true, diff --git a/web/src/components/VideoPlayer.jsx b/web/src/components/VideoPlayer.jsx index d4d6eb34b0..fa99fe0633 100644 --- a/web/src/components/VideoPlayer.jsx +++ b/web/src/components/VideoPlayer.jsx @@ -1,6 +1,6 @@ import { h, Component } from 'preact'; +import { useRef, useEffect } from 'preact/hooks'; import videojs from 'video.js'; -import 'videojs-mobile-ui'; import 'videojs-playlist'; import 'videojs-seek-buttons'; import 'video.js/dist/video-js.css'; @@ -11,49 +11,85 @@ const defaultOptions = { playbackRates: [0.5, 1, 2, 4, 8], fluid: true, }; +const defaultSeekOptions = { + forward: 30, + back: 10, +}; -export default class VideoPlayer extends Component { - componentDidMount() { - 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, +export default function VideoPlayer({ children, options, seekOptions = {}, onReady = () => {}, onDispose = () => {} }) { + const playerRef = useRef(); + + useEffect(() => { + const player = videojs(playerRef.current, { ...defaultOptions, ...options }, () => { + onReady(player); }); - this.player.mobileUi({ - fullscreen: { - iOS: true, - }, + player.seekButtons({ + ...defaultSeekOptions, + ...seekOptions, }); - } - componentWillUnmount() { - const { onDispose = () => {} } = this.props; - if (this.player) { - this.player.dispose(); - onDispose(); + // 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; + }; + } + + const screen = window.screen; + + const angle = () => { + // iOS + if (typeof window.orientation === 'number') { + return window.orientation; + } + // Android + if (screen && screen.orientation && screen.orientation.angle) { + return window.orientation; + } + 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; } - } - - // shouldComponentUpdate() { - // return false; - // } - - render() { - const { style, children } = this.props; - return ( -
-
-
-
- ); - } + + return () => { + if (videojs.browser.IS_IOS) { + window.removeEventListener('orientationchange', rotationHandler); + } + player.dispose(); + onDispose(); + }; + }, []); + + return ( +
+
+ ); } diff --git a/web/src/index.css b/web/src/index.css index a9490f0d9f..d55a9aabcf 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -25,7 +25,3 @@ transform: rotate(360deg); } } - -.video-js.vjs-has-started .vjs-touch-overlay { - display: none; -} diff --git a/web/src/routes/Event.jsx b/web/src/routes/Event.jsx index dbf7aaa4fc..bf778ec18e 100644 --- a/web/src/routes/Event.jsx +++ b/web/src/routes/Event.jsx @@ -3,10 +3,13 @@ import { useCallback, useState } from 'preact/hooks'; import { route } from 'preact-router'; import ActivityIndicator from '../components/ActivityIndicator'; 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 Heading from '../components/Heading'; import Link from '../components/Link'; +import VideoPlayer from '../components/VideoPlayer'; import { FetchStatus, useApiHost, useEvent } from '../api'; import { Table, Thead, Tbody, Th, Tr, Td } from '../components/Table'; @@ -24,9 +27,7 @@ export default function Event({ eventId }) { setShowDialog(false); }; - const handleClickDeleteDialog = useCallback(async () => { - let success; try { const response = await fetch(`${apiHost}/api/events/${eventId}`, { method: 'DELETE' }); @@ -40,12 +41,11 @@ export default function Event({ eventId }) { setDeleteStatus(FetchStatus.LOADED); setShowDialog(false); route('/events', true); - } }, [apiHost, eventId, setShowDialog]); if (status !== FetchStatus.LOADED) { - return + return ; } const startime = new Date(data.start_time * 1000); @@ -106,28 +106,44 @@ export default function Event({ eventId }) { {data.has_clip ? ( - Clip - ) : ( -

No clip available

+ + {data.has_snapshot ? 'Best Image' : 'Thumbnail'} + {`${data.label} + )} - - {data.has_snapshot ? 'Best image' : 'Thumbnail'} - {`${data.label} ); }