mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Add jsmpeg support to new webUI and make birdseye default for live page (#8995)
* Add jsmpeg and make birdseye default for live view * Fix jsmpeg * Fix
This commit is contained in:
		
							parent
							
								
									1a27c7db29
								
							
						
					
					
						commit
						2236ae5d3b
					
				
							
								
								
									
										36
									
								
								web/src/components/player/BirdseyeLivePlayer.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										36
									
								
								web/src/components/player/BirdseyeLivePlayer.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,36 @@
 | 
			
		||||
import WebRtcPlayer from "./WebRTCPlayer";
 | 
			
		||||
import { BirdseyeConfig } from "@/types/frigateConfig";
 | 
			
		||||
import ActivityIndicator from "../ui/activity-indicator";
 | 
			
		||||
import JSMpegPlayer from "./JSMpegPlayer";
 | 
			
		||||
 | 
			
		||||
type LivePlayerProps = {
 | 
			
		||||
  birdseyeConfig: BirdseyeConfig;
 | 
			
		||||
  liveMode: string;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default function BirdseyeLivePlayer({
 | 
			
		||||
  birdseyeConfig,
 | 
			
		||||
  liveMode,
 | 
			
		||||
}: LivePlayerProps) {
 | 
			
		||||
  if (liveMode == "webrtc") {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className="max-w-5xl">
 | 
			
		||||
        <WebRtcPlayer camera="birdseye" />
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  } else if (liveMode == "mse") {
 | 
			
		||||
    return <div className="max-w-5xl">Not yet implemented</div>;
 | 
			
		||||
  } else if (liveMode == "jsmpeg") {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className={`max-w-[${birdseyeConfig.width}px]`}>
 | 
			
		||||
        <JSMpegPlayer
 | 
			
		||||
          camera="birdseye"
 | 
			
		||||
          width={birdseyeConfig.width}
 | 
			
		||||
          height={birdseyeConfig.height}
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  } else {
 | 
			
		||||
    <ActivityIndicator />;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										94
									
								
								web/src/components/player/JSMpegPlayer.tsx
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										94
									
								
								web/src/components/player/JSMpegPlayer.tsx
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,94 @@
 | 
			
		||||
import { baseUrl } from "@/api/baseUrl";
 | 
			
		||||
import { useResizeObserver } from "@/hooks/resize-observer";
 | 
			
		||||
// @ts-ignore we know this doesn't have types
 | 
			
		||||
import JSMpeg from "@cycjimmy/jsmpeg-player";
 | 
			
		||||
import { useEffect, useMemo, useRef } from "react";
 | 
			
		||||
 | 
			
		||||
type JSMpegPlayerProps = {
 | 
			
		||||
  camera: string;
 | 
			
		||||
  width: number;
 | 
			
		||||
  height: number;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export default function JSMpegPlayer({
 | 
			
		||||
  camera,
 | 
			
		||||
  width,
 | 
			
		||||
  height,
 | 
			
		||||
}: JSMpegPlayerProps) {
 | 
			
		||||
  const url = `${baseUrl.replace(/^http/, "ws")}live/jsmpeg/${camera}`;
 | 
			
		||||
  const playerRef = useRef<HTMLDivElement | null>(null);
 | 
			
		||||
  const containerRef = useRef<HTMLDivElement | null>(null);
 | 
			
		||||
  const [{ width: containerWidth }] = useResizeObserver(containerRef);
 | 
			
		||||
 | 
			
		||||
  // Add scrollbar width (when visible) to the available observer width to eliminate screen juddering.
 | 
			
		||||
  // https://github.com/blakeblackshear/frigate/issues/1657
 | 
			
		||||
  let scrollBarWidth = 0;
 | 
			
		||||
  if (window.innerWidth && document.body.offsetWidth) {
 | 
			
		||||
    scrollBarWidth = window.innerWidth - document.body.offsetWidth;
 | 
			
		||||
  }
 | 
			
		||||
  const availableWidth = scrollBarWidth
 | 
			
		||||
    ? containerWidth + scrollBarWidth
 | 
			
		||||
    : containerWidth;
 | 
			
		||||
  const aspectRatio = width / height;
 | 
			
		||||
 | 
			
		||||
  const scaledHeight = useMemo(() => {
 | 
			
		||||
    const scaledHeight = Math.floor(availableWidth / aspectRatio);
 | 
			
		||||
    const finalHeight = Math.min(scaledHeight, height);
 | 
			
		||||
 | 
			
		||||
    if (finalHeight > 0) {
 | 
			
		||||
      return finalHeight;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return 100;
 | 
			
		||||
  }, [availableWidth, aspectRatio, height]);
 | 
			
		||||
  const scaledWidth = useMemo(
 | 
			
		||||
    () => Math.ceil(scaledHeight * aspectRatio - scrollBarWidth),
 | 
			
		||||
    [scaledHeight, aspectRatio, scrollBarWidth]
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  useEffect(() => {
 | 
			
		||||
    if (!playerRef.current) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    console.log("player ref exists and creating video");
 | 
			
		||||
    const video = new JSMpeg.VideoElement(
 | 
			
		||||
      playerRef.current,
 | 
			
		||||
      url,
 | 
			
		||||
      {},
 | 
			
		||||
      { protocols: [], audio: false, videoBufferSize: 1024 * 1024 * 4 }
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const fullscreen = () => {
 | 
			
		||||
      if (video.els.canvas.webkitRequestFullScreen) {
 | 
			
		||||
        video.els.canvas.webkitRequestFullScreen();
 | 
			
		||||
      } else {
 | 
			
		||||
        video.els.canvas.mozRequestFullScreen();
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    video.els.canvas.addEventListener("click", fullscreen);
 | 
			
		||||
 | 
			
		||||
    return () => {
 | 
			
		||||
      if (playerRef.current) {
 | 
			
		||||
        try {
 | 
			
		||||
          video.destroy();
 | 
			
		||||
        } catch (e) {}
 | 
			
		||||
        playerRef.current = null;
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
  }, [url]);
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div ref={containerRef}>
 | 
			
		||||
      <div
 | 
			
		||||
        ref={playerRef}
 | 
			
		||||
        className={`jsmpeg`}
 | 
			
		||||
        style={{
 | 
			
		||||
          height: `${scaledHeight}px`,
 | 
			
		||||
          width: `${scaledWidth}px`,
 | 
			
		||||
        }}
 | 
			
		||||
      />
 | 
			
		||||
    </div>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
@ -9,6 +9,7 @@ import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
 | 
			
		||||
import { Switch } from "../ui/switch";
 | 
			
		||||
import { Label } from "../ui/label";
 | 
			
		||||
import { usePersistence } from "@/hooks/use-persistence";
 | 
			
		||||
import JSMpegPlayer from "./JSMpegPlayer";
 | 
			
		||||
 | 
			
		||||
const emptyObject = Object.freeze({});
 | 
			
		||||
 | 
			
		||||
@ -66,7 +67,11 @@ export default function LivePlayer({
 | 
			
		||||
  } else if (liveMode == "jsmpeg") {
 | 
			
		||||
    return (
 | 
			
		||||
      <div className={`max-w-[${cameraConfig.detect.width}px]`}>
 | 
			
		||||
        Not Yet Implemented
 | 
			
		||||
        <JSMpegPlayer
 | 
			
		||||
          camera={cameraConfig.name}
 | 
			
		||||
          width={cameraConfig.detect.width}
 | 
			
		||||
          height={cameraConfig.detect.height}
 | 
			
		||||
        />
 | 
			
		||||
      </div>
 | 
			
		||||
    );
 | 
			
		||||
  } else if (liveMode == "debug") {
 | 
			
		||||
 | 
			
		||||
@ -1,3 +1,4 @@
 | 
			
		||||
import BirdseyeLivePlayer from "@/components/player/BirdseyeLivePlayer";
 | 
			
		||||
import LivePlayer from "@/components/player/LivePlayer";
 | 
			
		||||
import { Button } from "@/components/ui/button";
 | 
			
		||||
import {
 | 
			
		||||
@ -21,18 +22,19 @@ function Live() {
 | 
			
		||||
  const { camera: openedCamera } = useParams();
 | 
			
		||||
 | 
			
		||||
  const [camera, setCamera] = useState<string>(
 | 
			
		||||
    openedCamera ?? "Select A Camera"
 | 
			
		||||
    openedCamera ?? (config?.birdseye.enabled ? "birdseye" : "Select A Camera")
 | 
			
		||||
  );
 | 
			
		||||
  const cameraConfig = useMemo(() => {
 | 
			
		||||
    return config?.cameras[camera];
 | 
			
		||||
    return camera == "birdseye" ? undefined : config?.cameras[camera];
 | 
			
		||||
  }, [camera, config]);
 | 
			
		||||
  const sortedCameras = useMemo(() => {
 | 
			
		||||
    if (!config) {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return Object.values(config.cameras)
 | 
			
		||||
      .sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
 | 
			
		||||
    return Object.values(config.cameras).sort(
 | 
			
		||||
      (aConf, bConf) => aConf.ui.order - bConf.ui.order
 | 
			
		||||
    );
 | 
			
		||||
  }, [config]);
 | 
			
		||||
  const restreamEnabled = useMemo(() => {
 | 
			
		||||
    return (
 | 
			
		||||
@ -56,7 +58,7 @@ function Live() {
 | 
			
		||||
  }, [cameraConfig, restreamEnabled]);
 | 
			
		||||
  const [viewSource, setViewSource, sourceIsLoaded] = usePersistence(
 | 
			
		||||
    `${camera}-source`,
 | 
			
		||||
    defaultLiveMode
 | 
			
		||||
    camera == "birdseye" ? "jsmpeg" : defaultLiveMode
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
@ -74,7 +76,7 @@ function Live() {
 | 
			
		||||
              <DropdownMenuLabel>Select A Camera</DropdownMenuLabel>
 | 
			
		||||
              <DropdownMenuSeparator />
 | 
			
		||||
              <DropdownMenuRadioGroup value={camera} onValueChange={setCamera}>
 | 
			
		||||
                {(sortedCameras).map((item) => (
 | 
			
		||||
                {sortedCameras.map((item) => (
 | 
			
		||||
                  <DropdownMenuRadioItem
 | 
			
		||||
                    className="capitalize"
 | 
			
		||||
                    key={item.name}
 | 
			
		||||
@ -114,6 +116,12 @@ function Live() {
 | 
			
		||||
          </DropdownMenu>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
      {config && camera == "birdseye" && sourceIsLoaded && (
 | 
			
		||||
        <BirdseyeLivePlayer
 | 
			
		||||
          birdseyeConfig={config?.birdseye}
 | 
			
		||||
          liveMode={`${viewSource ?? defaultLiveMode}`}
 | 
			
		||||
        />
 | 
			
		||||
      )}
 | 
			
		||||
      {cameraConfig && sourceIsLoaded && (
 | 
			
		||||
        <LivePlayer
 | 
			
		||||
          liveMode={`${viewSource ?? defaultLiveMode}`}
 | 
			
		||||
 | 
			
		||||
@ -1,8 +1,8 @@
 | 
			
		||||
export interface UiConfig {
 | 
			
		||||
  timezone?: string;
 | 
			
		||||
  time_format?: 'browser' | '12hour' | '24hour';
 | 
			
		||||
  date_style?: 'full' | 'long' | 'medium' | 'short';
 | 
			
		||||
  time_style?: 'full' | 'long' | 'medium' | 'short';
 | 
			
		||||
  time_format?: "browser" | "12hour" | "24hour";
 | 
			
		||||
  date_style?: "full" | "long" | "medium" | "short";
 | 
			
		||||
  time_style?: "full" | "long" | "medium" | "short";
 | 
			
		||||
  strftime_fmt?: string;
 | 
			
		||||
  live_mode?: string;
 | 
			
		||||
  use_experimental?: boolean;
 | 
			
		||||
@ -10,6 +10,15 @@ export interface UiConfig {
 | 
			
		||||
  order: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface BirdseyeConfig {
 | 
			
		||||
  enabled: boolean;
 | 
			
		||||
  height: number;
 | 
			
		||||
  mode: "objects" | "continuous" | "motion";
 | 
			
		||||
  quality: number;
 | 
			
		||||
  restream: boolean;
 | 
			
		||||
  width: number;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface CameraConfig {
 | 
			
		||||
  audio: {
 | 
			
		||||
    enabled: boolean;
 | 
			
		||||
@ -23,7 +32,7 @@ export interface CameraConfig {
 | 
			
		||||
  best_image_timeout: number;
 | 
			
		||||
  birdseye: {
 | 
			
		||||
    enabled: boolean;
 | 
			
		||||
    mode: "objects";
 | 
			
		||||
    mode: "objects" | "continuous" | "motion";
 | 
			
		||||
    order: number;
 | 
			
		||||
  };
 | 
			
		||||
  detect: {
 | 
			
		||||
@ -204,14 +213,7 @@ export interface FrigateConfig {
 | 
			
		||||
    num_threads: number;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  birdseye: {
 | 
			
		||||
    enabled: boolean;
 | 
			
		||||
    height: number;
 | 
			
		||||
    mode: "objects";
 | 
			
		||||
    quality: number;
 | 
			
		||||
    restream: boolean;
 | 
			
		||||
    width: number;
 | 
			
		||||
  };
 | 
			
		||||
  birdseye: BirdseyeConfig;
 | 
			
		||||
 | 
			
		||||
  cameras: {
 | 
			
		||||
    [cameraName: string]: CameraConfig;
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user