use crate::error::{Error, Result};
#[derive(Debug)]
pub struct Playlist {
pub version: String,
pub play_items: Vec<PlayItem>,
pub streams: Vec<StreamEntry>,
}
#[derive(Debug)]
pub struct PlayItem {
pub clip_id: String,
pub in_time: u32,
pub out_time: u32,
pub connection_condition: u8,
}
#[derive(Debug, Clone)]
pub struct StreamEntry {
pub stream_type: u8,
pub pid: u16,
pub coding_type: u8,
pub video_format: u8,
pub video_rate: u8,
pub audio_format: u8,
pub audio_rate: u8,
pub language: String,
pub dynamic_range: u8,
pub color_space: u8,
pub secondary: bool,
}
pub fn parse(data: &[u8]) -> Result<Playlist> {
if data.len() < 40 {
return Err(Error::MplsParse);
}
if &data[0..4] != b"MPLS" {
return Err(Error::MplsParse);
}
let version = String::from_utf8_lossy(&data[4..8]).to_string();
let playlist_start = u32::from_be_bytes([data[8], data[9], data[10], data[11]]) as usize;
if playlist_start + 10 > data.len() {
return Err(Error::MplsParse);
}
let pl = &data[playlist_start..];
let num_play_items = u16::from_be_bytes([pl[6], pl[7]]) as usize;
let mut play_items = Vec::with_capacity(num_play_items);
let mut streams = Vec::new();
let mut pos = 10;
for item_idx in 0..num_play_items {
if pos + 2 > pl.len() { break; }
let item_length = u16::from_be_bytes([pl[pos], pl[pos + 1]]) as usize;
if pos + 2 + item_length > pl.len() { break; }
let item = &pl[pos + 2..pos + 2 + item_length];
if item.len() < 20 { pos += 2 + item_length; continue; }
let clip_id = String::from_utf8_lossy(&item[0..5]).to_string();
let connection_condition = item[9] & 0x0F;
let in_time = u32::from_be_bytes([item[12], item[13], item[14], item[15]]);
let out_time = u32::from_be_bytes([item[16], item[17], item[18], item[19]]);
const STN_OFFSET: usize = 32;
if item_idx == 0 && item.len() > STN_OFFSET + 16 {
let n_video = item[STN_OFFSET + 4] as usize;
let n_audio = item[STN_OFFSET + 5] as usize;
let n_pg = item[STN_OFFSET + 6] as usize;
let n_ig = item[STN_OFFSET + 7] as usize;
let n_sec_audio = item[STN_OFFSET + 8] as usize;
let n_sec_video = item[STN_OFFSET + 9] as usize;
let n_pip_pg = item[STN_OFFSET + 10] as usize;
let n_dv = item[STN_OFFSET + 11] as usize;
let mut spos = STN_OFFSET + 16;
for _ in 0..n_video {
if let Some((entry, next)) = parse_stream_entry(item, spos, 1) {
streams.push(entry);
spos = next;
} else { break; }
}
for _ in 0..n_audio {
if let Some((entry, next)) = parse_stream_entry(item, spos, 2) {
streams.push(entry);
spos = next;
} else { break; }
}
for _ in 0..n_pg {
if let Some((entry, next)) = parse_stream_entry(item, spos, 3) {
streams.push(entry);
spos = next;
} else { break; }
}
for _ in 0..n_ig {
if let Some((_, next)) = parse_stream_entry(item, spos, 4) {
spos = next;
} else { break; }
}
for _ in 0..n_sec_audio {
if let Some((mut entry, next)) = parse_stream_entry(item, spos, 2) {
entry.stream_type = 5;
entry.secondary = true;
streams.push(entry);
if next < item.len() {
let n_refs = item[next] as usize;
spos = next + 2 + n_refs + (n_refs % 2);
} else { spos = next; }
} else { break; }
}
for _ in 0..n_sec_video {
if let Some((mut entry, next)) = parse_stream_entry(item, spos, 1) {
entry.stream_type = 6;
entry.secondary = true;
streams.push(entry);
if next + 2 < item.len() {
let n_arefs = item[next] as usize;
let after_arefs = next + 2 + n_arefs + (n_arefs % 2);
if after_arefs < item.len() {
let n_prefs = item[after_arefs] as usize;
spos = after_arefs + 2 + n_prefs + (n_prefs % 2);
} else { spos = after_arefs; }
} else { spos = next; }
} else { break; }
}
for _ in 0..n_pip_pg {
if let Some((mut entry, next)) = parse_stream_entry(item, spos, 3) {
entry.secondary = true;
streams.push(entry);
if next < item.len() {
let n_refs = item[next] as usize;
spos = next + 2 + n_refs + (n_refs % 2);
} else { spos = next; }
} else { break; }
}
for _ in 0..n_dv {
if let Some((mut entry, next)) = parse_stream_entry(item, spos, 1) {
entry.stream_type = 7;
entry.secondary = true;
streams.push(entry);
spos = next;
} else { break; }
}
}
play_items.push(PlayItem {
clip_id,
in_time,
out_time,
connection_condition,
});
pos += 2 + item_length;
}
Ok(Playlist {
version,
play_items,
streams,
})
}
fn parse_stream_entry(item: &[u8], pos: usize, stream_type: u8) -> Option<(StreamEntry, usize)> {
if pos + 2 > item.len() { return None; }
let se_len = item[pos] as usize;
let se_end = pos + 1 + se_len;
if se_end > item.len() { return None; }
let pid = if item[pos + 1] == 0x01 && pos + 4 <= item.len() {
u16::from_be_bytes([item[pos + 2], item[pos + 3]])
} else {
0
};
if se_end + 2 > item.len() { return None; }
let sa_len = item[se_end] as usize;
let sa_end = se_end + 1 + sa_len;
if sa_end > item.len() || sa_len < 1 { return None; }
let sa = &item[se_end + 1..se_end + 1 + sa_len];
let coding_type = sa[0];
let mut video_format = 0u8;
let mut video_rate = 0u8;
let mut audio_format = 0u8;
let mut audio_rate = 0u8;
let mut dynamic_range = 0u8;
let mut color_space_val = 0u8;
let mut language = String::new();
match stream_type {
1 => {
if sa.len() >= 2 {
video_format = (sa[1] >> 4) & 0x0F;
video_rate = sa[1] & 0x0F;
}
if coding_type == 0x24 && sa.len() > 2 {
dynamic_range = (sa[2] >> 4) & 0x0F;
color_space_val = sa[2] & 0x0F;
}
}
2 => {
if coding_type == 0x90 || coding_type == 0x91 {
if sa.len() >= 4 {
language = String::from_utf8_lossy(&sa[1..4]).to_string();
}
} else {
if sa.len() >= 2 {
audio_format = (sa[1] >> 4) & 0x0F;
audio_rate = sa[1] & 0x0F;
}
if sa.len() >= 5 {
language = String::from_utf8_lossy(&sa[2..5]).to_string();
}
}
}
3 | 4 => {
if sa.len() >= 4 {
language = String::from_utf8_lossy(&sa[1..4]).to_string();
}
}
5 => {
if sa.len() >= 2 {
audio_format = (sa[1] >> 4) & 0x0F;
audio_rate = sa[1] & 0x0F;
}
if sa.len() >= 5 {
language = String::from_utf8_lossy(&sa[2..5]).to_string();
}
}
6 | 7 => {
if sa.len() >= 2 {
video_format = (sa[1] >> 4) & 0x0F;
video_rate = sa[1] & 0x0F;
}
if coding_type == 0x24 && sa.len() > 2 {
dynamic_range = (sa[2] >> 4) & 0x0F;
color_space_val = sa[2] & 0x0F;
}
}
_ => {}
}
Some((StreamEntry {
stream_type,
pid,
coding_type,
video_format,
video_rate,
audio_format,
audio_rate,
language,
dynamic_range,
color_space: color_space_val,
secondary: false,
}, sa_end))
}
#[cfg(test)]
mod tests {
use super::*;
fn build_mpls(
play_items_data: &[(/*clip_id*/&[u8;5], /*conn*/u8, /*in_time*/u32, /*out_time*/u32)],
stn_counts: (u8, u8, u8, u8, u8, u8, u8, u8),
stream_entries: &[Vec<u8>], ) -> Vec<u8> {
let playlist_start: u32 = 40; let mut buf = Vec::new();
buf.extend_from_slice(b"MPLS0200");
buf.extend_from_slice(&playlist_start.to_be_bytes());
buf.extend_from_slice(&[0u8; 28]);
let pl_start = buf.len();
buf.extend_from_slice(&[0u8; 4]); buf.extend_from_slice(&[0u8; 2]); buf.extend_from_slice(&(play_items_data.len() as u16).to_be_bytes());
buf.extend_from_slice(&[0u8; 2]);
for (idx, (clip_id, conn, in_time, out_time)) in play_items_data.iter().enumerate() {
let mut item = Vec::new();
item.extend_from_slice(*clip_id);
item.extend_from_slice(b"M2TS");
item.push(*conn & 0x0F);
item.extend_from_slice(&[0u8; 2]);
item.extend_from_slice(&in_time.to_be_bytes());
item.extend_from_slice(&out_time.to_be_bytes());
item.extend_from_slice(&[0u8; 8]);
item.push(0);
item.push(0);
item.extend_from_slice(&[0u8; 2]);
if idx == 0 {
let stn_header_start = item.len();
item.extend_from_slice(&[0u8; 2]); item.extend_from_slice(&[0u8; 2]); item.push(stn_counts.0); item.push(stn_counts.1); item.push(stn_counts.2); item.push(stn_counts.3); item.push(stn_counts.4); item.push(stn_counts.5); item.push(stn_counts.6); item.push(stn_counts.7); item.extend_from_slice(&[0u8; 4]);
for se in stream_entries {
item.extend_from_slice(se);
}
let stn_len = (item.len() - stn_header_start - 2) as u16;
let stn_len_bytes = stn_len.to_be_bytes();
item[stn_header_start] = stn_len_bytes[0];
item[stn_header_start + 1] = stn_len_bytes[1];
}
let item_length = item.len() as u16;
buf.extend_from_slice(&item_length.to_be_bytes());
buf.extend_from_slice(&item);
}
let pl_len = (buf.len() - pl_start - 4) as u32;
let pl_len_bytes = pl_len.to_be_bytes();
buf[pl_start] = pl_len_bytes[0];
buf[pl_start + 1] = pl_len_bytes[1];
buf[pl_start + 2] = pl_len_bytes[2];
buf[pl_start + 3] = pl_len_bytes[3];
buf
}
fn build_stream_entry_video(pid: u16, coding_type: u8, format: u8, rate: u8, hdr: Option<u8>) -> Vec<u8> {
let mut out = Vec::new();
out.push(3); out.push(0x01); out.extend_from_slice(&pid.to_be_bytes());
let mut attrs = vec![coding_type, (format << 4) | rate];
if let Some(h) = hdr {
attrs.push(h);
}
out.push(attrs.len() as u8); out.extend_from_slice(&attrs);
out
}
fn build_stream_entry_audio(pid: u16, coding_type: u8, ch_layout: u8, sample_rate: u8, lang: &[u8; 3]) -> Vec<u8> {
let mut out = Vec::new();
out.push(3);
out.push(0x01);
out.extend_from_slice(&pid.to_be_bytes());
let attrs = vec![coding_type, (ch_layout << 4) | sample_rate, lang[0], lang[1], lang[2]];
out.push(attrs.len() as u8);
out.extend_from_slice(&attrs);
out
}
fn build_stream_entry_pg(pid: u16, coding_type: u8, lang: &[u8; 3]) -> Vec<u8> {
let mut out = Vec::new();
out.push(3);
out.push(0x01);
out.extend_from_slice(&pid.to_be_bytes());
let attrs = vec![coding_type, lang[0], lang[1], lang[2]];
out.push(attrs.len() as u8);
out.extend_from_slice(&attrs);
out
}
#[test]
fn parse_valid_mpls() {
let in_time: u32 = 90000; let out_time: u32 = 4500000;
let video = build_stream_entry_video(0x1011, 0x1B, 6, 1, None); let audio = build_stream_entry_audio(0x1100, 0x83, 6, 1, b"eng"); let pg = build_stream_entry_pg(0x1200, 0x90, b"eng");
let data = build_mpls(
&[(b"00001", 1, in_time, out_time)],
(1, 1, 1, 0, 0, 0, 0, 0),
&[video, audio, pg],
);
let playlist = parse(&data).expect("should parse valid MPLS");
assert_eq!(playlist.version, "0200");
assert_eq!(playlist.play_items.len(), 1);
assert_eq!(playlist.play_items[0].clip_id, "00001");
assert_eq!(playlist.play_items[0].in_time, in_time);
assert_eq!(playlist.play_items[0].out_time, out_time);
assert_eq!(playlist.play_items[0].connection_condition, 1);
}
#[test]
fn parse_streams() {
let video = build_stream_entry_video(0x1011, 0x24, 8, 1, Some(0x12)); let audio = build_stream_entry_audio(0x1100, 0x83, 6, 1, b"eng");
let pg = build_stream_entry_pg(0x1200, 0x90, b"fra");
let data = build_mpls(
&[(b"00001", 1, 0, 9000000)],
(1, 1, 1, 0, 0, 0, 0, 0),
&[video, audio, pg],
);
let playlist = parse(&data).expect("should parse");
assert_eq!(playlist.streams.len(), 3);
let v = &playlist.streams[0];
assert_eq!(v.stream_type, 1);
assert_eq!(v.pid, 0x1011);
assert_eq!(v.coding_type, 0x24); assert_eq!(v.video_format, 8); assert_eq!(v.video_rate, 1); assert_eq!(v.dynamic_range, 1); assert_eq!(v.color_space, 2); assert!(!v.secondary);
let a = &playlist.streams[1];
assert_eq!(a.stream_type, 2);
assert_eq!(a.pid, 0x1100);
assert_eq!(a.coding_type, 0x83); assert_eq!(a.audio_format, 6); assert_eq!(a.audio_rate, 1); assert_eq!(a.language, "eng");
assert!(!a.secondary);
let s = &playlist.streams[2];
assert_eq!(s.stream_type, 3);
assert_eq!(s.pid, 0x1200);
assert_eq!(s.coding_type, 0x90); assert_eq!(s.language, "fra");
assert!(!s.secondary);
}
#[test]
fn parse_invalid_magic() {
let mut data = build_mpls(
&[(b"00001", 1, 0, 9000000)],
(0, 0, 0, 0, 0, 0, 0, 0),
&[],
);
data[0] = b'X';
data[1] = b'X';
data[2] = b'X';
data[3] = b'X';
assert!(parse(&data).is_err());
}
#[test]
fn parse_truncated() {
assert!(parse(&[0u8; 10]).is_err());
assert!(parse(b"MPLS0200").is_err());
assert!(parse(&[0u8; 39]).is_err());
}
#[test]
fn parse_multiple_play_items() {
let video = build_stream_entry_video(0x1011, 0x1B, 6, 1, None);
let data = build_mpls(
&[
(b"00001", 1, 90000, 4500000),
(b"00002", 5, 4500000, 9000000),
(b"00003", 6, 9000000, 13500000),
],
(1, 0, 0, 0, 0, 0, 0, 0),
&[video],
);
let playlist = parse(&data).expect("should parse multiple play items");
assert_eq!(playlist.play_items.len(), 3);
assert_eq!(playlist.play_items[0].clip_id, "00001");
assert_eq!(playlist.play_items[0].connection_condition, 1);
assert_eq!(playlist.play_items[1].clip_id, "00002");
assert_eq!(playlist.play_items[1].connection_condition, 5);
assert_eq!(playlist.play_items[1].in_time, 4500000);
assert_eq!(playlist.play_items[2].clip_id, "00003");
assert_eq!(playlist.play_items[2].connection_condition, 6);
assert_eq!(playlist.play_items[2].out_time, 13500000);
}
#[test]
fn parse_secondary_streams() {
let video = build_stream_entry_video(0x1011, 0x1B, 6, 1, None);
let sec_audio_se = build_stream_entry_audio(0x1A00, 0x83, 3, 1, b"eng");
let mut sec_audio_with_refs = sec_audio_se;
sec_audio_with_refs.push(0); sec_audio_with_refs.push(0);
let sec_video_se = build_stream_entry_video(0x1B00, 0x1B, 4, 1, None);
let mut sec_video_with_refs = sec_video_se;
sec_video_with_refs.push(0); sec_video_with_refs.push(0); sec_video_with_refs.push(0); sec_video_with_refs.push(0);
let data = build_mpls(
&[(b"00001", 1, 0, 9000000)],
(1, 0, 0, 0, 1, 1, 0, 0), &[video, sec_audio_with_refs, sec_video_with_refs],
);
let playlist = parse(&data).expect("should parse secondary streams");
assert_eq!(playlist.streams.len(), 3);
assert_eq!(playlist.streams[0].stream_type, 1);
assert!(!playlist.streams[0].secondary);
assert_eq!(playlist.streams[1].stream_type, 5);
assert!(playlist.streams[1].secondary);
assert_eq!(playlist.streams[1].pid, 0x1A00);
assert_eq!(playlist.streams[2].stream_type, 6);
assert!(playlist.streams[2].secondary);
assert_eq!(playlist.streams[2].pid, 0x1B00);
}
}