Skip to content

Commit

Permalink
feat: add video player
Browse files Browse the repository at this point in the history
  • Loading branch information
billyjacoby committed Sep 23, 2023
1 parent 6efb536 commit f4e192e
Show file tree
Hide file tree
Showing 10 changed files with 244 additions and 64 deletions.
21 changes: 21 additions & 0 deletions src/components/VideoPlayer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import Video from 'react-native-video';

export const VideoPlayer = ({
videoURI,
isPaused = true,
}: {
videoURI: string;
isPaused?: boolean;
}) => {
return (
<Video
className="w-full h-full rounded-md"
paused={isPaused}
bufferConfig={{minBufferMs: 1000}}
fullscreen={true}
pictureInPicture={true}
controls
source={{uri: videoURI}}
/>
);
};
1 change: 1 addition & 0 deletions src/components/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export * from './baseView';
export * from './WebRTCView';
export * from './label';
export * from './snapshotCard';
export * from './VideoPlayer';
2 changes: 1 addition & 1 deletion src/components/snapshotCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ export const SnapshotCard = ({
const imageHeight = imageWidth * 0.75;

return (
<View className="self-center my-2 border border-accent dark:border-accent-dark relative rounded-lg">
<View className="self-center border border-accent dark:border-accent-dark relative rounded-lg">
<Image
source={{uri: imageSource}}
resizeMode="contain"
Expand Down
63 changes: 18 additions & 45 deletions src/lib/api/hooks/useCameraEvents.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,13 @@ import {useQuery, UseQueryOptions} from 'react-query';

import {API_BASE} from '@env';

const URL = `${API_BASE}api/events`;

export interface FrigateEvent {
//? Unsure what the non null types should be here
area: null | any;
box: null | any;
plus_id: null | any;
false_positive: null | any;
ratio: null | any;
camera: string;
end_time: number;
has_clip: boolean;
has_snapshot: boolean;
id: string;
label: string;
region: null | string;
retain_indefinitely: boolean;
start_time: number;
sub_label: null | string;
thumbnail: string;
top_score: number;
snapshotURL?: string;
}
import {
CameraEventParams,
FrigateEvent,
SnapshotQueryParams,
} from '../types/events';

interface CameraEventParams extends Record<string, string | undefined> {
before?: string;
after?: string;
cameras?: string;
labels?: string;
zones?: string;
limit?: string;
has_snapshot?: string;
has_clip?: string;
include_thumbnails?: string;
in_progress?: string;
}

interface SnapshotQueryParams extends Record<string, string | undefined> {
h?: string;
bbox?: string;
timestamp?: string;
crop?: string;
quality?: string;
}
const URL = `${API_BASE}api/events`;

const buildSnapshotURL = (
eventId: string,
Expand All @@ -60,6 +23,11 @@ const buildSnapshotURL = (
return url;
};

const buildEventUrl = (eventId: string) => {
const VOD_URL = `${API_BASE}vod/event/${eventId}/index.m3u8`;
return VOD_URL;
};

const fetchEvents = async (
queryParams?: CameraEventParams,
snapShotQueryParams?: SnapshotQueryParams,
Expand All @@ -77,11 +45,16 @@ const fetchEvents = async (
if (data) {
const returnData: FrigateEvent[] = [];
for (const event of data) {
const enrichedEvent: FrigateEvent = {...event};

const vodURL = buildEventUrl(event.id);
enrichedEvent.vodURL = vodURL;
if (event.has_snapshot) {
const snapshotURL = buildSnapshotURL(event.id, snapShotQueryParams);
returnData.push({...event, snapshotURL});
enrichedEvent.snapshotURL = snapshotURL;
returnData.push(enrichedEvent);
} else {
returnData.push(event);
returnData.push(enrichedEvent);
}
}
return Promise.resolve(returnData);
Expand Down
3 changes: 3 additions & 0 deletions src/lib/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
export * from './hooks/useCameraEvents';
export * from './hooks/useConfig';
export * from './getLatestCameraFrame';

//? Types
export * from './types';
45 changes: 45 additions & 0 deletions src/lib/api/types/events.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
export interface FrigateEvent {
//? Unsure what the non null types should be here
area: null | any;
box: null | any;
plus_id: null | any;
false_positive: null | any;
ratio: null | any;
camera: string;
end_time: number;
has_clip: boolean;
has_snapshot: boolean;
id: string;
label: string;
region: null | string;
retain_indefinitely: boolean;
start_time: number;
sub_label: null | string;
thumbnail: string;
top_score: number;
zones?: string[];
snapshotURL?: string;
vodURL: string;
}

export interface CameraEventParams extends Record<string, string | undefined> {
before?: string;
after?: string;
cameras?: string;
labels?: string;
zones?: string;
limit?: string;
has_snapshot?: string;
has_clip?: string;
include_thumbnails?: string;
in_progress?: string;
}

export interface SnapshotQueryParams
extends Record<string, string | undefined> {
h?: string;
bbox?: string;
timestamp?: string;
crop?: string;
quality?: string;
}
2 changes: 2 additions & 0 deletions src/lib/api/types/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './config';
export * from './events';
33 changes: 24 additions & 9 deletions src/screens/EventsScreen/EventsScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,21 @@
import {ActivityIndicator} from 'react-native';
import {ActivityIndicator, FlatList, View} from 'react-native';

import clsx from 'clsx';

import {CameraEvent} from './components';
import {useCameraEvents} from '@api';
import {BaseText, BaseView} from '@components';
import {useAppDataStore} from '@stores';
import {bgBackground} from '@utils';

const FooterComponent = ({length}: {length: number}) => (
//? I don't know why but we get some layout shift and that requires adding this height value here
<BaseView className="h-[250]">
<BaseText className="text-center text-mutedForeground dark:text-mutedForeground-dark pt-2">
Showing {length} event{length > 1 && 's'}.
</BaseText>
</BaseView>
);

export const EventsScreen = () => {
const currentCamera = useAppDataStore(state => state.currentCamera);
Expand Down Expand Up @@ -46,14 +58,17 @@ export const EventsScreen = () => {
}

return (
<BaseView isScrollview showsVerticalScrollIndicator={false}>
{events.map(camEvent => {
return <CameraEvent camEvent={camEvent} key={camEvent.id} />;
})}
<View className="flex-1">
<FlatList
className={clsx(bgBackground)}
ListFooterComponent={<FooterComponent length={events.length} />}
showsVerticalScrollIndicator={false}
data={events}
renderItem={({item: camEvent}) => (
<CameraEvent camEvent={camEvent} key={camEvent.id} />
)}
/>
{/* // TODO: Get total event info and group by date. Add pagination heree */}
<BaseText className="text-center text-mutedForeground dark:text-mutedForeground-dark">
Showing {events.length} event{events.length > 1 && 's'}.
</BaseText>
</BaseView>
</View>
);
};
75 changes: 66 additions & 9 deletions src/screens/EventsScreen/components/CameraEvent.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,25 @@
import React from 'react';
import {useWindowDimensions} from 'react-native';
import {
NativeScrollEvent,
NativeSyntheticEvent,
Pressable,
ScrollView,
useWindowDimensions,
} from 'react-native';

import {EventDetails} from './EventDetails';
import {FrigateEvent} from '@api';
import {BaseView, SnapshotCard} from '@components';
import {BaseView, SnapshotCard, VideoPlayer} from '@components';

export const CameraEvent = ({camEvent}: {camEvent: FrigateEvent}) => {
const {width} = useWindowDimensions();
const imageWidth = width * 0.97;
const imageHeight = imageWidth * 0.75;

const scrollviewRef = React.useRef<ScrollView>(null);

const [videoIsPaused, setVideoIsPaused] = React.useState(true);

const getDateString = (date: Date) => {
return (
date.toLocaleString(undefined, {
Expand All @@ -21,19 +32,65 @@ export const CameraEvent = ({camEvent}: {camEvent: FrigateEvent}) => {
);
};

const onEventPress = () => {
scrollviewRef?.current?.scrollToEnd();
setVideoIsPaused(false);
};

const onScrollEnd = (event: NativeSyntheticEvent<NativeScrollEvent>) => {
if (scrollviewRef.current) {
const xOffset = event.nativeEvent.contentOffset.x;
const scrollIndex = Math.round(xOffset / width);
if (scrollIndex === 2) {
setVideoIsPaused(false);
} else {
if (videoIsPaused === false) {
setVideoIsPaused(true);
}
}
}
};

const lastEventEnded = getDateString(new Date(camEvent?.end_time * 1000));

const lastThumbnail = 'data:image/png;base64,' + camEvent.thumbnail;
const lastEventImage = camEvent && camEvent.snapshotURL;

return (
<BaseView className="px-2:" style={{width, minHeight: imageHeight}}>
{(lastEventImage || lastThumbnail) && (
<SnapshotCard
camEvent={{...camEvent, lastEventEnded}}
imageSource={lastEventImage || lastThumbnail}
/>
<ScrollView
ref={scrollviewRef}
onMomentumScrollEnd={onScrollEnd}
horizontal
contentOffset={{x: width, y: 0}}
showsHorizontalScrollIndicator={false}
pagingEnabled
style={{flex: 1}}
className="py-2">
{/* //? Details on left */}
<BaseView
style={{width, height: imageHeight}}
className="flex-1 justify-center">
<EventDetails camEvent={camEvent} />
</BaseView>

{/* //? snapshot in middle, default view */}
<BaseView style={{width, height: imageHeight}}>
{(lastEventImage || lastThumbnail) && (
<Pressable onPress={onEventPress}>
<SnapshotCard
camEvent={{...camEvent, lastEventEnded}}
imageSource={lastEventImage || lastThumbnail}
/>
</Pressable>
)}
</BaseView>

{/* //? if there's a video, show that to the right */}
{camEvent.has_clip && (
<BaseView style={{width, height: imageHeight}} className="py-2">
<VideoPlayer videoURI={camEvent.vodURL} isPaused={videoIsPaused} />
</BaseView>
)}
</BaseView>
</ScrollView>
);
};
63 changes: 63 additions & 0 deletions src/screens/EventsScreen/components/EventDetails.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import {ViewProps} from 'react-native';

import {FrigateEvent} from '@api';
import {BaseText, BaseView} from '@components';
import {toTitleCase} from '@utils';

const Row = (props: ViewProps) => (
<BaseView
className="flex-row justify-between mx-6 px-2 py-1 rounded-sm my-[2px] border border-accent dark:border-accent-dark"
{...props}
/>
);

export const EventDetails = ({camEvent}: {camEvent: FrigateEvent}) => {
const startDate = new Date(camEvent.start_time * 1000);
const endDate = new Date(camEvent.end_time * 1000);
// TODO: format minutes here too.
const eventDuration = Math.round(camEvent.end_time - camEvent.start_time);

return (
<BaseView className="flex-1">
<BaseText className="self-center text-lg mb-1">Event Details</BaseText>
<Row>
<BaseText>Start Time</BaseText>
<BaseText>
{startDate.toLocaleDateString() +
' @ ' +
startDate.toLocaleTimeString()}
</BaseText>
</Row>
<Row>
<BaseText>End Time</BaseText>
<BaseText>
{endDate.toLocaleDateString() + ' @ ' + endDate.toLocaleTimeString()}
</BaseText>
</Row>
<Row>
<BaseText>Event Duration</BaseText>
<BaseText>{eventDuration} seconds</BaseText>
</Row>
<Row>
<BaseText>Object Label</BaseText>
<BaseText>{toTitleCase(camEvent.label)}</BaseText>
</Row>
<Row>
<BaseText>Confidence</BaseText>
<BaseText>{Math.round(camEvent.top_score * 10000) / 100}%</BaseText>
</Row>
{!!camEvent?.zones?.length && (
<Row>
<BaseText>Zones</BaseText>
<BaseText>LABEL</BaseText>
</Row>
)}
{!!camEvent.region && (
<Row>
<BaseText>Region</BaseText>
<BaseText>LABEL</BaseText>
</Row>
)}
</BaseView>
);
};

0 comments on commit f4e192e

Please sign in to comment.