mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +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