#[derive(Debug, Clone)]
pub struct CmafConfig {
pub fragment_duration_ms: u32,
pub segment_duration_ms: u32,
pub use_chunked_transfer: bool,
pub low_latency: bool,
}
impl Default for CmafConfig {
fn default() -> Self {
Self {
fragment_duration_ms: 1_000,
segment_duration_ms: 6_000,
use_chunked_transfer: false,
low_latency: false,
}
}
}
#[derive(Debug, Clone)]
pub struct CmafTrack {
pub track_id: u32,
pub codec: String,
pub bitrate_kbps: u32,
pub is_video: bool,
}
impl CmafTrack {
#[must_use]
pub fn new(track_id: u32, codec: impl Into<String>, bitrate_kbps: u32, is_video: bool) -> Self {
Self {
track_id,
codec: codec.into(),
bitrate_kbps,
is_video,
}
}
#[must_use]
pub fn media_type(&self) -> &'static str {
if self.is_video {
"video"
} else {
"audio"
}
}
}
#[derive(Debug, Clone)]
pub struct CmafManifest {
pub tracks: Vec<CmafTrack>,
pub base_url: String,
pub presentation_duration_ms: Option<u64>,
}
impl CmafManifest {
#[must_use]
pub fn new(tracks: Vec<CmafTrack>, base_url: impl Into<String>) -> Self {
Self {
tracks,
base_url: base_url.into(),
presentation_duration_ms: None,
}
}
#[must_use]
pub fn with_duration_ms(mut self, duration_ms: u64) -> Self {
self.presentation_duration_ms = Some(duration_ms);
self
}
}
#[must_use]
pub fn generate_cmaf_manifest(tracks: &[CmafTrack], config: &CmafConfig, base_url: &str) -> String {
let mut out = String::new();
out.push_str("#CMAF-MANIFEST\n");
out.push_str(&format!(
"#FRAGMENT-DURATION:{}\n",
config.fragment_duration_ms
));
out.push_str(&format!(
"#SEGMENT-DURATION:{}\n",
config.segment_duration_ms
));
if config.low_latency {
out.push_str("#LOW-LATENCY:YES\n");
}
if config.use_chunked_transfer {
out.push_str("#CHUNKED-TRANSFER:YES\n");
}
out.push_str(&format!("#BASE-URL:{base_url}\n"));
out.push('\n');
for track in tracks {
out.push_str(&format!(
"TRACK id={} type={} codec={} bitrate={}kbps\n",
track.track_id,
track.media_type(),
track.codec,
track.bitrate_kbps,
));
out.push_str(&format!(
" INIT {base_url}/track{}/init.cmf{}\n",
track.track_id,
if track.is_video { "v" } else { "a" },
));
out.push_str(&format!(
" SEGMENTS {base_url}/track{}/seg-$Number$.cmf{}\n",
track.track_id,
if track.is_video { "v" } else { "a" },
));
}
out
}
#[must_use]
pub fn cmaf_track_url(track: &CmafTrack, segment_num: u32) -> String {
let ext = if track.is_video { "cmfv" } else { "cmfa" };
format!("track{}/seg-{:05}.{}", track.track_id, segment_num, ext)
}
#[derive(Debug, Clone)]
pub struct CmafSegment {
pub track_id: u32,
pub sequence: u32,
pub duration_ms: u32,
pub data: Vec<u8>,
}
impl CmafSegment {
#[must_use]
pub fn new(track_id: u32, seq: u32, duration_ms: u32) -> Self {
Self {
track_id,
sequence: seq,
duration_ms,
data: Vec::new(),
}
}
#[must_use]
pub fn size_bytes(&self) -> usize {
self.data.len()
}
pub fn push_bytes(&mut self, bytes: &[u8]) {
self.data.extend_from_slice(bytes);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_cmaf_config_default() {
let c = CmafConfig::default();
assert_eq!(c.fragment_duration_ms, 1_000);
assert_eq!(c.segment_duration_ms, 6_000);
assert!(!c.low_latency);
assert!(!c.use_chunked_transfer);
}
#[test]
fn test_cmaf_track_media_type_video() {
let t = CmafTrack::new(1, "av01", 3_000, true);
assert_eq!(t.media_type(), "video");
}
#[test]
fn test_cmaf_track_media_type_audio() {
let t = CmafTrack::new(2, "opus", 128, false);
assert_eq!(t.media_type(), "audio");
}
#[test]
fn test_cmaf_track_fields() {
let t = CmafTrack::new(5, "vp09", 1_500, true);
assert_eq!(t.track_id, 5);
assert_eq!(t.codec, "vp09");
assert_eq!(t.bitrate_kbps, 1_500);
assert!(t.is_video);
}
#[test]
fn test_generate_cmaf_manifest_contains_base_url() {
let tracks = vec![CmafTrack::new(1, "av01", 2_000, true)];
let config = CmafConfig::default();
let manifest = generate_cmaf_manifest(&tracks, &config, "https://cdn.example.com");
assert!(manifest.contains("https://cdn.example.com"));
}
#[test]
fn test_generate_cmaf_manifest_contains_track_info() {
let tracks = vec![
CmafTrack::new(1, "av01", 2_000, true),
CmafTrack::new(2, "opus", 128, false),
];
let config = CmafConfig::default();
let manifest = generate_cmaf_manifest(&tracks, &config, "https://cdn.example.com");
assert!(manifest.contains("type=video"));
assert!(manifest.contains("type=audio"));
assert!(manifest.contains("codec=av01"));
assert!(manifest.contains("codec=opus"));
}
#[test]
fn test_generate_cmaf_manifest_low_latency_flag() {
let tracks = vec![];
let config = CmafConfig {
low_latency: true,
..CmafConfig::default()
};
let manifest = generate_cmaf_manifest(&tracks, &config, "http://localhost");
assert!(manifest.contains("#LOW-LATENCY:YES"));
}
#[test]
fn test_generate_cmaf_manifest_chunked_transfer_flag() {
let config = CmafConfig {
use_chunked_transfer: true,
..CmafConfig::default()
};
let manifest = generate_cmaf_manifest(&[], &config, "http://localhost");
assert!(manifest.contains("#CHUNKED-TRANSFER:YES"));
}
#[test]
fn test_cmaf_track_url_video() {
let track = CmafTrack::new(3, "av01", 4_000, true);
let url = cmaf_track_url(&track, 0);
assert!(url.contains("track3"));
assert!(url.ends_with("cmfv"));
}
#[test]
fn test_cmaf_track_url_audio() {
let track = CmafTrack::new(4, "opus", 64, false);
let url = cmaf_track_url(&track, 7);
assert!(url.contains("track4"));
assert!(url.ends_with("cmfa"));
assert!(url.contains("00007"));
}
#[test]
fn test_cmaf_segment_new() {
let seg = CmafSegment::new(1, 0, 6_000);
assert_eq!(seg.track_id, 1);
assert_eq!(seg.sequence, 0);
assert_eq!(seg.duration_ms, 6_000);
assert_eq!(seg.size_bytes(), 0);
}
#[test]
fn test_cmaf_segment_push_bytes() {
let mut seg = CmafSegment::new(1, 0, 1_000);
seg.push_bytes(b"hello");
seg.push_bytes(b" world");
assert_eq!(seg.size_bytes(), 11);
assert_eq!(&seg.data, b"hello world");
}
#[test]
fn test_cmaf_manifest_with_duration() {
let m = CmafManifest::new(vec![], "http://example.com").with_duration_ms(120_000);
assert_eq!(m.presentation_duration_ms, Some(120_000));
}
}