mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-08-02 09:02:15 +03:00
Improve live streaming (#16447)
* config file changes * config migrator * stream selection on single camera live view * camera streaming settings dialog * manage persistent group streaming settings * apply streaming settings in camera groups * add ability to clear all streaming settings from settings * docs * update reference config * fixes * clarify docs * use first stream as default in dialog * ensure still image is visible after switching stream type to none * docs * clarify docs * add ability to continue playing stream in background * fix props * put stream selection inside dropdown on desktop * add capabilities to live mode hook * live context menu component * resize observer: only return new dimensions if they've actually changed * pass volume prop to players * fix slider bug, https://github.com/shadcn-ui/ui/issues/1448 * update react-grid-layout * prevent animated transitions on draggable grid layout * add context menu to dashboards * use provider * streaming dialog from context menu * docs * add jsmpeg warning to context menu * audio and two way talk indicators in single camera view * add link to debug view * don't use hook * create manual events from live camera view * maintain grow classes on grid items * fix initial volume state on default dashboard * fix pointer events causing context menu to end up underneath image on iOS * mobile drawer tweaks * stream stats * show settings menu for non-restreamed cameras * consistent settings icon * tweaks * optional stats to fix birdseye player * add toaster to live camera view * fix crash on initial save in streaming dialog * don't require restreaming for context menu streaming settings * add debug view to context menu * stats fixes * update docs * always show stream info when restreamed * update camera streaming dialog * make note of no h265 support for webrtc * docs clarity * ensure docs show streams as a dict * docs clarity * fix css file * tweaks
This commit is contained in:
@@ -1,5 +1,9 @@
|
||||
import { baseUrl } from "@/api/baseUrl";
|
||||
import { LivePlayerError, VideoResolutionType } from "@/types/live";
|
||||
import {
|
||||
LivePlayerError,
|
||||
PlayerStatsType,
|
||||
VideoResolutionType,
|
||||
} from "@/types/live";
|
||||
import {
|
||||
SetStateAction,
|
||||
useCallback,
|
||||
@@ -15,7 +19,11 @@ type MSEPlayerProps = {
|
||||
className?: string;
|
||||
playbackEnabled?: boolean;
|
||||
audioEnabled?: boolean;
|
||||
volume?: number;
|
||||
playInBackground?: boolean;
|
||||
pip?: boolean;
|
||||
getStats?: boolean;
|
||||
setStats?: (stats: PlayerStatsType) => void;
|
||||
onPlaying?: () => void;
|
||||
setFullResolution?: React.Dispatch<SetStateAction<VideoResolutionType>>;
|
||||
onError?: (error: LivePlayerError) => void;
|
||||
@@ -26,7 +34,11 @@ function MSEPlayer({
|
||||
className,
|
||||
playbackEnabled = true,
|
||||
audioEnabled = false,
|
||||
volume,
|
||||
playInBackground = false,
|
||||
pip = false,
|
||||
getStats = false,
|
||||
setStats,
|
||||
onPlaying,
|
||||
setFullResolution,
|
||||
onError,
|
||||
@@ -57,6 +69,7 @@ function MSEPlayer({
|
||||
const [connectTS, setConnectTS] = useState<number>(0);
|
||||
const [bufferTimeout, setBufferTimeout] = useState<NodeJS.Timeout>();
|
||||
const [errorCount, setErrorCount] = useState<number>(0);
|
||||
const totalBytesLoaded = useRef(0);
|
||||
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const wsRef = useRef<WebSocket | null>(null);
|
||||
@@ -316,6 +329,8 @@ function MSEPlayer({
|
||||
let bufLen = 0;
|
||||
|
||||
ondataRef.current = (data) => {
|
||||
totalBytesLoaded.current += data.byteLength;
|
||||
|
||||
if (sb?.updating || bufLen > 0) {
|
||||
const b = new Uint8Array(data);
|
||||
buf.set(b, bufLen);
|
||||
@@ -508,12 +523,22 @@ function MSEPlayer({
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("visibilitychange", listener);
|
||||
if (!playInBackground) {
|
||||
document.addEventListener("visibilitychange", listener);
|
||||
}
|
||||
|
||||
return () => {
|
||||
document.removeEventListener("visibilitychange", listener);
|
||||
if (!playInBackground) {
|
||||
document.removeEventListener("visibilitychange", listener);
|
||||
}
|
||||
};
|
||||
}, [playbackEnabled, visibilityCheck, onConnect, onDisconnect]);
|
||||
}, [
|
||||
playbackEnabled,
|
||||
visibilityCheck,
|
||||
playInBackground,
|
||||
onConnect,
|
||||
onDisconnect,
|
||||
]);
|
||||
|
||||
// control pip
|
||||
|
||||
@@ -525,6 +550,16 @@ function MSEPlayer({
|
||||
videoRef.current.requestPictureInPicture();
|
||||
}, [pip, videoRef]);
|
||||
|
||||
// control volume
|
||||
|
||||
useEffect(() => {
|
||||
if (!videoRef.current || volume == undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
videoRef.current.volume = volume;
|
||||
}, [volume, videoRef]);
|
||||
|
||||
// ensure we disconnect for slower connections
|
||||
|
||||
useEffect(() => {
|
||||
@@ -542,6 +577,68 @@ function MSEPlayer({
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [playbackEnabled]);
|
||||
|
||||
// stats
|
||||
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
let lastLoadedBytes = totalBytesLoaded.current;
|
||||
let lastTimestamp = Date.now();
|
||||
|
||||
if (!getStats) return;
|
||||
|
||||
const updateStats = () => {
|
||||
if (video) {
|
||||
const now = Date.now();
|
||||
const bytesLoaded = totalBytesLoaded.current;
|
||||
const timeElapsed = (now - lastTimestamp) / 1000; // seconds
|
||||
const bandwidth = (bytesLoaded - lastLoadedBytes) / timeElapsed / 1024; // kbps
|
||||
|
||||
lastLoadedBytes = bytesLoaded;
|
||||
lastTimestamp = now;
|
||||
|
||||
const latency =
|
||||
video.seekable.length > 0
|
||||
? Math.max(
|
||||
0,
|
||||
video.seekable.end(video.seekable.length - 1) -
|
||||
video.currentTime,
|
||||
)
|
||||
: 0;
|
||||
|
||||
const videoQuality = video.getVideoPlaybackQuality();
|
||||
const { totalVideoFrames, droppedVideoFrames } = videoQuality;
|
||||
const droppedFrameRate = totalVideoFrames
|
||||
? (droppedVideoFrames / totalVideoFrames) * 100
|
||||
: 0;
|
||||
|
||||
setStats?.({
|
||||
streamType: "MSE",
|
||||
bandwidth,
|
||||
latency,
|
||||
totalFrames: totalVideoFrames,
|
||||
droppedFrames: droppedVideoFrames || undefined,
|
||||
decodedFrames: totalVideoFrames - droppedVideoFrames,
|
||||
droppedFrameRate,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const interval = setInterval(updateStats, 1000); // Update every second
|
||||
|
||||
return () => {
|
||||
clearInterval(interval);
|
||||
setStats?.({
|
||||
streamType: "-",
|
||||
bandwidth: 0,
|
||||
latency: undefined,
|
||||
totalFrames: 0,
|
||||
droppedFrames: undefined,
|
||||
decodedFrames: 0,
|
||||
droppedFrameRate: 0,
|
||||
});
|
||||
};
|
||||
}, [setStats, getStats]);
|
||||
|
||||
return (
|
||||
<video
|
||||
ref={videoRef}
|
||||
|
||||
Reference in New Issue
Block a user