Skip to main content

mp4_writer/
lib.rs

1// Copyright (C) The Strand-Braid Authors
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! # mp4-writer
5//!
6//! MP4 data (or in .h264 files) can be inspected with:
7//!     ffprobe -select_streams v -show_packets -show_data <filename.ext>
8
9// Regarding timestamps
10//
11// This code should ensure that an MP4 file with H264 video data should ideally
12// have the creation time in the start metadata
13// (`strand_cam_remote_control::H264Metadata`) equal to the precision time stamp of the
14// initial frame. (Although, to specify the timezone, the creation time may be
15// in a timezone other than UTC.)
16
17#[cfg(feature = "nv-encode")]
18use std::rc::Rc;
19
20#[cfg(feature = "nv-encode")]
21use machine_vision_formats::image_ref::ImageRefMut;
22use strand_cam_remote_control::{H264_METADATA_UUID, H264Metadata, Mp4RecordingConfig};
23#[cfg(feature = "nv-encode")]
24use tracing::info;
25use tracing::{debug, trace};
26
27use strand_dynamic_frame::DynamicFrame;
28
29#[cfg(feature = "nv-encode")]
30use machine_vision_formats::pixel_format;
31
32#[cfg(feature = "nv-encode")]
33use nvenc::{InputBuffer, OutputBuffer, RateControlMode};
34
35use thiserror::Error;
36
37mod h264_annexb_split;
38use h264_annexb_split::h264_annexb_split;
39
40// The number of time units that pass in one second.
41const MOVIE_TIMESCALE: u32 = 90_000;
42const TRACK_ID: u32 = 1;
43
44#[derive(Error, Debug)]
45pub enum Error {
46    #[error("{source}")]
47    Mp4Error {
48        #[from]
49        source: mp4::Error,
50    },
51    #[error("IO error: {source}")]
52    IoError {
53        #[from]
54        source: std::io::Error,
55    },
56    #[error("required h264 data (SPS or PPS) not found")]
57    RequiredH264DataNotFound {},
58    #[error("file already closed")]
59    FileAlreadyClosed {},
60    #[error("cannot encode frame when copying h264 stream")]
61    RawH264CopyCannotEncodeFrame {},
62    #[error("bad input data")]
63    BadInputData {},
64    #[error("inconsistent state")]
65    InconsistentState {},
66    #[error("timestamp too large")]
67    TimestampTooLarge {},
68    #[error("convert image error")]
69    ConvertImageError(#[from] convert_image::Error),
70    #[cfg(feature = "openh264")]
71    #[error("openhs264 error {}", inner)]
72    OpenH264Error {
73        #[from]
74        inner: openh264::Error,
75    },
76    #[cfg(not(feature = "nv-encode"))]
77    #[error("no nvenc at compile time")]
78    NoNvencCompiledError,
79    #[cfg(feature = "nv-encode")]
80    #[error("nvenc error")]
81    NvencError(#[from] nvenc::NvEncError),
82    #[error("nvenc libraries not loaded")]
83    NvencLibsNotLoaded,
84    #[error("less-avc error {}", inner)]
85    LessAvcWrapperError {
86        #[from]
87        inner: less_avc_wrapper::Error,
88    },
89    #[error("y4m-writer error {0}")]
90    Y4mWriterError(#[from] y4m_writer::Error),
91}
92
93#[cfg(feature = "nv-encode")]
94impl From<dynlink_nvidia_encode::NvencError> for Error {
95    fn from(orig: dynlink_nvidia_encode::NvencError) -> Self {
96        Error::NvencError(orig.into())
97    }
98}
99
100#[cfg(feature = "nv-encode")]
101impl From<dynlink_cuda::CudaError> for Error {
102    fn from(orig: dynlink_cuda::CudaError) -> Self {
103        Error::NvencError(orig.into())
104    }
105}
106
107type Result<T> = std::result::Result<T, Error>;
108
109enum MyEncoder<'lib> {
110    CopyRawH264 {
111        h264_parser: H264Parser,
112    },
113    #[cfg(feature = "nv-encode")]
114    Nvidia(NvEncoder<'lib>),
115    #[cfg(not(feature = "nv-encode"))]
116    #[expect(dead_code)]
117    NoNvidia(std::marker::PhantomData<&'lib u8>),
118    #[cfg(feature = "openh264")]
119    OpenH264(Box<OpenH264Encoder>),
120    LessH264(LessEncoderWrapper),
121}
122
123pub struct Mp4Writer<'lib, T>
124where
125    T: std::io::Write + std::io::Seek,
126{
127    inner: Option<WriteState<'lib, T>>,
128    #[cfg(feature = "nv-encode")]
129    nv_enc: Option<nvenc::NvEnc<'lib>>,
130    first_sps: Option<Vec<u8>>,
131    first_pps: Option<Vec<u8>>,
132}
133
134impl<'lib, T> Mp4Writer<'lib, T>
135where
136    T: std::io::Write + std::io::Seek,
137{
138    pub fn new(
139        fd: T,
140        config: Mp4RecordingConfig,
141        #[cfg(feature = "nv-encode")] nv_enc: Option<nvenc::NvEnc<'lib>>,
142    ) -> Result<Self> {
143        let h264_parser = H264Parser::new(config.h264_metadata.clone());
144        Ok(Self {
145            inner: Some(WriteState::Configured(Box::new((fd, config, h264_parser)))),
146            #[cfg(feature = "nv-encode")]
147            nv_enc,
148            first_sps: None,
149            first_pps: None,
150        })
151    }
152
153    pub fn set_first_sps_pps(&mut self, first_sps: Option<Vec<u8>>, first_pps: Option<Vec<u8>>) {
154        self.first_sps = first_sps;
155        self.first_pps = first_pps;
156    }
157
158    /// Low-level writer which saves a buffer which is already h264 encoded.
159    ///
160    /// This skips the automatic encoding which would normally be done.
161    pub fn write_h264_buf<TS1, TS2>(
162        &mut self,
163        data: &frame_source::H264EncodingVariant,
164        width: u32,
165        height: u32,
166        timestamp: TS1,
167        frame0_time: TS2,
168        insert_precision_timestamp: bool,
169    ) -> Result<()>
170    where
171        TS1: Into<chrono::DateTime<chrono::Local>>,
172        TS2: Into<chrono::DateTime<chrono::Local>>,
173    {
174        let timestamp: chrono::DateTime<chrono::Local> = timestamp.into();
175        let frame0_time: chrono::DateTime<chrono::Local> = frame0_time.into();
176
177        let pts = timestamp - frame0_time;
178        let mp4_sample_start_time = dur2raw(&pts.to_std().unwrap());
179        let precision_timestamp = insert_precision_timestamp.then_some(timestamp);
180        // The sample duration is derived from consecutive presentation-time
181        // deltas and no composition offset (ctts) is emitted, so this path
182        // requires monotonic timestamps and cannot represent reordered streams.
183        self.write_h264_sample(
184            data,
185            width,
186            height,
187            mp4_sample_start_time,
188            pts,
189            None,
190            0,
191            precision_timestamp,
192        )
193    }
194
195    /// Low-level writer for already-h264-encoded frames that carries per-sample
196    /// timing through from a source file: the decode duration (stts) and
197    /// composition offset (ctts), rather than deriving the duration from
198    /// presentation-time deltas.
199    ///
200    /// This is required to re-mux reordered (B-frame) streams, which store
201    /// samples in decode order with non-zero composition offsets that
202    /// [`Self::write_h264_buf`] cannot represent. The precise capture time is
203    /// still written into the per-frame precision-timestamp SEI.
204    #[allow(clippy::too_many_arguments)]
205    pub fn write_h264_buf_passthrough<TS>(
206        &mut self,
207        data: &frame_source::H264EncodingVariant,
208        width: u32,
209        height: u32,
210        decode_duration: std::time::Duration,
211        composition_offset: chrono::Duration,
212        sei_timestamp: TS,
213        insert_precision_timestamp: bool,
214    ) -> Result<()>
215    where
216        TS: Into<chrono::DateTime<chrono::Local>>,
217    {
218        let sei_timestamp: chrono::DateTime<chrono::Local> = sei_timestamp.into();
219        let explicit_duration = dur2raw(&decode_duration);
220        let rendering_offset = composition_offset_to_raw(composition_offset);
221        let precision_timestamp = insert_precision_timestamp.then_some(sei_timestamp);
222        // `start_time` is ignored by the mp4 crate; the duration and composition
223        // offset define the timing, so `pts` here is unused.
224        self.write_h264_sample(
225            data,
226            width,
227            height,
228            0,
229            chrono::Duration::zero(),
230            Some(explicit_duration),
231            rendering_offset,
232            precision_timestamp,
233        )
234    }
235
236    #[allow(clippy::too_many_arguments)]
237    fn write_h264_sample(
238        &mut self,
239        data: &frame_source::H264EncodingVariant,
240        width: u32,
241        height: u32,
242        mp4_sample_start_time: u64,
243        pts: chrono::Duration,
244        explicit_duration: Option<u64>,
245        rendering_offset: i32,
246        precision_timestamp: Option<chrono::DateTime<chrono::Local>>,
247    ) -> Result<()> {
248        let is_keyframe = parse_h264_is_idr_frame(data)?;
249
250        let sample = match &data {
251            frame_source::H264EncodingVariant::AnnexB(buf) => {
252                let nals = h264_annexb_split(&buf[..]).collect();
253
254                EbspNals {
255                    pts,
256                    mp4_sample_start_time,
257                    is_keyframe,
258                    nals,
259                }
260            }
261            frame_source::H264EncodingVariant::Avcc(bufs) => {
262                let nal_iter = iter_avcc_bufs(bufs);
263                let mut nals = Vec::new();
264                for nal_ebsp_bytes in nal_iter {
265                    let nal_ebsp_bytes = nal_ebsp_bytes?;
266                    nals.push(nal_ebsp_bytes.to_vec());
267                }
268                EbspNals {
269                    pts,
270                    mp4_sample_start_time,
271                    is_keyframe,
272                    nals,
273                }
274            }
275            frame_source::H264EncodingVariant::RawEbsp(nals) => EbspNals {
276                pts,
277                mp4_sample_start_time,
278                is_keyframe,
279                nals: nals.clone(),
280            },
281        };
282
283        let inner = self.inner.take();
284
285        let mut state = match inner {
286            Some(WriteState::Configured(mut mybox)) => {
287                let (fd, _cfg, ref mut h264_parser) = *mybox;
288                h264_parser.push_nals(
289                    sample,
290                    precision_timestamp,
291                    explicit_duration,
292                    rendering_offset,
293                );
294
295                let sps = if let Some(sps) = self.first_sps.as_ref() {
296                    sps
297                } else {
298                    h264_parser
299                        .sps
300                        .as_ref()
301                        .ok_or(Error::RequiredH264DataNotFound {})?
302                };
303
304                let pps = if let Some(pps) = self.first_pps.as_ref() {
305                    pps
306                } else {
307                    h264_parser
308                        .pps
309                        .as_ref()
310                        .ok_or(Error::RequiredH264DataNotFound {})?
311                };
312
313                let mp4_writer = start_mp4_writer(fd, sps, pps, width, height)?;
314                let mp4_segment = MaybeMp4Writer::Mp4Writer(mp4_writer);
315                let my_encoder = MyEncoder::CopyRawH264 {
316                    h264_parser: h264_parser.clone(),
317                };
318                Box::new(RecordingState {
319                    mp4_segment,
320                    my_encoder,
321                    inner: None,
322                })
323            }
324            Some(WriteState::Recording(mut state)) => {
325                match &mut state.my_encoder {
326                    &mut MyEncoder::CopyRawH264 {
327                        ref mut h264_parser,
328                    } => {
329                        h264_parser.push_nals(
330                            sample,
331                            precision_timestamp,
332                            explicit_duration,
333                            rendering_offset,
334                        );
335                    }
336                    _ => {
337                        panic!();
338                    }
339                }
340                state
341            }
342            None | Some(WriteState::Finished) => {
343                return inconsistent_state_err();
344            }
345        };
346
347        if state.inner.is_some() {
348            return inconsistent_state_err();
349        }
350
351        let sample = match &mut state.my_encoder {
352            &mut MyEncoder::CopyRawH264 {
353                ref mut h264_parser,
354            } => h264_parser.avcc_sample().unwrap(),
355            _ => {
356                panic!();
357            }
358        };
359
360        let write_result = match &mut state.mp4_segment {
361            MaybeMp4Writer::Mp4Writer(mp4_writer) => mp4_writer
362                .write_sample(TRACK_ID, &sample)
363                .map_err(Error::from),
364            _ => {
365                return inconsistent_state_err();
366            }
367        };
368        self.inner = Some(WriteState::Recording(state));
369        write_result?;
370
371        Ok(())
372    }
373
374    pub fn write_dynamic<TS>(&mut self, frame: &DynamicFrame, timestamp: TS) -> Result<()>
375    where
376        TS: Into<chrono::DateTime<chrono::Local>>,
377    {
378        let timestamp: chrono::DateTime<chrono::Local> = timestamp.into();
379        let inner = self.inner.take();
380
381        match inner {
382            Some(WriteState::Configured(mybox)) => {
383                let (fd, cfg, h264_parser) = *mybox;
384
385                let width = frame.width();
386                let height = frame.height();
387
388                #[cfg(feature = "nv-encode")]
389                let mut opt_nv_h264_encoder = None;
390
391                match &cfg.codec {
392                    strand_cam_remote_control::Mp4Codec::H264RawStream => {}
393                    strand_cam_remote_control::Mp4Codec::H264LessAvc => {}
394                    strand_cam_remote_control::Mp4Codec::H264OpenH264(_) => {}
395                    #[cfg(not(feature = "nv-encode"))]
396                    strand_cam_remote_control::Mp4Codec::H264NvEnc(_) => {
397                        return Err(Error::NoNvencCompiledError);
398                    }
399                    #[cfg(feature = "nv-encode")]
400                    strand_cam_remote_control::Mp4Codec::H264NvEnc(opts) => {
401                        // scope for anonymous lifetime of ref
402                        match &self.nv_enc {
403                            Some(nv_enc) => {
404                                debug!("Using codec H264 in mp4 file.");
405
406                                // Setup the encoder.
407                                let cuda_version = nv_enc.cuda_version()?;
408                                info!("CUDA version {}", cuda_version);
409
410                                let nvenc_version = nv_enc
411                                    .libnvenc
412                                    .api_get_max_supported_version()
413                                    .map_err(nvenc::NvEncError::from)?;
414                                info!(
415                                    "NV_ENC version {}.{}",
416                                    nvenc_version.major, nvenc_version.minor
417                                );
418
419                                // From the Nvidia SDK docs for NvEncCreateInputBuffer: "The number of input
420                                // buffers to be allocated by the client must be at least 4 more than the
421                                // number of B frames being used for encoding."
422                                let num_bufs = 60;
423
424                                let dev = nv_enc.libcuda.new_device(opts.cuda_device)?;
425
426                                info!("CUDA device: {}, name: {}", opts.cuda_device, dev.name()?);
427                                let ctx = dev.into_context()?;
428                                let encoder: Rc<nvenc::Encoder<'lib>> =
429                                    nv_enc.functions.new_encoder(ctx)?;
430
431                                let encode = nvenc::NV_ENC_CODEC_H264_GUID;
432                                // let encode = nvenc::NV_ENC_CODEC_HEVC_GUID;
433                                let preset = nvenc::NV_ENC_PRESET_HP_GUID;
434                                // let preset = nvenc::NV_ENC_PRESET_DEFAULT_GUID;
435                                let format = nvenc::BufferFormat::NV12;
436
437                                let param_builder =
438                                    nvenc::InitParamsBuilder::new(encode, width, height)
439                                        // .ptd(true)
440                                        .preset_guid(preset);
441
442                                let param_builder =
443                                    match cfg.max_framerate.as_numerator_denominator() {
444                                        Some((num, den)) => param_builder.set_framerate(num, den),
445                                        None => param_builder,
446                                    };
447
448                                let mut encoder_config =
449                                    encoder.get_encode_preset_config(encode, preset)?;
450                                encoder_config.set_rate_control_mode(RateControlMode::Vbr);
451                                if let Some(bitrate) = opts.bitrate.as_ref() {
452                                    encoder_config.set_average_bit_rate(bitrate * 1000);
453                                    encoder_config.set_max_bit_rate(bitrate * 1000);
454                                }
455
456                                let params =
457                                    param_builder.set_encode_config(encoder_config).build()?;
458
459                                match encoder.initialize(&params) {
460                                    Ok(()) => Ok(()),
461                                    Err(e) => {
462                                        tracing::error!(
463                                            "failed initializing nvenc with params: {:?}",
464                                            params
465                                        );
466                                        Err(e)
467                                    }
468                                }?;
469
470                                let input_buffers: Vec<InputBuffer<'lib>> = (0..num_bufs)
471                                    .map(|_| {
472                                        nvenc::Encoder::alloc_input_buffer(
473                                            &encoder, width, height, format,
474                                        )
475                                    })
476                                    .collect::<std::result::Result<Vec<_>, _>>()?;
477
478                                let output_buffers: Vec<_> = (0..num_bufs)
479                                    .map(|_| nvenc::Encoder::alloc_output_buffer(&encoder))
480                                    .collect::<std::result::Result<Vec<_>, _>>()?;
481
482                                let vram_buffers: Vec<IOBuffer<_, _>> = input_buffers
483                                    .into_iter()
484                                    .zip(output_buffers)
485                                    .map(|(i, o)| IOBuffer {
486                                        in_buf: i,
487                                        out_buf: o,
488                                    })
489                                    .collect();
490
491                                let vram_queue = nvenc::Queue::new(vram_buffers);
492
493                                opt_nv_h264_encoder = Some(NvEncoder {
494                                    encoder,
495                                    h264_parser: h264_parser.clone(),
496                                    // annex_b_reader,
497                                    vram_queue,
498                                    first_timestamp: timestamp,
499                                });
500                            }
501                            None => return Err(Error::NvencLibsNotLoaded),
502                        }
503                    }
504                };
505
506                let my_encoder = match cfg.codec {
507                    strand_cam_remote_control::Mp4Codec::H264RawStream => MyEncoder::CopyRawH264 {
508                        // metadata,
509                        h264_parser,
510                    },
511                    strand_cam_remote_control::Mp4Codec::H264LessAvc => {
512                        MyEncoder::LessH264(LessEncoderWrapper {
513                            encoder: Default::default(),
514                            h264_parser,
515                            first_timestamp: timestamp,
516                        })
517                    }
518                    #[cfg_attr(not(feature = "openh264"), expect(unused_variables))]
519                    strand_cam_remote_control::Mp4Codec::H264OpenH264(opts) => {
520                        #[cfg(feature = "openh264")]
521                        {
522                            let cfg = openh264::encoder::EncoderConfig::new()
523                                .debug(opts.debug())
524                                .skip_frames(opts.enable_skip_frame())
525                                .rate_control_mode(convert_openh264_rc_mode(
526                                    opts.rate_control_mode(),
527                                ))
528                                .bitrate(openh264::encoder::BitRate::from_bps(opts.bitrate_bps()));
529
530                            MyEncoder::OpenH264(Box::new(OpenH264Encoder {
531                                encoder: openh264::encoder::Encoder::with_api_config(
532                                    openh264::OpenH264API::from_source(),
533                                    cfg,
534                                )?,
535                                h264_parser,
536                                first_timestamp: timestamp,
537                            }))
538                        }
539                        #[cfg(not(feature = "openh264"))]
540                        {
541                            // We should never get here.
542                            panic!("No Open H264 support at compilation time.");
543                        }
544                    }
545                    #[cfg(feature = "nv-encode")]
546                    strand_cam_remote_control::Mp4Codec::H264NvEnc(_) => {
547                        let enc = opt_nv_h264_encoder.unwrap();
548                        MyEncoder::Nvidia(enc)
549                    }
550                    #[cfg(not(feature = "nv-encode"))]
551                    strand_cam_remote_control::Mp4Codec::H264NvEnc(_) => {
552                        return Err(Error::NoNvencCompiledError);
553                    }
554                };
555
556                let inner = RecordingStateInner {
557                    first_timestamp: timestamp,
558                    previous_timestamp: timestamp,
559                    interval_for_limiting_fps: chrono::Duration::from_std(
560                        cfg.max_framerate.interval(),
561                    )
562                    .unwrap(),
563                    trim_width: width,
564                    trim_height: height,
565                };
566
567                let mut state = RecordingState {
568                    mp4_segment: MaybeMp4Writer::Starting(fd),
569                    my_encoder,
570                    inner: Some(inner),
571                };
572
573                write_dynamic_frame(&mut state, frame, timestamp)?;
574
575                self.inner = Some(WriteState::Recording(Box::new(state)));
576
577                Ok(())
578            }
579            Some(WriteState::Recording(mut state)) => {
580                let frame = if let Some(state_inner) = &mut state.inner {
581                    let interval = timestamp.signed_duration_since(state_inner.previous_timestamp);
582                    if interval >= state_inner.interval_for_limiting_fps {
583                        let frame = frame
584                            .roi(0, 0, state_inner.trim_width, state_inner.trim_height)
585                            .unwrap();
586                        debug!("Saving frame at {}: interval {}", timestamp, interval);
587
588                        state_inner.previous_timestamp = timestamp;
589                        Some(frame)
590                    } else {
591                        debug!(
592                            "Not saving frame at {}: interval {} too small",
593                            timestamp, interval
594                        );
595                        None
596                    }
597                } else {
598                    return inconsistent_state_err();
599                };
600                if let Some(frame) = frame {
601                    write_dynamic_frame(&mut state, &frame, timestamp)?;
602                }
603                self.inner = Some(WriteState::Recording(state));
604
605                Ok(())
606            }
607            Some(WriteState::Finished) => {
608                self.inner = Some(WriteState::Finished);
609                Err(Error::FileAlreadyClosed {})
610            }
611
612            None => Err(Error::InconsistentState {}),
613        }
614    }
615
616    /// Finish writing the MP4 file.
617    ///
618    /// Calling this allows any errors to be caught explicitly. Otherwise,
619    /// the MP4 file will be finished when the writer is dropped. In that case,
620    /// any errors will result in a panic.
621    pub fn finish(&mut self) -> Result<()> {
622        let inner = self.inner.take();
623        match inner {
624            Some(WriteState::Configured(_)) => {
625                // no frames written.
626                self.inner = Some(WriteState::Finished);
627                Ok(())
628            }
629            #[cfg_attr(not(feature = "nv-encode"), expect(unused_mut))]
630            Some(WriteState::Recording(mut state)) => {
631                match state.my_encoder {
632                    MyEncoder::CopyRawH264 { h264_parser: _ } | MyEncoder::LessH264(_) => { /* nothing to do */
633                    }
634                    #[cfg(feature = "openh264")]
635                    MyEncoder::OpenH264(_encoder) => { /* nothing to do */ }
636                    #[cfg(not(feature = "nv-encode"))]
637                    MyEncoder::NoNvidia(_) => {
638                        return Err(Error::NoNvencCompiledError);
639                    }
640                    #[cfg(feature = "nv-encode")]
641                    MyEncoder::Nvidia(ref mut nv_encoder) => {
642                        nv_encoder.encoder.end_stream()?;
643                        // Now done with all frames, drain the pending data.
644                        loop {
645                            let sample = match nv_encoder.vram_queue.get_pending() {
646                                None => break,
647                                Some(iobuf) => {
648                                    // scope for locked output buffer
649                                    let outbuf = iobuf.out_buf.lock()?;
650                                    nv_outbuf_to_sample(outbuf)
651                                }
652                            };
653                            if let Some(state_inner) = state.inner.as_ref() {
654                                nv_encoder.inner_save_data(
655                                    &mut state.mp4_segment,
656                                    sample,
657                                    state_inner.trim_width,
658                                    state_inner.trim_height,
659                                )?;
660                            } else {
661                                return Err(Error::InconsistentState {});
662                            }
663                        }
664                    }
665                }
666
667                if let MaybeMp4Writer::Mp4Writer(mut mp4_writer) = state.mp4_segment {
668                    mp4_writer.write_end()?;
669                }
670
671                trace!("Finalized video.");
672                self.inner = Some(WriteState::Finished);
673                Ok(())
674            }
675            Some(WriteState::Finished) => {
676                self.inner = Some(WriteState::Finished);
677                Err(Error::FileAlreadyClosed {})
678            }
679            None => Err(Error::InconsistentState {}),
680        }
681    }
682}
683
684#[cfg(feature = "nv-encode")]
685fn nv_outbuf_to_sample(outbuf: dynlink_nvidia_encode::api::LockedOutputBuffer) -> EbspNals {
686    let nals = h264_annexb_split(outbuf.mem()).collect();
687
688    EbspNals {
689        pts: chrono::Duration::from_std(*outbuf.pts()).unwrap(),
690        mp4_sample_start_time: outbuf.output_time_stamp(),
691        is_keyframe: outbuf.is_keyframe(),
692        nals,
693    }
694}
695
696impl<T> Drop for Mp4Writer<'_, T>
697where
698    T: std::io::Write + std::io::Seek,
699{
700    fn drop(&mut self) {
701        match &self.inner {
702            // Happy path when .finish() already called.
703            Some(WriteState::Finished) => {}
704            // Error happened in self.write().
705            None => {}
706            // When .finished() not already called.
707            Some(_) if !std::thread::panicking() => {
708                // We are being dropping, so finish the file.
709                self.finish().unwrap()
710            }
711            // We are being dropped, but we are unwinding, so just leave
712            // the file as-is. (Should we even truncate it?)
713            Some(_) => {}
714        }
715    }
716}
717
718fn write_dynamic_frame<T>(
719    state: &mut RecordingState<'_, T>,
720    raw_frame: &DynamicFrame,
721    timestamp: chrono::DateTime<chrono::Local>,
722) -> Result<()>
723where
724    T: std::io::Write + std::io::Seek,
725{
726    match (&mut state.my_encoder, &state.inner) {
727        (MyEncoder::CopyRawH264 { h264_parser: _ }, _) => {
728            return Err(Error::RawH264CopyCannotEncodeFrame {});
729        }
730        (MyEncoder::LessH264(encoder), Some(state_inner)) => {
731            let nals = encoder.encoder.encode_dynamic_to_nal_units(raw_frame)?;
732
733            let is_keyframe = true;
734
735            let pts = timestamp - encoder.first_timestamp;
736            let mp4_sample_start_time = dur2raw(&pts.to_std().unwrap());
737
738            let sample = EbspNals {
739                pts,
740                mp4_sample_start_time,
741                is_keyframe,
742                nals,
743            };
744
745            encoder.inner_save_data(
746                &mut state.mp4_segment,
747                sample,
748                state_inner.trim_width,
749                state_inner.trim_height,
750            )?;
751        }
752        #[cfg(feature = "openh264")]
753        (MyEncoder::OpenH264(encoder), Some(state_inner)) => {
754            // todo: bitrate, keyframes, timestamp check and duration finding.
755
756            let y4m =
757                y4m_writer::encode_y4m_dynamic_frame(raw_frame, y4m::Colorspace::C420paldv, None)?;
758
759            let encoded = encoder.encoder.encode(&YUVData::from(y4m)).unwrap();
760
761            use openh264::encoder::FrameType;
762            let is_keyframe =
763                (encoded.frame_type() == FrameType::IDR) | (encoded.frame_type() == FrameType::I);
764
765            // todo: preallocate and keep buffer available by using write_vec
766            let annex_b_data = encoded.to_vec();
767
768            let nals = h264_annexb_split(&annex_b_data).collect();
769
770            let pts = timestamp - encoder.first_timestamp;
771            let mp4_sample_start_time = dur2raw(&pts.to_std().unwrap());
772
773            let sample = EbspNals {
774                pts,
775                mp4_sample_start_time,
776                is_keyframe,
777                nals,
778            };
779
780            encoder.inner_save_data(
781                &mut state.mp4_segment,
782                sample,
783                state_inner.trim_width,
784                state_inner.trim_height,
785            )?;
786        }
787        #[cfg(not(feature = "nv-encode"))]
788        (MyEncoder::NoNvidia(_), Some(_)) => {
789            return Err(Error::NoNvencCompiledError);
790        }
791        #[cfg(feature = "nv-encode")]
792        (MyEncoder::Nvidia(nv_encoder), Some(state_inner)) => {
793            let vram_buf: &mut IOBuffer<_, _> = match nv_encoder.vram_queue.get_available() {
794                Some(iobuf) => iobuf,
795                None => {
796                    let sample = {
797                        let iobuf = nv_encoder.vram_queue.get_pending().expect("get pending");
798                        // scope for locked output buffer
799                        let outbuf = iobuf.out_buf.lock()?;
800                        nv_outbuf_to_sample(outbuf)
801                    };
802                    nv_encoder.inner_save_data(
803                        &mut state.mp4_segment,
804                        sample,
805                        state_inner.trim_width,
806                        state_inner.trim_height,
807                    )?;
808                    nv_encoder
809                        .vram_queue
810                        .get_available()
811                        .expect("get available")
812                }
813            };
814
815            // Now we have an "available" buffer in the encoder.
816
817            let pitch = {
818                // Scope for locked input buffer.
819                let mut inbuf = vram_buf.in_buf.lock()?;
820                let dest_stride = inbuf.pitch();
821
822                let mut dest = ImageRefMut::<pixel_format::NV12>::new(
823                    raw_frame.width(),
824                    raw_frame.height(),
825                    dest_stride,
826                    inbuf.mem_mut(),
827                )
828                .unwrap();
829
830                raw_frame.into_pixel_format_dest(&mut dest)?;
831                // Now vram_buf.in_buf has the nv12 encoded data.
832                dest_stride
833            };
834
835            let elapsed = timestamp.signed_duration_since(state_inner.first_timestamp);
836            let pts = elapsed.to_std().unwrap();
837
838            nv_encoder
839                .encoder
840                .encode_picture(&vram_buf.in_buf, &vram_buf.out_buf, pitch, pts)?;
841        }
842        (_encoder, None) => {
843            return inconsistent_state_err();
844        }
845    }
846    Ok(())
847}
848
849enum WriteState<'lib, T>
850where
851    T: std::io::Write + std::io::Seek,
852{
853    Configured(Box<(T, Mp4RecordingConfig, H264Parser)>),
854    Recording(Box<RecordingState<'lib, T>>),
855    Finished,
856}
857
858struct RecordingState<'lib, T>
859where
860    T: std::io::Write + std::io::Seek,
861{
862    mp4_segment: MaybeMp4Writer<T>,
863    my_encoder: MyEncoder<'lib>,
864    inner: Option<RecordingStateInner>,
865}
866
867struct RecordingStateInner {
868    #[cfg_attr(not(feature = "nv-encode"), expect(unused))]
869    first_timestamp: chrono::DateTime<chrono::Local>,
870    previous_timestamp: chrono::DateTime<chrono::Local>,
871    /// limits the maximum framerate
872    interval_for_limiting_fps: chrono::Duration,
873    trim_width: u32,
874    trim_height: u32,
875}
876
877struct LessEncoderWrapper {
878    encoder: less_avc_wrapper::WrappedLessEncoder,
879    h264_parser: H264Parser,
880    first_timestamp: chrono::DateTime<chrono::Local>,
881}
882
883impl LessEncoderWrapper {
884    fn compute_local_timestamp(&self, sample: &EbspNals) -> chrono::DateTime<chrono::Local> {
885        self.first_timestamp + sample.pts
886    }
887    fn inner_save_data<T>(
888        &mut self,
889        mp4_segment: &mut MaybeMp4Writer<T>,
890        sample: EbspNals,
891        trim_width: u32,
892        trim_height: u32,
893    ) -> Result<()>
894    where
895        T: std::io::Write + std::io::Seek,
896    {
897        let local_timestamp = self.compute_local_timestamp(&sample);
898        self.h264_parser
899            .push_nals(sample, Some(local_timestamp), None, 0);
900        let sps = self.h264_parser.sps().unwrap();
901        let pps = self.h264_parser.pps().unwrap();
902
903        let mut mp4_writer = match std::mem::replace(mp4_segment, MaybeMp4Writer::Nothing) {
904            MaybeMp4Writer::Mp4Writer(mp4_writer) => mp4_writer,
905            MaybeMp4Writer::Starting(fd) => {
906                start_mp4_writer(fd, sps, pps, trim_width, trim_height)?
907            }
908            MaybeMp4Writer::Nothing => {
909                panic!("inconsistent state");
910            }
911        };
912
913        let avcc_sample = self.h264_parser.avcc_sample().unwrap();
914        mp4_writer.write_sample(TRACK_ID, &avcc_sample)?;
915
916        *mp4_segment = MaybeMp4Writer::Mp4Writer(mp4_writer);
917
918        Ok(())
919    }
920}
921
922#[cfg(feature = "nv-encode")]
923struct NvEncoder<'lib> {
924    encoder: Rc<nvenc::Encoder<'lib>>,
925    h264_parser: H264Parser,
926    vram_queue: nvenc::Queue<IOBuffer<InputBuffer<'lib>, OutputBuffer<'lib>>>,
927    first_timestamp: chrono::DateTime<chrono::Local>,
928}
929
930#[cfg(feature = "nv-encode")]
931impl NvEncoder<'_> {
932    fn compute_local_timestamp(&self, sample: &EbspNals) -> chrono::DateTime<chrono::Local> {
933        self.first_timestamp + sample.pts
934    }
935    fn inner_save_data<T>(
936        &mut self,
937        mp4_segment: &mut MaybeMp4Writer<T>,
938        sample: EbspNals,
939        trim_width: u32,
940        trim_height: u32,
941    ) -> Result<()>
942    where
943        T: std::io::Write + std::io::Seek,
944    {
945        let local_timestamp = self.compute_local_timestamp(&sample);
946        self.h264_parser
947            .push_nals(sample, Some(local_timestamp), None, 0);
948        let mut mp4_writer = match std::mem::replace(mp4_segment, MaybeMp4Writer::Nothing) {
949            MaybeMp4Writer::Mp4Writer(mp4_writer) => mp4_writer,
950            MaybeMp4Writer::Starting(fd) => {
951                let sps = self.h264_parser.sps().unwrap();
952                let pps = self.h264_parser.pps().unwrap();
953                start_mp4_writer(fd, sps, pps, trim_width, trim_height)?
954            }
955            MaybeMp4Writer::Nothing => {
956                panic!("inconsistent state");
957            }
958        };
959
960        let avcc_sample = self.h264_parser.avcc_sample().unwrap();
961        mp4_writer.write_sample(TRACK_ID, &avcc_sample)?;
962
963        *mp4_segment = MaybeMp4Writer::Mp4Writer(mp4_writer);
964
965        Ok(())
966    }
967}
968
969fn start_mp4_writer<T>(
970    fd: T,
971    sps: &[u8],
972    pps: &[u8],
973    trim_width: u32,
974    trim_height: u32,
975) -> Result<mp4::Mp4Writer<T>>
976where
977    T: std::io::Write + std::io::Seek,
978{
979    let mp4_config = mp4::Mp4Config {
980        major_brand: str::parse("isom").unwrap(),
981        minor_version: 512,
982        compatible_brands: vec![str::parse("isom").unwrap()],
983        // This is `movie_timescale`, the number of
984        // time units that pass in one second.
985        timescale: MOVIE_TIMESCALE,
986    };
987
988    let mut mp4_writer = mp4::Mp4Writer::write_start(fd, &mp4_config)?;
989
990    let media_conf = mp4::MediaConfig::AvcConfig(mp4::AvcConfig {
991        width: trim_width.try_into().unwrap(),
992        height: trim_height.try_into().unwrap(),
993        seq_param_set: sps.to_vec(),
994        pic_param_set: pps.to_vec(),
995    });
996
997    let track_conf = mp4::TrackConfig {
998        track_type: mp4::TrackType::Video,
999        timescale: MOVIE_TIMESCALE,
1000        language: String::from("eng"),
1001        media_conf,
1002    };
1003
1004    mp4_writer.add_track(&track_conf)?;
1005    Ok(mp4_writer)
1006}
1007
1008#[cfg(feature = "openh264")]
1009struct OpenH264Encoder {
1010    encoder: openh264::encoder::Encoder,
1011    h264_parser: H264Parser,
1012    first_timestamp: chrono::DateTime<chrono::Local>,
1013}
1014
1015#[cfg(feature = "openh264")]
1016impl OpenH264Encoder {
1017    fn compute_local_timestamp(&self, sample: &EbspNals) -> chrono::DateTime<chrono::Local> {
1018        self.first_timestamp + sample.pts
1019    }
1020    fn inner_save_data<T>(
1021        &mut self,
1022        mp4_segment: &mut MaybeMp4Writer<T>,
1023        sample: EbspNals,
1024        trim_width: u32,
1025        trim_height: u32,
1026    ) -> Result<()>
1027    where
1028        T: std::io::Write + std::io::Seek,
1029    {
1030        let local_timestamp = self.compute_local_timestamp(&sample);
1031        self.h264_parser
1032            .push_nals(sample, Some(local_timestamp), None, 0);
1033        let sps = self.h264_parser.sps().unwrap();
1034        let pps = self.h264_parser.pps().unwrap();
1035
1036        let mut mp4_writer = match std::mem::replace(mp4_segment, MaybeMp4Writer::Nothing) {
1037            MaybeMp4Writer::Mp4Writer(mp4_writer) => mp4_writer,
1038            MaybeMp4Writer::Starting(fd) => {
1039                start_mp4_writer(fd, sps, pps, trim_width, trim_height)?
1040            }
1041            MaybeMp4Writer::Nothing => {
1042                panic!("inconsistent state");
1043            }
1044        };
1045
1046        let avcc_sample = self.h264_parser.avcc_sample().unwrap();
1047        mp4_writer.write_sample(TRACK_ID, &avcc_sample)?;
1048
1049        *mp4_segment = MaybeMp4Writer::Mp4Writer(mp4_writer);
1050
1051        Ok(())
1052    }
1053}
1054
1055pub struct IOBuffer<I, O> {
1056    pub in_buf: I,
1057    pub out_buf: O,
1058}
1059
1060enum MaybeMp4Writer<T>
1061where
1062    T: std::io::Write + std::io::Seek,
1063{
1064    Nothing,
1065    Starting(T),
1066    Mp4Writer(mp4::Mp4Writer<T>),
1067}
1068
1069#[derive(Clone)]
1070struct H264Parser {
1071    sps: Option<Vec<u8>>,
1072    pps: Option<Vec<u8>>,
1073    previous_stamp: Option<u64>,
1074    /// stores MP4 sample until written
1075    last_sample: Option<ParsedH264Frame>,
1076    first_frame_done: bool,
1077    h264_metadata: Option<H264Metadata>,
1078}
1079
1080impl H264Parser {
1081    /// Create a new [H264Parser].
1082    fn new(h264_metadata: Option<H264Metadata>) -> Self {
1083        Self {
1084            sps: None,
1085            pps: None,
1086            previous_stamp: None,
1087            last_sample: None,
1088            first_frame_done: false,
1089            h264_metadata,
1090        }
1091    }
1092    fn sps(&self) -> Option<&[u8]> {
1093        self.sps.as_deref()
1094    }
1095    fn pps(&self) -> Option<&[u8]> {
1096        self.pps.as_deref()
1097    }
1098
1099    fn push_nals(
1100        &mut self,
1101        nals: EbspNals,
1102        mut precision_timestamp: Option<chrono::DateTime<chrono::Local>>,
1103        explicit_duration: Option<u64>,
1104        rendering_offset: i32,
1105    ) {
1106        // We assume that sample contains one or more compete NAL units and
1107        // starts with a NAL unit. Furthermore, we assume the start bytes can
1108        // only be [0x00, 0x00, 0x00, 0x01]. This is not a real Annex B parser
1109        // because of these assumptions but rather tuned to the output of
1110        // less-avc, nvenc and openh264 as we use them.
1111
1112        let mut all_avcc_nal_units: Vec<u8> = Vec::with_capacity(nals.annex_b_size() + 32);
1113
1114        if !self.first_frame_done {
1115            use less_avc::{nal_unit::*, sei::UserDataUnregistered};
1116
1117            if let Some(h264_metadata) = &self.h264_metadata {
1118                // Update the `creation_time` field of the metadata with the
1119                // timestamp of the first frame.
1120                let h264_metadata_updated = if let Some(ts) = precision_timestamp {
1121                    let creation_time = ts.into();
1122                    H264Metadata {
1123                        creation_time,
1124                        ..h264_metadata.clone()
1125                    }
1126                } else {
1127                    h264_metadata.clone()
1128                };
1129
1130                let msg = serde_json::to_vec(&h264_metadata_updated).unwrap();
1131
1132                let payload = UserDataUnregistered::new(H264_METADATA_UUID, msg);
1133
1134                use less_avc::sei::SupplementalEnhancementInformation;
1135                let rbsp_data =
1136                    SupplementalEnhancementInformation::UserDataUnregistered(payload).to_rbsp();
1137                let annex_b_data = NalUnit::new(
1138                    less_avc::nal_unit::NalRefIdc::Zero,
1139                    less_avc::nal_unit::NalUnitType::SupplementalEnhancementInformation,
1140                    rbsp_data,
1141                )
1142                .to_annex_b_data();
1143
1144                const ANNEX_B_START: &[u8] = &[0x00, 0x00, 0x00, 0x01];
1145                debug_assert_eq!(&annex_b_data[..4], ANNEX_B_START);
1146
1147                // Don't use the start code from Annex B but do use the raw EBSP
1148                // NALU.
1149                all_avcc_nal_units.extend(buf_to_avcc(&annex_b_data[4..]));
1150            }
1151
1152            self.first_frame_done = true;
1153        }
1154
1155        // Split into Encapsulated Byte Sequence Payload (EBSP) message
1156        for ebsp_msg in nals.nals.iter() {
1157            let mut is_this_sps_or_pps = false;
1158            if !ebsp_msg.is_empty() {
1159                let code = ebsp_msg[0];
1160                match code {
1161                    0x67 => {
1162                        self.sps = Some(ebsp_msg[..].to_vec());
1163                        is_this_sps_or_pps = true;
1164                    }
1165                    0x68 => {
1166                        self.pps = Some(ebsp_msg[..].to_vec());
1167                        is_this_sps_or_pps = true;
1168                    }
1169                    _ => {}
1170                }
1171                if !is_this_sps_or_pps {
1172                    // Insert our timeestamp data, but only if we did not write
1173                    // an SPS or PPS because we do not want to write our
1174                    // timestamp prior to SPS or PPS.
1175                    if let Some(ts) = precision_timestamp.take() {
1176                        let mut rbsp_msg = [0u8; 32];
1177                        rbsp_msg[0] = 0x06; // code 6 - SEI
1178                        rbsp_msg[1] = 0x05; // header type: UserDataUnregistered
1179                        rbsp_msg[2] = 28; // size
1180                        timestamp_to_sei_payload(ts.into(), &mut rbsp_msg[3..31]);
1181                        rbsp_msg[31] = 0x80; // rbsp_trailing_bits
1182
1183                        // Create new NAL unit for precision timestamp. In
1184                        // theory we should ensure that this does not have start
1185                        // code bytes and thus we should convert from RBSP to
1186                        // EBSP. However, the standard ensures that there is no
1187                        // need for encoding and thus the RBSP is the EBSP for
1188                        // this case.
1189                        let ebsp_msg = rbsp_msg;
1190                        all_avcc_nal_units.extend(buf_to_avcc(&ebsp_msg[..]));
1191                    }
1192                }
1193                all_avcc_nal_units.extend(buf_to_avcc(ebsp_msg));
1194            }
1195        }
1196
1197        if self
1198            .last_sample
1199            .replace(ParsedH264Frame {
1200                mp4_sample_start_time: nals.mp4_sample_start_time,
1201                is_keyframe: nals.is_keyframe,
1202                avcc_buf: all_avcc_nal_units,
1203                explicit_duration,
1204                rendering_offset,
1205            })
1206            .is_some()
1207        {
1208            eprintln!("unused NAL unit");
1209        };
1210    }
1211
1212    fn avcc_sample(&mut self) -> Option<mp4::Mp4Sample> {
1213        let explicit_duration = self.last_sample.as_ref().and_then(|f| f.explicit_duration);
1214        let mut sample = self.last_sample.take().map(parsed_to_mp4_sample);
1215        if let Some(ref mut s) = sample {
1216            match explicit_duration {
1217                // Timing passed through from a source file: use the sample's own
1218                // duration (stts) directly. The composition offset (ctts) is
1219                // already set on the sample, so reordered streams stay correct.
1220                Some(dur) => {
1221                    s.duration = dur.try_into().unwrap();
1222                }
1223                // Otherwise derive the duration from the delta between
1224                // consecutive presentation times. This requires monotonic
1225                // start_times and cannot represent reordered streams.
1226                None => {
1227                    if let Some(prev) = self.previous_stamp {
1228                        // FIXME: This will be off by one frame because it calculates duration
1229                        // of this frame as delta between previous frame and this frame. (It
1230                        // should be delta between this frame and next frame.)
1231                        let dur = s.start_time - prev;
1232                        s.duration = dur.try_into().unwrap();
1233                    }
1234                    self.previous_stamp = Some(s.start_time);
1235                }
1236            }
1237        }
1238        // Note: as far as I can tell, as of version 0.13.0, the mp4 crate does not
1239        // use `start_time` for writing the sample. (So we have gone to the trouble
1240        // of ensuring it has a good PTS value but it is ignored.)
1241        sample
1242    }
1243}
1244
1245fn parsed_to_mp4_sample(orig: ParsedH264Frame) -> mp4::Mp4Sample {
1246    let bytes = orig.avcc_buf.into();
1247
1248    mp4::Mp4Sample {
1249        start_time: orig.mp4_sample_start_time,
1250        duration: 0,
1251        rendering_offset: orig.rendering_offset,
1252        is_sync: orig.is_keyframe,
1253        bytes,
1254    }
1255}
1256
1257/// Encapsulated NAL Units
1258///
1259/// Stored neither in AnnexB nor AVCC format, just as buffers of encapsulated
1260/// bytes. A single MP4 sample can be composed of multiple such H264 NAL units.
1261struct EbspNals {
1262    pts: chrono::Duration,
1263    /// in units of `movie_timescale`
1264    mp4_sample_start_time: u64,
1265    is_keyframe: bool,
1266    nals: Vec<Vec<u8>>,
1267}
1268
1269impl EbspNals {
1270    fn annex_b_size(&self) -> usize {
1271        let raw_sz: usize = self.nals.iter().map(|x| x.len()).sum();
1272        raw_sz + 4 * self.nals.len()
1273    }
1274}
1275
1276#[derive(Clone)]
1277struct ParsedH264Frame {
1278    /// in units of `movie_timescale`
1279    mp4_sample_start_time: u64,
1280    is_keyframe: bool,
1281    avcc_buf: Vec<u8>,
1282    /// When `Some`, the exact sample duration (in `movie_timescale` units) to
1283    /// use, instead of deriving it from consecutive start-time deltas. Used when
1284    /// passing through timing (stts) from a source file.
1285    explicit_duration: Option<u64>,
1286    /// Composition time offset (ctts), in `movie_timescale` units. Non-zero for
1287    /// reordered (B-frame) streams, where presentation order != decode order.
1288    rendering_offset: i32,
1289}
1290
1291fn buf_to_avcc(nal: &[u8]) -> Vec<u8> {
1292    let sz: u32 = nal.len().try_into().unwrap();
1293    let mut result = vec![0u8; nal.len() + 4];
1294    result[0..4].copy_from_slice(&sz.to_be_bytes());
1295    result[4..].copy_from_slice(nal);
1296    result
1297}
1298
1299#[cfg(feature = "openh264")]
1300struct YUVData {
1301    width: usize,
1302    height: usize,
1303    data: Vec<u8>,
1304    y_stride: usize,
1305    u_stride: usize,
1306    v_stride: usize,
1307}
1308
1309// fn print_buf(buf: &[u8]) {
1310//     use sha2::Digest;
1311//     let digest = sha2::Sha256::digest(&buf);
1312//     print!("buf: (chk {:x}, len {}) ", digest, buf.len());
1313
1314//     let blen = buf.len().min(10);
1315//     for b in &buf[0..blen] {
1316//         print!("{:x} ", b);
1317//     }
1318
1319//     println!("");
1320// }
1321
1322// #[cfg(feature = "openh264")]
1323// impl std::fmt::Debug for YUVData {
1324//     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
1325//         use sha2::Digest;
1326//         let digest = sha2::Sha256::digest(&self.data);
1327//         write!(
1328//             f,
1329//             "YUVData {{ width: {}, height: {}, data: (chk {:x}, len {}) ",
1330//             self.width,
1331//             self.height,
1332//             digest,
1333//             self.data.len()
1334//         )?;
1335
1336//         let blen = self.data.len().min(10);
1337//         for b in &self.data[0..blen] {
1338//             write!(f, "{:x} ", b)?;
1339//         }
1340
1341//         write!(f, ".. }}")
1342//     }
1343// }
1344
1345#[cfg(feature = "openh264")]
1346impl From<y4m_writer::Y4MFrame> for YUVData {
1347    fn from(orig: y4m_writer::Y4MFrame) -> YUVData {
1348        let width = orig.width.try_into().unwrap();
1349        let height = orig.height.try_into().unwrap();
1350        let y_stride = orig.y_stride.try_into().unwrap();
1351        let u_stride = orig.u_stride();
1352        let v_stride = orig.v_stride();
1353        Self {
1354            width,
1355            height,
1356            data: orig.into_data(),
1357            y_stride,
1358            u_stride,
1359            v_stride,
1360        }
1361    }
1362}
1363
1364#[cfg(feature = "openh264")]
1365impl YUVData {
1366    #[inline]
1367    fn u_start(&self) -> usize {
1368        self.height * self.y_stride
1369    }
1370    #[inline]
1371    fn v_start(&self) -> usize {
1372        self.u_start() + self.height / 2 * self.u_stride
1373    }
1374    #[inline]
1375    fn v_end(&self) -> usize {
1376        self.v_start() + self.height / 2 * self.u_stride
1377    }
1378}
1379
1380#[cfg(feature = "openh264")]
1381impl openh264::formats::YUVSource for YUVData {
1382    fn dimensions(&self) -> (usize, usize) {
1383        (self.width, self.height)
1384    }
1385    fn y(&self) -> &[u8] {
1386        &self.data[0..self.u_start()]
1387    }
1388    fn u(&self) -> &[u8] {
1389        &self.data[self.u_start()..self.v_start()]
1390    }
1391    fn v(&self) -> &[u8] {
1392        &self.data[self.v_start()..self.v_end()]
1393    }
1394    fn strides(&self) -> (usize, usize, usize) {
1395        (self.y_stride, self.u_stride, self.v_stride)
1396    }
1397}
1398
1399fn dur2raw(dur: &std::time::Duration) -> u64 {
1400    (dur.as_secs_f64() * MOVIE_TIMESCALE as f64).round() as u64
1401}
1402
1403/// Convert a (possibly negative) composition time offset into raw
1404/// `MOVIE_TIMESCALE` units for the `ctts` box.
1405fn composition_offset_to_raw(offset: chrono::Duration) -> i32 {
1406    let nanos = offset.num_nanoseconds().unwrap_or(0) as i128;
1407    ((nanos * MOVIE_TIMESCALE as i128) / 1_000_000_000i128) as i32
1408}
1409
1410fn timestamp_to_sei_payload(timestamp: chrono::DateTime<chrono::Utc>, payload: &mut [u8]) {
1411    assert_eq!(payload.len(), 28);
1412    let precision_time_stamp = timestamp.timestamp_micros();
1413
1414    let precision_time_stamp_bytes: [u8; 8] = precision_time_stamp.to_be_bytes();
1415
1416    payload[0..16].copy_from_slice(b"MISPmicrosectime"); // uuid_iso_iec_11578
1417
1418    payload[16] = 0x1F; // Time Stamp Status byte from MISB Standard 0603
1419
1420    // The standard has 0xFF present after every two bytes as "Start Code
1421    // Emulation Prevention". This means that the raw byte sequence is identical
1422    // to the encoded byte sequence as there is nothing to encode.
1423    payload[17..19].copy_from_slice(&precision_time_stamp_bytes[0..2]);
1424    payload[19] = 0xff;
1425    payload[20..22].copy_from_slice(&precision_time_stamp_bytes[2..4]);
1426    payload[22] = 0xff;
1427    payload[23..25].copy_from_slice(&precision_time_stamp_bytes[4..6]);
1428    payload[25] = 0xff;
1429    payload[26..28].copy_from_slice(&precision_time_stamp_bytes[6..8]);
1430}
1431
1432#[cfg(feature = "openh264")]
1433fn convert_openh264_rc_mode(
1434    orig: strand_cam_remote_control::OpenH264RateControlMode,
1435) -> openh264::encoder::RateControlMode {
1436    use openh264::encoder::RateControlMode::*;
1437    use strand_cam_remote_control::OpenH264RateControlMode as mode;
1438    match orig {
1439        mode::Quality => Quality,
1440        mode::Bitrate => Bitrate,
1441        mode::Bufferbased => Bufferbased,
1442        mode::Timestamp => Timestamp,
1443        mode::Off => Off,
1444    }
1445}
1446
1447struct NalAvccBufIter<'a> {
1448    cur_buf: &'a [u8],
1449}
1450
1451impl<'a> Iterator for NalAvccBufIter<'a> {
1452    type Item = Result<&'a [u8]>;
1453    fn next(&mut self) -> Option<Self::Item> {
1454        if self.cur_buf.is_empty() {
1455            return None;
1456        }
1457        if self.cur_buf.len() < 4 {
1458            return Some(Err(Error::BadInputData {}));
1459        }
1460        let bytes: [u8; 4] = self.cur_buf[0..4].try_into().unwrap();
1461        let nal_unit_payload_len = usize::try_from(u32::from_be_bytes(bytes)).unwrap();
1462        let nal_ebsp_bytes = &self.cur_buf[4..4 + nal_unit_payload_len];
1463        if nal_ebsp_bytes.len() != nal_unit_payload_len {
1464            return Some(Err(Error::BadInputData {}));
1465        }
1466        self.cur_buf = &self.cur_buf[4 + nal_unit_payload_len..];
1467        Some(Ok(nal_ebsp_bytes))
1468    }
1469}
1470
1471fn iter_avcc_bufs(buf: &[u8]) -> NalAvccBufIter<'_> {
1472    NalAvccBufIter { cur_buf: buf }
1473}
1474
1475/// parse h264 NAL unit and return if it is an IDR frame
1476fn parse_h264_is_idr_frame(data: &frame_source::H264EncodingVariant) -> Result<bool> {
1477    use h264_reader::nal::{Nal, RefNal, UnitType};
1478    use h264_reader::push::NalInterest;
1479    let mut calls = Vec::new();
1480    match data {
1481        frame_source::H264EncodingVariant::Avcc(buf) => {
1482            let nal_iter = iter_avcc_bufs(buf);
1483            for nal_ebsp_bytes in nal_iter {
1484                let nal_ebsp_bytes = nal_ebsp_bytes?;
1485                let nal = RefNal::new(nal_ebsp_bytes, &[], true);
1486                let nal_unit_type = nal.header().unwrap().nal_unit_type();
1487                calls.push(nal_unit_type);
1488            }
1489        }
1490        frame_source::H264EncodingVariant::AnnexB(buf) => {
1491            use h264_reader::annexb::AnnexBReader;
1492            let mut reader = AnnexBReader::accumulate(|nal: RefNal<'_>| {
1493                let nal_unit_type = nal.header().unwrap().nal_unit_type();
1494                calls.push(nal_unit_type);
1495                match nal_unit_type {
1496                    UnitType::SeqParameterSet => NalInterest::Buffer,
1497                    _ => NalInterest::Ignore,
1498                }
1499            });
1500            reader.push(&buf[..]);
1501        }
1502        frame_source::H264EncodingVariant::RawEbsp(nals) => {
1503            for nal_ebsp_bytes in nals.iter() {
1504                let nal = RefNal::new(nal_ebsp_bytes, &[], true);
1505                let nal_unit_type = nal.header().unwrap().nal_unit_type();
1506                calls.push(nal_unit_type);
1507            }
1508        }
1509    }
1510    let mut is_keyframe = None;
1511    for nal_unit_type in calls.into_iter() {
1512        match nal_unit_type {
1513            UnitType::SliceLayerWithoutPartitioningIdr => {
1514                if is_keyframe.is_some() {
1515                    // cannot have multiple frames
1516                    return Err(Error::BadInputData {});
1517                };
1518                is_keyframe = Some(true);
1519            }
1520            UnitType::SliceLayerWithoutPartitioningNonIdr => {
1521                if is_keyframe.is_some() {
1522                    // cannot have multiple frames
1523                    return Err(Error::BadInputData {});
1524                };
1525                is_keyframe = Some(false);
1526            }
1527            _ => {}
1528        }
1529    }
1530    is_keyframe.ok_or_else(|| Error::BadInputData {})
1531}
1532
1533fn inconsistent_state_err<T>() -> Result<T> {
1534    Err(Error::InconsistentState {})
1535}
1536
1537#[cfg(test)]
1538mod tests {
1539    use std::cell::{Cell, RefCell};
1540    use std::io::{Cursor, Seek, SeekFrom, Write};
1541    use std::rc::Rc;
1542
1543    use chrono::TimeDelta;
1544    use frame_source::H264EncodingVariant;
1545    use strand_cam_remote_control::{Mp4Codec, Mp4RecordingConfig};
1546
1547    use super::Mp4Writer;
1548
1549    #[derive(Clone, Default)]
1550    struct FailOnceWriter {
1551        cursor: Rc<RefCell<Cursor<Vec<u8>>>>,
1552        fail_next_write: Rc<Cell<bool>>,
1553    }
1554
1555    impl Write for FailOnceWriter {
1556        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1557            if self.fail_next_write.replace(false) {
1558                return Err(std::io::Error::other("injected write failure"));
1559            }
1560            self.cursor.borrow_mut().write(buf)
1561        }
1562
1563        fn flush(&mut self) -> std::io::Result<()> {
1564            self.cursor.borrow_mut().flush()
1565        }
1566    }
1567
1568    impl Seek for FailOnceWriter {
1569        fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
1570            self.cursor.borrow_mut().seek(pos)
1571        }
1572    }
1573
1574    #[test]
1575    fn finish_recovers_after_h264_sample_write_error() {
1576        let output = FailOnceWriter::default();
1577        let fail_next_write = output.fail_next_write.clone();
1578        let output_cursor = output.cursor.clone();
1579        let config = Mp4RecordingConfig {
1580            codec: Mp4Codec::H264RawStream,
1581            max_framerate: Default::default(),
1582            h264_metadata: None,
1583        };
1584        let mut writer = Mp4Writer::new(
1585            output,
1586            config,
1587            #[cfg(feature = "nv-encode")]
1588            None,
1589        )
1590        .unwrap();
1591        writer.set_first_sps_pps(
1592            Some(vec![0x67, 0x42, 0xc0, 0x0a]),
1593            Some(vec![0x68, 0xce, 0x0f, 0xc8]),
1594        );
1595
1596        let start = chrono::DateTime::<chrono::Local>::from(std::time::SystemTime::UNIX_EPOCH);
1597        writer
1598            .write_h264_buf(
1599                &H264EncodingVariant::RawEbsp(vec![vec![0x65, 0x88]]),
1600                16,
1601                16,
1602                start,
1603                start,
1604                false,
1605            )
1606            .unwrap();
1607
1608        fail_next_write.set(true);
1609        let write_error = writer
1610            .write_h264_buf(
1611                &H264EncodingVariant::RawEbsp(vec![vec![0x41, 0x9a]]),
1612                16,
1613                16,
1614                start + TimeDelta::seconds(1),
1615                start,
1616                false,
1617            )
1618            .unwrap_err();
1619        assert!(write_error.to_string().contains("injected write failure"));
1620
1621        writer.finish().unwrap();
1622        drop(writer);
1623
1624        let output = output_cursor.borrow().get_ref().clone();
1625        let output_len = output.len() as u64;
1626        let reader = mp4::Mp4Reader::read_header(Cursor::new(output), output_len).unwrap();
1627        assert_eq!(reader.sample_count(super::TRACK_ID).unwrap(), 2);
1628    }
1629}