Implement uploading image in frontend

This commit is contained in:
Nicolas Mowen 2024-05-02 13:07:24 -06:00
parent 40faa35199
commit c23ebe0e4e
6 changed files with 53 additions and 15 deletions

View File

@ -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(

View File

@ -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]]

View File

@ -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={{

View File

@ -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>

View File

@ -101,7 +101,7 @@ export class DynamicVideoController {
this.playerController.pause(); this.playerController.pause();
} }
} else { } else {
console.log(`seek time is 0`); // no op
} }
} }

View File

@ -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}`}