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:
Josh Hawkins
2025-02-10 09:42:35 -07:00
committed by GitHub
parent 2a28964e63
commit dd7820e4ee
31 changed files with 2681 additions and 219 deletions
+62 -4
View File
@@ -11,6 +11,7 @@ import { useCameraActivity } from "@/hooks/use-camera-activity";
import {
LivePlayerError,
LivePlayerMode,
PlayerStatsType,
VideoResolutionType,
} from "@/types/live";
import { getIconForLabel } from "@/utils/iconUtil";
@@ -20,20 +21,26 @@ import { cn } from "@/lib/utils";
import { TbExclamationCircle } from "react-icons/tb";
import { TooltipPortal } from "@radix-ui/react-tooltip";
import { baseUrl } from "@/api/baseUrl";
import { PlayerStats } from "./PlayerStats";
type LivePlayerProps = {
cameraRef?: (ref: HTMLDivElement | null) => void;
containerRef?: React.MutableRefObject<HTMLDivElement | null>;
className?: string;
cameraConfig: CameraConfig;
streamName: string;
preferredLiveMode: LivePlayerMode;
showStillWithoutActivity?: boolean;
useWebGL: boolean;
windowVisible?: boolean;
playAudio?: boolean;
volume?: number;
playInBackground: boolean;
micEnabled?: boolean; // only webrtc supports mic
iOSCompatFullScreen?: boolean;
pip?: boolean;
autoLive?: boolean;
showStats?: boolean;
onClick?: () => void;
setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
onError?: (error: LivePlayerError) => void;
@@ -45,14 +52,19 @@ export default function LivePlayer({
containerRef,
className,
cameraConfig,
streamName,
preferredLiveMode,
showStillWithoutActivity = true,
useWebGL = false,
windowVisible = true,
playAudio = false,
volume,
playInBackground = false,
micEnabled = false,
iOSCompatFullScreen = false,
pip,
autoLive = true,
showStats = false,
onClick,
setFullResolution,
onError,
@@ -60,6 +72,18 @@ export default function LivePlayer({
}: LivePlayerProps) {
const internalContainerRef = useRef<HTMLDivElement | null>(null);
// stats
const [stats, setStats] = useState<PlayerStatsType>({
streamType: "-",
bandwidth: 0, // in kbps
latency: undefined, // in seconds
totalFrames: 0,
droppedFrames: undefined,
decodedFrames: 0,
droppedFrameRate: 0, // percentage
});
// camera activity
const { activeMotion, activeTracking, objects, offline } =
@@ -144,6 +168,25 @@ export default function LivePlayer({
setLiveReady(false);
}, [preferredLiveMode]);
const [key, setKey] = useState(0);
const resetPlayer = () => {
setLiveReady(false);
setKey((prevKey) => prevKey + 1);
};
useEffect(() => {
if (streamName) {
resetPlayer();
}
}, [streamName]);
useEffect(() => {
if (showStillWithoutActivity && !autoLive) {
setLiveReady(false);
}
}, [showStillWithoutActivity, autoLive]);
const playerIsPlaying = useCallback(() => {
setLiveReady(true);
}, []);
@@ -153,15 +196,19 @@ export default function LivePlayer({
}
let player;
if (!autoLive) {
if (!autoLive || !streamName) {
player = null;
} else if (preferredLiveMode == "webrtc") {
player = (
<WebRtcPlayer
key={"webrtc_" + key}
className={`size-full rounded-lg md:rounded-2xl ${liveReady ? "" : "hidden"}`}
camera={cameraConfig.live.stream_name}
camera={streamName}
playbackEnabled={cameraActive || liveReady}
getStats={showStats}
setStats={setStats}
audioEnabled={playAudio}
volume={volume}
microphoneEnabled={micEnabled}
iOSCompatFullScreen={iOSCompatFullScreen}
onPlaying={playerIsPlaying}
@@ -173,10 +220,15 @@ export default function LivePlayer({
if ("MediaSource" in window || "ManagedMediaSource" in window) {
player = (
<MSEPlayer
key={"mse_" + key}
className={`size-full rounded-lg md:rounded-2xl ${liveReady ? "" : "hidden"}`}
camera={cameraConfig.live.stream_name}
camera={streamName}
playbackEnabled={cameraActive || liveReady}
audioEnabled={playAudio}
volume={volume}
playInBackground={playInBackground}
getStats={showStats}
setStats={setStats}
onPlaying={playerIsPlaying}
pip={pip}
setFullResolution={setFullResolution}
@@ -194,6 +246,7 @@ export default function LivePlayer({
if (cameraActive || !showStillWithoutActivity || liveReady) {
player = (
<JSMpegPlayer
key={"jsmpeg_" + key}
className="flex justify-center overflow-hidden rounded-lg md:rounded-2xl"
camera={cameraConfig.name}
width={cameraConfig.detect.width}
@@ -201,6 +254,8 @@ export default function LivePlayer({
playbackEnabled={
cameraActive || !showStillWithoutActivity || liveReady
}
useWebGL={useWebGL}
setStats={setStats}
containerRef={containerRef ?? internalContainerRef}
onPlaying={playerIsPlaying}
/>
@@ -293,7 +348,7 @@ export default function LivePlayer({
)}
>
<AutoUpdatingCameraImage
className="size-full"
className="pointer-events-none size-full"
cameraClasses="relative size-full flex justify-center"
camera={cameraConfig.name}
showFps={false}
@@ -331,6 +386,9 @@ export default function LivePlayer({
</Chip>
)}
</div>
{showStats && (
<PlayerStats stats={stats} minimal={cameraRef !== undefined} />
)}
</div>
);
}