diff --git a/web/src/components/MsePlayer.jsx b/web/src/components/MsePlayer.jsx new file mode 100644 index 000000000..247867d69 --- /dev/null +++ b/web/src/components/MsePlayer.jsx @@ -0,0 +1,93 @@ +import { h } from 'preact'; +import { baseUrl } from '../api/baseUrl'; +import { useEffect } from 'preact/hooks'; + +export default function MsePlayer({ camera, width, height }) { + const url = `${baseUrl.replace(/^http/, 'ws')}live/webrtc/api/ws?src=${camera}`; + + useEffect(() => { + const video = document.querySelector('#video'); + + // support api_path + const ws = new WebSocket(url); + ws.binaryType = 'arraybuffer'; + + let mediaSource; + + ws.onopen = () => { + // https://web.dev/i18n/en/fast-playback-with-preload/#manual_buffering + // https://developer.mozilla.org/en-US/docs/Web/API/Media_Source_Extensions_API + mediaSource = new MediaSource(); + video.src = URL.createObjectURL(mediaSource); + mediaSource.onsourceopen = () => { + mediaSource.onsourceopen = null; + URL.revokeObjectURL(video.src); + ws.send(JSON.stringify({ type: 'mse' })); + }; + }; + + let sourceBuffer, + queueBuffer = []; + + ws.onmessage = (ev) => { + if (typeof ev.data === 'string') { + const data = JSON.parse(ev.data); + + if (data.type === 'mse') { + sourceBuffer = mediaSource.addSourceBuffer(data.value); + // important: segments supports TrackFragDecodeTime + // sequence supports only TrackFragRunEntry Duration + sourceBuffer.mode = 'segments'; + sourceBuffer.onupdateend = () => { + if (!sourceBuffer.updating && queueBuffer.length > 0) { + sourceBuffer.appendBuffer(queueBuffer.shift()); + } + }; + } + } else if (sourceBuffer.updating) { + queueBuffer.push(ev.data); + } else { + sourceBuffer.appendBuffer(ev.data); + } + }; + + let offsetTime = 1, + noWaiting = 0; + + setInterval(() => { + if (video.paused || video.seekable.length === 0) return; + + if (noWaiting < 0) { + offsetTime = Math.min(offsetTime * 1.1, 5); + } else if (noWaiting >= 30) { + noWaiting = 0; + offsetTime = Math.max(offsetTime * 0.9, 0.5); + } + noWaiting += 1; + + const endTime = video.seekable.end(video.seekable.length - 1); + let playbackRate = (endTime - video.currentTime) / offsetTime; + if (playbackRate < 0.1) { + // video.currentTime = endTime - offsetTime; + playbackRate = 0.1; + } else if (playbackRate > 10) { + // video.currentTime = endTime - offsetTime; + playbackRate = 10; + } + // https://github.com/GoogleChrome/developer.chrome.com/issues/135 + video.playbackRate = playbackRate; + }, 1000); + + video.onwaiting = () => { + const endTime = video.seekable.end(video.seekable.length - 1); + video.currentTime = endTime - offsetTime; + noWaiting = -1; + }; + }, [url]); + + return ( +
+
+ ); +} diff --git a/web/src/routes/Camera.jsx b/web/src/routes/Camera.jsx index 05fa8d8dd..aaf78239c 100644 --- a/web/src/routes/Camera.jsx +++ b/web/src/routes/Camera.jsx @@ -15,6 +15,7 @@ import { useApiHost } from '../api'; import useSWR from 'swr'; import VideoPlayer from '../components/VideoPlayer'; import WebRtcPlayer from '../components/WebRtcPlayer'; +import MsePlayer from '../components/MsePlayer'; const emptyObject = Object.freeze({}); @@ -29,7 +30,7 @@ export default function Camera({ camera }) { ? Math.round(cameraConfig.restream.jsmpeg.height * (cameraConfig.detect.width / cameraConfig.detect.height)) : 0; const [viewSource, setViewSource, sourceIsLoaded] = usePersistence(`${camera}-source`, 'jsmpeg'); - const sourceValues = cameraConfig && cameraConfig.restream.enabled ? ['jsmpeg', 'mp4', 'webrtc'] : ['jsmpeg']; + const sourceValues = cameraConfig && cameraConfig.restream.enabled ? ['jsmpeg', 'mp4', 'mse', 'webrtc'] : ['jsmpeg']; const [options, setOptions] = usePersistence(`${camera}-feed`, emptyObject); const handleSetOption = useCallback( @@ -116,11 +117,17 @@ export default function Camera({ camera }) { ], }} seekOptions={{ forward: false, back: false }} - onReady={() => {}} + onReady={() => { }} /> ); + } else if (viewSource == 'mse') { + player = ( + + + + ); } else if (viewSource == 'webrtc') { player = (