1use std::sync::Arc;23use crate::{4 MAX_MEDIA_PAYLOAD_LEN, MAX_VIDEO_DIMENSION, MAX_VIDEO_PIXELS, PROTOCOL_VERSION, ProtocolError,5 Result,6};78const MEDIA_MAGIC: [u8; 4] = *b"SGV1";9/// Size of a v1 media header in bytes.10pub const MEDIA_HEADER_LEN: usize = 48;11const MEDIA_HEADER_LEN_U8: u8 = MEDIA_HEADER_LEN as u8;1213/// Encoded media codec. V1 deliberately supports only H.264 video.141516pub enum MediaCodec {17 H264 = 1,18}1920impl TryFrom<u8> for MediaCodec {21 type Error = ProtocolError;2223 fn try_from(value: u8) -> Result<Self> {24 match value {25 1 => Ok(Self::H264),26 other => Err(ProtocolError::UnsupportedCodec(other)),27 }28 }29}3031/// Flags attached to one encoded video access unit.3233pub struct FrameFlags(u8);3435impl FrameFlags {36 pub const NONE: Self = Self(0);37 pub const KEYFRAME: Self = Self(1 << 0);38 pub const CODEC_CONFIG: Self = Self(1 << 1);39 pub const DISCONTINUITY: Self = Self(1 << 2);4041 const KNOWN: u8 = Self::KEYFRAME.0 | Self::CODEC_CONFIG.0 | Self::DISCONTINUITY.0;4243 pub const fn from_bits(bits: u8) -> Option<Self> {44 if bits & !Self::KNOWN == 0 {45 Some(Self(bits))46 } else {47 None48 }49 }5051 pub const fn bits(self) -> u8 {52 self.053 }5455 pub const fn contains(self, flag: Self) -> bool {56 self.0 & flag.0 == flag.057 }5859 pub const fn union(self, other: Self) -> Self {60 Self(self.0 | other.0)61 }62}6364/// Fixed-width v1 header. Integer fields use network byte order.65///66/// `capture_timestamp_us` is microseconds from one host-monotonic media-session67/// epoch shared by every stream. A source that cannot preserve capture68/// metadata may use its post-encode observation time, but must not present that69/// value as capture or glass-to-glass latency. `pts_us` is the presentation70/// timestamp in the same host timebase and may be negative for encoder preroll.7172pub struct MediaFrameHeader {73 pub codec: MediaCodec,74 pub flags: FrameFlags,75 pub width: u16,76 pub height: u16,77 pub payload_len: u32,78 pub sequence: u64,79 pub capture_timestamp_us: u64,80 pub pts_us: i64,81}8283impl MediaFrameHeader {84 pub fn h264(85 width: u16,86 height: u16,87 payload_len: usize,88 sequence: u64,89 capture_timestamp_us: u64,90 pts_us: i64,91 flags: FrameFlags,92 ) -> Result<Self> {93 let payload_len =94 u32::try_from(payload_len).map_err(|_| ProtocolError::InvalidMediaPayloadLength {95 actual: payload_len,96 maximum: MAX_MEDIA_PAYLOAD_LEN,97 })?;98 let header = Self {99 codec: MediaCodec::H264,100 flags,101 width,102 height,103 payload_len,104 sequence,105 capture_timestamp_us,106 pts_us,107 };108 header.validate()?;109 Ok(header)110 }111112 pub fn validate(&self) -> Result<()> {113 let pixels = u32::from(self.width) * u32::from(self.height);114 if self.width == 0115 || self.height == 0116 || self.width > MAX_VIDEO_DIMENSION117 || self.height > MAX_VIDEO_DIMENSION118 || pixels > MAX_VIDEO_PIXELS119 {120 return Err(ProtocolError::InvalidDimensions {121 width: self.width,122 height: self.height,123 });124 }125 let payload_len = self.payload_len as usize;126 if payload_len == 0 || payload_len > MAX_MEDIA_PAYLOAD_LEN {127 return Err(ProtocolError::InvalidMediaPayloadLength {128 actual: payload_len,129 maximum: MAX_MEDIA_PAYLOAD_LEN,130 });131 }132 if FrameFlags::from_bits(self.flags.bits()).is_none() {133 return Err(ProtocolError::UnsupportedFlags(self.flags.bits()));134 }135 Ok(())136 }137138 pub fn encode(&self) -> Result<[u8; MEDIA_HEADER_LEN]> {139 self.validate()?;140 let mut wire = [0; MEDIA_HEADER_LEN];141 wire[0..4].copy_from_slice(&MEDIA_MAGIC);142 wire[4..6].copy_from_slice(&PROTOCOL_VERSION.to_be_bytes());143 wire[6] = MEDIA_HEADER_LEN_U8;144 wire[7] = self.codec as u8;145 wire[8] = self.flags.bits();146 // Bytes 9..12 are reserved and must remain zero.147 wire[12..14].copy_from_slice(&self.width.to_be_bytes());148 wire[14..16].copy_from_slice(&self.height.to_be_bytes());149 wire[16..20].copy_from_slice(&self.payload_len.to_be_bytes());150 // Bytes 20..24 are reserved and must remain zero.151 wire[24..32].copy_from_slice(&self.sequence.to_be_bytes());152 wire[32..40].copy_from_slice(&self.capture_timestamp_us.to_be_bytes());153 wire[40..48].copy_from_slice(&self.pts_us.to_be_bytes());154 Ok(wire)155 }156157 pub fn decode(wire: &[u8; MEDIA_HEADER_LEN]) -> Result<Self> {158 let magic: [u8; 4] = wire[0..4].try_into().expect("fixed slice length");159 if magic != MEDIA_MAGIC {160 return Err(ProtocolError::InvalidMediaMagic(magic));161 }162 let version = u16::from_be_bytes(wire[4..6].try_into().expect("fixed slice length"));163 if version != PROTOCOL_VERSION {164 return Err(ProtocolError::UnsupportedVersion {165 expected: PROTOCOL_VERSION,166 actual: version,167 });168 }169 if wire[6] != MEDIA_HEADER_LEN_U8 {170 return Err(ProtocolError::InvalidHeaderLength {171 expected: MEDIA_HEADER_LEN_U8,172 actual: wire[6],173 });174 }175 if wire[9..12]176 .iter()177 .chain(&wire[20..24])178 .any(|byte| *byte != 0)179 {180 return Err(ProtocolError::InvalidMessage {181 message_type: "media header",182 reason: "reserved fields must be zero",183 });184 }185 let flags =186 FrameFlags::from_bits(wire[8]).ok_or(ProtocolError::UnsupportedFlags(wire[8]))?;187 let header = Self {188 codec: MediaCodec::try_from(wire[7])?,189 flags,190 width: u16::from_be_bytes(wire[12..14].try_into().expect("fixed slice length")),191 height: u16::from_be_bytes(wire[14..16].try_into().expect("fixed slice length")),192 payload_len: u32::from_be_bytes(wire[16..20].try_into().expect("fixed slice length")),193 sequence: u64::from_be_bytes(wire[24..32].try_into().expect("fixed slice length")),194 capture_timestamp_us: u64::from_be_bytes(195 wire[32..40].try_into().expect("fixed slice length"),196 ),197 pts_us: i64::from_be_bytes(wire[40..48].try_into().expect("fixed slice length")),198 };199 header.validate()?;200 Ok(header)201 }202}203204/// One complete encoded access unit.205206pub struct MediaFrame {207 pub header: MediaFrameHeader,208 /// The compressed access unit. Held behind an `Arc` so the host can fan the209 /// same encoded frame out to every connected client without copying the210 /// payload per connection.211 pub payload: Arc<[u8]>,212}213214/// Encode one complete application media frame for an object transport such215/// as MoQ. The MoQ group and frame boundaries carry delivery semantics; this216/// envelope carries only Sigil's bounded codec metadata and compressed access217/// unit.218pub fn encode_media_frame_object(frame: &MediaFrame) -> Result<Vec<u8>> {219 frame.header.validate()?;220 if frame.payload.len() != frame.header.payload_len as usize {221 return Err(ProtocolError::InvalidMessage {222 message_type: "media frame object",223 reason: "payload does not match declared length",224 });225 }226 let object_len = MEDIA_HEADER_LEN.checked_add(frame.payload.len()).ok_or(227 ProtocolError::InvalidMediaPayloadLength {228 actual: frame.payload.len(),229 maximum: MAX_MEDIA_PAYLOAD_LEN,230 },231 )?;232 let mut object = Vec::with_capacity(object_len);233 object.extend_from_slice(&frame.header.encode()?);234 object.extend_from_slice(&frame.payload);235 Ok(object)236}237238/// Decode exactly one application media frame from an object transport.239/// Header validation, including the declared payload bound, occurs before the240/// payload is copied into its owned buffer.241pub fn decode_media_frame_object(object: &[u8]) -> Result<MediaFrame> {242 if object.len() < MEDIA_HEADER_LEN {243 return Err(ProtocolError::InvalidMessage {244 message_type: "media frame object",245 reason: "object ended before the media header",246 });247 }248 let wire: &[u8; MEDIA_HEADER_LEN] = object[..MEDIA_HEADER_LEN]249 .try_into()250 .expect("fixed media header slice");251 let header = MediaFrameHeader::decode(wire)?;252 let expected_len = MEDIA_HEADER_LEN253 .checked_add(header.payload_len as usize)254 .ok_or(ProtocolError::InvalidMediaPayloadLength {255 actual: header.payload_len as usize,256 maximum: MAX_MEDIA_PAYLOAD_LEN,257 })?;258 if object.len() != expected_len {259 return Err(ProtocolError::InvalidMessage {260 message_type: "media frame object",261 reason: "object length does not match declared payload",262 });263 }264 MediaFrame::new(header, &object[MEDIA_HEADER_LEN..])265}266267impl MediaFrame {268 pub fn new(header: MediaFrameHeader, payload: impl Into<Arc<[u8]>>) -> Result<Self> {269 header.validate()?;270 let payload = payload.into();271 if payload.len() != header.payload_len as usize {272 return Err(ProtocolError::InvalidMessage {273 message_type: "media frame",274 reason: "payload does not match declared length",275 });276 }277 Ok(Self { header, payload })278 }279}280281282mod tests {283 use super::*;284285 fn header() -> MediaFrameHeader {286 MediaFrameHeader::h264(287 1280,288 800,289 4,290 0x0102_0304_0506_0708,291 0x1112_1314_1516_1718,292 -2,293 FrameFlags::KEYFRAME.union(FrameFlags::CODEC_CONFIG),294 )295 .unwrap()296 }297298 299 fn media_header_golden_vector() {300 assert_eq!(301 header().encode().unwrap(),302 [303 0x53, 0x47, 0x56, 0x31, // SGV1304 0x00, 0x01, 0x30, 0x01, // version, length, H.264305 0x03, 0x00, 0x00, 0x00, // flags, reserved306 0x05, 0x00, 0x03, 0x20, // 1280x800307 0x00, 0x00, 0x00, 0x04, // payload length308 0x00, 0x00, 0x00, 0x00, // reserved309 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16,310 0x17, 0x18, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe,311 ]312 );313 }314315 316 fn header_round_trips() {317 let expected = header();318 assert_eq!(319 MediaFrameHeader::decode(&expected.encode().unwrap()).unwrap(),320 expected321 );322 }323324 325 fn malformed_headers_fail_closed() {326 let valid = header().encode().unwrap();327 let cases: &[(usize, u8)] = &[328 (0, 0), // magic329 (5, 2), // version330 (6, 47), // header length331 (7, 9), // codec332 (8, 128), // flags333 (9, 1), // reserved334 ];335 for &(offset, value) in cases {336 let mut malformed = valid;337 malformed[offset] = value;338 assert!(339 MediaFrameHeader::decode(&malformed).is_err(),340 "offset {offset}"341 );342 }343 }344345 346 fn dimensions_and_payload_are_bounded() {347 assert!(MediaFrameHeader::h264(0, 800, 1, 0, 0, 0, FrameFlags::NONE).is_err());348 assert!(MediaFrameHeader::h264(8192, 8192, 1, 0, 0, 0, FrameFlags::NONE).is_err());349 assert!(MediaFrameHeader::h264(1, 1, 0, 0, 0, 0, FrameFlags::NONE).is_err());350 assert!(351 MediaFrameHeader::h264(1, 1, MAX_MEDIA_PAYLOAD_LEN + 1, 0, 0, 0, FrameFlags::NONE,)352 .is_err()353 );354 }355356 357 fn object_transport_envelope_round_trips_exactly() {358 let frame = MediaFrame::new(header(), vec![0, 0, 0, 1]).unwrap();359 let object = encode_media_frame_object(&frame).unwrap();360 assert_eq!(object.len(), MEDIA_HEADER_LEN + frame.payload.len());361 assert_eq!(decode_media_frame_object(&object).unwrap(), frame);362363 let mut trailing = object.clone();364 trailing.push(0);365 assert!(decode_media_frame_object(&trailing).is_err());366 assert!(decode_media_frame_object(&object[..MEDIA_HEADER_LEN - 1]).is_err());367 }368}