mirror of
https://github.com/blakeblackshear/frigate.git
synced 2024-11-21 19:07:46 +01:00
revamp recordings
This commit is contained in:
parent
78e1782084
commit
691ed6a4c7
198
frigate/http.py
198
frigate/http.py
@ -638,122 +638,100 @@ def latest_frame(camera_name):
|
||||
return "Camera named {} not found".format(camera_name), 404
|
||||
|
||||
|
||||
# return hourly summary for recordings of camera
|
||||
@bp.route("/<camera_name>/recordings/summary")
|
||||
def recordings_summary(camera_name):
|
||||
recording_groups = (
|
||||
Recordings.select(
|
||||
fn.strftime(
|
||||
"%Y-%m-%d %H",
|
||||
fn.datetime(Recordings.start_time, "unixepoch", "localtime"),
|
||||
).alias("hour"),
|
||||
fn.SUM(Recordings.duration).alias("duration"),
|
||||
fn.SUM(Recordings.motion).alias("motion"),
|
||||
fn.SUM(Recordings.objects).alias("objects"),
|
||||
)
|
||||
.where(Recordings.camera == camera_name)
|
||||
.group_by(
|
||||
fn.strftime(
|
||||
"%Y-%m-%d %H",
|
||||
fn.datetime(Recordings.start_time, "unixepoch", "localtime"),
|
||||
)
|
||||
)
|
||||
.order_by(
|
||||
fn.strftime(
|
||||
"%Y-%m-%d H",
|
||||
fn.datetime(Recordings.start_time, "unixepoch", "localtime"),
|
||||
).desc()
|
||||
)
|
||||
)
|
||||
|
||||
event_groups = (
|
||||
Event.select(
|
||||
fn.strftime(
|
||||
"%Y-%m-%d %H", fn.datetime(Event.start_time, "unixepoch", "localtime")
|
||||
).alias("hour"),
|
||||
fn.COUNT(Event.id).alias("count"),
|
||||
)
|
||||
.where(Event.camera == camera_name, Event.has_clip)
|
||||
.group_by(
|
||||
fn.strftime(
|
||||
"%Y-%m-%d %H", fn.datetime(Event.start_time, "unixepoch", "localtime")
|
||||
),
|
||||
)
|
||||
.objects()
|
||||
)
|
||||
|
||||
event_map = {g.hour: g.count for g in event_groups}
|
||||
|
||||
days = {}
|
||||
|
||||
for recording_group in recording_groups.objects():
|
||||
parts = recording_group.hour.split()
|
||||
hour = parts[1]
|
||||
day = parts[0]
|
||||
events_count = event_map.get(recording_group.hour, 0)
|
||||
hour_data = {
|
||||
"hour": hour,
|
||||
"events": events_count,
|
||||
"motion": recording_group.motion,
|
||||
"objects": recording_group.objects,
|
||||
"duration": round(recording_group.duration),
|
||||
}
|
||||
if day not in days:
|
||||
days[day] = {"events": events_count, "hours": [hour_data], "day": day}
|
||||
else:
|
||||
days[day]["events"] += events_count
|
||||
days[day]["hours"].append(hour_data)
|
||||
|
||||
return jsonify(list(days.values()))
|
||||
|
||||
|
||||
# return hour of recordings data for camera
|
||||
@bp.route("/<camera_name>/recordings")
|
||||
def recordings(camera_name):
|
||||
dates = OrderedDict()
|
||||
after = request.args.get(
|
||||
"after", type=float, default=(datetime.now() - timedelta(hours=1)).timestamp()
|
||||
)
|
||||
before = request.args.get("before", type=float, default=datetime.now().timestamp())
|
||||
|
||||
# Retrieve all recordings for this camera
|
||||
recordings = (
|
||||
Recordings.select()
|
||||
.where(Recordings.camera == camera_name)
|
||||
.order_by(Recordings.start_time.asc())
|
||||
Recordings.select(
|
||||
Recordings.id,
|
||||
Recordings.start_time,
|
||||
Recordings.end_time,
|
||||
Recordings.motion,
|
||||
Recordings.objects,
|
||||
)
|
||||
.where(
|
||||
Recordings.camera == camera_name,
|
||||
Recordings.end_time >= after,
|
||||
Recordings.start_time <= before,
|
||||
)
|
||||
.order_by(Recordings.start_time)
|
||||
)
|
||||
|
||||
last_end = 0
|
||||
recording: Recordings
|
||||
for recording in recordings:
|
||||
date = datetime.fromtimestamp(recording.start_time)
|
||||
key = date.strftime("%Y-%m-%d")
|
||||
hour = date.strftime("%H")
|
||||
|
||||
# Create Day Record
|
||||
if key not in dates:
|
||||
dates[key] = OrderedDict()
|
||||
|
||||
# Create Hour Record
|
||||
if hour not in dates[key]:
|
||||
dates[key][hour] = {"delay": {}, "events": []}
|
||||
|
||||
# Check for delay
|
||||
the_hour = datetime.strptime(f"{key} {hour}", "%Y-%m-%d %H").timestamp()
|
||||
# diff current recording start time and the greater of the previous end time or top of the hour
|
||||
diff = recording.start_time - max(last_end, the_hour)
|
||||
# Determine seconds into recording
|
||||
seconds = 0
|
||||
if datetime.fromtimestamp(last_end).strftime("%H") == hour:
|
||||
seconds = int(last_end - the_hour)
|
||||
# Determine the delay
|
||||
delay = min(int(diff), 3600 - seconds)
|
||||
if delay > 1:
|
||||
# Add an offset for any delay greater than a second
|
||||
dates[key][hour]["delay"][seconds] = delay
|
||||
|
||||
last_end = recording.end_time
|
||||
|
||||
# Packing intervals to return all events with same label and overlapping times as one row.
|
||||
# See: https://blogs.solidq.com/en/sqlserver/packing-intervals/
|
||||
events = Event.raw(
|
||||
"""WITH C1 AS
|
||||
(
|
||||
SELECT id, label, camera, top_score, start_time AS ts, +1 AS type, 1 AS sub
|
||||
FROM event
|
||||
WHERE camera = ?
|
||||
UNION ALL
|
||||
SELECT id, label, camera, top_score, end_time + 15 AS ts, -1 AS type, 0 AS sub
|
||||
FROM event
|
||||
WHERE camera = ?
|
||||
),
|
||||
C2 AS
|
||||
(
|
||||
SELECT C1.*,
|
||||
SUM(type) OVER(PARTITION BY label ORDER BY ts, type DESC
|
||||
ROWS BETWEEN UNBOUNDED PRECEDING
|
||||
AND CURRENT ROW) - sub AS cnt
|
||||
FROM C1
|
||||
),
|
||||
C3 AS
|
||||
(
|
||||
SELECT id, label, camera, top_score, ts,
|
||||
(ROW_NUMBER() OVER(PARTITION BY label ORDER BY ts) - 1) / 2 + 1
|
||||
AS grpnum
|
||||
FROM C2
|
||||
WHERE cnt = 0
|
||||
)
|
||||
SELECT id, label, camera, top_score, start_time, end_time
|
||||
FROM event
|
||||
WHERE camera = ? AND end_time IS NULL
|
||||
UNION ALL
|
||||
SELECT MIN(id) as id, label, camera, MAX(top_score) as top_score, MIN(ts) AS start_time, max(ts) AS end_time
|
||||
FROM C3
|
||||
GROUP BY label, grpnum
|
||||
ORDER BY start_time;""",
|
||||
camera_name,
|
||||
camera_name,
|
||||
camera_name,
|
||||
)
|
||||
|
||||
event: Event
|
||||
for event in events:
|
||||
date = datetime.fromtimestamp(event.start_time)
|
||||
key = date.strftime("%Y-%m-%d")
|
||||
hour = date.strftime("%H")
|
||||
if key in dates and hour in dates[key]:
|
||||
dates[key][hour]["events"].append(
|
||||
model_to_dict(
|
||||
event,
|
||||
exclude=[
|
||||
Event.false_positive,
|
||||
Event.zones,
|
||||
Event.thumbnail,
|
||||
Event.has_clip,
|
||||
Event.has_snapshot,
|
||||
],
|
||||
)
|
||||
)
|
||||
|
||||
return jsonify(
|
||||
[
|
||||
{
|
||||
"date": date,
|
||||
"events": sum([len(value["events"]) for value in hours.values()]),
|
||||
"recordings": [
|
||||
{"hour": hour, "delay": value["delay"], "events": value["events"]}
|
||||
for hour, value in hours.items()
|
||||
],
|
||||
}
|
||||
for date, hours in dates.items()
|
||||
]
|
||||
)
|
||||
return jsonify([e for e in recordings.dicts()])
|
||||
|
||||
|
||||
@bp.route("/<camera>/start/<int:start_ts>/end/<int:end_ts>/clip.mp4")
|
||||
|
@ -31,7 +31,10 @@ export default function App() {
|
||||
<AsyncRoute path="/cameras/:camera" getComponent={cameraComponent} />
|
||||
<AsyncRoute path="/birdseye" getComponent={Routes.getBirdseye} />
|
||||
<AsyncRoute path="/events" getComponent={Routes.getEvents} />
|
||||
<AsyncRoute path="/recording/:camera/:date?/:hour?/:seconds?" getComponent={Routes.getRecording} />
|
||||
<AsyncRoute
|
||||
path="/recording/:camera/:date?/:hour?/:minute?/:second?"
|
||||
getComponent={Routes.getRecording}
|
||||
/>
|
||||
<AsyncRoute path="/debug" getComponent={Routes.getDebug} />
|
||||
<AsyncRoute path="/styleguide" getComponent={Routes.getStyleGuide} />
|
||||
<Cameras default path="/" />
|
||||
|
@ -10,7 +10,13 @@ export function ApiProvider({ children, options }) {
|
||||
return (
|
||||
<SWRConfig
|
||||
value={{
|
||||
fetcher: (path) => axios.get(path).then((res) => res.data),
|
||||
fetcher: (arg) => {
|
||||
if (typeof arg === 'string') {
|
||||
return axios.get(arg).then((res) => res.data);
|
||||
}
|
||||
const [path, params] = arg;
|
||||
return axios.get(path, { params }).then((res) => res.data);
|
||||
},
|
||||
...options,
|
||||
}}
|
||||
>
|
||||
|
@ -1,59 +1,39 @@
|
||||
import { h } from 'preact';
|
||||
import { useState } from 'preact/hooks';
|
||||
import { useState, useMemo } from 'preact/hooks';
|
||||
import {
|
||||
differenceInSeconds,
|
||||
getUnixTime,
|
||||
fromUnixTime,
|
||||
format,
|
||||
parseISO,
|
||||
startOfHour,
|
||||
differenceInMinutes,
|
||||
differenceInHours
|
||||
intervalToDuration,
|
||||
formatDuration,
|
||||
endOfDay,
|
||||
startOfDay,
|
||||
isSameDay,
|
||||
} from 'date-fns';
|
||||
import ArrowDropdown from '../icons/ArrowDropdown';
|
||||
import ArrowDropup from '../icons/ArrowDropup';
|
||||
import Link from '../components/Link';
|
||||
import ActivityIndicator from '../components/ActivityIndicator';
|
||||
import Menu from '../icons/Menu';
|
||||
import MenuOpen from '../icons/MenuOpen';
|
||||
import { useApiHost } from '../api';
|
||||
import useSWR from 'swr';
|
||||
|
||||
export default function RecordingPlaylist({ camera, recordings, selectedDate }) {
|
||||
const [active, setActive] = useState(true);
|
||||
const toggle = () => setActive(!active);
|
||||
|
||||
const result = [];
|
||||
for (const recording of recordings.slice().reverse()) {
|
||||
const date = parseISO(recording.date);
|
||||
for (const recording of recordings) {
|
||||
const date = parseISO(recording.day);
|
||||
result.push(
|
||||
<ExpandableList
|
||||
title={format(date, 'MMM d, yyyy')}
|
||||
events={recording.events}
|
||||
selected={recording.date === selectedDate}
|
||||
selected={isSameDay(date, selectedDate)}
|
||||
>
|
||||
{recording.recordings
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((item, i) => (
|
||||
<div key={i} className="mb-2 w-full">
|
||||
<div
|
||||
className={`flex w-full text-md text-white px-8 py-2 mb-2 ${
|
||||
i === 0 ? 'border-t border-white border-opacity-50' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<Link href={`/recording/${camera}/${recording.date}/${item.hour}`} type="text">
|
||||
{item.hour}:00
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex-1 text-right">{item.events.length} Events</div>
|
||||
</div>
|
||||
{item.events
|
||||
.slice()
|
||||
.reverse()
|
||||
.map((event) => (
|
||||
<EventCard key={event.id} camera={camera} event={event} delay={item.delay} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
<DayOfEvents camera={camera} day={recording.day} hours={recording.hours} />
|
||||
</ExpandableList>
|
||||
);
|
||||
}
|
||||
@ -79,6 +59,71 @@ export default function RecordingPlaylist({ camera, recordings, selectedDate })
|
||||
);
|
||||
}
|
||||
|
||||
export function DayOfEvents({ camera, day, hours }) {
|
||||
const date = parseISO(day);
|
||||
const { data: events } = useSWR([
|
||||
`events`,
|
||||
{
|
||||
before: getUnixTime(endOfDay(date)),
|
||||
after: getUnixTime(startOfDay(date)),
|
||||
camera,
|
||||
has_clip: '1',
|
||||
include_thumbnails: 0,
|
||||
limit: 5000,
|
||||
},
|
||||
]);
|
||||
|
||||
// maps all the events under the keys for the hour by hour recordings view
|
||||
const eventMap = useMemo(() => {
|
||||
const eventMap = {};
|
||||
for (const hour of hours) {
|
||||
eventMap[`${day}-${hour.hour}`] = [];
|
||||
}
|
||||
|
||||
if (!events) {
|
||||
return eventMap;
|
||||
}
|
||||
|
||||
for (const event of events) {
|
||||
const key = format(fromUnixTime(event.start_time), 'yyyy-MM-dd-HH');
|
||||
// if the hour of recordings is missing for the event start time, skip it
|
||||
if (key in eventMap) {
|
||||
eventMap[key].push(event);
|
||||
}
|
||||
}
|
||||
|
||||
return eventMap;
|
||||
}, [events, day, hours]);
|
||||
|
||||
if (!events) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{hours.map((hour, i) => (
|
||||
<div key={i} className="mb-2 w-full">
|
||||
<div
|
||||
className={`flex w-full text-md text-white px-8 py-2 mb-2 ${
|
||||
i === 0 ? 'border-t border-white border-opacity-50' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex-1">
|
||||
<Link href={`/recording/${camera}/${day}/${hour.hour}`} type="text">
|
||||
{hour.hour}:00
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex-1 text-right">{hour.events} Events</div>
|
||||
</div>
|
||||
{eventMap[`${day}-${hour.hour}`].map((event) => (
|
||||
<EventCard key={event.id} camera={camera} event={event} />
|
||||
))}
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function ExpandableList({ title, events = 0, children, selected = false }) {
|
||||
const [active, setActive] = useState(selected);
|
||||
const toggle = () => setActive(!active);
|
||||
@ -89,35 +134,26 @@ export function ExpandableList({ title, events = 0, children, selected = false }
|
||||
<div className="flex-1 text-right mr-4">{events} Events</div>
|
||||
<div className="w-6 md:w-10 h-6 md:h-10">{active ? <ArrowDropup /> : <ArrowDropdown />}</div>
|
||||
</div>
|
||||
<div className={`bg-gray-800 bg-opacity-50 ${active ? '' : 'hidden'}`}>{children}</div>
|
||||
{/* Only render the child when expanded to lazy load events for the day */}
|
||||
{active && <div className={`bg-gray-800 bg-opacity-50`}>{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function EventCard({ camera, event, delay }) {
|
||||
export function EventCard({ camera, event }) {
|
||||
const apiHost = useApiHost();
|
||||
const start = fromUnixTime(event.start_time);
|
||||
const end = fromUnixTime(event.end_time);
|
||||
let duration = 'In Progress';
|
||||
if (event.end_time) {
|
||||
const end = fromUnixTime(event.end_time);
|
||||
const hours = differenceInHours(end, start);
|
||||
const minutes = differenceInMinutes(end, start) - hours * 60;
|
||||
const seconds = differenceInSeconds(end, start) - hours * 60 * 60 - minutes * 60;
|
||||
duration = '';
|
||||
if (hours) duration += `${hours}h `;
|
||||
if (minutes) duration += `${minutes}m `;
|
||||
duration += `${seconds}s`;
|
||||
duration = formatDuration(intervalToDuration({ start, end }));
|
||||
}
|
||||
const position = differenceInSeconds(start, startOfHour(start));
|
||||
const offset = Object.entries(delay)
|
||||
.map(([p, d]) => (position > p ? d : 0))
|
||||
.reduce((p, c) => p + c, 0);
|
||||
const seconds = Math.max(position - offset - 10, 0);
|
||||
|
||||
return (
|
||||
<Link className="" href={`/recording/${camera}/${format(start, 'yyyy-MM-dd')}/${format(start, 'HH')}/${seconds}`}>
|
||||
<Link className="" href={`/recording/${camera}/${format(start, 'yyyy-MM-dd/HH/mm/ss')}`}>
|
||||
<div className="flex flex-row mb-2">
|
||||
<div className="w-28 mr-4">
|
||||
<img className="antialiased" src={`${apiHost}/api/events/${event.id}/thumbnail.jpg`} />
|
||||
<img className="antialiased" loading="lazy" src={`${apiHost}/api/events/${event.id}/thumbnail.jpg`} />
|
||||
</div>
|
||||
<div className="flex flex-row w-full border-b">
|
||||
<div className="w-full text-gray-700 font-semibold relative pt-0">
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { h } from 'preact';
|
||||
import { closestTo, format, parseISO } from 'date-fns';
|
||||
import { parseISO, endOfHour, startOfHour, getUnixTime } from 'date-fns';
|
||||
import { useEffect, useMemo } from 'preact/hooks';
|
||||
import ActivityIndicator from '../components/ActivityIndicator';
|
||||
import Heading from '../components/Heading';
|
||||
import RecordingPlaylist from '../components/RecordingPlaylist';
|
||||
@ -7,15 +8,106 @@ import VideoPlayer from '../components/VideoPlayer';
|
||||
import { useApiHost } from '../api';
|
||||
import useSWR from 'swr';
|
||||
|
||||
export default function Recording({ camera, date, hour, seconds }) {
|
||||
const apiHost = useApiHost();
|
||||
const { data } = useSWR(`${camera}/recordings`);
|
||||
export default function Recording({ camera, date, hour = '00', minute = '00', second = '00' }) {
|
||||
const currentDate = useMemo(
|
||||
() => (date ? parseISO(`${date}T${hour || '00'}:${minute || '00'}:${second || '00'}`) : new Date()),
|
||||
[date, hour, minute, second]
|
||||
);
|
||||
|
||||
if (!data) {
|
||||
const apiHost = useApiHost();
|
||||
const { data: recordingsSummary } = useSWR(`${camera}/recordings/summary`);
|
||||
|
||||
const recordingParams = {
|
||||
before: getUnixTime(endOfHour(currentDate)),
|
||||
after: getUnixTime(startOfHour(currentDate)),
|
||||
};
|
||||
const { data: recordings } = useSWR([`${camera}/recordings`, recordingParams]);
|
||||
|
||||
// calculates the seek seconds by adding up all the seconds in the segments prior to the playback time
|
||||
const seekSeconds = useMemo(() => {
|
||||
if (!recordings) {
|
||||
return 0;
|
||||
}
|
||||
const currentUnix = getUnixTime(currentDate);
|
||||
|
||||
const hourStart = getUnixTime(startOfHour(currentDate));
|
||||
let seekSeconds = 0;
|
||||
recordings.every((segment) => {
|
||||
// if the next segment is past the desired time, stop calculating
|
||||
if (segment.start_time > currentUnix) {
|
||||
return false;
|
||||
}
|
||||
// if the segment starts before the hour, skip the seconds before the hour
|
||||
const start = segment.start_time < hourStart ? hourStart : segment.start_time;
|
||||
// if the segment ends after the selected time, use the selected time for end
|
||||
const end = segment.end_time > currentUnix ? currentUnix : segment.end_time;
|
||||
seekSeconds += end - start;
|
||||
return true;
|
||||
});
|
||||
return seekSeconds;
|
||||
}, [recordings, currentDate]);
|
||||
|
||||
const playlist = useMemo(() => {
|
||||
if (!recordingsSummary) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const selectedDayRecordingData = recordingsSummary.find((s) => !date || s.day === date);
|
||||
|
||||
const [year, month, day] = selectedDayRecordingData.day.split('-');
|
||||
return selectedDayRecordingData.hours
|
||||
.map((h) => {
|
||||
return {
|
||||
name: h.hour,
|
||||
description: `${camera} recording @ ${h.hour}:00.`,
|
||||
sources: [
|
||||
{
|
||||
src: `${apiHost}/vod/${year}-${month}/${day}/${h.hour}/${camera}/index.m3u8`,
|
||||
type: 'application/vnd.apple.mpegurl',
|
||||
},
|
||||
],
|
||||
};
|
||||
})
|
||||
.reverse();
|
||||
}, [apiHost, date, recordingsSummary, camera]);
|
||||
|
||||
const playlistIndex = useMemo(() => {
|
||||
const index = playlist.findIndex((item) => item.name === hour);
|
||||
if (index === -1) {
|
||||
return 0;
|
||||
}
|
||||
return index;
|
||||
}, [playlist, hour]);
|
||||
|
||||
useEffect(() => {
|
||||
if (this.player) {
|
||||
this.player.playlist(playlist);
|
||||
}
|
||||
}, [playlist]);
|
||||
|
||||
useEffect(() => {
|
||||
if (this.player) {
|
||||
this.player.playlist.currentItem(playlistIndex);
|
||||
}
|
||||
}, [playlistIndex]);
|
||||
|
||||
useEffect(() => {
|
||||
if (this.player) {
|
||||
// if the playlist has moved on to the next item, then reset
|
||||
if (this.player.playlist.currentItem() !== playlistIndex) {
|
||||
this.player.playlist.currentItem(playlistIndex);
|
||||
}
|
||||
this.player.currentTime(seekSeconds);
|
||||
// try and play since the user is likely to have interacted with the dom
|
||||
this.player.play();
|
||||
}
|
||||
}, [seekSeconds, playlistIndex]);
|
||||
|
||||
if (!recordingsSummary) {
|
||||
return <ActivityIndicator />;
|
||||
}
|
||||
|
||||
if (data.length === 0) {
|
||||
if (recordingsSummary.length === 0) {
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Heading>{camera} Recordings</Heading>
|
||||
@ -27,66 +119,18 @@ export default function Recording({ camera, date, hour, seconds }) {
|
||||
);
|
||||
}
|
||||
|
||||
const recordingDates = data.map((item) => item.date);
|
||||
const selectedDate = closestTo(
|
||||
date ? parseISO(date) : new Date(),
|
||||
recordingDates.map((i) => parseISO(i))
|
||||
);
|
||||
const selectedKey = format(selectedDate, 'yyyy-MM-dd');
|
||||
const [year, month, day] = selectedKey.split('-');
|
||||
const playlist = [];
|
||||
const hours = [];
|
||||
|
||||
for (const item of data) {
|
||||
if (item.date === selectedKey) {
|
||||
for (const recording of item.recordings) {
|
||||
playlist.push({
|
||||
name: `${selectedKey} ${recording.hour}:00`,
|
||||
description: `${camera} recording @ ${recording.hour}:00.`,
|
||||
sources: [
|
||||
{
|
||||
src: `${apiHost}/vod/${year}-${month}/${day}/${recording.hour}/${camera}/index.m3u8`,
|
||||
type: 'application/vnd.apple.mpegurl',
|
||||
},
|
||||
],
|
||||
});
|
||||
hours.push(recording.hour);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const selectedHour = hours.indexOf(hour);
|
||||
|
||||
if (this.player) {
|
||||
this.player.playlist([]);
|
||||
this.player.playlist(playlist);
|
||||
this.player.playlist.autoadvance(0);
|
||||
if (selectedHour !== -1) {
|
||||
this.player.playlist.currentItem(selectedHour);
|
||||
if (seconds !== undefined) {
|
||||
this.player.currentTime(seconds);
|
||||
}
|
||||
}
|
||||
// Force playback rate to be correct
|
||||
const playbackRate = this.player.playbackRate();
|
||||
this.player.defaultPlaybackRate(playbackRate);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4 p-2 px-4">
|
||||
<Heading>{camera} Recordings</Heading>
|
||||
|
||||
<VideoPlayer
|
||||
onReady={(player) => {
|
||||
player.on('ratechange', () => player.defaultPlaybackRate(player.playbackRate()));
|
||||
if (player.playlist) {
|
||||
player.playlist(playlist);
|
||||
player.playlist.autoadvance(0);
|
||||
if (selectedHour !== -1) {
|
||||
player.playlist.currentItem(selectedHour);
|
||||
if (seconds !== undefined) {
|
||||
player.currentTime(seconds);
|
||||
}
|
||||
}
|
||||
player.playlist.currentItem(playlistIndex);
|
||||
player.currentTime(seekSeconds);
|
||||
this.player = player;
|
||||
}
|
||||
}}
|
||||
@ -94,7 +138,7 @@ export default function Recording({ camera, date, hour, seconds }) {
|
||||
this.player = null;
|
||||
}}
|
||||
>
|
||||
<RecordingPlaylist camera={camera} recordings={data} selectedDate={selectedKey} selectedHour={hour} />
|
||||
<RecordingPlaylist camera={camera} recordings={recordingsSummary} selectedDate={currentDate} />
|
||||
</VideoPlayer>
|
||||
</div>
|
||||
);
|
||||
|
Loading…
Reference in New Issue
Block a user