use honzo_core::HonzoError;
use serde::{Deserialize, Serialize};
pub const NAMESPACE: &str = super::SYNC_NAMESPACE;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
#[repr(u8)]
pub enum SyncType {
#[default]
Audio = 0,
Video = 1,
Animation = 2,
Page = 3,
Custom = 255,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyncCue {
#[serde(default)]
pub sync_type: SyncType,
pub chunk_id: u32,
pub offset: u32,
pub timestamp_ms: u64,
#[serde(default)]
pub media_id: Option<String>,
#[serde(default)]
pub duration_ms: Option<u64>,
#[serde(default)]
pub metadata: Option<SyncMetadata>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum SyncMetadata {
String(String),
Number(u64),
Boolean(bool),
Array(Vec<SyncMetadata>),
Map(Vec<(String, SyncMetadata)>),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyncTrack {
pub track_id: String,
pub track_type: SyncType,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub media_duration_ms: Option<u64>,
pub cues: Vec<SyncCue>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<SyncMetadata>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyncDocument {
pub version: u8,
pub tracks: Vec<SyncTrack>,
#[serde(skip_serializing_if = "Option::is_none")]
pub metadata: Option<SyncMetadata>,
}
pub fn validate_cue(cue: &SyncCue) -> Result<(), HonzoError> {
if let Some(duration) = cue.duration_ms {
if duration == 0 {
return Err(HonzoError::InvalidSyncCue);
}
}
if cue.sync_type == SyncType::Page && cue.timestamp_ms > 100000 {
return Err(HonzoError::InvalidSyncCue);
}
Ok(())
}
pub fn validate_track(track: &SyncTrack) -> Result<(), HonzoError> {
for cue in &track.cues {
if cue.sync_type != track.track_type && track.track_type != SyncType::Custom {
return Err(HonzoError::InvalidSyncCue);
}
validate_cue(cue)?;
}
Ok(())
}
pub fn validate_document(doc: &SyncDocument) -> Result<(), HonzoError> {
if doc.version != 1 {
return Err(HonzoError::InvalidSyncCue);
}
for track in &doc.tracks {
validate_track(track)?;
}
Ok(())
}
pub fn parse_sync(body: &[u8]) -> Result<Vec<SyncCue>, HonzoError> {
if body.is_empty() {
return Ok(Vec::new());
}
let cues: Vec<SyncCue> = rmp_serde::from_slice(body).map_err(|e| {
eprintln!("Failed to deserialize sync cues: {:?}", e);
HonzoError::Truncated
})?;
for cue in &cues {
if let Err(e) = validate_cue(cue) {
eprintln!("Invalid sync cue: {:?}", cue);
return Err(e);
}
}
Ok(cues)
}
pub fn parse_sync_document(body: &[u8]) -> Result<SyncDocument, HonzoError> {
if body.is_empty() {
return Ok(SyncDocument {
version: 1,
tracks: Vec::new(),
metadata: None,
});
}
let doc: SyncDocument = rmp_serde::from_slice(body).map_err(|e| {
eprintln!("Failed to deserialize sync document: {:?}", e);
HonzoError::Truncated
})?;
if let Err(e) = validate_document(&doc) {
eprintln!("Invalid sync document: {:?}", doc);
return Err(e);
}
Ok(doc)
}
pub fn build_sync(cues: &[SyncCue]) -> Result<Vec<u8>, HonzoError> {
if cues.is_empty() {
return Ok(Vec::new());
}
for cue in cues {
if let Err(e) = validate_cue(cue) {
eprintln!("Invalid sync cue during build: {:?}", cue);
return Err(e);
}
}
rmp_serde::to_vec_named(cues).map_err(|e| {
eprintln!("Failed to serialize sync cues: {:?}", e);
HonzoError::Truncated
})
}
pub fn build_sync_document(doc: &SyncDocument) -> Result<Vec<u8>, HonzoError> {
if let Err(e) = validate_document(doc) {
eprintln!("Invalid sync document during build: {:?}", doc);
return Err(e);
}
rmp_serde::to_vec_named(doc).map_err(|e| {
eprintln!("Failed to serialize sync document: {:?}", e);
HonzoError::Truncated
})
}
pub fn new_audio_cue(chunk_id: u32, offset: u32, timestamp_ms: u64) -> SyncCue {
SyncCue {
sync_type: SyncType::Audio,
chunk_id,
offset,
timestamp_ms,
media_id: None,
duration_ms: None,
metadata: None,
}
}
pub fn new_video_cue(chunk_id: u32, offset: u32, timestamp_ms: u64) -> SyncCue {
SyncCue {
sync_type: SyncType::Video,
chunk_id,
offset,
timestamp_ms,
media_id: None,
duration_ms: None,
metadata: None,
}
}
pub fn new_page_cue(chunk_id: u32, offset: u32, page_number: u32) -> SyncCue {
SyncCue {
sync_type: SyncType::Page,
chunk_id,
offset,
timestamp_ms: page_number as u64,
media_id: Some("page".to_string()),
duration_ms: None,
metadata: None,
}
}
pub fn new_media_segment_cue(
sync_type: SyncType,
chunk_id: u32,
offset: u32,
timestamp_ms: u64,
duration_ms: u64,
media_id: &str,
) -> SyncCue {
SyncCue {
sync_type,
chunk_id,
offset,
timestamp_ms,
media_id: Some(media_id.to_string()),
duration_ms: Some(duration_ms),
metadata: None,
}
}
pub fn new_sync_track(
track_id: &str,
track_type: SyncType,
media_id: Option<&str>,
media_duration_ms: Option<u64>,
) -> SyncTrack {
SyncTrack {
track_id: track_id.to_string(),
track_type,
media_id: media_id.map(|s| s.to_string()),
media_duration_ms,
cues: Vec::new(),
metadata: None,
}
}
pub fn new_sync_document() -> SyncDocument {
SyncDocument {
version: 1,
tracks: Vec::new(),
metadata: None,
}
}
pub fn sync_cues_to_debug_string(cues: &[SyncCue]) -> String {
cues.iter()
.map(|cue| {
format!(
"SyncCue {{ type: {:?}, chunk: {}, offset: {}, time: {}ms, media: {:?}, duration: {:?}, metadata: {:?} }}",
cue.sync_type,
cue.chunk_id,
cue.offset,
cue.timestamp_ms,
cue.media_id,
cue.duration_ms,
cue.metadata
)
})
.collect::<Vec<_>>()
.join("\n")
}
pub fn filter_sync_cues(cues: &[SyncCue], sync_type: SyncType) -> Vec<SyncCue> {
cues.iter()
.filter(|cue| cue.sync_type == sync_type)
.cloned()
.collect()
}
pub fn filter_sync_cues_by_media(cues: &[SyncCue], media_id: &str) -> Vec<SyncCue> {
cues.iter()
.filter(|cue| cue.media_id.as_deref() == Some(media_id))
.cloned()
.collect()
}
pub fn find_closest_cue(cues: &[SyncCue], timestamp_ms: u64) -> Option<&SyncCue> {
cues.iter()
.min_by_key(|cue| cue.timestamp_ms.abs_diff(timestamp_ms))
}
pub fn find_page_cue(cues: &[SyncCue], page_number: u32) -> Option<&SyncCue> {
cues.iter()
.find(|cue| cue.sync_type == SyncType::Page && cue.timestamp_ms == page_number as u64)
}
pub fn sort_sync_cues(cues: &mut [SyncCue]) {
cues.sort_by_key(|a| a.timestamp_ms);
}
pub fn merge_sync_cues(cues_sets: &[&[SyncCue]]) -> Vec<SyncCue> {
let mut merged = Vec::new();
for cues in cues_sets {
merged.extend_from_slice(cues);
}
sort_sync_cues(&mut merged);
merged
}
pub fn legacy_cues_to_document(cues: Vec<SyncCue>) -> SyncDocument {
let mut doc = new_sync_document();
let mut audio_cues = Vec::new();
let mut video_cues = Vec::new();
let mut page_cues = Vec::new();
let mut custom_cues = Vec::new();
for cue in cues {
match cue.sync_type {
SyncType::Audio => audio_cues.push(cue),
SyncType::Video => video_cues.push(cue),
SyncType::Page => page_cues.push(cue),
SyncType::Animation | SyncType::Custom => custom_cues.push(cue),
}
}
if !audio_cues.is_empty() {
let mut track = new_sync_track("audio", SyncType::Audio, None, None);
track.cues = audio_cues;
doc.tracks.push(track);
}
if !video_cues.is_empty() {
let mut track = new_sync_track("video", SyncType::Video, None, None);
track.cues = video_cues;
doc.tracks.push(track);
}
if !page_cues.is_empty() {
let mut track = new_sync_track("pages", SyncType::Page, Some("page"), None);
track.cues = page_cues;
doc.tracks.push(track);
}
if !custom_cues.is_empty() {
let mut track = new_sync_track("custom", SyncType::Custom, None, None);
track.cues = custom_cues;
doc.tracks.push(track);
}
doc
}