mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
Use webrtc for safari live view (#9839)
* Move safari function * Use webrtc for safari * Use taller aspect ratio for tall thumbnails * Simplify thumbnail sizing * Use correct aspect ratio on all devices
This commit is contained in:
parent
764736b223
commit
8c4811ed69
@ -22,30 +22,23 @@ export function AnimatedEventThumbnail({ event }: AnimatedEventThumbnailProps) {
|
|||||||
return `${baseUrl}api/events/${event.id}/preview.gif`;
|
return `${baseUrl}api/events/${event.id}/preview.gif`;
|
||||||
}, [event]);
|
}, [event]);
|
||||||
|
|
||||||
const aspect = useMemo(() => {
|
const aspectRatio = useMemo(() => {
|
||||||
if (!config) {
|
if (!config) {
|
||||||
return "";
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
const detect = config.cameras[event.camera].detect;
|
const detect = config.cameras[event.camera].detect;
|
||||||
const aspect = detect.width / detect.height;
|
return detect.width / detect.height;
|
||||||
|
}, [config]);
|
||||||
if (aspect > 2) {
|
|
||||||
return "aspect-wide";
|
|
||||||
} else if (aspect < 1) {
|
|
||||||
return "aspect-tall";
|
|
||||||
} else {
|
|
||||||
return "aspect-video";
|
|
||||||
}
|
|
||||||
}, [config, event]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<div
|
<div
|
||||||
className={`relative rounded bg-cover h-24 bg-no-repeat bg-center mr-4 ${aspect}`}
|
className="h-24 relative rounded bg-cover bg-no-repeat bg-center mr-4"
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: `url(${imageUrl})`,
|
backgroundImage: `url(${imageUrl})`,
|
||||||
|
aspectRatio: aspectRatio,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="absolute bottom-0 w-full h-6 bg-gradient-to-t from-slate-900/50 to-transparent rounded">
|
<div className="absolute bottom-0 w-full h-6 bg-gradient-to-t from-slate-900/50 to-transparent rounded">
|
||||||
|
@ -87,6 +87,7 @@ export default function LivePlayer({
|
|||||||
<WebRtcPlayer
|
<WebRtcPlayer
|
||||||
className={`rounded-2xl h-full ${liveReady ? "" : "hidden"}`}
|
className={`rounded-2xl h-full ${liveReady ? "" : "hidden"}`}
|
||||||
camera={cameraConfig.live.stream_name}
|
camera={cameraConfig.live.stream_name}
|
||||||
|
playbackEnabled={cameraActive}
|
||||||
onPlaying={() => setLiveReady(true)}
|
onPlaying={() => setLiveReady(true)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -2,7 +2,6 @@ import VideoPlayer from "./VideoPlayer";
|
|||||||
import React, {
|
import React, {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from "react";
|
||||||
@ -11,6 +10,7 @@ import Player from "video.js/dist/types/player";
|
|||||||
import { AspectRatio } from "../ui/aspect-ratio";
|
import { AspectRatio } from "../ui/aspect-ratio";
|
||||||
import { LuPlayCircle } from "react-icons/lu";
|
import { LuPlayCircle } from "react-icons/lu";
|
||||||
import { isCurrentHour } from "@/utils/dateUtil";
|
import { isCurrentHour } from "@/utils/dateUtil";
|
||||||
|
import { isSafari } from "@/utils/browserUtil";
|
||||||
|
|
||||||
type PreviewPlayerProps = {
|
type PreviewPlayerProps = {
|
||||||
camera: string;
|
camera: string;
|
||||||
@ -38,9 +38,6 @@ export default function PreviewThumbnailPlayer({
|
|||||||
onClick,
|
onClick,
|
||||||
}: PreviewPlayerProps) {
|
}: PreviewPlayerProps) {
|
||||||
const playerRef = useRef<Player | null>(null);
|
const playerRef = useRef<Player | null>(null);
|
||||||
const isSafari = useMemo(() => {
|
|
||||||
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
const [isInitiallyVisible, setIsInitiallyVisible] = useState(false);
|
const [isInitiallyVisible, setIsInitiallyVisible] = useState(false);
|
||||||
@ -135,7 +132,6 @@ export default function PreviewThumbnailPlayer({
|
|||||||
camera={camera}
|
camera={camera}
|
||||||
eventId={eventId}
|
eventId={eventId}
|
||||||
isMobile={isMobile}
|
isMobile={isMobile}
|
||||||
isSafari={isSafari}
|
|
||||||
onClick={onClick}
|
onClick={onClick}
|
||||||
/>
|
/>
|
||||||
</AspectRatio>
|
</AspectRatio>
|
||||||
@ -151,7 +147,6 @@ type PreviewContentProps = {
|
|||||||
isInitiallyVisible: boolean;
|
isInitiallyVisible: boolean;
|
||||||
startTs: number;
|
startTs: number;
|
||||||
isMobile: boolean;
|
isMobile: boolean;
|
||||||
isSafari: boolean;
|
|
||||||
onClick?: () => void;
|
onClick?: () => void;
|
||||||
};
|
};
|
||||||
function PreviewContent({
|
function PreviewContent({
|
||||||
@ -163,10 +158,10 @@ function PreviewContent({
|
|||||||
isInitiallyVisible,
|
isInitiallyVisible,
|
||||||
startTs,
|
startTs,
|
||||||
isMobile,
|
isMobile,
|
||||||
isSafari,
|
|
||||||
onClick,
|
onClick,
|
||||||
}: PreviewContentProps) {
|
}: PreviewContentProps) {
|
||||||
const apiHost = useApiHost();
|
const apiHost = useApiHost();
|
||||||
|
const slowPlayack = isSafari();
|
||||||
|
|
||||||
// handle touchstart -> touchend as click
|
// handle touchstart -> touchend as click
|
||||||
const [touchStart, setTouchStart] = useState(0);
|
const [touchStart, setTouchStart] = useState(0);
|
||||||
@ -237,7 +232,7 @@ function PreviewContent({
|
|||||||
player.pause(); // autoplay + pause is required for iOS
|
player.pause(); // autoplay + pause is required for iOS
|
||||||
}
|
}
|
||||||
|
|
||||||
player.playbackRate(isSafari ? 2 : 8);
|
player.playbackRate(slowPlayack ? 2 : 8);
|
||||||
player.currentTime(startTs - relevantPreview.start);
|
player.currentTime(startTs - relevantPreview.start);
|
||||||
if (isMobile && onClick) {
|
if (isMobile && onClick) {
|
||||||
player.on("touchstart", handleTouchStart);
|
player.on("touchstart", handleTouchStart);
|
||||||
|
@ -4,16 +4,21 @@ import { useCallback, useEffect, useRef } from "react";
|
|||||||
type WebRtcPlayerProps = {
|
type WebRtcPlayerProps = {
|
||||||
className?: string;
|
className?: string;
|
||||||
camera: string;
|
camera: string;
|
||||||
|
playbackEnabled?: boolean;
|
||||||
onPlaying?: () => void;
|
onPlaying?: () => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function WebRtcPlayer({
|
export default function WebRtcPlayer({
|
||||||
className,
|
className,
|
||||||
camera,
|
camera,
|
||||||
|
playbackEnabled = true,
|
||||||
onPlaying,
|
onPlaying,
|
||||||
}: WebRtcPlayerProps) {
|
}: WebRtcPlayerProps) {
|
||||||
|
// camera states
|
||||||
|
|
||||||
const pcRef = useRef<RTCPeerConnection | undefined>();
|
const pcRef = useRef<RTCPeerConnection | undefined>();
|
||||||
const videoRef = useRef<HTMLVideoElement | null>(null);
|
const videoRef = useRef<HTMLVideoElement | null>(null);
|
||||||
|
|
||||||
const PeerConnection = useCallback(
|
const PeerConnection = useCallback(
|
||||||
async (media: string) => {
|
async (media: string) => {
|
||||||
if (!videoRef.current) {
|
if (!videoRef.current) {
|
||||||
@ -129,6 +134,10 @@ export default function WebRtcPlayer({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!playbackEnabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const url = `${baseUrl.replace(
|
const url = `${baseUrl.replace(
|
||||||
/^http/,
|
/^http/,
|
||||||
"ws"
|
"ws"
|
||||||
@ -143,10 +152,9 @@ export default function WebRtcPlayer({
|
|||||||
pcRef.current = undefined;
|
pcRef.current = undefined;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}, [camera, connect, PeerConnection, pcRef, videoRef]);
|
}, [camera, connect, PeerConnection, pcRef, videoRef, playbackEnabled]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
|
||||||
<video
|
<video
|
||||||
ref={videoRef}
|
ref={videoRef}
|
||||||
className={className}
|
className={className}
|
||||||
@ -155,6 +163,5 @@ export default function WebRtcPlayer({
|
|||||||
muted
|
muted
|
||||||
onLoadedData={onPlaying}
|
onLoadedData={onPlaying}
|
||||||
/>
|
/>
|
||||||
</div>
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area";
|
|||||||
import { TooltipProvider } from "@/components/ui/tooltip";
|
import { TooltipProvider } from "@/components/ui/tooltip";
|
||||||
import { Event as FrigateEvent } from "@/types/event";
|
import { Event as FrigateEvent } from "@/types/event";
|
||||||
import { FrigateConfig } from "@/types/frigateConfig";
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
|
import { isSafari } from "@/utils/browserUtil";
|
||||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
|
||||||
@ -64,6 +65,7 @@ function Live() {
|
|||||||
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
|
.sort((aConf, bConf) => aConf.ui.order - bConf.ui.order);
|
||||||
}, [config]);
|
}, [config]);
|
||||||
|
|
||||||
|
const safari = isSafari();
|
||||||
const [windowVisible, setWindowVisible] = useState(true);
|
const [windowVisible, setWindowVisible] = useState(true);
|
||||||
const visibilityListener = useCallback(() => {
|
const visibilityListener = useCallback(() => {
|
||||||
setWindowVisible(document.visibilityState == "visible");
|
setWindowVisible(document.visibilityState == "visible");
|
||||||
@ -109,7 +111,7 @@ function Live() {
|
|||||||
className={`mb-2 md:mb-0 rounded-2xl bg-black ${grow}`}
|
className={`mb-2 md:mb-0 rounded-2xl bg-black ${grow}`}
|
||||||
windowVisible={windowVisible}
|
windowVisible={windowVisible}
|
||||||
cameraConfig={camera}
|
cameraConfig={camera}
|
||||||
preferredLiveMode="mse"
|
preferredLiveMode={safari ? "webrtc" : "mse"}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
7
web/src/utils/browserUtil.ts
Normal file
7
web/src/utils/browserUtil.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { useMemo } from "react";
|
||||||
|
|
||||||
|
export function isSafari() {
|
||||||
|
return useMemo(() => {
|
||||||
|
return /^((?!chrome|android).)*safari/i.test(navigator.userAgent);
|
||||||
|
}, []);
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user