#![forbid(unsafe_code)]
use bytes::Bytes;
use oximedia_core::{OxiError, OxiResult};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum DataTrackType {
Gps,
Telemetry,
Binary,
Json,
Xml,
}
impl DataTrackType {
#[must_use]
pub const fn mime_type(&self) -> &'static str {
match self {
Self::Gps => "application/x-gps",
Self::Telemetry => "application/x-telemetry",
Self::Binary => "application/octet-stream",
Self::Json => "application/json",
Self::Xml => "application/xml",
}
}
}
#[derive(Debug, Clone)]
pub struct DataSample {
pub timestamp: i64,
pub data: Bytes,
pub duration: Option<i64>,
}
impl DataSample {
#[must_use]
pub const fn new(timestamp: i64, data: Bytes) -> Self {
Self {
timestamp,
data,
duration: None,
}
}
#[must_use]
pub const fn with_duration(mut self, duration: i64) -> Self {
self.duration = Some(duration);
self
}
#[must_use]
pub fn size(&self) -> usize {
self.data.len()
}
}
#[derive(Debug, Clone)]
pub struct DataTrack {
pub track_type: DataTrackType,
pub track_id: u32,
samples: Vec<DataSample>,
pub codec_id: Option<String>,
pub codec_private: Option<Bytes>,
}
impl DataTrack {
#[must_use]
pub const fn new(track_type: DataTrackType, track_id: u32) -> Self {
Self {
track_type,
track_id,
samples: Vec::new(),
codec_id: None,
codec_private: None,
}
}
#[must_use]
pub fn with_codec_id(mut self, codec_id: impl Into<String>) -> Self {
self.codec_id = Some(codec_id.into());
self
}
#[must_use]
pub fn with_codec_private(mut self, data: Bytes) -> Self {
self.codec_private = Some(data);
self
}
pub fn add_sample(&mut self, sample: DataSample) {
self.samples.push(sample);
}
#[must_use]
pub fn samples(&self) -> &[DataSample] {
&self.samples
}
#[must_use]
pub fn get_sample_at(&self, timestamp: i64) -> Option<&DataSample> {
self.samples.iter().rev().find(|s| s.timestamp <= timestamp)
}
#[must_use]
pub fn sample_count(&self) -> usize {
self.samples.len()
}
#[must_use]
pub fn total_size(&self) -> usize {
self.samples.iter().map(DataSample::size).sum()
}
pub fn validate(&self) -> OxiResult<()> {
let mut last_timestamp = None;
for sample in &self.samples {
if let Some(last) = last_timestamp {
if sample.timestamp < last {
return Err(OxiError::InvalidData(
"Timestamps are not monotonically increasing".into(),
));
}
}
last_timestamp = Some(sample.timestamp);
}
Ok(())
}
}
pub struct DataTrackBuilder {
track: DataTrack,
}
impl DataTrackBuilder {
#[must_use]
pub const fn new(track_type: DataTrackType, track_id: u32) -> Self {
Self {
track: DataTrack::new(track_type, track_id),
}
}
pub fn codec_id(&mut self, codec_id: impl Into<String>) -> &mut Self {
self.track.codec_id = Some(codec_id.into());
self
}
pub fn codec_private(&mut self, data: Bytes) -> &mut Self {
self.track.codec_private = Some(data);
self
}
pub fn add_sample(&mut self, sample: DataSample) -> &mut Self {
self.track.add_sample(sample);
self
}
#[must_use]
pub fn build(self) -> DataTrack {
self.track
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_data_track_type() {
assert_eq!(DataTrackType::Gps.mime_type(), "application/x-gps");
assert_eq!(DataTrackType::Json.mime_type(), "application/json");
}
#[test]
fn test_data_sample() {
let data = Bytes::from_static(b"test");
let sample = DataSample::new(1000, data).with_duration(100);
assert_eq!(sample.timestamp, 1000);
assert_eq!(sample.duration, Some(100));
assert_eq!(sample.size(), 4);
}
#[test]
fn test_data_track() {
let mut track = DataTrack::new(DataTrackType::Gps, 1);
let sample1 = DataSample::new(0, Bytes::from_static(b"gps1"));
let sample2 = DataSample::new(1000, Bytes::from_static(b"gps2"));
track.add_sample(sample1);
track.add_sample(sample2);
assert_eq!(track.sample_count(), 2);
assert_eq!(track.total_size(), 8);
let found = track.get_sample_at(500);
assert!(found.is_some());
assert_eq!(found.expect("operation should succeed").timestamp, 0);
assert!(track.validate().is_ok());
}
#[test]
fn test_data_track_builder() {
let mut builder = DataTrackBuilder::new(DataTrackType::Telemetry, 1);
builder
.codec_id("telemetry/v1")
.add_sample(DataSample::new(0, Bytes::from_static(b"data1")))
.add_sample(DataSample::new(1000, Bytes::from_static(b"data2")));
let track = builder.build();
assert_eq!(track.track_id, 1);
assert_eq!(track.codec_id, Some("telemetry/v1".into()));
assert_eq!(track.sample_count(), 2);
}
}