mirror of
				https://github.com/scottlamb/moonfire-nvr.git
				synced 2025-10-30 00:05:03 -04:00 
			
		
		
		
	view in-progress recordings!
The time from recorded to viewable was previously 60-120 sec for the first recording of a RTSP session, 0-60 sec otherwise. Now it's one frame.
This commit is contained in:
		
							parent
							
								
									45f7b30619
								
							
						
					
					
						commit
						b78ffc3808
					
				
							
								
								
									
										135
									
								
								db/db.rs
									
									
									
									
									
								
							
							
						
						
									
										135
									
								
								db/db.rs
									
									
									
									
									
								
							| @ -161,6 +161,7 @@ pub struct ListAggregatedRecordingsRow { | |||||||
|     pub run_start_id: i32, |     pub run_start_id: i32, | ||||||
|     pub open_id: u32, |     pub open_id: u32, | ||||||
|     pub first_uncommitted: Option<i32>, |     pub first_uncommitted: Option<i32>, | ||||||
|  |     pub growing: bool, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// Select fields from the `recordings_playback` table. Retrieve with `with_recording_playback`.
 | /// Select fields from the `recordings_playback` table. Retrieve with `with_recording_playback`.
 | ||||||
| @ -174,16 +175,18 @@ pub enum RecordingFlags { | |||||||
|     TrailingZero = 1, |     TrailingZero = 1, | ||||||
| 
 | 
 | ||||||
|     // These values (starting from high bit on down) are never written to the database.
 |     // These values (starting from high bit on down) are never written to the database.
 | ||||||
|     Uncommitted = 2147483648, |     Growing = 1 << 30, | ||||||
|  |     Uncommitted = 1 << 31, | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// A recording to pass to `insert_recording`.
 | /// A recording to pass to `insert_recording`.
 | ||||||
| #[derive(Clone, Debug)] | #[derive(Clone, Debug, Default)] | ||||||
| pub(crate) struct RecordingToInsert { | pub struct RecordingToInsert { | ||||||
|     pub run_offset: i32, |     pub run_offset: i32, | ||||||
|     pub flags: i32, |     pub flags: i32, | ||||||
|     pub sample_file_bytes: i32, |     pub sample_file_bytes: i32, | ||||||
|     pub time: Range<recording::Time>, |     pub start: recording::Time, | ||||||
|  |     pub duration_90k: i32,  // a recording::Duration, but guaranteed to fit in i32.
 | ||||||
|     pub local_time_delta: recording::Duration, |     pub local_time_delta: recording::Duration, | ||||||
|     pub video_samples: i32, |     pub video_samples: i32, | ||||||
|     pub video_sync_samples: i32, |     pub video_sync_samples: i32, | ||||||
| @ -195,10 +198,10 @@ pub(crate) struct RecordingToInsert { | |||||||
| impl RecordingToInsert { | impl RecordingToInsert { | ||||||
|     fn to_list_row(&self, id: CompositeId, open_id: u32) -> ListRecordingsRow { |     fn to_list_row(&self, id: CompositeId, open_id: u32) -> ListRecordingsRow { | ||||||
|         ListRecordingsRow { |         ListRecordingsRow { | ||||||
|             start: self.time.start, |             start: self.start, | ||||||
|             video_sample_entry_id: self.video_sample_entry_id, |             video_sample_entry_id: self.video_sample_entry_id, | ||||||
|             id, |             id, | ||||||
|             duration_90k: (self.time.end - self.time.start).0 as i32, |             duration_90k: self.duration_90k, | ||||||
|             video_samples: self.video_samples, |             video_samples: self.video_samples, | ||||||
|             video_sync_samples: self.video_sync_samples, |             video_sync_samples: self.video_sync_samples, | ||||||
|             sample_file_bytes: self.sample_file_bytes, |             sample_file_bytes: self.sample_file_bytes, | ||||||
| @ -399,7 +402,7 @@ pub struct Stream { | |||||||
|     /// `next_recording_id` should be advanced when one is committed to maintain this invariant.
 |     /// `next_recording_id` should be advanced when one is committed to maintain this invariant.
 | ||||||
|     ///
 |     ///
 | ||||||
|     /// TODO: alter the serving path to show these just as if they were already committed.
 |     /// TODO: alter the serving path to show these just as if they were already committed.
 | ||||||
|     uncommitted: VecDeque<Arc<Mutex<UncommittedRecording>>>, |     uncommitted: VecDeque<Arc<Mutex<RecordingToInsert>>>, | ||||||
| 
 | 
 | ||||||
|     /// The number of recordings in `uncommitted` which are synced and ready to commit.
 |     /// The number of recordings in `uncommitted` which are synced and ready to commit.
 | ||||||
|     synced_recordings: usize, |     synced_recordings: usize, | ||||||
| @ -410,22 +413,12 @@ impl Stream { | |||||||
|     /// Note recordings must be flushed in order, so a recording is considered unsynced if any
 |     /// Note recordings must be flushed in order, so a recording is considered unsynced if any
 | ||||||
|     /// before it are unsynced.
 |     /// before it are unsynced.
 | ||||||
|     pub(crate) fn unflushed(&self) -> recording::Duration { |     pub(crate) fn unflushed(&self) -> recording::Duration { | ||||||
|         let mut dur = recording::Duration(0); |         let mut sum = 0; | ||||||
|         for i in 0 .. self.synced_recordings { |         for i in 0..self.synced_recordings { | ||||||
|             let l = self.uncommitted[i].lock(); |             sum += self.uncommitted[i].lock().duration_90k as i64; | ||||||
|             if let Some(ref r) = l.recording { |  | ||||||
|                 dur += r.time.end - r.time.start; |  | ||||||
|         } |         } | ||||||
|  |         recording::Duration(sum) | ||||||
|     } |     } | ||||||
|         dur |  | ||||||
|     } |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| #[derive(Debug)] |  | ||||||
| pub(crate) struct UncommittedRecording { |  | ||||||
|     /// The recording to add. Absent if not yet ready.
 |  | ||||||
|     /// TODO: modify `SampleIndexEncoder` to update this record as it goes.
 |  | ||||||
|     pub(crate) recording: Option<RecordingToInsert>, |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| #[derive(Clone, Debug, Default)] | #[derive(Clone, Debug, Default)] | ||||||
| @ -764,21 +757,19 @@ impl LockedDatabase { | |||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Adds a placeholder for an uncommitted recording.
 |     /// Adds a placeholder for an uncommitted recording.
 | ||||||
|     /// The caller should write and sync the file and populate the returned `UncommittedRecording`
 |     /// The caller should write samples and fill the returned `RecordingToInsert` as it goes
 | ||||||
|     /// (noting that the database lock must not be acquired while holding the
 |     /// (noting that while holding the lock, it should not perform I/O or acquire the database
 | ||||||
|     /// `UncommittedRecording`'s lock) then call `mark_synced`. The data will be written to the
 |     /// lock). Then it should sync to permanent storage and call `mark_synced`. The data will
 | ||||||
|     /// database on the next `flush`.
 |     /// be written to the database on the next `flush`.
 | ||||||
|     pub(crate) fn add_recording(&mut self, stream_id: i32) |     pub(crate) fn add_recording(&mut self, stream_id: i32, r: RecordingToInsert) | ||||||
|                              -> Result<(CompositeId, Arc<Mutex<UncommittedRecording>>), Error> { |                              -> Result<(CompositeId, Arc<Mutex<RecordingToInsert>>), Error> { | ||||||
|         let stream = match self.streams_by_id.get_mut(&stream_id) { |         let stream = match self.streams_by_id.get_mut(&stream_id) { | ||||||
|             None => bail!("no such stream {}", stream_id), |             None => bail!("no such stream {}", stream_id), | ||||||
|             Some(s) => s, |             Some(s) => s, | ||||||
|         }; |         }; | ||||||
|         let id = CompositeId::new(stream_id, |         let id = CompositeId::new(stream_id, | ||||||
|                                   stream.next_recording_id + (stream.uncommitted.len() as i32)); |                                   stream.next_recording_id + (stream.uncommitted.len() as i32)); | ||||||
|         let recording = Arc::new(Mutex::new(UncommittedRecording { |         let recording = Arc::new(Mutex::new(r)); | ||||||
|             recording: None, |  | ||||||
|         })); |  | ||||||
|         stream.uncommitted.push_back(Arc::clone(&recording)); |         stream.uncommitted.push_back(Arc::clone(&recording)); | ||||||
|         Ok((id, recording)) |         Ok((id, recording)) | ||||||
|     } |     } | ||||||
| @ -799,11 +790,7 @@ impl LockedDatabase { | |||||||
|             bail!("can't sync un-added recording {}", id); |             bail!("can't sync un-added recording {}", id); | ||||||
|         } |         } | ||||||
|         let l = stream.uncommitted[stream.synced_recordings].lock(); |         let l = stream.uncommitted[stream.synced_recordings].lock(); | ||||||
|         let r = match l.recording.as_ref() { |         stream.bytes_to_add += l.sample_file_bytes as i64; | ||||||
|             None => bail!("can't sync unfinished recording {}", id), |  | ||||||
|             Some(r) => r, |  | ||||||
|         }; |  | ||||||
|         stream.bytes_to_add += r.sample_file_bytes as i64; |  | ||||||
|         stream.synced_recordings += 1; |         stream.synced_recordings += 1; | ||||||
|         Ok(()) |         Ok(()) | ||||||
|     } |     } | ||||||
| @ -841,9 +828,8 @@ impl LockedDatabase { | |||||||
|                 // Process additions.
 |                 // Process additions.
 | ||||||
|                 for i in 0..s.synced_recordings { |                 for i in 0..s.synced_recordings { | ||||||
|                     let l = s.uncommitted[i].lock(); |                     let l = s.uncommitted[i].lock(); | ||||||
|                     let r = l.recording.as_ref().unwrap(); |  | ||||||
|                     raw::insert_recording( |                     raw::insert_recording( | ||||||
|                         &tx, o, CompositeId::new(stream_id, s.next_recording_id + i as i32), &r)?; |                         &tx, o, CompositeId::new(stream_id, s.next_recording_id + i as i32), &l)?; | ||||||
|                 } |                 } | ||||||
|                 if s.synced_recordings > 0 { |                 if s.synced_recordings > 0 { | ||||||
|                     new_ranges.entry(stream_id).or_insert(None); |                     new_ranges.entry(stream_id).or_insert(None); | ||||||
| @ -911,9 +897,8 @@ impl LockedDatabase { | |||||||
|             for _ in 0..s.synced_recordings { |             for _ in 0..s.synced_recordings { | ||||||
|                 let u = s.uncommitted.pop_front().unwrap(); |                 let u = s.uncommitted.pop_front().unwrap(); | ||||||
|                 let l = u.lock(); |                 let l = u.lock(); | ||||||
|                 if let Some(ref r) = l.recording { |                 let end = l.start + recording::Duration(l.duration_90k as i64); | ||||||
|                     s.add_recording(r.time.clone(), r.sample_file_bytes); |                 s.add_recording(l.start .. end, l.sample_file_bytes); | ||||||
|                 } |  | ||||||
|             } |             } | ||||||
|             s.synced_recordings = 0; |             s.synced_recordings = 0; | ||||||
| 
 | 
 | ||||||
| @ -1036,14 +1021,15 @@ impl LockedDatabase { | |||||||
|             Some(s) => s, |             Some(s) => s, | ||||||
|         }; |         }; | ||||||
|         raw::list_recordings_by_time(&self.conn, stream_id, desired_time.clone(), f)?; |         raw::list_recordings_by_time(&self.conn, stream_id, desired_time.clone(), f)?; | ||||||
|         for i in 0 .. s.synced_recordings { |         for (i, u) in s.uncommitted.iter().enumerate() { | ||||||
|             let row = { |             let row = { | ||||||
|                 let l = s.uncommitted[i].lock(); |                 let l = u.lock(); | ||||||
|                 if let Some(ref r) = l.recording { |                 if l.video_samples > 0 { | ||||||
|                     if r.time.start > desired_time.end || r.time.end < r.time.start { |                     let end = l.start + recording::Duration(l.duration_90k as i64); | ||||||
|  |                     if l.start > desired_time.end || end < desired_time.start { | ||||||
|                         continue;  // there's no overlap with the requested range.
 |                         continue;  // there's no overlap with the requested range.
 | ||||||
|                     } |                     } | ||||||
|                     r.to_list_row(CompositeId::new(stream_id, s.next_recording_id + i as i32), |                     l.to_list_row(CompositeId::new(stream_id, s.next_recording_id + i as i32), | ||||||
|                                   self.open.unwrap().id) |                                   self.open.unwrap().id) | ||||||
|                 } else { |                 } else { | ||||||
|                     continue; |                     continue; | ||||||
| @ -1066,12 +1052,14 @@ impl LockedDatabase { | |||||||
|             raw::list_recordings_by_id(&self.conn, stream_id, desired_ids.clone(), f)?; |             raw::list_recordings_by_id(&self.conn, stream_id, desired_ids.clone(), f)?; | ||||||
|         } |         } | ||||||
|         if desired_ids.end > s.next_recording_id { |         if desired_ids.end > s.next_recording_id { | ||||||
|             let start = cmp::min(0, desired_ids.start - s.next_recording_id); |             let start = cmp::max(0, desired_ids.start - s.next_recording_id) as usize; | ||||||
|             for i in start .. desired_ids.end - s.next_recording_id { |             let end = cmp::min((desired_ids.end - s.next_recording_id) as usize, | ||||||
|  |                                s.uncommitted.len()); | ||||||
|  |             for i in start .. end { | ||||||
|                 let row = { |                 let row = { | ||||||
|                     let l = s.uncommitted[i as usize].lock(); |                     let l = s.uncommitted[i].lock(); | ||||||
|                     if let Some(ref r) = l.recording { |                     if l.video_samples > 0 { | ||||||
|                         r.to_list_row(CompositeId::new(stream_id, s.next_recording_id + i as i32), |                         l.to_list_row(CompositeId::new(stream_id, s.next_recording_id + i as i32), | ||||||
|                                       self.open.unwrap().id) |                                       self.open.unwrap().id) | ||||||
|                     } else { |                     } else { | ||||||
|                         continue; |                         continue; | ||||||
| @ -1122,11 +1110,19 @@ impl LockedDatabase { | |||||||
|                 f(&a)?; |                 f(&a)?; | ||||||
|             } |             } | ||||||
|             let uncommitted = (row.flags & RecordingFlags::Uncommitted as i32) != 0; |             let uncommitted = (row.flags & RecordingFlags::Uncommitted as i32) != 0; | ||||||
|             let need_insert = if let Some(ref mut a) = aggs.get_mut(&run_start_id) { |             let growing = (row.flags & RecordingFlags::Growing as i32) != 0; | ||||||
|  |             use std::collections::btree_map::Entry; | ||||||
|  |             match aggs.entry(run_start_id) { | ||||||
|  |                 Entry::Occupied(mut e) => { | ||||||
|  |                     let a = e.get_mut(); | ||||||
|                     if a.time.end != row.start { |                     if a.time.end != row.start { | ||||||
|                         bail!("stream {} recording {} ends at {}; {} starts at {}; expected same", |                         bail!("stream {} recording {} ends at {}; {} starts at {}; expected same", | ||||||
|                               stream_id, a.ids.end - 1, a.time.end, row.id, row.start); |                               stream_id, a.ids.end - 1, a.time.end, row.id, row.start); | ||||||
|                     } |                     } | ||||||
|  |                     if a.open_id != row.open_id { | ||||||
|  |                         bail!("stream {} recording {} has open id {}; {} has {}; expected same", | ||||||
|  |                               stream_id, a.ids.end - 1, a.open_id, row.id, row.open_id); | ||||||
|  |                     } | ||||||
|                     a.time.end.0 += row.duration_90k as i64; |                     a.time.end.0 += row.duration_90k as i64; | ||||||
|                     a.ids.end = recording_id + 1; |                     a.ids.end = recording_id + 1; | ||||||
|                     a.video_samples += row.video_samples as i64; |                     a.video_samples += row.video_samples as i64; | ||||||
| @ -1135,12 +1131,10 @@ impl LockedDatabase { | |||||||
|                     if uncommitted { |                     if uncommitted { | ||||||
|                         a.first_uncommitted = a.first_uncommitted.or(Some(recording_id)); |                         a.first_uncommitted = a.first_uncommitted.or(Some(recording_id)); | ||||||
|                     } |                     } | ||||||
|                 false |                     a.growing = growing; | ||||||
|             } else { |                 }, | ||||||
|                 true |                 Entry::Vacant(e) => { | ||||||
|             }; |                     e.insert(ListAggregatedRecordingsRow { | ||||||
|             if need_insert { |  | ||||||
|                 aggs.insert(run_start_id, ListAggregatedRecordingsRow{ |  | ||||||
|                         time: row.start ..  recording::Time(row.start.0 + row.duration_90k as i64), |                         time: row.start ..  recording::Time(row.start.0 + row.duration_90k as i64), | ||||||
|                         ids: recording_id .. recording_id+1, |                         ids: recording_id .. recording_id+1, | ||||||
|                         video_samples: row.video_samples as i64, |                         video_samples: row.video_samples as i64, | ||||||
| @ -1151,7 +1145,9 @@ impl LockedDatabase { | |||||||
|                         run_start_id, |                         run_start_id, | ||||||
|                         open_id: row.open_id, |                         open_id: row.open_id, | ||||||
|                         first_uncommitted: if uncommitted { Some(recording_id) } else { None }, |                         first_uncommitted: if uncommitted { Some(recording_id) } else { None }, | ||||||
|  |                         growing, | ||||||
|                     }); |                     }); | ||||||
|  |                 }, | ||||||
|             }; |             }; | ||||||
|             Ok(()) |             Ok(()) | ||||||
|         })?; |         })?; | ||||||
| @ -1177,11 +1173,7 @@ impl LockedDatabase { | |||||||
|                       id, s.next_recording_id, s.next_recording_id + s.uncommitted.len() as i32); |                       id, s.next_recording_id, s.next_recording_id + s.uncommitted.len() as i32); | ||||||
|             } |             } | ||||||
|             let l = s.uncommitted[i as usize].lock(); |             let l = s.uncommitted[i as usize].lock(); | ||||||
|             if let Some(ref r) = l.recording { |             return f(&RecordingPlayback { video_index: &l.video_index }); | ||||||
|                 return f(&RecordingPlayback { video_index: &r.video_index }); |  | ||||||
|             } else { |  | ||||||
|                 bail!("recording {} is not ready", id); |  | ||||||
|             } |  | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Committed path.
 |         // Committed path.
 | ||||||
| @ -1861,9 +1853,10 @@ mod tests { | |||||||
|         { |         { | ||||||
|             let db = db.lock(); |             let db = db.lock(); | ||||||
|             let stream = db.streams_by_id().get(&stream_id).unwrap(); |             let stream = db.streams_by_id().get(&stream_id).unwrap(); | ||||||
|             assert_eq!(Some(r.time.clone()), stream.range); |             let dur = recording::Duration(r.duration_90k as i64); | ||||||
|  |             assert_eq!(Some(r.start .. r.start + dur), stream.range); | ||||||
|             assert_eq!(r.sample_file_bytes as i64, stream.sample_file_bytes); |             assert_eq!(r.sample_file_bytes as i64, stream.sample_file_bytes); | ||||||
|             assert_eq!(r.time.end - r.time.start, stream.duration); |             assert_eq!(dur, stream.duration); | ||||||
|             db.cameras_by_id().get(&stream.camera_id).unwrap(); |             db.cameras_by_id().get(&stream.camera_id).unwrap(); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
| @ -1877,8 +1870,8 @@ mod tests { | |||||||
|             db.list_recordings_by_time(stream_id, all_time, &mut |row| { |             db.list_recordings_by_time(stream_id, all_time, &mut |row| { | ||||||
|                 rows += 1; |                 rows += 1; | ||||||
|                 recording_id = Some(row.id); |                 recording_id = Some(row.id); | ||||||
|                 assert_eq!(r.time, |                 assert_eq!(r.start, row.start); | ||||||
|                            row.start .. row.start + recording::Duration(row.duration_90k as i64)); |                 assert_eq!(r.duration_90k, row.duration_90k); | ||||||
|                 assert_eq!(r.video_samples, row.video_samples); |                 assert_eq!(r.video_samples, row.video_samples); | ||||||
|                 assert_eq!(r.video_sync_samples, row.video_sync_samples); |                 assert_eq!(r.video_sync_samples, row.video_sync_samples); | ||||||
|                 assert_eq!(r.sample_file_bytes, row.sample_file_bytes); |                 assert_eq!(r.sample_file_bytes, row.sample_file_bytes); | ||||||
| @ -1893,8 +1886,8 @@ mod tests { | |||||||
|         raw::list_oldest_recordings(&db.lock().conn, CompositeId::new(stream_id, 0), &mut |row| { |         raw::list_oldest_recordings(&db.lock().conn, CompositeId::new(stream_id, 0), &mut |row| { | ||||||
|             rows += 1; |             rows += 1; | ||||||
|             assert_eq!(recording_id, Some(row.id)); |             assert_eq!(recording_id, Some(row.id)); | ||||||
|             assert_eq!(r.time.start, row.start); |             assert_eq!(r.start, row.start); | ||||||
|             assert_eq!(r.time.end - r.time.start, recording::Duration(row.duration as i64)); |             assert_eq!(r.duration_90k, row.duration); | ||||||
|             assert_eq!(r.sample_file_bytes, row.sample_file_bytes); |             assert_eq!(r.sample_file_bytes, row.sample_file_bytes); | ||||||
|             true |             true | ||||||
|         }).unwrap(); |         }).unwrap(); | ||||||
| @ -2084,7 +2077,8 @@ mod tests { | |||||||
|             sample_file_bytes: 42, |             sample_file_bytes: 42, | ||||||
|             run_offset: 0, |             run_offset: 0, | ||||||
|             flags: 0, |             flags: 0, | ||||||
|             time: start .. start + recording::Duration(TIME_UNITS_PER_SEC), |             start, | ||||||
|  |             duration_90k: TIME_UNITS_PER_SEC as i32, | ||||||
|             local_time_delta: recording::Duration(0), |             local_time_delta: recording::Duration(0), | ||||||
|             video_samples: 1, |             video_samples: 1, | ||||||
|             video_sync_samples: 1, |             video_sync_samples: 1, | ||||||
| @ -2094,8 +2088,7 @@ mod tests { | |||||||
|         }; |         }; | ||||||
|         let id = { |         let id = { | ||||||
|             let mut db = db.lock(); |             let mut db = db.lock(); | ||||||
|             let (id, u) = db.add_recording(main_stream_id).unwrap(); |             let (id, _) = db.add_recording(main_stream_id, recording.clone()).unwrap(); | ||||||
|             u.lock().recording = Some(recording.clone()); |  | ||||||
|             db.mark_synced(id).unwrap(); |             db.mark_synced(id).unwrap(); | ||||||
|             db.flush("add test").unwrap(); |             db.flush("add test").unwrap(); | ||||||
|             id |             id | ||||||
|  | |||||||
							
								
								
									
										89
									
								
								db/dir.rs
									
									
									
									
									
								
							
							
						
						
									
										89
									
								
								db/dir.rs
									
									
									
									
									
								
							| @ -646,14 +646,11 @@ enum WriterState { | |||||||
| /// with at least one sample. The sample may have zero duration.
 | /// with at least one sample. The sample may have zero duration.
 | ||||||
| struct InnerWriter { | struct InnerWriter { | ||||||
|     f: fs::File, |     f: fs::File, | ||||||
|     r: Arc<Mutex<db::UncommittedRecording>>, |     r: Arc<Mutex<db::RecordingToInsert>>, | ||||||
|     index: recording::SampleIndexEncoder, |     e: recording::SampleIndexEncoder, | ||||||
|     id: CompositeId, |     id: CompositeId, | ||||||
|     hasher: hash::Hasher, |     hasher: hash::Hasher, | ||||||
| 
 | 
 | ||||||
|     /// The end time of the previous segment in this run, if any.
 |  | ||||||
|     prev_end: Option<recording::Time>, |  | ||||||
| 
 |  | ||||||
|     /// The start time of this segment, based solely on examining the local clock after frames in
 |     /// The start time of this segment, based solely on examining the local clock after frames in
 | ||||||
|     /// this segment were received. Frames can suffer from various kinds of delay (initial
 |     /// this segment were received. Frames can suffer from various kinds of delay (initial
 | ||||||
|     /// buffering, encoding, and network transmission), so this time is set to far in the future on
 |     /// buffering, encoding, and network transmission), so this time is set to far in the future on
 | ||||||
| @ -663,8 +660,6 @@ struct InnerWriter { | |||||||
| 
 | 
 | ||||||
|     adjuster: ClockAdjuster, |     adjuster: ClockAdjuster, | ||||||
| 
 | 
 | ||||||
|     run_offset: i32, |  | ||||||
| 
 |  | ||||||
|     /// A sample which has been written to disk but not added to `index`. Index writes are one
 |     /// A sample which has been written to disk but not added to `index`. Index writes are one
 | ||||||
|     /// sample behind disk writes because the duration of a sample is the difference between its
 |     /// sample behind disk writes because the duration of a sample is the difference between its
 | ||||||
|     /// pts and the next sample's pts. A sample is flushed when the next sample is written, when
 |     /// pts and the next sample's pts. A sample is flushed when the next sample is written, when
 | ||||||
| @ -735,7 +730,7 @@ struct UnflushedSample { | |||||||
| /// State associated with a run's previous recording; used within `Writer`.
 | /// State associated with a run's previous recording; used within `Writer`.
 | ||||||
| #[derive(Copy, Clone)] | #[derive(Copy, Clone)] | ||||||
| struct PreviousWriter { | struct PreviousWriter { | ||||||
|     end_time: recording::Time, |     end: recording::Time, | ||||||
|     local_time_delta: recording::Duration, |     local_time_delta: recording::Duration, | ||||||
|     run_offset: i32, |     run_offset: i32, | ||||||
| } | } | ||||||
| @ -762,7 +757,13 @@ impl<'a> Writer<'a> { | |||||||
|             WriterState::Open(ref mut w) => return Ok(w), |             WriterState::Open(ref mut w) => return Ok(w), | ||||||
|             WriterState::Closed(prev) => Some(prev), |             WriterState::Closed(prev) => Some(prev), | ||||||
|         }; |         }; | ||||||
|         let (id, r) = self.db.lock().add_recording(self.stream_id)?; |         let (id, r) = self.db.lock().add_recording(self.stream_id, db::RecordingToInsert { | ||||||
|  |             run_offset: prev.map(|p| p.run_offset + 1).unwrap_or(0), | ||||||
|  |             start: prev.map(|p| p.end).unwrap_or(recording::Time(i64::max_value())), | ||||||
|  |             video_sample_entry_id: self.video_sample_entry_id, | ||||||
|  |             flags: db::RecordingFlags::Growing as i32, | ||||||
|  |             ..Default::default() | ||||||
|  |         })?; | ||||||
|         let p = SampleFileDir::get_rel_pathname(id); |         let p = SampleFileDir::get_rel_pathname(id); | ||||||
|         let f = retry_forever(&mut || unsafe { |         let f = retry_forever(&mut || unsafe { | ||||||
|             self.dir.fd.openat(p.as_ptr(), libc::O_WRONLY | libc::O_EXCL | libc::O_CREAT, 0o600) |             self.dir.fd.openat(p.as_ptr(), libc::O_WRONLY | libc::O_EXCL | libc::O_CREAT, 0o600) | ||||||
| @ -771,13 +772,11 @@ impl<'a> Writer<'a> { | |||||||
|         self.state = WriterState::Open(InnerWriter { |         self.state = WriterState::Open(InnerWriter { | ||||||
|             f, |             f, | ||||||
|             r, |             r, | ||||||
|             index: recording::SampleIndexEncoder::new(), |             e: recording::SampleIndexEncoder::new(), | ||||||
|             id, |             id, | ||||||
|             hasher: hash::Hasher::new(hash::MessageDigest::sha1())?, |             hasher: hash::Hasher::new(hash::MessageDigest::sha1())?, | ||||||
|             prev_end: prev.map(|p| p.end_time), |  | ||||||
|             local_start: recording::Time(i64::max_value()), |             local_start: recording::Time(i64::max_value()), | ||||||
|             adjuster: ClockAdjuster::new(prev.map(|p| p.local_time_delta.0)), |             adjuster: ClockAdjuster::new(prev.map(|p| p.local_time_delta.0)), | ||||||
|             run_offset: prev.map(|p| p.run_offset + 1).unwrap_or(0), |  | ||||||
|             unflushed_sample: None, |             unflushed_sample: None, | ||||||
|         }); |         }); | ||||||
|         match self.state { |         match self.state { | ||||||
| @ -812,8 +811,7 @@ impl<'a> Writer<'a> { | |||||||
|                       unflushed.pts_90k, pts_90k); |                       unflushed.pts_90k, pts_90k); | ||||||
|             } |             } | ||||||
|             let duration = w.adjuster.adjust(duration); |             let duration = w.adjuster.adjust(duration); | ||||||
|             w.index.add_sample(duration, unflushed.len, unflushed.is_key); |             w.add_sample(duration, unflushed.len, unflushed.is_key, unflushed.local_time); | ||||||
|             w.extend_local_start(unflushed.local_time); |  | ||||||
|         } |         } | ||||||
|         let mut remaining = pkt; |         let mut remaining = pkt; | ||||||
|         while !remaining.is_empty() { |         while !remaining.is_empty() { | ||||||
| @ -836,7 +834,7 @@ impl<'a> Writer<'a> { | |||||||
|     pub fn close(&mut self, next_pts: Option<i64>) { |     pub fn close(&mut self, next_pts: Option<i64>) { | ||||||
|         self.state = match mem::replace(&mut self.state, WriterState::Unopened) { |         self.state = match mem::replace(&mut self.state, WriterState::Unopened) { | ||||||
|             WriterState::Open(w) => { |             WriterState::Open(w) => { | ||||||
|                 let prev = w.close(self.channel, self.video_sample_entry_id, next_pts); |                 let prev = w.close(self.channel, next_pts); | ||||||
|                 WriterState::Closed(prev) |                 WriterState::Closed(prev) | ||||||
|             }, |             }, | ||||||
|             s => s, |             s => s, | ||||||
| @ -845,45 +843,42 @@ impl<'a> Writer<'a> { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl InnerWriter { | impl InnerWriter { | ||||||
|     fn extend_local_start(&mut self, pkt_local_time: recording::Time) { |     fn add_sample(&mut self, duration_90k: i32, bytes: i32, is_key: bool, | ||||||
|         let new = pkt_local_time - recording::Duration(self.index.total_duration_90k as i64); |                   pkt_local_time: recording::Time) { | ||||||
|  |         let mut l = self.r.lock(); | ||||||
|  |         self.e.add_sample(duration_90k, bytes, is_key, &mut l); | ||||||
|  |         let new = pkt_local_time - recording::Duration(l.duration_90k as i64); | ||||||
|         self.local_start = cmp::min(self.local_start, new); |         self.local_start = cmp::min(self.local_start, new); | ||||||
|  |         if l.run_offset == 0 {  // start time isn't anchored to previous recording's end; adjust.
 | ||||||
|  |             l.start = self.local_start; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     fn close(mut self, channel: &SyncerChannel, video_sample_entry_id: i32, |     fn close(mut self, channel: &SyncerChannel, next_pts: Option<i64>) -> PreviousWriter { | ||||||
|              next_pts: Option<i64>) -> PreviousWriter { |  | ||||||
|         let unflushed = self.unflushed_sample.take().expect("should always be an unflushed sample"); |         let unflushed = self.unflushed_sample.take().expect("should always be an unflushed sample"); | ||||||
|         let duration = self.adjuster.adjust(match next_pts { |         let (duration, flags) = match next_pts { | ||||||
|             None => 0, |             None => (self.adjuster.adjust(0), db::RecordingFlags::TrailingZero as i32), | ||||||
|             Some(p) => (p - unflushed.pts_90k) as i32, |             Some(p) => (self.adjuster.adjust((p - unflushed.pts_90k) as i32), 0), | ||||||
|         }); |         }; | ||||||
|         self.index.add_sample(duration, unflushed.len, unflushed.is_key); |  | ||||||
|         self.extend_local_start(unflushed.local_time); |  | ||||||
|         let mut sha1_bytes = [0u8; 20]; |         let mut sha1_bytes = [0u8; 20]; | ||||||
|         sha1_bytes.copy_from_slice(&self.hasher.finish().unwrap()[..]); |         sha1_bytes.copy_from_slice(&self.hasher.finish().unwrap()[..]); | ||||||
|         let start = self.prev_end.unwrap_or(self.local_start); |         let (local_time_delta, run_offset, end); | ||||||
|         let end = start + recording::Duration(self.index.total_duration_90k as i64); |         self.add_sample(duration, unflushed.len, unflushed.is_key, unflushed.local_time); | ||||||
|         let flags = if self.index.has_trailing_zero() { db::RecordingFlags::TrailingZero as i32 } |         { | ||||||
|                     else { 0 }; |             let mut l = self.r.lock(); | ||||||
|         let local_start_delta = self.local_start - start; |             l.flags = flags; | ||||||
|         let recording = db::RecordingToInsert { |             local_time_delta = self.local_start - l.start; | ||||||
|             sample_file_bytes: self.index.sample_file_bytes, |             l.local_time_delta = local_time_delta; | ||||||
|             time: start .. end, |             l.sample_file_sha1 = sha1_bytes; | ||||||
|             local_time_delta: local_start_delta, |             run_offset = l.run_offset; | ||||||
|             video_samples: self.index.video_samples, |             end = l.start + recording::Duration(l.duration_90k as i64); | ||||||
|             video_sync_samples: self.index.video_sync_samples, |         } | ||||||
|             video_sample_entry_id, |         drop(self.r); | ||||||
|             video_index: self.index.video_index, |  | ||||||
|             sample_file_sha1: sha1_bytes, |  | ||||||
|             run_offset: self.run_offset, |  | ||||||
|             flags: flags, |  | ||||||
|         }; |  | ||||||
|         self.r.lock().recording = Some(recording); |  | ||||||
|         channel.async_save_recording(self.id, self.f); |         channel.async_save_recording(self.id, self.f); | ||||||
|         PreviousWriter { |         PreviousWriter { | ||||||
|             end_time: end, |             end, | ||||||
|             local_time_delta: local_start_delta, |             local_time_delta, | ||||||
|             run_offset: self.run_offset, |             run_offset, | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
| @ -894,7 +889,7 @@ impl<'a> Drop for Writer<'a> { | |||||||
|             // Swallow any error. The caller should only drop the Writer without calling close()
 |             // Swallow any error. The caller should only drop the Writer without calling close()
 | ||||||
|             // if there's already been an error. The caller should report that. No point in
 |             // if there's already been an error. The caller should report that. No point in
 | ||||||
|             // complaining again.
 |             // complaining again.
 | ||||||
|             let _ = w.close(self.channel, self.video_sample_entry_id, None); |             let _ = w.close(self.channel, None); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  | |||||||
| @ -191,10 +191,6 @@ pub(crate) fn get_db_uuid(conn: &rusqlite::Connection) -> Result<Uuid, Error> { | |||||||
| /// Inserts the specified recording (for from `try_flush` only).
 | /// Inserts the specified recording (for from `try_flush` only).
 | ||||||
| pub(crate) fn insert_recording(tx: &rusqlite::Transaction, o: &db::Open, id: CompositeId, | pub(crate) fn insert_recording(tx: &rusqlite::Transaction, o: &db::Open, id: CompositeId, | ||||||
|                     r: &db::RecordingToInsert) -> Result<(), Error> { |                     r: &db::RecordingToInsert) -> Result<(), Error> { | ||||||
|     if r.time.end < r.time.start { |  | ||||||
|         bail!("end time {} must be >= start time {}", r.time.end, r.time.start); |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     let mut stmt = tx.prepare_cached(INSERT_RECORDING_SQL)?; |     let mut stmt = tx.prepare_cached(INSERT_RECORDING_SQL)?; | ||||||
|     stmt.execute_named(&[ |     stmt.execute_named(&[ | ||||||
|         (":composite_id", &id.0), |         (":composite_id", &id.0), | ||||||
| @ -203,8 +199,8 @@ pub(crate) fn insert_recording(tx: &rusqlite::Transaction, o: &db::Open, id: Com | |||||||
|         (":run_offset", &r.run_offset), |         (":run_offset", &r.run_offset), | ||||||
|         (":flags", &r.flags), |         (":flags", &r.flags), | ||||||
|         (":sample_file_bytes", &r.sample_file_bytes), |         (":sample_file_bytes", &r.sample_file_bytes), | ||||||
|         (":start_time_90k", &r.time.start.0), |         (":start_time_90k", &r.start.0), | ||||||
|         (":duration_90k", &(r.time.end.0 - r.time.start.0)), |         (":duration_90k", &r.duration_90k), | ||||||
|         (":local_time_delta_90k", &r.local_time_delta.0), |         (":local_time_delta_90k", &r.local_time_delta.0), | ||||||
|         (":video_samples", &r.video_samples), |         (":video_samples", &r.video_samples), | ||||||
|         (":video_sync_samples", &r.video_sync_samples), |         (":video_sync_samples", &r.video_sync_samples), | ||||||
|  | |||||||
| @ -43,7 +43,7 @@ pub const DESIRED_RECORDING_DURATION: i64 = 60 * TIME_UNITS_PER_SEC; | |||||||
| pub const MAX_RECORDING_DURATION: i64 = 5 * 60 * TIME_UNITS_PER_SEC; | pub const MAX_RECORDING_DURATION: i64 = 5 * 60 * TIME_UNITS_PER_SEC; | ||||||
| 
 | 
 | ||||||
| /// A time specified as 90,000ths of a second since 1970-01-01 00:00:00 UTC.
 | /// A time specified as 90,000ths of a second since 1970-01-01 00:00:00 UTC.
 | ||||||
| #[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)] | #[derive(Clone, Copy, Debug, Default, Eq, Ord, PartialEq, PartialOrd)] | ||||||
| pub struct Time(pub i64); | pub struct Time(pub i64); | ||||||
| 
 | 
 | ||||||
| impl Time { | impl Time { | ||||||
| @ -290,43 +290,30 @@ impl SampleIndexIterator { | |||||||
| 
 | 
 | ||||||
| #[derive(Debug)] | #[derive(Debug)] | ||||||
| pub struct SampleIndexEncoder { | pub struct SampleIndexEncoder { | ||||||
|     // Internal state.
 |  | ||||||
|     prev_duration_90k: i32, |     prev_duration_90k: i32, | ||||||
|     prev_bytes_key: i32, |     prev_bytes_key: i32, | ||||||
|     prev_bytes_nonkey: i32, |     prev_bytes_nonkey: i32, | ||||||
| 
 |  | ||||||
|     // Eventual output.
 |  | ||||||
|     // TODO: move to another struct?
 |  | ||||||
|     pub sample_file_bytes: i32, |  | ||||||
|     pub total_duration_90k: i32, |  | ||||||
|     pub video_samples: i32, |  | ||||||
|     pub video_sync_samples: i32, |  | ||||||
|     pub video_index: Vec<u8>, |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| impl SampleIndexEncoder { | impl SampleIndexEncoder { | ||||||
|     pub fn new() -> Self { |     pub fn new() -> Self { | ||||||
|         SampleIndexEncoder{ |         SampleIndexEncoder { | ||||||
|             prev_duration_90k: 0, |             prev_duration_90k: 0, | ||||||
|             prev_bytes_key: 0, |             prev_bytes_key: 0, | ||||||
|             prev_bytes_nonkey: 0, |             prev_bytes_nonkey: 0, | ||||||
|             total_duration_90k: 0, |  | ||||||
|             sample_file_bytes: 0, |  | ||||||
|             video_samples: 0, |  | ||||||
|             video_sync_samples: 0, |  | ||||||
|             video_index: Vec::new(), |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn add_sample(&mut self, duration_90k: i32, bytes: i32, is_key: bool) { |     pub fn add_sample(&mut self, duration_90k: i32, bytes: i32, is_key: bool, | ||||||
|  |                       r: &mut db::RecordingToInsert) { | ||||||
|         let duration_delta = duration_90k - self.prev_duration_90k; |         let duration_delta = duration_90k - self.prev_duration_90k; | ||||||
|         self.prev_duration_90k = duration_90k; |         self.prev_duration_90k = duration_90k; | ||||||
|         self.total_duration_90k += duration_90k; |         r.duration_90k += duration_90k; | ||||||
|         self.sample_file_bytes += bytes; |         r.sample_file_bytes += bytes; | ||||||
|         self.video_samples += 1; |         r.video_samples += 1; | ||||||
|         let bytes_delta = bytes - if is_key { |         let bytes_delta = bytes - if is_key { | ||||||
|             let prev = self.prev_bytes_key; |             let prev = self.prev_bytes_key; | ||||||
|             self.video_sync_samples += 1; |             r.video_sync_samples += 1; | ||||||
|             self.prev_bytes_key = bytes; |             self.prev_bytes_key = bytes; | ||||||
|             prev |             prev | ||||||
|         } else { |         } else { | ||||||
| @ -334,11 +321,9 @@ impl SampleIndexEncoder { | |||||||
|             self.prev_bytes_nonkey = bytes; |             self.prev_bytes_nonkey = bytes; | ||||||
|             prev |             prev | ||||||
|         }; |         }; | ||||||
|         append_varint32((zigzag32(duration_delta) << 1) | (is_key as u32), &mut self.video_index); |         append_varint32((zigzag32(duration_delta) << 1) | (is_key as u32), &mut r.video_index); | ||||||
|         append_varint32(zigzag32(bytes_delta), &mut self.video_index); |         append_varint32(zigzag32(bytes_delta), &mut r.video_index); | ||||||
|     } |     } | ||||||
| 
 |  | ||||||
|     pub fn has_trailing_zero(&self) -> bool { self.prev_duration_90k == 0 } |  | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| /// A segment represents a view of some or all of a single recording, starting from a key frame.
 | /// A segment represents a view of some or all of a single recording, starting from a key frame.
 | ||||||
| @ -566,16 +551,17 @@ mod tests { | |||||||
|     #[test] |     #[test] | ||||||
|     fn test_encode_example() { |     fn test_encode_example() { | ||||||
|         testutil::init(); |         testutil::init(); | ||||||
|  |         let mut r = db::RecordingToInsert::default(); | ||||||
|         let mut e = SampleIndexEncoder::new(); |         let mut e = SampleIndexEncoder::new(); | ||||||
|         e.add_sample(10, 1000, true); |         e.add_sample(10, 1000, true, &mut r); | ||||||
|         e.add_sample(9, 10, false); |         e.add_sample(9, 10, false, &mut r); | ||||||
|         e.add_sample(11, 15, false); |         e.add_sample(11, 15, false, &mut r); | ||||||
|         e.add_sample(10, 12, false); |         e.add_sample(10, 12, false, &mut r); | ||||||
|         e.add_sample(10, 1050, true); |         e.add_sample(10, 1050, true, &mut r); | ||||||
|         assert_eq!(e.video_index, b"\x29\xd0\x0f\x02\x14\x08\x0a\x02\x05\x01\x64"); |         assert_eq!(r.video_index, b"\x29\xd0\x0f\x02\x14\x08\x0a\x02\x05\x01\x64"); | ||||||
|         assert_eq!(10 + 9 + 11 + 10 + 10, e.total_duration_90k); |         assert_eq!(10 + 9 + 11 + 10 + 10, r.duration_90k); | ||||||
|         assert_eq!(5, e.video_samples); |         assert_eq!(5, r.video_samples); | ||||||
|         assert_eq!(2, e.video_sync_samples); |         assert_eq!(2, r.video_sync_samples); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Tests a round trip from `SampleIndexEncoder` to `SampleIndexIterator`.
 |     /// Tests a round trip from `SampleIndexEncoder` to `SampleIndexIterator`.
 | ||||||
| @ -595,19 +581,20 @@ mod tests { | |||||||
|             Sample{duration_90k: 18, bytes: 31000, is_key: true}, |             Sample{duration_90k: 18, bytes: 31000, is_key: true}, | ||||||
|             Sample{duration_90k:  0, bytes:  1000, is_key: false}, |             Sample{duration_90k:  0, bytes:  1000, is_key: false}, | ||||||
|         ]; |         ]; | ||||||
|  |         let mut r = db::RecordingToInsert::default(); | ||||||
|         let mut e = SampleIndexEncoder::new(); |         let mut e = SampleIndexEncoder::new(); | ||||||
|         for sample in &samples { |         for sample in &samples { | ||||||
|             e.add_sample(sample.duration_90k, sample.bytes, sample.is_key); |             e.add_sample(sample.duration_90k, sample.bytes, sample.is_key, &mut r); | ||||||
|         } |         } | ||||||
|         let mut it = SampleIndexIterator::new(); |         let mut it = SampleIndexIterator::new(); | ||||||
|         for sample in &samples { |         for sample in &samples { | ||||||
|             assert!(it.next(&e.video_index).unwrap()); |             assert!(it.next(&r.video_index).unwrap()); | ||||||
|             assert_eq!(sample, |             assert_eq!(sample, | ||||||
|                        &Sample{duration_90k: it.duration_90k, |                        &Sample{duration_90k: it.duration_90k, | ||||||
|                                bytes: it.bytes, |                                bytes: it.bytes, | ||||||
|                                is_key: it.is_key()}); |                                is_key: it.is_key()}); | ||||||
|         } |         } | ||||||
|         assert!(!it.next(&e.video_index).unwrap()); |         assert!(!it.next(&r.video_index).unwrap()); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /// Tests that `SampleIndexIterator` spots several classes of errors.
 |     /// Tests that `SampleIndexIterator` spots several classes of errors.
 | ||||||
| @ -649,14 +636,15 @@ mod tests { | |||||||
|     #[test] |     #[test] | ||||||
|     fn test_segment_clipping_with_all_sync() { |     fn test_segment_clipping_with_all_sync() { | ||||||
|         testutil::init(); |         testutil::init(); | ||||||
|  |         let mut r = db::RecordingToInsert::default(); | ||||||
|         let mut encoder = SampleIndexEncoder::new(); |         let mut encoder = SampleIndexEncoder::new(); | ||||||
|         for i in 1..6 { |         for i in 1..6 { | ||||||
|             let duration_90k = 2 * i; |             let duration_90k = 2 * i; | ||||||
|             let bytes = 3 * i; |             let bytes = 3 * i; | ||||||
|             encoder.add_sample(duration_90k, bytes, true); |             encoder.add_sample(duration_90k, bytes, true, &mut r); | ||||||
|         } |         } | ||||||
|         let db = TestDb::new(); |         let db = TestDb::new(); | ||||||
|         let row = db.create_recording_from_encoder(encoder); |         let row = db.insert_recording_from_encoder(r); | ||||||
|         // Time range [2, 2 + 4 + 6 + 8) means the 2nd, 3rd, 4th samples should be
 |         // Time range [2, 2 + 4 + 6 + 8) means the 2nd, 3rd, 4th samples should be
 | ||||||
|         // included.
 |         // included.
 | ||||||
|         let segment = Segment::new(&db.db.lock(), &row, 2 .. 2+4+6+8).unwrap(); |         let segment = Segment::new(&db.db.lock(), &row, 2 .. 2+4+6+8).unwrap(); | ||||||
| @ -667,14 +655,15 @@ mod tests { | |||||||
|     #[test] |     #[test] | ||||||
|     fn test_segment_clipping_with_half_sync() { |     fn test_segment_clipping_with_half_sync() { | ||||||
|         testutil::init(); |         testutil::init(); | ||||||
|  |         let mut r = db::RecordingToInsert::default(); | ||||||
|         let mut encoder = SampleIndexEncoder::new(); |         let mut encoder = SampleIndexEncoder::new(); | ||||||
|         for i in 1..6 { |         for i in 1..6 { | ||||||
|             let duration_90k = 2 * i; |             let duration_90k = 2 * i; | ||||||
|             let bytes = 3 * i; |             let bytes = 3 * i; | ||||||
|             encoder.add_sample(duration_90k, bytes, (i % 2) == 1); |             encoder.add_sample(duration_90k, bytes, (i % 2) == 1, &mut r); | ||||||
|         } |         } | ||||||
|         let db = TestDb::new(); |         let db = TestDb::new(); | ||||||
|         let row = db.create_recording_from_encoder(encoder); |         let row = db.insert_recording_from_encoder(r); | ||||||
|         // Time range [2 + 4 + 6, 2 + 4 + 6 + 8) means the 4th sample should be included.
 |         // Time range [2 + 4 + 6, 2 + 4 + 6 + 8) means the 4th sample should be included.
 | ||||||
|         // The 3rd also gets pulled in because it is a sync frame and the 4th is not.
 |         // The 3rd also gets pulled in because it is a sync frame and the 4th is not.
 | ||||||
|         let segment = Segment::new(&db.db.lock(), &row, 2+4+6 .. 2+4+6+8).unwrap(); |         let segment = Segment::new(&db.db.lock(), &row, 2+4+6 .. 2+4+6+8).unwrap(); | ||||||
| @ -684,12 +673,13 @@ mod tests { | |||||||
|     #[test] |     #[test] | ||||||
|     fn test_segment_clipping_with_trailing_zero() { |     fn test_segment_clipping_with_trailing_zero() { | ||||||
|         testutil::init(); |         testutil::init(); | ||||||
|  |         let mut r = db::RecordingToInsert::default(); | ||||||
|         let mut encoder = SampleIndexEncoder::new(); |         let mut encoder = SampleIndexEncoder::new(); | ||||||
|         encoder.add_sample(1, 1, true); |         encoder.add_sample(1, 1, true, &mut r); | ||||||
|         encoder.add_sample(1, 2, true); |         encoder.add_sample(1, 2, true, &mut r); | ||||||
|         encoder.add_sample(0, 3, true); |         encoder.add_sample(0, 3, true, &mut r); | ||||||
|         let db = TestDb::new(); |         let db = TestDb::new(); | ||||||
|         let row = db.create_recording_from_encoder(encoder); |         let row = db.insert_recording_from_encoder(r); | ||||||
|         let segment = Segment::new(&db.db.lock(), &row, 1 .. 2).unwrap(); |         let segment = Segment::new(&db.db.lock(), &row, 1 .. 2).unwrap(); | ||||||
|         assert_eq!(&get_frames(&db.db, &segment, |it| it.bytes), &[2, 3]); |         assert_eq!(&get_frames(&db.db, &segment, |it| it.bytes), &[2, 3]); | ||||||
|     } |     } | ||||||
| @ -698,10 +688,11 @@ mod tests { | |||||||
|     #[test] |     #[test] | ||||||
|     fn test_segment_zero_desired_duration() { |     fn test_segment_zero_desired_duration() { | ||||||
|         testutil::init(); |         testutil::init(); | ||||||
|  |         let mut r = db::RecordingToInsert::default(); | ||||||
|         let mut encoder = SampleIndexEncoder::new(); |         let mut encoder = SampleIndexEncoder::new(); | ||||||
|         encoder.add_sample(1, 1, true); |         encoder.add_sample(1, 1, true, &mut r); | ||||||
|         let db = TestDb::new(); |         let db = TestDb::new(); | ||||||
|         let row = db.create_recording_from_encoder(encoder); |         let row = db.insert_recording_from_encoder(r); | ||||||
|         let segment = Segment::new(&db.db.lock(), &row, 0 .. 0).unwrap(); |         let segment = Segment::new(&db.db.lock(), &row, 0 .. 0).unwrap(); | ||||||
|         assert_eq!(&get_frames(&db.db, &segment, |it| it.bytes), &[1]); |         assert_eq!(&get_frames(&db.db, &segment, |it| it.bytes), &[1]); | ||||||
|     } |     } | ||||||
| @ -711,14 +702,15 @@ mod tests { | |||||||
|     #[test] |     #[test] | ||||||
|     fn test_segment_fast_path() { |     fn test_segment_fast_path() { | ||||||
|         testutil::init(); |         testutil::init(); | ||||||
|  |         let mut r = db::RecordingToInsert::default(); | ||||||
|         let mut encoder = SampleIndexEncoder::new(); |         let mut encoder = SampleIndexEncoder::new(); | ||||||
|         for i in 1..6 { |         for i in 1..6 { | ||||||
|             let duration_90k = 2 * i; |             let duration_90k = 2 * i; | ||||||
|             let bytes = 3 * i; |             let bytes = 3 * i; | ||||||
|             encoder.add_sample(duration_90k, bytes, (i % 2) == 1); |             encoder.add_sample(duration_90k, bytes, (i % 2) == 1, &mut r); | ||||||
|         } |         } | ||||||
|         let db = TestDb::new(); |         let db = TestDb::new(); | ||||||
|         let row = db.create_recording_from_encoder(encoder); |         let row = db.insert_recording_from_encoder(r); | ||||||
|         let segment = Segment::new(&db.db.lock(), &row, 0 .. 2+4+6+8+10).unwrap(); |         let segment = Segment::new(&db.db.lock(), &row, 0 .. 2+4+6+8+10).unwrap(); | ||||||
|         assert_eq!(&get_frames(&db.db, &segment, |it| it.duration_90k), &[2, 4, 6, 8, 10]); |         assert_eq!(&get_frames(&db.db, &segment, |it| it.duration_90k), &[2, 4, 6, 8, 10]); | ||||||
|     } |     } | ||||||
| @ -726,12 +718,13 @@ mod tests { | |||||||
|     #[test] |     #[test] | ||||||
|     fn test_segment_fast_path_with_trailing_zero() { |     fn test_segment_fast_path_with_trailing_zero() { | ||||||
|         testutil::init(); |         testutil::init(); | ||||||
|  |         let mut r = db::RecordingToInsert::default(); | ||||||
|         let mut encoder = SampleIndexEncoder::new(); |         let mut encoder = SampleIndexEncoder::new(); | ||||||
|         encoder.add_sample(1, 1, true); |         encoder.add_sample(1, 1, true, &mut r); | ||||||
|         encoder.add_sample(1, 2, true); |         encoder.add_sample(1, 2, true, &mut r); | ||||||
|         encoder.add_sample(0, 3, true); |         encoder.add_sample(0, 3, true, &mut r); | ||||||
|         let db = TestDb::new(); |         let db = TestDb::new(); | ||||||
|         let row = db.create_recording_from_encoder(encoder); |         let row = db.insert_recording_from_encoder(r); | ||||||
|         let segment = Segment::new(&db.db.lock(), &row, 0 .. 2).unwrap(); |         let segment = Segment::new(&db.db.lock(), &row, 0 .. 2).unwrap(); | ||||||
|         assert_eq!(&get_frames(&db.db, &segment, |it| it.bytes), &[1, 2, 3]); |         assert_eq!(&get_frames(&db.db, &segment, |it| it.bytes), &[1, 2, 3]); | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -34,7 +34,6 @@ use db; | |||||||
| use dir; | use dir; | ||||||
| use fnv::FnvHashMap; | use fnv::FnvHashMap; | ||||||
| use mylog; | use mylog; | ||||||
| use recording::{self, TIME_UNITS_PER_SEC}; |  | ||||||
| use rusqlite; | use rusqlite; | ||||||
| use std::env; | use std::env; | ||||||
| use std::sync::{self, Arc}; | use std::sync::{self, Arc}; | ||||||
| @ -125,26 +124,20 @@ impl TestDb { | |||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     pub fn create_recording_from_encoder(&self, encoder: recording::SampleIndexEncoder) |     /// Creates a recording with a fresh `RecordingToInsert` row which has been touched only by
 | ||||||
|  |     /// a `SampleIndexEncoder`. Fills in a video sample entry id and such to make it valid.
 | ||||||
|  |     /// There will no backing sample file, so it won't be possible to generate a full `.mp4`.
 | ||||||
|  |     pub fn insert_recording_from_encoder(&self, r: db::RecordingToInsert) | ||||||
|                                                 -> db::ListRecordingsRow { |                                                 -> db::ListRecordingsRow { | ||||||
|  |         use recording::{self, TIME_UNITS_PER_SEC}; | ||||||
|         let mut db = self.db.lock(); |         let mut db = self.db.lock(); | ||||||
|         let video_sample_entry_id = db.insert_video_sample_entry( |         let video_sample_entry_id = db.insert_video_sample_entry( | ||||||
|             1920, 1080, [0u8; 100].to_vec(), "avc1.000000".to_owned()).unwrap(); |             1920, 1080, [0u8; 100].to_vec(), "avc1.000000".to_owned()).unwrap(); | ||||||
|         const START_TIME: recording::Time = recording::Time(1430006400i64 * TIME_UNITS_PER_SEC); |         let (id, _) = db.add_recording(TEST_STREAM_ID, db::RecordingToInsert { | ||||||
|         let (id, u) = db.add_recording(TEST_STREAM_ID).unwrap(); |             start: recording::Time(1430006400i64 * TIME_UNITS_PER_SEC), | ||||||
|         u.lock().recording = Some(db::RecordingToInsert { |             video_sample_entry_id, | ||||||
|             sample_file_bytes: encoder.sample_file_bytes, |             ..r | ||||||
|             time: START_TIME .. |         }).unwrap(); | ||||||
|                   START_TIME + recording::Duration(encoder.total_duration_90k as i64), |  | ||||||
|             local_time_delta: recording::Duration(0), |  | ||||||
|             video_samples: encoder.video_samples, |  | ||||||
|             video_sync_samples: encoder.video_sync_samples, |  | ||||||
|             video_sample_entry_id: video_sample_entry_id, |  | ||||||
|             video_index: encoder.video_index, |  | ||||||
|             sample_file_sha1: [0u8; 20], |  | ||||||
|             run_offset: 0, |  | ||||||
|             flags: db::RecordingFlags::TrailingZero as i32, |  | ||||||
|         }); |  | ||||||
|         db.mark_synced(id).unwrap(); |         db.mark_synced(id).unwrap(); | ||||||
|         db.flush("create_recording_from_encoder").unwrap(); |         db.flush("create_recording_from_encoder").unwrap(); | ||||||
|         let mut row = None; |         let mut row = None; | ||||||
| @ -157,30 +150,26 @@ impl TestDb { | |||||||
| // For benchmarking
 | // For benchmarking
 | ||||||
| #[cfg(feature="nightly")] | #[cfg(feature="nightly")] | ||||||
| pub fn add_dummy_recordings_to_db(db: &db::Database, num: usize) { | pub fn add_dummy_recordings_to_db(db: &db::Database, num: usize) { | ||||||
|  |     use recording::{self, TIME_UNITS_PER_SEC}; | ||||||
|     let mut data = Vec::new(); |     let mut data = Vec::new(); | ||||||
|     data.extend_from_slice(include_bytes!("testdata/video_sample_index.bin")); |     data.extend_from_slice(include_bytes!("testdata/video_sample_index.bin")); | ||||||
|     let mut db = db.lock(); |     let mut db = db.lock(); | ||||||
|     let video_sample_entry_id = db.insert_video_sample_entry( |     let video_sample_entry_id = db.insert_video_sample_entry( | ||||||
|         1920, 1080, [0u8; 100].to_vec(), "avc1.000000".to_owned()).unwrap(); |         1920, 1080, [0u8; 100].to_vec(), "avc1.000000".to_owned()).unwrap(); | ||||||
|     const START_TIME: recording::Time = recording::Time(1430006400i64 * TIME_UNITS_PER_SEC); |  | ||||||
|     const DURATION: recording::Duration = recording::Duration(5399985); |  | ||||||
|     let mut recording = db::RecordingToInsert { |     let mut recording = db::RecordingToInsert { | ||||||
|         sample_file_bytes: 30104460, |         sample_file_bytes: 30104460, | ||||||
|         flags: 0, |         start: recording::Time(1430006400i64 * TIME_UNITS_PER_SEC), | ||||||
|         time: START_TIME .. (START_TIME + DURATION), |         duration_90k: 5399985, | ||||||
|         local_time_delta: recording::Duration(0), |  | ||||||
|         video_samples: 1800, |         video_samples: 1800, | ||||||
|         video_sync_samples: 60, |         video_sync_samples: 60, | ||||||
|         video_sample_entry_id: video_sample_entry_id, |         video_sample_entry_id: video_sample_entry_id, | ||||||
|         video_index: data, |         video_index: data, | ||||||
|         sample_file_sha1: [0; 20], |  | ||||||
|         run_offset: 0, |         run_offset: 0, | ||||||
|  |         ..Default::default() | ||||||
|     }; |     }; | ||||||
|     for _ in 0..num { |     for _ in 0..num { | ||||||
|         let (id, u) = db.add_recording(TEST_STREAM_ID).unwrap(); |         let (id, _) = db.add_recording(TEST_STREAM_ID, recording.clone()).unwrap(); | ||||||
|         u.lock().recording = Some(recording.clone()); |         recording.start += recording::Duration(recording.duration_90k as i64); | ||||||
|         recording.time.start += DURATION; |  | ||||||
|         recording.time.end += DURATION; |  | ||||||
|         recording.run_offset += 1; |         recording.run_offset += 1; | ||||||
|         db.mark_synced(id).unwrap(); |         db.mark_synced(id).unwrap(); | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -175,6 +175,11 @@ Each recording object has the following properties: | |||||||
|     it's possible that after a crash and restart, this id will refer to a |     it's possible that after a crash and restart, this id will refer to a | ||||||
|     completely different recording. That recording will have a different |     completely different recording. That recording will have a different | ||||||
|     `openId`. |     `openId`. | ||||||
|  | *   `growing` (optional). If this boolean is true, the recording `endId` is | ||||||
|  |     still being written to. Accesses to this id (such as `view.mp4`) may | ||||||
|  |     retrieve more data than described here if not bounded by duration. | ||||||
|  |     Additionally, if `startId` == `endId`, the start time of the recording is | ||||||
|  |     "unanchored" and may change in subsequent accesses. | ||||||
| *   `openId`. Each time Moonfire NVR starts in read-write mode, it is assigned | *   `openId`. Each time Moonfire NVR starts in read-write mode, it is assigned | ||||||
|     an increasing "open id". This field is the open id as of when these |     an increasing "open id". This field is the open id as of when these | ||||||
|     recordings were written. This can be used to disambiguate ids referring to |     recordings were written. This can be used to disambiguate ids referring to | ||||||
|  | |||||||
| @ -32,6 +32,7 @@ use db; | |||||||
| use failure::Error; | use failure::Error; | ||||||
| use serde::ser::{SerializeMap, SerializeSeq, Serializer}; | use serde::ser::{SerializeMap, SerializeSeq, Serializer}; | ||||||
| use std::collections::BTreeMap; | use std::collections::BTreeMap; | ||||||
|  | use std::ops::Not; | ||||||
| use uuid::Uuid; | use uuid::Uuid; | ||||||
| 
 | 
 | ||||||
| #[derive(Serialize)] | #[derive(Serialize)] | ||||||
| @ -182,4 +183,7 @@ pub struct Recording { | |||||||
|     pub end_id: Option<i32>, |     pub end_id: Option<i32>, | ||||||
|     pub video_sample_entry_width: u16, |     pub video_sample_entry_width: u16, | ||||||
|     pub video_sample_entry_height: u16, |     pub video_sample_entry_height: u16, | ||||||
|  | 
 | ||||||
|  |     #[serde(skip_serializing_if = "Not::not")] | ||||||
|  |     pub growing: bool, | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										49
									
								
								src/mp4.rs
									
									
									
									
									
								
							
							
						
						
									
										49
									
								
								src/mp4.rs
									
									
									
									
									
								
							| @ -1857,12 +1857,12 @@ mod tests { | |||||||
|     /// Makes a `.mp4` file which is only good for exercising the `Slice` logic for producing
 |     /// Makes a `.mp4` file which is only good for exercising the `Slice` logic for producing
 | ||||||
|     /// sample tables that match the supplied encoder.
 |     /// sample tables that match the supplied encoder.
 | ||||||
|     fn make_mp4_from_encoders(type_: Type, db: &TestDb, |     fn make_mp4_from_encoders(type_: Type, db: &TestDb, | ||||||
|                               mut encoders: Vec<recording::SampleIndexEncoder>, |                               mut recordings: Vec<db::RecordingToInsert>, | ||||||
|                               desired_range_90k: Range<i32>) -> File { |                               desired_range_90k: Range<i32>) -> File { | ||||||
|         let mut builder = FileBuilder::new(type_); |         let mut builder = FileBuilder::new(type_); | ||||||
|         let mut duration_so_far = 0; |         let mut duration_so_far = 0; | ||||||
|         for e in encoders.drain(..) { |         for r in recordings.drain(..) { | ||||||
|             let row = db.create_recording_from_encoder(e); |             let row = db.insert_recording_from_encoder(r); | ||||||
|             let d_start = if desired_range_90k.start < duration_so_far { 0 } |             let d_start = if desired_range_90k.start < duration_so_far { 0 } | ||||||
|                           else { desired_range_90k.start - duration_so_far }; |                           else { desired_range_90k.start - duration_so_far }; | ||||||
|             let d_end = if desired_range_90k.end > duration_so_far + row.duration_90k |             let d_end = if desired_range_90k.end > duration_so_far + row.duration_90k | ||||||
| @ -1878,15 +1878,16 @@ mod tests { | |||||||
|     fn test_all_sync_frames() { |     fn test_all_sync_frames() { | ||||||
|         testutil::init(); |         testutil::init(); | ||||||
|         let db = TestDb::new(); |         let db = TestDb::new(); | ||||||
|  |         let mut r = db::RecordingToInsert::default(); | ||||||
|         let mut encoder = recording::SampleIndexEncoder::new(); |         let mut encoder = recording::SampleIndexEncoder::new(); | ||||||
|         for i in 1..6 { |         for i in 1..6 { | ||||||
|             let duration_90k = 2 * i; |             let duration_90k = 2 * i; | ||||||
|             let bytes = 3 * i; |             let bytes = 3 * i; | ||||||
|             encoder.add_sample(duration_90k, bytes, true); |             encoder.add_sample(duration_90k, bytes, true, &mut r); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Time range [2, 2+4+6+8) means the 2nd, 3rd, and 4th samples should be included.
 |         // Time range [2, 2+4+6+8) means the 2nd, 3rd, and 4th samples should be included.
 | ||||||
|         let mp4 = make_mp4_from_encoders(Type::Normal, &db, vec![encoder], 2 .. 2+4+6+8); |         let mp4 = make_mp4_from_encoders(Type::Normal, &db, vec![r], 2 .. 2+4+6+8); | ||||||
|         let track = find_track(mp4, 1); |         let track = find_track(mp4, 1); | ||||||
|         assert!(track.edts_cursor.is_none()); |         assert!(track.edts_cursor.is_none()); | ||||||
|         let mut cursor = track.stbl_cursor; |         let mut cursor = track.stbl_cursor; | ||||||
| @ -1931,16 +1932,17 @@ mod tests { | |||||||
|     fn test_half_sync_frames() { |     fn test_half_sync_frames() { | ||||||
|         testutil::init(); |         testutil::init(); | ||||||
|         let db = TestDb::new(); |         let db = TestDb::new(); | ||||||
|  |         let mut r = db::RecordingToInsert::default(); | ||||||
|         let mut encoder = recording::SampleIndexEncoder::new(); |         let mut encoder = recording::SampleIndexEncoder::new(); | ||||||
|         for i in 1..6 { |         for i in 1..6 { | ||||||
|             let duration_90k = 2 * i; |             let duration_90k = 2 * i; | ||||||
|             let bytes = 3 * i; |             let bytes = 3 * i; | ||||||
|             encoder.add_sample(duration_90k, bytes, (i % 2) == 1); |             encoder.add_sample(duration_90k, bytes, (i % 2) == 1, &mut r); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Time range [2+4+6, 2+4+6+8) means the 4th sample should be included.
 |         // Time range [2+4+6, 2+4+6+8) means the 4th sample should be included.
 | ||||||
|         // The 3rd gets pulled in also because it's a sync frame and the 4th isn't.
 |         // The 3rd gets pulled in also because it's a sync frame and the 4th isn't.
 | ||||||
|         let mp4 = make_mp4_from_encoders(Type::Normal, &db, vec![encoder], 2+4+6 .. 2+4+6+8); |         let mp4 = make_mp4_from_encoders(Type::Normal, &db, vec![r], 2+4+6 .. 2+4+6+8); | ||||||
|         let track = find_track(mp4, 1); |         let track = find_track(mp4, 1); | ||||||
| 
 | 
 | ||||||
|         // Examine edts. It should skip the 3rd frame.
 |         // Examine edts. It should skip the 3rd frame.
 | ||||||
| @ -1994,15 +1996,17 @@ mod tests { | |||||||
|         testutil::init(); |         testutil::init(); | ||||||
|         let db = TestDb::new(); |         let db = TestDb::new(); | ||||||
|         let mut encoders = Vec::new(); |         let mut encoders = Vec::new(); | ||||||
|  |         let mut r = db::RecordingToInsert::default(); | ||||||
|         let mut encoder = recording::SampleIndexEncoder::new(); |         let mut encoder = recording::SampleIndexEncoder::new(); | ||||||
|         encoder.add_sample(1, 1, true); |         encoder.add_sample(1, 1, true, &mut r); | ||||||
|         encoder.add_sample(2, 2, false); |         encoder.add_sample(2, 2, false, &mut r); | ||||||
|         encoder.add_sample(3, 3, true); |         encoder.add_sample(3, 3, true, &mut r); | ||||||
|         encoders.push(encoder); |         encoders.push(r); | ||||||
|  |         let mut r = db::RecordingToInsert::default(); | ||||||
|         let mut encoder = recording::SampleIndexEncoder::new(); |         let mut encoder = recording::SampleIndexEncoder::new(); | ||||||
|         encoder.add_sample(4, 4, true); |         encoder.add_sample(4, 4, true, &mut r); | ||||||
|         encoder.add_sample(5, 5, false); |         encoder.add_sample(5, 5, false, &mut r); | ||||||
|         encoders.push(encoder); |         encoders.push(r); | ||||||
| 
 | 
 | ||||||
|         // This should include samples 3 and 4 only, both sync frames.
 |         // This should include samples 3 and 4 only, both sync frames.
 | ||||||
|         let mp4 = make_mp4_from_encoders(Type::Normal, &db, encoders, 1+2 .. 1+2+3+4); |         let mp4 = make_mp4_from_encoders(Type::Normal, &db, encoders, 1+2 .. 1+2+3+4); | ||||||
| @ -2029,13 +2033,15 @@ mod tests { | |||||||
|         testutil::init(); |         testutil::init(); | ||||||
|         let db = TestDb::new(); |         let db = TestDb::new(); | ||||||
|         let mut encoders = Vec::new(); |         let mut encoders = Vec::new(); | ||||||
|  |         let mut r = db::RecordingToInsert::default(); | ||||||
|         let mut encoder = recording::SampleIndexEncoder::new(); |         let mut encoder = recording::SampleIndexEncoder::new(); | ||||||
|         encoder.add_sample(2, 1, true); |         encoder.add_sample(2, 1, true, &mut r); | ||||||
|         encoder.add_sample(3, 2, false); |         encoder.add_sample(3, 2, false, &mut r); | ||||||
|         encoders.push(encoder); |         encoders.push(r); | ||||||
|  |         let mut r = db::RecordingToInsert::default(); | ||||||
|         let mut encoder = recording::SampleIndexEncoder::new(); |         let mut encoder = recording::SampleIndexEncoder::new(); | ||||||
|         encoder.add_sample(0, 3, true); |         encoder.add_sample(0, 3, true, &mut r); | ||||||
|         encoders.push(encoder); |         encoders.push(r); | ||||||
| 
 | 
 | ||||||
|         // Multi-segment recording with an edit list, encoding with a zero-duration recording.
 |         // Multi-segment recording with an edit list, encoding with a zero-duration recording.
 | ||||||
|         let mp4 = make_mp4_from_encoders(Type::Normal, &db, encoders, 1 .. 2+3); |         let mp4 = make_mp4_from_encoders(Type::Normal, &db, encoders, 1 .. 2+3); | ||||||
| @ -2052,16 +2058,17 @@ mod tests { | |||||||
|     fn test_media_segment() { |     fn test_media_segment() { | ||||||
|         testutil::init(); |         testutil::init(); | ||||||
|         let db = TestDb::new(); |         let db = TestDb::new(); | ||||||
|  |         let mut r = db::RecordingToInsert::default(); | ||||||
|         let mut encoder = recording::SampleIndexEncoder::new(); |         let mut encoder = recording::SampleIndexEncoder::new(); | ||||||
|         for i in 1..6 { |         for i in 1..6 { | ||||||
|             let duration_90k = 2 * i; |             let duration_90k = 2 * i; | ||||||
|             let bytes = 3 * i; |             let bytes = 3 * i; | ||||||
|             encoder.add_sample(duration_90k, bytes, (i % 2) == 1); |             encoder.add_sample(duration_90k, bytes, (i % 2) == 1, &mut r); | ||||||
|         } |         } | ||||||
| 
 | 
 | ||||||
|         // Time range [2+4+6, 2+4+6+8+1) means the 4th sample and part of the 5th are included.
 |         // Time range [2+4+6, 2+4+6+8+1) means the 4th sample and part of the 5th are included.
 | ||||||
|         // The 3rd gets pulled in also because it's a sync frame and the 4th isn't.
 |         // The 3rd gets pulled in also because it's a sync frame and the 4th isn't.
 | ||||||
|         let mp4 = make_mp4_from_encoders(Type::MediaSegment, &db, vec![encoder], |         let mp4 = make_mp4_from_encoders(Type::MediaSegment, &db, vec![r], | ||||||
|                                          2+4+6 .. 2+4+6+8+1); |                                          2+4+6 .. 2+4+6+8+1); | ||||||
|         let mut cursor = BoxCursor::new(mp4); |         let mut cursor = BoxCursor::new(mp4); | ||||||
|         cursor.down(); |         cursor.down(); | ||||||
|  | |||||||
| @ -279,6 +279,7 @@ impl ServiceInner { | |||||||
|                     video_sample_entry_width: vse.width, |                     video_sample_entry_width: vse.width, | ||||||
|                     video_sample_entry_height: vse.height, |                     video_sample_entry_height: vse.height, | ||||||
|                     video_sample_entry_sha1: strutil::hex(&vse.sha1), |                     video_sample_entry_sha1: strutil::hex(&vse.sha1), | ||||||
|  |                     growing: row.growing, | ||||||
|                 }); |                 }); | ||||||
|                 Ok(()) |                 Ok(()) | ||||||
|             })?; |             })?; | ||||||
|  | |||||||
| @ -85,6 +85,9 @@ function onSelectVideo(camera, streamType, range, recording) { | |||||||
|   if (trim && recording.endTime90k > range.endTime90k) { |   if (trim && recording.endTime90k > range.endTime90k) { | ||||||
|     rel += range.endTime90k - recording.startTime90k; |     rel += range.endTime90k - recording.startTime90k; | ||||||
|     endTime90k = range.endTime90k; |     endTime90k = range.endTime90k; | ||||||
|  |   } else if (recording.growing !== undefined) { | ||||||
|  |     // View just the portion described here.
 | ||||||
|  |     rel += recording.endTime90k - recording.startTime90k; | ||||||
|   } |   } | ||||||
|   if (rel !== '-') { |   if (rel !== '-') { | ||||||
|     url += '.' + rel; |     url += '.' + rel; | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user