libfreemkv 0.31.5

Open source raw disc access library for optical drives
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
//! HEVC (H.265) elementary stream muxer — Annex B byte stream.
//!
//! Consumes [`PesFrame`](crate::pes::PesFrame)s for a single video track
//! and writes them as a raw `.hevc` / `.h265` Annex B byte stream:
//! `00 00 00 01 | NAL_unit | 00 00 00 01 | NAL_unit | …` with no
//! container framing.
//!
//! On the first frame the muxer emits the codec_private's VPS, SPS, PPS
//! (parsed from a `HEVCDecoderConfigurationRecord` in
//! `length-prefixed-in-hvcC` form), then converts each PES frame's
//! length-prefixed NAL units to Annex B and writes them.
//!
//! Sequential-only — no Cues, no backpatch. Target sink is any
//! [`SequentialSink`](crate::io::sink::SequentialSink): file, socket,
//! pipe, anything `Write + Send`.

use std::io::{self, Write};

/// Annex B 4-byte start code.
pub(crate) const START_CODE: [u8; 4] = [0x00, 0x00, 0x00, 0x01];

/// HEVC NAL unit type bits live in `(byte0 >> 1) & 0x3F` in Annex B.
/// We don't filter NAL types here — the muxer is format-only — but we
/// keep the constant as documentation of the field layout.
#[allow(dead_code)]
const HEVC_NAL_TYPE_MASK: u8 = 0x3F;

/// Streaming HEVC Annex B muxer.
///
/// One instance per output stream. Tracks whether parameter sets have
/// already been emitted so they're written exactly once at the head of
/// the stream, mirroring the convention used by `ffmpeg -c:v copy -f
/// hevc`.
pub struct HevcMux<W: Write> {
    writer: W,
    /// `HEVCDecoderConfigurationRecord` payload (hvcC). Parsed lazily
    /// on the first `write_frame` so callers can set it after
    /// construction but before the first frame.
    codec_private: Option<Vec<u8>>,
    /// Set once VPS/SPS/PPS have been written to the stream. Subsequent
    /// frames write only their own NAL units.
    params_written: bool,
}

impl<W: Write> HevcMux<W> {
    /// Construct over `writer`. The muxer does not impose any extra
    /// buffering of its own — the sink owns its write buffering policy
    /// (see [`LocalFileSink`](crate::io::sink::LocalFileSink) and
    /// [`SocketSink`](crate::io::sink::SocketSink)).
    pub fn new(writer: W) -> Self {
        Self {
            writer,
            codec_private: None,
            params_written: false,
        }
    }

    /// Provide the `HEVCDecoderConfigurationRecord` (hvcC) so the muxer
    /// can prepend VPS/SPS/PPS Annex B NALs at stream start. Optional —
    /// if the PES frames already carry inline parameter sets (some
    /// upstream demuxers do this), skipping this call is fine.
    pub fn set_codec_private(&mut self, data: Vec<u8>) {
        self.codec_private = Some(data);
    }

    /// Write one PES frame (= one access unit) as Annex B NAL units.
    ///
    /// Input may be either:
    ///   - Length-prefixed: `[u32-BE len][NAL bytes]` repeated. This is
    ///     the form emitted by libfreemkv's HEVC parser (the MKV-native
    ///     layout). Converted to Annex B.
    ///   - Already Annex B: a buffer beginning with a `00 00 00 01` or
    ///     `00 00 01` start code. Passed through unchanged.
    ///
    /// `_pts_ns` is accepted for symmetry with other muxers but ignored
    /// — Annex B has no timing layer.
    pub fn write_frame(&mut self, _pts_ns: i64, data: &[u8]) -> io::Result<()> {
        if !self.params_written {
            // Mark written *before* the write: a partial write that then
            // errors must not cause a later re-entry to re-emit the full
            // parameter set on top of the bytes the sink already
            // received (duplicate/split VPS/SPS/PPS). Callers discard the
            // mux on any write error.
            self.params_written = true;
            if let Some(cp) = &self.codec_private {
                match hvcc_to_annex_b(cp) {
                    Some(params) => self.writer.write_all(&params)?,
                    // A non-empty hvcC that yields no NAL is a caller
                    // contract violation: emitting the stream without
                    // VPS/SPS/PPS produces undecodable output. Surface it
                    // rather than dropping the parameter sets silently.
                    None if !cp.is_empty() => {
                        return Err(crate::error::Error::HevcParamParse.into());
                    }
                    None => {}
                }
            }
        }
        let annex_b = length_prefixed_to_annex_b(data);
        self.writer.write_all(&annex_b)
    }

    /// Flush the underlying writer. No trailer NAL is needed — an Annex
    /// B stream ends whenever the file/socket ends.
    pub fn finish(&mut self) -> io::Result<()> {
        self.writer.flush()
    }
}

/// Convert a `HEVCDecoderConfigurationRecord` (hvcC) into Annex B NAL
/// units. Returns `Some(bytes)` if at least one NAL was extracted, else
/// `None`.
///
/// Layout (per ISO/IEC 14496-15 §8.3.3.1.2):
///   - 22-byte fixed header
///   - byte 22 = `numOfArrays`
///   - each array: `array_completeness:1 | reserved:1 | NAL_unit_type:6`,
///     `numNalus:u16-BE`, then `numNalus` × `(nalUnitLength:u16-BE +
///     NAL bytes)`.
///
/// We don't filter on NAL type — VPS (32), SPS (33), PPS (34), and any
/// SEI arrays included in hvcC all get the same Annex B treatment.
///
/// This is the single source of truth for hvcC → Annex B across all
/// muxers (HEVC ES, BD-TS, standard MPEG-TS). Do not reimplement it.
pub(crate) fn hvcc_to_annex_b(hvcc: &[u8]) -> Option<Vec<u8>> {
    if hvcc.len() < 23 {
        return None;
    }
    let num_arrays = hvcc[22] as usize;
    let mut out = Vec::new();
    let mut offset = 23;
    // Set when an inner loop exits on truncation so the outer loop stops
    // too — otherwise it would re-interpret mid-NAL bytes as the next
    // array header and synthesize spurious parameter-set NALs.
    let mut truncated = false;
    for _ in 0..num_arrays {
        if truncated || offset + 3 > hvcc.len() {
            break;
        }
        offset += 1; // array_completeness + nal_type byte
        let num_nalus = u16::from_be_bytes([hvcc[offset], hvcc[offset + 1]]) as usize;
        offset += 2;
        for _ in 0..num_nalus {
            if offset + 2 > hvcc.len() {
                truncated = true;
                break;
            }
            let nal_len = u16::from_be_bytes([hvcc[offset], hvcc[offset + 1]]) as usize;
            offset += 2;
            if offset + nal_len > hvcc.len() {
                truncated = true;
                break;
            }
            // ISO/IEC 14496-15 disallows zero-length NAL entries; emitting
            // a bare start code with no RBSP yields an invalid Annex B NAL.
            if nal_len == 0 {
                continue;
            }
            out.extend_from_slice(&START_CODE);
            out.extend_from_slice(&hvcc[offset..offset + nal_len]);
            offset += nal_len;
        }
    }
    if out.is_empty() { None } else { Some(out) }
}

/// Convert length-prefixed NAL units (`[u32-BE len][NAL]` repeated) to
/// Annex B (`00 00 00 01 [NAL]` repeated).
///
/// Already-Annex-B input (a buffer beginning with a `00 00 00 01` or
/// `00 00 01` start code) is detected up front and passed through
/// unchanged — some upstream paths (raw HEVC ES from disc) hand Annex B
/// straight through the PES layer, and a genuine start code would
/// otherwise be misread as a u32-BE length prefix.
///
/// Truncation policy (single source of truth across all muxers): if a
/// length prefix runs past the end of the buffer (e.g. a NAL truncated
/// by a bad disc sector), the truncated trailing NAL is dropped and only
/// the valid Annex-B prefix accumulated so far is emitted. We never emit
/// a half-NAL nor leak raw length-prefixed bytes into the Annex-B stream.
pub(crate) fn length_prefixed_to_annex_b(data: &[u8]) -> Vec<u8> {
    // Probe for a leading Annex B start code before attempting to parse
    // length prefixes: `00 00 00 01` would otherwise parse as length 1.
    if starts_with_start_code(data) {
        return data.to_vec();
    }
    let mut out = Vec::with_capacity(data.len() + (data.len() / 32));
    append_length_prefixed_as_annex_b(&mut out, data);
    out
}

/// Append the Annex B form of `data` (length-prefixed NALs) into `out`.
///
/// Same conversion as [`length_prefixed_to_annex_b`] but writes directly
/// into a caller-owned buffer, avoiding an intermediate allocation on
/// hot paths (e.g. per-frame video muxing). If `data` doesn't parse as
/// length-prefixed (no NALs extracted), it's appended unchanged on the
/// assumption it's already Annex B.
pub(crate) fn append_length_prefixed_as_annex_b(out: &mut Vec<u8>, data: &[u8]) {
    let mut offset = 0;
    // True once we've consumed at least one well-formed length prefix
    // (even a zero-length one). Distinguishes "parsed as length-prefixed,
    // all NALs empty" (emit nothing) from "not length-prefixed at all"
    // (pass through as already-Annex B).
    let mut parsed_any = false;
    while offset + 4 <= data.len() {
        let len = u32::from_be_bytes([
            data[offset],
            data[offset + 1],
            data[offset + 2],
            data[offset + 3],
        ]) as usize;
        offset += 4;
        if offset + len > data.len() {
            // Mid-NAL truncation (e.g. a NAL cut by a bad disc sector) —
            // drop the truncated trailing NAL and emit only the valid
            // Annex-B prefix accumulated so far. We never emit a half-NAL
            // nor leak raw length-prefixed bytes into the Annex-B stream.
            break;
        }
        parsed_any = true;
        if len == 0 {
            // A zero-length prefix (e.g. pad bytes read off a damaged
            // sector) would otherwise emit a bare start code with no
            // RBSP — an invalid empty Annex B NAL. Skip it, mirroring
            // the `nal_len == 0` guard in `hvcc_to_annex_b` (ISO/IEC
            // 14496-15).
            continue;
        }
        out.extend_from_slice(&START_CODE);
        out.extend_from_slice(&data[offset..offset + len]);
        offset += len;
    }
    if !parsed_any && !data.is_empty() {
        // No length prefixes parsed at all and no leading start code:
        // pass the bytes through rather than discard them (recover-100%
        // goal — a decoder can attempt its own resync; dropping them
        // guarantees loss). This is distinct from "parsed as length-
        // prefixed but every NAL was zero-length", which emits nothing.
        out.extend_from_slice(data);
    }
}

/// Whether `data` begins with a 4-byte (`00 00 00 01`) or 3-byte
/// (`00 00 01`) Annex B start code.
fn starts_with_start_code(data: &[u8]) -> bool {
    data.starts_with(&START_CODE) || data.starts_with(&[0x00, 0x00, 0x01])
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn length_prefixed_converts_to_annex_b() {
        // Two NALs: [3-byte payload AA BB CC] and [2-byte payload DD EE].
        let mut buf = Vec::new();
        buf.extend_from_slice(&3u32.to_be_bytes());
        buf.extend_from_slice(&[0xAA, 0xBB, 0xCC]);
        buf.extend_from_slice(&2u32.to_be_bytes());
        buf.extend_from_slice(&[0xDD, 0xEE]);

        let got = length_prefixed_to_annex_b(&buf);
        let want = [
            0x00, 0x00, 0x00, 0x01, 0xAA, 0xBB, 0xCC, // first NAL
            0x00, 0x00, 0x00, 0x01, 0xDD, 0xEE, // second NAL
        ];
        assert_eq!(&got[..], &want[..]);
    }

    #[test]
    fn already_annex_b_passes_through_when_no_lengths_match() {
        // A buffer < 4 bytes can't parse a length prefix at all →
        // pass-through path triggers.
        let raw = [0xAA, 0xBB, 0xCC];
        let got = length_prefixed_to_annex_b(&raw);
        assert_eq!(&got[..], &raw[..]);
    }

    #[test]
    fn mid_nal_truncation_drops_trailing_nal_keeps_prefix() {
        // First NAL is valid (2-byte payload), second has a length prefix
        // claiming 100 bytes with only 3 present. Policy: emit the valid
        // first NAL as Annex B, drop the truncated trailing NAL — never
        // leak raw length-prefixed bytes into the Annex B stream.
        let mut raw = Vec::new();
        raw.extend_from_slice(&2u32.to_be_bytes());
        raw.extend_from_slice(&[0x11, 0x22]);
        raw.extend_from_slice(&100u32.to_be_bytes());
        raw.extend_from_slice(&[0xAA, 0xBB, 0xCC]);
        let got = length_prefixed_to_annex_b(&raw);
        let want = [0x00, 0x00, 0x00, 0x01, 0x11, 0x22];
        assert_eq!(&got[..], &want[..]);
    }

    #[test]
    fn leading_annex_b_start_code_passes_through() {
        // Genuine Annex B beginning with 00 00 00 01 must NOT be reframed:
        // the start code would otherwise parse as a u32-BE length of 1.
        let raw = [
            0x00, 0x00, 0x00, 0x01, 0x26, 0x01, 0xDE, 0xAD, // NAL 1
            0x00, 0x00, 0x00, 0x01, 0x02, 0x01, 0xBE, 0xEF, // NAL 2
        ];
        let got = length_prefixed_to_annex_b(&raw);
        assert_eq!(
            &got[..],
            &raw[..],
            "Annex B input must pass through verbatim"
        );
    }

    #[test]
    fn leading_three_byte_start_code_passes_through() {
        let raw = [0x00, 0x00, 0x01, 0x26, 0x01, 0xDE, 0xAD];
        let got = length_prefixed_to_annex_b(&raw);
        assert_eq!(&got[..], &raw[..]);
    }

    #[test]
    fn hvcc_skips_zero_length_nal_entries() {
        // hvcC with one array containing a zero-length NAL followed by a
        // valid one: the zero-length entry must be skipped, not emitted as
        // a bare start code.
        let mut hvcc = vec![0u8; 22];
        hvcc.push(1); // numArrays
        hvcc.push(33); // SPS
        hvcc.extend_from_slice(&2u16.to_be_bytes()); // numNalus = 2
        hvcc.extend_from_slice(&0u16.to_be_bytes()); // NAL 0: length 0
        hvcc.extend_from_slice(&3u16.to_be_bytes()); // NAL 1: length 3
        hvcc.extend_from_slice(&[0x42, 0x01, 0x01]);
        let annex_b = hvcc_to_annex_b(&hvcc).expect("one valid NAL");
        let want = [0x00, 0x00, 0x00, 0x01, 0x42, 0x01, 0x01];
        assert_eq!(&annex_b[..], &want[..]);
    }

    #[test]
    fn write_frame_errors_on_unparseable_non_empty_hvcc() {
        // A non-empty hvcC that yields no NAL must surface an error
        // instead of silently producing a parameter-set-less stream.
        let mut sink: Vec<u8> = Vec::new();
        let mut mux = HevcMux::new(&mut sink);
        mux.set_codec_private(vec![0xDE, 0xAD]); // too short to be valid hvcC
        let err = mux.write_frame(0, &[]).unwrap_err();
        assert_eq!(err.kind(), io::ErrorKind::InvalidData);
    }

    #[test]
    fn zero_length_nal_is_skipped_not_bare_start_code() {
        // A zero-length prefix between two real NALs must be skipped, not
        // turned into a bare `00 00 00 01` with no RBSP.
        let mut buf = Vec::new();
        buf.extend_from_slice(&3u32.to_be_bytes());
        buf.extend_from_slice(&[0xAA, 0xBB, 0xCC]);
        buf.extend_from_slice(&0u32.to_be_bytes()); // zero-length NAL
        buf.extend_from_slice(&2u32.to_be_bytes());
        buf.extend_from_slice(&[0xDD, 0xEE]);

        let got = length_prefixed_to_annex_b(&buf);
        let want = [
            0x00, 0x00, 0x00, 0x01, 0xAA, 0xBB, 0xCC, // first NAL
            0x00, 0x00, 0x00, 0x01, 0xDD, 0xEE, // second NAL (zero-length skipped)
        ];
        assert_eq!(&got[..], &want[..]);
    }

    #[test]
    fn all_zero_length_nals_emit_nothing() {
        // A buffer of only zero-length prefixes parses as length-prefixed
        // but yields no NALs — output must be empty, not a pass-through of
        // the raw zero bytes.
        let mut buf = Vec::new();
        buf.extend_from_slice(&0u32.to_be_bytes());
        buf.extend_from_slice(&0u32.to_be_bytes());
        let got = length_prefixed_to_annex_b(&buf);
        assert!(got.is_empty(), "expected empty output, got {got:?}");
    }

    #[test]
    fn hvcc_extracts_vps_sps_pps() {
        // Build a minimal-but-valid hvcC: 22-byte header, then 3 arrays
        // (VPS / SPS / PPS), each with 1 NAL of a 4-byte payload that
        // we can spot in the output.
        let mut hvcc = vec![0u8; 22];
        hvcc.push(3); // numOfArrays
        for (nal_type, payload) in [
            (32u8, [0x40, 0x01, 0x0C, 0x01]),
            (33, [0x42, 0x01, 0x01, 0x01]),
            (34, [0x44, 0x01, 0xC1, 0x72]),
        ] {
            hvcc.push(nal_type & 0x3F);
            hvcc.extend_from_slice(&1u16.to_be_bytes()); // numNalus
            hvcc.extend_from_slice(&(payload.len() as u16).to_be_bytes());
            hvcc.extend_from_slice(&payload);
        }

        let annex_b = hvcc_to_annex_b(&hvcc).expect("at least one NAL");
        // Three NALs × (4-byte start + 4-byte payload) = 24 bytes.
        assert_eq!(annex_b.len(), 24);
        assert_eq!(&annex_b[..4], &START_CODE);
        assert_eq!(&annex_b[8..12], &START_CODE);
        assert_eq!(&annex_b[16..20], &START_CODE);
        assert_eq!(annex_b[4], 0x40); // VPS first byte
        assert_eq!(annex_b[12], 0x42); // SPS first byte
        assert_eq!(annex_b[20], 0x44); // PPS first byte
    }

    // --- hvcc_to_annex_b: truncation handling (ISO/IEC 14496-15 §8.3.3.1.2) ---

    #[test]
    fn hvcc_too_short_for_header_returns_none() {
        // < 23 bytes (22 fixed + numArrays) can't be a valid hvcC → None.
        assert!(hvcc_to_annex_b(&[0u8; 22]).is_none());
        assert!(hvcc_to_annex_b(&[]).is_none());
    }

    #[test]
    fn hvcc_zero_arrays_returns_none() {
        // numArrays = 0 → no NALs extracted → None (out.is_empty()).
        let mut hvcc = vec![0u8; 22];
        hvcc.push(0); // numArrays = 0
        assert!(hvcc_to_annex_b(&hvcc).is_none());
    }

    #[test]
    fn hvcc_array_with_multiple_nalus() {
        // One array, numNalus = 2: both NALs must be emitted, each with a start
        // code. (The inner numNalus loop, not just one NAL per array.)
        let mut hvcc = vec![0u8; 22];
        hvcc.push(1); // numArrays
        hvcc.push(33); // SPS
        hvcc.extend_from_slice(&2u16.to_be_bytes()); // numNalus = 2
        hvcc.extend_from_slice(&2u16.to_be_bytes()); // NAL0 len 2
        hvcc.extend_from_slice(&[0x42, 0x01]);
        hvcc.extend_from_slice(&3u16.to_be_bytes()); // NAL1 len 3
        hvcc.extend_from_slice(&[0x44, 0x02, 0x03]);
        let out = hvcc_to_annex_b(&hvcc).expect("two NALs");
        let want = [
            0x00, 0x00, 0x00, 0x01, 0x42, 0x01, // NAL0
            0x00, 0x00, 0x00, 0x01, 0x44, 0x02, 0x03, // NAL1
        ];
        assert_eq!(&out[..], &want[..]);
    }

    #[test]
    fn hvcc_truncated_nal_length_stops_cleanly() {
        // A NAL length field claiming more bytes than remain must stop parsing
        // (truncated flag), emitting only the complete NALs — never a partial
        // NAL nor garbage from re-interpreting mid-NAL bytes as an array header.
        let mut hvcc = vec![0u8; 22];
        hvcc.push(2); // numArrays = 2
        // Array 0: one valid 3-byte NAL.
        hvcc.push(32);
        hvcc.extend_from_slice(&1u16.to_be_bytes());
        hvcc.extend_from_slice(&3u16.to_be_bytes());
        hvcc.extend_from_slice(&[0x40, 0x01, 0x02]);
        // Array 1: one NAL declaring 100 bytes but only 2 present → truncated.
        hvcc.push(33);
        hvcc.extend_from_slice(&1u16.to_be_bytes());
        hvcc.extend_from_slice(&100u16.to_be_bytes());
        hvcc.extend_from_slice(&[0xAA, 0xBB]);
        let out = hvcc_to_annex_b(&hvcc).expect("the one valid NAL");
        // Only array 0's NAL is emitted.
        assert_eq!(
            &out[..],
            &[0x00, 0x00, 0x00, 0x01, 0x40, 0x01, 0x02],
            "truncated trailing NAL dropped, valid prefix kept"
        );
    }

    #[test]
    fn hvcc_truncated_length_field_itself_stops() {
        // The 2-byte NAL length field itself runs past the buffer end → truncated
        // (offset + 2 > len guard). Emit only what completed.
        let mut hvcc = vec![0u8; 22];
        hvcc.push(1);
        hvcc.push(33);
        hvcc.extend_from_slice(&2u16.to_be_bytes()); // numNalus = 2
        hvcc.extend_from_slice(&2u16.to_be_bytes()); // NAL0 len 2
        hvcc.extend_from_slice(&[0x42, 0x01]);
        hvcc.push(0x00); // dangling single byte — can't form NAL1's length field
        let out = hvcc_to_annex_b(&hvcc).expect("NAL0");
        assert_eq!(&out[..], &[0x00, 0x00, 0x00, 0x01, 0x42, 0x01]);
    }

    #[test]
    fn hvcc_array_header_truncated_stops_outer_loop() {
        // numArrays claims 3 but only one array's header fits (offset + 3 > len).
        // The outer loop must break, not read out of bounds.
        let mut hvcc = vec![0u8; 22];
        hvcc.push(3); // numArrays = 3 (lie)
        hvcc.push(32);
        hvcc.extend_from_slice(&1u16.to_be_bytes());
        hvcc.extend_from_slice(&2u16.to_be_bytes());
        hvcc.extend_from_slice(&[0x40, 0x01]);
        // No bytes for arrays 2 and 3 → outer loop's `offset + 3 > len` breaks.
        let out = hvcc_to_annex_b(&hvcc).expect("the one present NAL");
        assert_eq!(&out[..], &[0x00, 0x00, 0x00, 0x01, 0x40, 0x01]);
    }

    // --- length_prefixed_to_annex_b additional branches ---

    #[test]
    fn empty_input_yields_empty() {
        // Empty input → empty output (no pass-through of nothing).
        assert!(length_prefixed_to_annex_b(&[]).is_empty());
    }

    #[test]
    fn single_nal_length_prefix() {
        // One NAL: 4-byte len + body → one Annex B NAL.
        let mut buf = 5u32.to_be_bytes().to_vec();
        buf.extend_from_slice(&[0x26, 0x01, 0xAA, 0xBB, 0xCC]);
        let got = length_prefixed_to_annex_b(&buf);
        let mut want = START_CODE.to_vec();
        want.extend_from_slice(&[0x26, 0x01, 0xAA, 0xBB, 0xCC]);
        assert_eq!(got, want);
    }

    #[test]
    fn non_length_prefixed_three_plus_bytes_passes_through() {
        // A 4+ byte buffer that does NOT parse as length-prefixed (the first
        // u32 length exceeds the remaining bytes on the very first NAL, parsing
        // nothing) is passed through unchanged (parsed_any == false branch).
        // 0xFFFFFFFF length with no body → parsed_any stays false → pass-through.
        let raw = [0xFF, 0xFF, 0xFF, 0xFF, 0x11, 0x22];
        let got = length_prefixed_to_annex_b(&raw);
        assert_eq!(&got[..], &raw[..], "unparseable → passed through verbatim");
    }

    #[test]
    fn append_into_caller_buffer_preserves_existing() {
        // append_length_prefixed_as_annex_b writes into a caller buffer without
        // clobbering its existing contents (hot-path no-alloc API).
        let mut out = vec![0xDE, 0xAD];
        let mut nal = 2u32.to_be_bytes().to_vec();
        nal.extend_from_slice(&[0x11, 0x22]);
        append_length_prefixed_as_annex_b(&mut out, &nal);
        let mut want = vec![0xDE, 0xAD];
        want.extend_from_slice(&START_CODE);
        want.extend_from_slice(&[0x11, 0x22]);
        assert_eq!(out, want);
    }

    #[test]
    fn starts_with_start_code_detects_both_forms() {
        assert!(starts_with_start_code(&[0x00, 0x00, 0x00, 0x01, 0x42]));
        assert!(starts_with_start_code(&[0x00, 0x00, 0x01, 0x42]));
        assert!(!starts_with_start_code(&[0x00, 0x00, 0x02, 0x42]));
        assert!(!starts_with_start_code(&[0x42, 0x00, 0x00, 0x01]));
        assert!(!starts_with_start_code(&[]));
    }

    // --- HevcMux: params-once + error semantics ---

    #[test]
    fn mux_empty_codec_private_emits_no_params() {
        // An EMPTY (not absent) hvcC: hvcc_to_annex_b returns None, but cp is
        // empty so it's NOT a contract violation → no error, just no params.
        let mut sink: Vec<u8> = Vec::new();
        let mut mux = HevcMux::new(&mut sink);
        mux.set_codec_private(Vec::new());
        let mut frame = 2u32.to_be_bytes().to_vec();
        frame.extend_from_slice(&[0xAA, 0xBB]);
        mux.write_frame(0, &frame).unwrap();
        mux.finish().unwrap();
        // Only the frame NAL, no parameter sets.
        let mut want = START_CODE.to_vec();
        want.extend_from_slice(&[0xAA, 0xBB]);
        assert_eq!(sink, want);
    }

    #[test]
    fn mux_no_codec_private_writes_frames_only() {
        // No hvcC set at all: frames pass through, no params, no error.
        let mut sink: Vec<u8> = Vec::new();
        let mut mux = HevcMux::new(&mut sink);
        let mut frame = 2u32.to_be_bytes().to_vec();
        frame.extend_from_slice(&[0xAA, 0xBB]);
        mux.write_frame(0, &frame).unwrap();
        let mut want = START_CODE.to_vec();
        want.extend_from_slice(&[0xAA, 0xBB]);
        assert_eq!(sink, want);
    }

    #[test]
    fn mux_params_not_re_emitted_after_unparseable_error() {
        // params_written is set BEFORE the write, so after an error on the first
        // frame, a retry must NOT re-emit params (the comment's invariant).
        let mut sink: Vec<u8> = Vec::new();
        let mut mux = HevcMux::new(&mut sink);
        mux.set_codec_private(vec![0xDE, 0xAD]); // unparseable, non-empty → error
        assert!(mux.write_frame(0, &[]).is_err(), "first frame errors");
        // A second frame must not retry the (already-marked) params.
        let mut frame = 2u32.to_be_bytes().to_vec();
        frame.extend_from_slice(&[0xAA, 0xBB]);
        mux.write_frame(0, &frame).unwrap();
        // Sink holds only the frame NAL — no parameter bytes, no duplication.
        let mut want = START_CODE.to_vec();
        want.extend_from_slice(&[0xAA, 0xBB]);
        assert_eq!(sink, want, "params not re-emitted after the error");
    }

    #[test]
    fn mux_annex_b_frame_passes_through() {
        // A frame already in Annex B (leading start code) is written verbatim,
        // not re-framed as length-prefixed.
        let mut sink: Vec<u8> = Vec::new();
        let mut mux = HevcMux::new(&mut sink);
        let frame = [0x00, 0x00, 0x00, 0x01, 0x26, 0x01, 0xDE];
        mux.write_frame(0, &frame).unwrap();
        assert_eq!(sink, frame);
    }

    #[test]
    fn mux_writes_params_then_frames() {
        // Build hvcC with one SPS to verify params-once semantics.
        let mut hvcc = vec![0u8; 22];
        hvcc.push(1);
        hvcc.push(33);
        hvcc.extend_from_slice(&1u16.to_be_bytes());
        hvcc.extend_from_slice(&3u16.to_be_bytes());
        hvcc.extend_from_slice(&[0x42, 0x01, 0x01]);

        let mut frame_data = Vec::new();
        frame_data.extend_from_slice(&2u32.to_be_bytes());
        frame_data.extend_from_slice(&[0xAA, 0xBB]);

        let mut sink: Vec<u8> = Vec::new();
        let mut mux = HevcMux::new(&mut sink);
        mux.set_codec_private(hvcc);
        mux.write_frame(0, &frame_data).unwrap();
        // Second frame — no SPS re-emission.
        mux.write_frame(40_000_000, &frame_data).unwrap();
        mux.finish().unwrap();

        // SPS NAL (7 bytes) + 2× frame NAL (6 bytes) = 19 bytes.
        assert_eq!(sink.len(), 7 + 6 + 6);
        // Start codes at offsets 0 (SPS), 7 (frame1), 13 (frame2).
        assert_eq!(&sink[0..4], &START_CODE);
        assert_eq!(&sink[7..11], &START_CODE);
        assert_eq!(&sink[13..17], &START_CODE);
        assert_eq!(sink[4], 0x42);
        assert_eq!(sink[11], 0xAA);
        assert_eq!(sink[17], 0xAA);
    }
}