1use chrono::{DateTime, Local};
5use frame_source::{FrameDataSource, h264_source::SeekableH264Source};
6use serde::{Deserialize, Serialize};
7use std::path::PathBuf;
8
9use ffmpeg_writer::{FfmpegCodecArgs, FfmpegWriter};
10use strand_cam_remote_control::{H264Metadata, Mp4Codec, Mp4RecordingConfig, RecordingFrameRate};
11
12#[derive(thiserror::Error, Debug)]
13pub enum Error {
14 #[error("IO error: {0}")]
15 Io(#[from] std::io::Error),
16 #[error("ffmpeg_writer {0}")]
17 FfmpegWriter(#[from] ffmpeg_writer::Error),
18 #[error("cannot reencode")]
19 CannotReencode,
20 #[error("filename does not end with '.mp4'")]
21 FilenameDoesNotEndWithMp4,
22 #[error("filename not unicode")]
23 FilenameNotUnicode,
24 #[error("source does not contain H264 video")]
25 SourceIsNotH264,
26 #[error("MP4 writer error: {0}")]
27 Mp4WriterError(#[from] mp4_writer::Error),
28 #[error("frame source error: {0}")]
29 FrameSourceError(#[from] frame_source::Error),
30 #[error("serde json error: {0}")]
31 SerdeJsonError(#[from] serde_json::Error),
32}
33type Result<T> = std::result::Result<T, Error>;
34
35#[derive(Serialize, Deserialize)]
36struct SrtMsg {
37 timestamp: DateTime<chrono::Local>,
38}
39
40pub struct FfmpegReWriter {
44 mp4_filename: String,
45 ffmpeg_wtr: FfmpegWriter,
46 mp4_cfg: Mp4RecordingConfig,
47 srt_file_path: String,
48 swtr: srt_writer::BufferingSrtFrameWriter,
49 json_file_path: Option<String>,
50}
51
52impl FfmpegReWriter {
53 pub fn new(
54 mp4_path: impl AsRef<std::path::Path>,
55 ffmpeg_codec_args: FfmpegCodecArgs,
56 rate: Option<(usize, usize)>,
57 h264_metadata: Option<H264Metadata>,
58 ) -> Result<Self> {
59 tracing::debug!(
60 "Creating FfmpegReWriter for {} with h264_metadata: {h264_metadata:?}",
61 mp4_path.as_ref().display()
62 );
63 let mp4_filename = PathBuf::from(mp4_path.as_ref())
64 .into_os_string()
65 .into_string()
66 .map_err(|_| Error::FilenameNotUnicode)?;
67 let basename = if let Some(basename) = mp4_filename.strip_suffix(".mp4") {
68 basename
69 } else {
70 return Err(Error::FilenameDoesNotEndWithMp4);
71 };
72
73 let srt_file_path = format!("{basename}-ffmpeg-rewriter.srt");
79 let json_file_path = if let Some(h264_metadata) = &h264_metadata {
80 let jpath = format!("{basename}-metadata.json");
83 let buf = serde_json::to_string(h264_metadata)?;
84 std::fs::write(&jpath, buf)?;
85 Some(jpath)
86 } else {
87 None
88 };
89
90 let ffmpeg_wtr = FfmpegWriter::new(&mp4_filename, ffmpeg_codec_args, rate)?;
91 let mp4_cfg = Mp4RecordingConfig {
92 codec: Mp4Codec::H264RawStream,
93 max_framerate: RecordingFrameRate::Unlimited,
94 h264_metadata: h264_metadata.clone(),
95 };
96
97 let out_fd = std::fs::File::create(&srt_file_path)?;
98 let swtr = srt_writer::BufferingSrtFrameWriter::new(Box::new(out_fd));
99 Ok(Self {
100 mp4_filename: mp4_filename.to_string(),
101 ffmpeg_wtr,
102 mp4_cfg,
103 srt_file_path,
104 swtr,
105 json_file_path,
106 })
107 }
108
109 pub fn write_dynamic_frame<TS>(
111 &mut self,
112 frame: &strand_dynamic_frame::DynamicFrame,
113 timestamp: TS,
114 ) -> Result<()>
115 where
116 TS: Into<DateTime<Local>>,
117 {
118 let timestamp = timestamp.into();
119
120 let mp4_pts = self
121 .ffmpeg_wtr
122 .write_dynamic_frame(frame)
123 .map_err(Error::FfmpegWriter)?;
124
125 let msg = SrtMsg { timestamp };
126 let msg = serde_json::to_string(&msg).unwrap();
127 self.swtr.add_frame(mp4_pts, msg)?;
128 self.swtr.flush()?;
129
130 Ok(())
131 }
132
133 pub fn close(self) -> Result<()> {
134 self.ffmpeg_wtr.close()?;
136 self.swtr.close()?;
137 tracing::debug!("Done creating original .mp4 and .srt files.");
138
139 let mut frame_src = frame_source::FrameSourceBuilder::new(&self.mp4_filename)
141 .do_decode_h264(false)
142 .timestamp_source(frame_source::TimestampSource::SrtFile)
143 .srt_file_path(Some(PathBuf::from(&self.srt_file_path)))
144 .build_h264_in_mp4_source()?;
145
146 let frame0_time = frame_src.frame0_time().unwrap();
147
148 let fname2 = format!("{}-rewritten.mp4", self.mp4_filename);
150 tracing::debug!(
151 "Copying original .mp4 file into new .mp4 files with timestamps and metadata. frame0_time: {frame0_time}, mp4_cfg: {:?}",
152 self.mp4_cfg
153 );
154 let fd = std::fs::File::create(&fname2)?;
155 let mut new_mp4 = mp4_writer::Mp4Writer::new(fd, self.mp4_cfg, None)?;
156 let h264_src = frame_src.as_seekable_h264_source();
157 new_mp4.set_first_sps_pps(h264_src.first_sps(), h264_src.first_pps());
158
159 let insert_precision_timestamp = true;
160 let width = frame_src.width();
161 let height = frame_src.height();
162
163 let sample_timing: Option<Vec<_>> = frame_src.mp4_sample_timing().map(|t| t.to_vec());
169
170 let mut count = 0;
171 for frame in frame_src.decode_order_iter() {
172 let frame = frame?;
173 let timestamp = frame0_time + frame.timestamp().unwrap_duration();
174 let idx = frame.idx();
175 let data = match frame.image() {
176 frame_source::ImageData::EncodedH264(data) => &data.data,
177 _ => {
178 return Err(Error::SourceIsNotH264);
179 }
180 };
181 match sample_timing.as_ref().and_then(|t| t.get(idx)) {
182 Some(st) => new_mp4.write_h264_buf_passthrough(
183 data,
184 width,
185 height,
186 st.decode_duration,
187 st.composition_offset,
188 timestamp,
189 insert_precision_timestamp,
190 )?,
191 None => new_mp4.write_h264_buf(
192 data,
193 width,
194 height,
195 timestamp,
196 frame0_time,
197 insert_precision_timestamp,
198 )?,
199 }
200 count += 1;
201 }
202
203 new_mp4.finish()?;
204 tracing::debug!("Finished writing new .mp4 file with {count} frames.");
205
206 tracing::debug!(
207 "Renaming new .mp4 file to original .mp4 \
208 name, thereby deleting original."
209 );
210 std::fs::rename(fname2, self.mp4_filename)?;
211
212 std::fs::remove_file(&self.srt_file_path)?;
214 if let Some(jpath) = self.json_file_path {
215 std::fs::remove_file(jpath)?;
216 }
217
218 Ok(())
219 }
220}
221
222#[cfg(test)]
223mod test {
224 use super::*;
225 use machine_vision_formats::{owned::OImage, pixel_format::RGB8};
226
227 use test_log::test;
228
229 #[test]
230 fn test_ffmpeg_rewriter() -> Result<()> {
231 let tempdir = tempfile::tempdir()?;
232 let mp4_fname = tempdir.path().join("out.mp4");
233
234 let timestamp_micros: i64 = 1_662_921_288_000_000; let mut timestamps = vec![
239 DateTime::from_timestamp_micros(timestamp_micros).unwrap(),
240 DateTime::from_timestamp_micros(timestamp_micros + 1).unwrap(),
241 DateTime::from_timestamp_micros(timestamp_micros + 100).unwrap(),
242 ];
243
244 for delta in 1..10 {
245 let micros = delta * 10_000;
246 timestamps.push(DateTime::from_timestamp_micros(timestamp_micros + micros).unwrap());
247 }
248
249 tracing::debug!("Encoding {} frames", timestamps.len());
250
251 let w = 640;
252 let h = 480;
253 {
254 let ffmpeg_codec_args = Default::default();
256
257 let rate = None;
258 let h264_metadata = None;
259 let mut wtr = FfmpegReWriter::new(&mp4_fname, ffmpeg_codec_args, rate, h264_metadata)?;
260
261 for (i, ts) in timestamps.iter().enumerate() {
262 let value = (i % 255) as u8;
263 let frame: OImage<RGB8> = OImage::new(
264 w,
265 h,
266 w as usize * 3,
267 vec![value; w as usize * h as usize * 3],
268 )
269 .unwrap();
270 let frame = strand_dynamic_frame::DynamicFrameOwned::from_static(frame);
271 wtr.write_dynamic_frame(&frame.borrow(), *ts)?;
272 }
273 wtr.close()?;
274 }
275
276 let mut frame_src = frame_source::FrameSourceBuilder::new(&mp4_fname)
277 .do_decode_h264(false)
278 .timestamp_source(frame_source::TimestampSource::MispMicrosectime)
279 .build_source()?;
280
281 let frame0_time = frame_src.frame0_time().unwrap();
282 assert_eq!(frame0_time, timestamps[0]);
283
284 assert_eq!(frame_src.width(), w);
285 assert_eq!(frame_src.height(), h);
286
287 let mut got: Vec<_> = Vec::new();
293 for frame in frame_src.decode_order_iter() {
294 let frame = frame?;
295 got.push(frame0_time + frame.timestamp().unwrap_duration());
296 }
297 assert_eq!(got.len(), timestamps.len());
298 got.sort();
299 let mut expected = timestamps.clone();
300 expected.sort();
301 assert_eq!(got, expected);
302
303 Ok(())
304 }
305
306 #[test]
324 fn test_bframe_stream_remuxes_in_presentation_order() -> Result<()> {
325 use frame_source::h264_source::Mp4SampleTiming;
326
327 let tempdir = tempfile::tempdir()?;
328 let mp4_fname = tempdir.path().join("out.mp4");
329
330 let n_frames = 24usize;
333 let base_micros: i64 = 1_662_921_288_000_000; let frame_interval_micros = 40_000i64; let timestamps: Vec<_> = (0..n_frames)
336 .map(|i| {
337 DateTime::from_timestamp_micros(base_micros + i as i64 * frame_interval_micros)
338 .unwrap()
339 })
340 .collect();
341
342 let w = 64u32;
343 let h = 48u32;
344 {
345 let ffmpeg_codec_args = FfmpegCodecArgs {
349 device_args: None,
350 pre_codec_args: None,
351 codec: Some("libx264".to_string()),
352 post_codec_args: Some(vec![
353 ("-bf".to_string(), "3".to_string()),
354 (
355 "-x264-params".to_string(),
356 "b_adapt=0:scenecut=0:keyint=1000:min-keyint=1000".to_string(),
357 ),
358 ]),
359 pixfmt: Some("yuv420p".to_string()),
360 max_bframes: None,
364 };
365
366 let mut wtr = FfmpegReWriter::new(&mp4_fname, ffmpeg_codec_args, None, None)?;
367
368 for (i, ts) in timestamps.iter().enumerate() {
369 let mut data = vec![0u8; w as usize * h as usize * 3];
372 for (px, chunk) in data.chunks_exact_mut(3).enumerate() {
373 let v = ((px + i * 7) % 256) as u8;
374 chunk[0] = v;
375 chunk[1] = v.wrapping_mul(3);
376 chunk[2] = v.wrapping_add(i as u8 * 11);
377 }
378 let frame: OImage<RGB8> = OImage::new(w, h, w as usize * 3, data).unwrap();
379 let frame = strand_dynamic_frame::DynamicFrameOwned::from_static(frame);
380 wtr.write_dynamic_frame(&frame.borrow(), *ts)?;
381 }
382 wtr.close()?;
383 }
384
385 let mut frame_src = frame_source::FrameSourceBuilder::new(&mp4_fname)
389 .do_decode_h264(false)
390 .timestamp_source(frame_source::TimestampSource::MispMicrosectime)
391 .build_h264_in_mp4_source()?;
392
393 let frame0_time = frame_src.frame0_time().unwrap();
394
395 let sample_timing: Vec<Mp4SampleTiming> = frame_src
398 .mp4_sample_timing()
399 .expect("MP4 source must expose per-sample timing")
400 .to_vec();
401 assert_eq!(sample_timing.len(), n_frames);
402
403 let has_reordering = sample_timing
406 .iter()
407 .any(|t| t.composition_offset != chrono::Duration::zero());
408 assert!(
409 has_reordering,
410 "expected libx264 to emit B-frames (non-zero ctts); test would be vacuous otherwise"
411 );
412
413 let mut sei_times = vec![None; n_frames];
415 for frame in frame_src.decode_order_iter() {
416 let frame = frame?;
417 sei_times[frame.idx()] = Some(frame0_time + frame.timestamp().unwrap_duration());
418 }
419
420 let mut decode_time = chrono::Duration::zero();
424 let mut presentation = Vec::with_capacity(n_frames);
425 for (i, timing) in sample_timing.iter().enumerate() {
426 let pts = decode_time
427 + chrono::Duration::from_std(timing.decode_duration).unwrap()
428 + timing.composition_offset;
429 let sei = sei_times[i].expect("every sample must carry a SEI timestamp");
430 presentation.push((pts, sei));
431 decode_time += chrono::Duration::from_std(timing.decode_duration).unwrap();
432 }
433
434 presentation.sort_by_key(|(pts, _)| *pts);
438 let ordered_sei: Vec<_> = presentation.iter().map(|(_, sei)| *sei).collect();
439 for pair in ordered_sei.windows(2) {
440 assert!(
441 pair[0] < pair[1],
442 "SEI capture times must strictly increase in presentation order, \
443 but got {:?} then {:?} (out-of-order playback)",
444 pair[0],
445 pair[1]
446 );
447 }
448
449 assert_eq!(ordered_sei, timestamps);
451
452 Ok(())
453 }
454}