use crate::error::{PackagerError, PackagerResult};
use std::time::Duration;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ByteRangeEntry {
pub index: u32,
pub offset: u64,
pub length: u64,
pub duration: Duration,
}
impl ByteRangeEntry {
#[must_use]
pub fn new(index: u32, offset: u64, length: u64, duration: Duration) -> Self {
Self {
index,
offset,
length,
duration,
}
}
#[must_use]
pub fn end_byte(&self) -> u64 {
self.offset + self.length
}
#[must_use]
pub fn hls_byterange_attr(&self, force_offset: bool) -> String {
if self.offset == 0 && !force_offset {
format!("{}", self.length)
} else {
format!("{}@{}", self.length, self.offset)
}
}
#[must_use]
pub fn dash_media_range(&self) -> String {
format!("{}-{}", self.offset, self.end_byte().saturating_sub(1))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InitSegmentRange {
pub length: u64,
}
impl InitSegmentRange {
#[must_use]
pub fn new(length: u64) -> Self {
Self { length }
}
#[must_use]
pub fn dash_index_range(&self) -> String {
format!("0-{}", self.length.saturating_sub(1))
}
}
#[derive(Debug, Clone)]
pub struct ByteRangeIndex {
init: InitSegmentRange,
segments: Vec<ByteRangeEntry>,
cursor: u64,
}
impl ByteRangeIndex {
#[must_use]
pub fn new(init_length: u64) -> Self {
Self {
init: InitSegmentRange::new(init_length),
segments: Vec::new(),
cursor: init_length,
}
}
pub fn append_segment(&mut self, segment_length: u64, duration: Duration) -> ByteRangeEntry {
let index = self.segments.len() as u32;
let entry = ByteRangeEntry::new(index, self.cursor, segment_length, duration);
self.cursor += segment_length;
self.segments.push(entry.clone());
entry
}
#[must_use]
pub fn init(&self) -> &InitSegmentRange {
&self.init
}
#[must_use]
pub fn segments(&self) -> &[ByteRangeEntry] {
&self.segments
}
#[must_use]
pub fn total_bytes(&self) -> u64 {
self.cursor
}
#[must_use]
pub fn segment_count(&self) -> usize {
self.segments.len()
}
#[must_use]
pub fn get(&self, index: u32) -> Option<&ByteRangeEntry> {
self.segments.get(index as usize)
}
#[must_use]
pub fn total_duration(&self) -> Duration {
self.segments.iter().map(|s| s.duration).sum()
}
pub fn validate(&self) -> PackagerResult<()> {
let mut expected_offset = self.init.length;
for (i, entry) in self.segments.iter().enumerate() {
if entry.offset != expected_offset {
return Err(PackagerError::PackagingError(format!(
"Segment {i}: expected offset {expected_offset}, found {}",
entry.offset
)));
}
if entry.length == 0 {
return Err(PackagerError::PackagingError(format!(
"Segment {i}: length must not be zero"
)));
}
expected_offset += entry.length;
}
if expected_offset != self.cursor {
return Err(PackagerError::PackagingError(format!(
"Cursor mismatch: expected {expected_offset}, found {}",
self.cursor
)));
}
Ok(())
}
#[must_use]
pub fn to_hls_segments(&self, container_uri: &str) -> String {
let mut out = String::new();
for entry in &self.segments {
let secs = entry.duration.as_secs_f64();
out.push_str(&format!("#EXTINF:{secs:.6},\n"));
out.push_str(&format!(
"#EXT-X-BYTERANGE:{}\n",
entry.hls_byterange_attr(true)
));
out.push_str(container_uri);
out.push('\n');
}
out
}
#[must_use]
pub fn to_dash_segment_base(&self, timescale: u32) -> String {
let init_end = self.init.length.saturating_sub(1);
format!(
r#"<SegmentBase indexRange="0-{init_end}" timescale="{timescale}"><Initialization range="0-{init_end}"/></SegmentBase>"#
)
}
}
#[derive(Debug, Default)]
pub struct ByteRangeWriter {
data: Vec<u8>,
index: Option<ByteRangeIndex>,
}
impl ByteRangeWriter {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn write_init(&mut self, init_bytes: &[u8]) -> PackagerResult<()> {
if self.index.is_some() {
return Err(PackagerError::PackagingError(
"init segment already written".into(),
));
}
self.data.extend_from_slice(init_bytes);
self.index = Some(ByteRangeIndex::new(init_bytes.len() as u64));
Ok(())
}
pub fn append_segment(
&mut self,
segment_bytes: &[u8],
duration: Duration,
) -> PackagerResult<ByteRangeEntry> {
let idx = self.index.as_mut().ok_or_else(|| {
PackagerError::PackagingError("init segment must be written first".into())
})?;
let entry = idx.append_segment(segment_bytes.len() as u64, duration);
self.data.extend_from_slice(segment_bytes);
Ok(entry)
}
#[must_use]
pub fn data(&self) -> &[u8] {
&self.data
}
#[must_use]
pub fn index(&self) -> Option<&ByteRangeIndex> {
self.index.as_ref()
}
pub fn finish(self) -> PackagerResult<(Vec<u8>, ByteRangeIndex)> {
let index = self.index.ok_or_else(|| {
PackagerError::PackagingError("cannot finish: init segment was never written".into())
})?;
index.validate()?;
Ok((self.data, index))
}
}
#[cfg(test)]
mod tests {
use super::*;
fn dur_secs(s: u64) -> Duration {
Duration::from_secs(s)
}
#[test]
fn test_entry_end_byte() {
let e = ByteRangeEntry::new(0, 100, 200, dur_secs(6));
assert_eq!(e.end_byte(), 300);
}
#[test]
fn test_entry_hls_byterange_attr_with_offset() {
let e = ByteRangeEntry::new(1, 1024, 512, dur_secs(6));
assert_eq!(e.hls_byterange_attr(false), "512@1024");
}
#[test]
fn test_entry_hls_byterange_attr_no_offset_at_zero() {
let e = ByteRangeEntry::new(0, 0, 512, dur_secs(6));
assert_eq!(e.hls_byterange_attr(false), "512");
}
#[test]
fn test_entry_hls_byterange_attr_force_offset_at_zero() {
let e = ByteRangeEntry::new(0, 0, 512, dur_secs(6));
assert_eq!(e.hls_byterange_attr(true), "512@0");
}
#[test]
fn test_entry_dash_media_range() {
let e = ByteRangeEntry::new(0, 0, 1024, dur_secs(6));
assert_eq!(e.dash_media_range(), "0-1023");
}
#[test]
fn test_entry_dash_media_range_nonzero_offset() {
let e = ByteRangeEntry::new(1, 1024, 512, dur_secs(6));
assert_eq!(e.dash_media_range(), "1024-1535");
}
#[test]
fn test_init_range_dash_index_range() {
let r = InitSegmentRange::new(512);
assert_eq!(r.dash_index_range(), "0-511");
}
#[test]
fn test_init_range_length_one() {
let r = InitSegmentRange::new(1);
assert_eq!(r.dash_index_range(), "0-0");
}
#[test]
fn test_index_new() {
let idx = ByteRangeIndex::new(512);
assert_eq!(idx.total_bytes(), 512);
assert_eq!(idx.segment_count(), 0);
assert!(idx.validate().is_ok());
}
#[test]
fn test_index_append_segment_offsets() {
let mut idx = ByteRangeIndex::new(256);
let e0 = idx.append_segment(1000, dur_secs(6));
let e1 = idx.append_segment(900, dur_secs(6));
assert_eq!(e0.offset, 256);
assert_eq!(e0.length, 1000);
assert_eq!(e1.offset, 256 + 1000);
assert_eq!(e1.length, 900);
assert_eq!(idx.total_bytes(), 256 + 1000 + 900);
}
#[test]
fn test_index_segment_count() {
let mut idx = ByteRangeIndex::new(100);
idx.append_segment(50, dur_secs(2));
idx.append_segment(60, dur_secs(2));
assert_eq!(idx.segment_count(), 2);
}
#[test]
fn test_index_get() {
let mut idx = ByteRangeIndex::new(100);
idx.append_segment(400, dur_secs(6));
let entry = idx.get(0).expect("segment 0 should exist");
assert_eq!(entry.offset, 100);
assert_eq!(entry.length, 400);
}
#[test]
fn test_index_get_out_of_bounds() {
let idx = ByteRangeIndex::new(100);
assert!(idx.get(0).is_none());
}
#[test]
fn test_index_total_duration() {
let mut idx = ByteRangeIndex::new(100);
idx.append_segment(400, dur_secs(6));
idx.append_segment(380, dur_secs(4));
assert_eq!(idx.total_duration(), dur_secs(10));
}
#[test]
fn test_index_validate_ok() {
let mut idx = ByteRangeIndex::new(256);
idx.append_segment(1000, dur_secs(6));
idx.append_segment(900, dur_secs(6));
assert!(idx.validate().is_ok());
}
#[test]
fn test_index_to_hls_segments() {
let mut idx = ByteRangeIndex::new(256);
idx.append_segment(1000, dur_secs(6));
idx.append_segment(900, dur_secs(6));
let output = idx.to_hls_segments("media.mp4");
assert!(output.contains("#EXTINF:6"));
assert!(output.contains("#EXT-X-BYTERANGE:1000@256"));
assert!(output.contains("#EXT-X-BYTERANGE:900@1256"));
assert_eq!(output.matches("media.mp4").count(), 2);
}
#[test]
fn test_index_to_dash_segment_base() {
let idx = ByteRangeIndex::new(512);
let xml = idx.to_dash_segment_base(90_000);
assert!(xml.contains("indexRange=\"0-511\""));
assert!(xml.contains("timescale=\"90000\""));
assert!(xml.contains("<Initialization"));
}
#[test]
fn test_writer_no_init_no_index() {
let writer = ByteRangeWriter::new();
assert!(writer.index().is_none());
assert!(writer.data().is_empty());
}
#[test]
fn test_writer_write_init() {
let mut writer = ByteRangeWriter::new();
writer
.write_init(b"init_data")
.expect("write_init should succeed");
assert!(writer.index().is_some());
assert_eq!(writer.data(), b"init_data");
}
#[test]
fn test_writer_double_init_fails() {
let mut writer = ByteRangeWriter::new();
writer
.write_init(b"init")
.expect("first write should succeed");
assert!(writer.write_init(b"init2").is_err());
}
#[test]
fn test_writer_append_without_init_fails() {
let mut writer = ByteRangeWriter::new();
assert!(writer.append_segment(b"seg", dur_secs(6)).is_err());
}
#[test]
fn test_writer_append_segment() {
let mut writer = ByteRangeWriter::new();
writer
.write_init(&[0u8; 64])
.expect("write_init should succeed");
let entry = writer
.append_segment(&[1u8; 128], dur_secs(6))
.expect("append should succeed");
assert_eq!(entry.offset, 64);
assert_eq!(entry.length, 128);
assert_eq!(writer.data().len(), 64 + 128);
}
#[test]
fn test_writer_finish() {
let mut writer = ByteRangeWriter::new();
writer
.write_init(&[0u8; 64])
.expect("write_init should succeed");
writer
.append_segment(&[1u8; 128], dur_secs(6))
.expect("append should succeed");
writer
.append_segment(&[2u8; 96], dur_secs(4))
.expect("append should succeed");
let (data, index) = writer.finish().expect("finish should succeed");
assert_eq!(data.len(), 64 + 128 + 96);
assert_eq!(index.segment_count(), 2);
assert!(index.validate().is_ok());
}
#[test]
fn test_writer_finish_without_init_fails() {
let writer = ByteRangeWriter::new();
assert!(writer.finish().is_err());
}
#[test]
fn test_writer_hls_playlist_integration() {
let mut writer = ByteRangeWriter::new();
let init = vec![0u8; 100];
let seg0 = vec![1u8; 500];
let seg1 = vec![2u8; 400];
writer.write_init(&init).expect("write_init should succeed");
writer
.append_segment(&seg0, dur_secs(6))
.expect("append should succeed");
writer
.append_segment(&seg1, dur_secs(6))
.expect("append should succeed");
let (_, index) = writer.finish().expect("finish should succeed");
let hls = index.to_hls_segments("video.mp4");
assert_eq!(hls.matches("video.mp4").count(), 2);
assert!(hls.contains("500@100"));
assert!(hls.contains("400@600"));
}
}