mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-02-11 05:35:25 +03:00
Implement uploading image in frontend
This commit is contained in:
parent
40faa35199
commit
c23ebe0e4e
@ -234,7 +234,7 @@ def get_snapshot_from_recording(camera_name: str, frame_time: str):
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@MediaBp.route("/<camera_name>/plus/<frame_time>")
|
@MediaBp.route("/<camera_name>/plus/<frame_time>", methods=("POST",))
|
||||||
def submit_recording_snapshot_to_plus(camera_name: str, frame_time: str):
|
def submit_recording_snapshot_to_plus(camera_name: str, frame_time: str):
|
||||||
if camera_name not in current_app.frigate_config.cameras:
|
if camera_name not in current_app.frigate_config.cameras:
|
||||||
return make_response(
|
return make_response(
|
||||||
|
|||||||
@ -361,17 +361,14 @@ def yuv_crop_and_resize(frame, region, height=None):
|
|||||||
# copy u2
|
# copy u2
|
||||||
yuv_cropped_frame[
|
yuv_cropped_frame[
|
||||||
size + uv_channel_y_offset : size + uv_channel_y_offset + uv_crop_height,
|
size + uv_channel_y_offset : size + uv_channel_y_offset + uv_crop_height,
|
||||||
size // 2
|
size // 2 + uv_channel_x_offset : size // 2
|
||||||
+ uv_channel_x_offset : size // 2
|
|
||||||
+ uv_channel_x_offset
|
+ uv_channel_x_offset
|
||||||
+ uv_crop_width,
|
+ uv_crop_width,
|
||||||
] = frame[u2[1] : u2[3], u2[0] : u2[2]]
|
] = frame[u2[1] : u2[3], u2[0] : u2[2]]
|
||||||
|
|
||||||
# copy v1
|
# copy v1
|
||||||
yuv_cropped_frame[
|
yuv_cropped_frame[
|
||||||
size
|
size + size // 4 + uv_channel_y_offset : size
|
||||||
+ size // 4
|
|
||||||
+ uv_channel_y_offset : size
|
|
||||||
+ size // 4
|
+ size // 4
|
||||||
+ uv_channel_y_offset
|
+ uv_channel_y_offset
|
||||||
+ uv_crop_height,
|
+ uv_crop_height,
|
||||||
@ -380,14 +377,11 @@ def yuv_crop_and_resize(frame, region, height=None):
|
|||||||
|
|
||||||
# copy v2
|
# copy v2
|
||||||
yuv_cropped_frame[
|
yuv_cropped_frame[
|
||||||
size
|
size + size // 4 + uv_channel_y_offset : size
|
||||||
+ size // 4
|
|
||||||
+ uv_channel_y_offset : size
|
|
||||||
+ size // 4
|
+ size // 4
|
||||||
+ uv_channel_y_offset
|
+ uv_channel_y_offset
|
||||||
+ uv_crop_height,
|
+ uv_crop_height,
|
||||||
size // 2
|
size // 2 + uv_channel_x_offset : size // 2
|
||||||
+ uv_channel_x_offset : size // 2
|
|
||||||
+ uv_channel_x_offset
|
+ uv_channel_x_offset
|
||||||
+ uv_crop_width,
|
+ uv_crop_width,
|
||||||
] = frame[v2[1] : v2[3], v2[0] : v2[2]]
|
] = frame[v2[1] : v2[3], v2[0] : v2[2]]
|
||||||
|
|||||||
@ -10,6 +10,10 @@ import { isAndroid, isDesktop, isMobile } from "react-device-detect";
|
|||||||
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch";
|
||||||
import VideoControls from "./VideoControls";
|
import VideoControls from "./VideoControls";
|
||||||
import { VideoResolutionType } from "@/types/live";
|
import { VideoResolutionType } from "@/types/live";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { FrigateConfig } from "@/types/frigateConfig";
|
||||||
|
import { AxiosResponse } from "axios";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
|
||||||
// Android native hls does not seek correctly
|
// Android native hls does not seek correctly
|
||||||
const USE_NATIVE_HLS = !isAndroid;
|
const USE_NATIVE_HLS = !isAndroid;
|
||||||
@ -29,6 +33,7 @@ type HlsVideoPlayerProps = {
|
|||||||
onTimeUpdate?: (time: number) => void;
|
onTimeUpdate?: (time: number) => void;
|
||||||
onPlaying?: () => void;
|
onPlaying?: () => void;
|
||||||
setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
setFullResolution?: React.Dispatch<React.SetStateAction<VideoResolutionType>>;
|
||||||
|
onUploadFrame?: (playTime: number) => Promise<AxiosResponse> | undefined;
|
||||||
};
|
};
|
||||||
export default function HlsVideoPlayer({
|
export default function HlsVideoPlayer({
|
||||||
videoRef,
|
videoRef,
|
||||||
@ -40,7 +45,10 @@ export default function HlsVideoPlayer({
|
|||||||
onTimeUpdate,
|
onTimeUpdate,
|
||||||
onPlaying,
|
onPlaying,
|
||||||
setFullResolution,
|
setFullResolution,
|
||||||
|
onUploadFrame,
|
||||||
}: HlsVideoPlayerProps) {
|
}: HlsVideoPlayerProps) {
|
||||||
|
const { data: config } = useSWR<FrigateConfig>("config");
|
||||||
|
|
||||||
// playback
|
// playback
|
||||||
|
|
||||||
const hlsRef = useRef<Hls>();
|
const hlsRef = useRef<Hls>();
|
||||||
@ -144,7 +152,7 @@ export default function HlsVideoPlayer({
|
|||||||
volume: true,
|
volume: true,
|
||||||
seek: true,
|
seek: true,
|
||||||
playbackRate: true,
|
playbackRate: true,
|
||||||
plusUpload: true,
|
plusUpload: config?.plus?.enabled == true,
|
||||||
}}
|
}}
|
||||||
setControlsOpen={setControlsOpen}
|
setControlsOpen={setControlsOpen}
|
||||||
setMuted={setMuted}
|
setMuted={setMuted}
|
||||||
@ -173,6 +181,21 @@ export default function HlsVideoPlayer({
|
|||||||
onSetPlaybackRate={(rate) =>
|
onSetPlaybackRate={(rate) =>
|
||||||
videoRef.current ? (videoRef.current.playbackRate = rate) : null
|
videoRef.current ? (videoRef.current.playbackRate = rate) : null
|
||||||
}
|
}
|
||||||
|
onUploadFrame={async () => {
|
||||||
|
if (videoRef.current && onUploadFrame) {
|
||||||
|
const resp = await onUploadFrame(videoRef.current.currentTime);
|
||||||
|
|
||||||
|
if (resp && resp.status == 200) {
|
||||||
|
toast.success("Successfully submitted frame to Frigate Plus", {
|
||||||
|
position: "top-center",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
toast.success("Failed to submit frame to Frigate Plus", {
|
||||||
|
position: "top-center",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<TransformComponent
|
<TransformComponent
|
||||||
wrapperStyle={{
|
wrapperStyle={{
|
||||||
|
|||||||
@ -61,6 +61,7 @@ type VideoControlsProps = {
|
|||||||
onPlayPause: (play: boolean) => void;
|
onPlayPause: (play: boolean) => void;
|
||||||
onSeek: (diff: number) => void;
|
onSeek: (diff: number) => void;
|
||||||
onSetPlaybackRate: (rate: number) => void;
|
onSetPlaybackRate: (rate: number) => void;
|
||||||
|
onUploadFrame?: () => void;
|
||||||
};
|
};
|
||||||
export default function VideoControls({
|
export default function VideoControls({
|
||||||
className,
|
className,
|
||||||
@ -78,6 +79,7 @@ export default function VideoControls({
|
|||||||
onPlayPause,
|
onPlayPause,
|
||||||
onSeek,
|
onSeek,
|
||||||
onSetPlaybackRate,
|
onSetPlaybackRate,
|
||||||
|
onUploadFrame,
|
||||||
}: VideoControlsProps) {
|
}: VideoControlsProps) {
|
||||||
const onReplay = useCallback(
|
const onReplay = useCallback(
|
||||||
(e: React.MouseEvent<SVGElement>) => {
|
(e: React.MouseEvent<SVGElement>) => {
|
||||||
@ -224,7 +226,7 @@ export default function VideoControls({
|
|||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
)}
|
)}
|
||||||
{features.plusUpload && (
|
{features.plusUpload && onUploadFrame && (
|
||||||
<FrigatePlusUploadButton
|
<FrigatePlusUploadButton
|
||||||
video={video}
|
video={video}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
@ -239,6 +241,7 @@ export default function VideoControls({
|
|||||||
setControlsOpen(true);
|
setControlsOpen(true);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
|
onUploadFrame={onUploadFrame}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@ -249,11 +252,13 @@ type FrigatePlusUploadButtonProps = {
|
|||||||
video?: HTMLVideoElement | null;
|
video?: HTMLVideoElement | null;
|
||||||
onOpen: () => void;
|
onOpen: () => void;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
|
onUploadFrame: () => void;
|
||||||
};
|
};
|
||||||
function FrigatePlusUploadButton({
|
function FrigatePlusUploadButton({
|
||||||
video,
|
video,
|
||||||
onOpen,
|
onOpen,
|
||||||
onClose,
|
onClose,
|
||||||
|
onUploadFrame,
|
||||||
}: FrigatePlusUploadButtonProps) {
|
}: FrigatePlusUploadButtonProps) {
|
||||||
const [videoImg, setVideoImg] = useState<string>();
|
const [videoImg, setVideoImg] = useState<string>();
|
||||||
|
|
||||||
@ -295,7 +300,9 @@ function FrigatePlusUploadButton({
|
|||||||
</AlertDialogHeader>
|
</AlertDialogHeader>
|
||||||
<img className="w-full object-contain" src={videoImg} />
|
<img className="w-full object-contain" src={videoImg} />
|
||||||
<AlertDialogFooter>
|
<AlertDialogFooter>
|
||||||
<AlertDialogAction>Submit</AlertDialogAction>
|
<AlertDialogAction className="bg-selected" onClick={onUploadFrame}>
|
||||||
|
Submit
|
||||||
|
</AlertDialogAction>
|
||||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||||
</AlertDialogFooter>
|
</AlertDialogFooter>
|
||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
|
|||||||
@ -101,7 +101,7 @@ export class DynamicVideoController {
|
|||||||
this.playerController.pause();
|
this.playerController.pause();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`seek time is 0`);
|
// no op
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import HlsVideoPlayer from "../HlsVideoPlayer";
|
|||||||
import { TimeRange } from "@/types/timeline";
|
import { TimeRange } from "@/types/timeline";
|
||||||
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
import ActivityIndicator from "@/components/indicators/activity-indicator";
|
||||||
import { VideoResolutionType } from "@/types/live";
|
import { VideoResolutionType } from "@/types/live";
|
||||||
|
import axios from "axios";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dynamically switches between video playback and scrubbing preview player.
|
* Dynamically switches between video playback and scrubbing preview player.
|
||||||
@ -127,6 +128,18 @@ export default function DynamicVideoPlayer({
|
|||||||
[controller, onTimestampUpdate, isScrubbing, isLoading],
|
[controller, onTimestampUpdate, isScrubbing, isLoading],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onUploadFrameToPlus = useCallback(
|
||||||
|
(playTime: number) => {
|
||||||
|
if (!controller) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const time = controller.getProgress(playTime);
|
||||||
|
return axios.post(`/${camera}/plus/${time}`);
|
||||||
|
},
|
||||||
|
[camera, controller],
|
||||||
|
);
|
||||||
|
|
||||||
// state of playback player
|
// state of playback player
|
||||||
|
|
||||||
const recordingParams = useMemo(() => {
|
const recordingParams = useMemo(() => {
|
||||||
@ -186,6 +199,7 @@ export default function DynamicVideoPlayer({
|
|||||||
setNoRecording(false);
|
setNoRecording(false);
|
||||||
}}
|
}}
|
||||||
setFullResolution={setFullResolution}
|
setFullResolution={setFullResolution}
|
||||||
|
onUploadFrame={onUploadFrameToPlus}
|
||||||
/>
|
/>
|
||||||
<PreviewPlayer
|
<PreviewPlayer
|
||||||
className={`${isScrubbing || isLoading ? "visible" : "hidden"} ${className}`}
|
className={`${isScrubbing || isLoading ? "visible" : "hidden"} ${className}`}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user