Skip to main content

flv_core/
mux.rs

1//! FLV file-header + tag writer.
2
3#![forbid(unsafe_code)]
4
5use crate::error::Error;
6use crate::types::Tag;
7
8const TAG_HEADER_LEN: usize = 11;
9const MAX_DATA_SIZE: usize = 0x00FF_FFFF; // 24-bit DataSize field
10
11/// Writes an FLV file header followed by tags, each self-trailed with its own
12/// `PreviousTagSize` (FLV has no incremental flush concept beyond "append the
13/// next complete tag" — there is no `finish()` step).
14#[derive(Debug, Clone, Copy, Default)]
15pub struct Muxer {
16    header_written: bool,
17}
18
19impl Muxer {
20    /// New mux session.
21    #[must_use]
22    pub fn new() -> Self {
23        Self::default()
24    }
25
26    /// Write the 9-byte FLV file header + the leading 4-byte `PreviousTagSize0` (always 0).
27    pub fn write_header(&mut self, has_audio: bool, has_video: bool, out: &mut Vec<u8>) {
28        out.extend_from_slice(b"FLV");
29        out.push(1); // version
30        let flags = (u8::from(has_audio) << 2) | u8::from(has_video);
31        out.push(flags);
32        out.extend_from_slice(&9u32.to_be_bytes()); // DataOffset: standard 9-byte header
33        out.extend_from_slice(&0u32.to_be_bytes()); // PreviousTagSize0
34        self.header_written = true;
35    }
36
37    /// Append one tag (11-byte header + data + trailing `PreviousTagSize`) to `out`.
38    #[allow(
39        clippy::cast_possible_truncation,
40        reason = "data_size is bounds-checked against MAX_DATA_SIZE just above; ts>>24 always fits u8"
41    )]
42    pub fn write_tag(&self, tag: &Tag, out: &mut Vec<u8>) -> Result<(), Error> {
43        if !self.header_written {
44            return Err(Error::HeaderNotWritten);
45        }
46        if tag.data.len() > MAX_DATA_SIZE {
47            return Err(Error::TagDataTooLarge(tag.data.len()));
48        }
49        let data_size = tag.data.len();
50        let data_size_bytes = (data_size as u32).to_be_bytes(); // top byte always 0 (checked above)
51
52        out.push(tag.tag_type.value());
53        out.extend_from_slice(&data_size_bytes[1..4]);
54        let ts = tag.timestamp_ms;
55        out.extend_from_slice(&ts.to_be_bytes()[1..4]); // lower 24 bits, big-endian
56        out.push((ts >> 24) as u8); // TimestampExtended: upper 8 bits
57        out.extend_from_slice(&[0, 0, 0]); // StreamID: always 0
58        out.extend_from_slice(&tag.data);
59
60        let tag_len = u32::try_from(TAG_HEADER_LEN + data_size).unwrap_or(u32::MAX);
61        out.extend_from_slice(&tag_len.to_be_bytes());
62        Ok(())
63    }
64}
65
66#[cfg(test)]
67#[path = "mux_tests.rs"]
68mod tests;