1use honzo_core::HonzoError;
2use serde::{Deserialize, Serialize};
3
4pub const NAMESPACE: &str = super::SYNC_NAMESPACE;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
8#[repr(u8)]
9pub enum SyncType {
10 #[default]
12 Audio = 0,
13
14 Video = 1,
16
17 Animation = 2,
19
20 Page = 3,
22
23 Custom = 255,
25}
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
29pub struct SyncCue {
30 #[serde(default)]
32 pub sync_type: SyncType,
33
34 pub chunk_id: u32,
36
37 pub offset: u32,
39
40 pub timestamp_ms: u64,
42
43 #[serde(default)]
45 pub media_id: Option<String>,
46
47 #[serde(default)]
49 pub duration_ms: Option<u64>,
50
51 #[serde(default)]
53 pub metadata: Option<SyncMetadata>,
54}
55
56#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
58#[serde(untagged)]
59pub enum SyncMetadata {
60 String(String),
62
63 Number(u64),
65
66 Boolean(bool),
68
69 Array(Vec<SyncMetadata>),
71
72 Map(Vec<(String, SyncMetadata)>),
74}
75
76#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
78pub struct SyncTrack {
79 pub track_id: String,
81
82 pub track_type: SyncType,
84
85 #[serde(skip_serializing_if = "Option::is_none")]
87 pub media_id: Option<String>,
88
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub media_duration_ms: Option<u64>,
92
93 pub cues: Vec<SyncCue>,
95
96 #[serde(skip_serializing_if = "Option::is_none")]
98 pub metadata: Option<SyncMetadata>,
99}
100
101#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
103pub struct SyncDocument {
104 pub version: u8,
106
107 pub tracks: Vec<SyncTrack>,
109
110 #[serde(skip_serializing_if = "Option::is_none")]
112 pub metadata: Option<SyncMetadata>,
113}
114
115pub fn validate_cue(cue: &SyncCue) -> Result<(), HonzoError> {
117 if let Some(duration) = cue.duration_ms {
118 if duration == 0 {
119 return Err(HonzoError::InvalidSyncCue);
120 }
121 }
122
123 if cue.sync_type == SyncType::Page && cue.timestamp_ms > 100000 {
125 return Err(HonzoError::InvalidSyncCue);
127 }
128
129 Ok(())
130}
131
132pub fn validate_track(track: &SyncTrack) -> Result<(), HonzoError> {
134 for cue in &track.cues {
141 if cue.sync_type != track.track_type && track.track_type != SyncType::Custom {
143 return Err(HonzoError::InvalidSyncCue);
144 }
145
146 validate_cue(cue)?;
147 }
148
149 Ok(())
150}
151
152pub fn validate_document(doc: &SyncDocument) -> Result<(), HonzoError> {
154 if doc.version != 1 {
155 return Err(HonzoError::InvalidSyncCue);
156 }
157
158 for track in &doc.tracks {
165 validate_track(track)?;
166 }
167
168 Ok(())
169}
170
171pub fn parse_sync(body: &[u8]) -> Result<Vec<SyncCue>, HonzoError> {
173 if body.is_empty() {
174 return Ok(Vec::new());
175 }
176
177 let cues: Vec<SyncCue> = rmp_serde::from_slice(body).map_err(|e| {
178 eprintln!("Failed to deserialize sync cues: {:?}", e);
179 HonzoError::Truncated
180 })?;
181
182 for cue in &cues {
184 if let Err(e) = validate_cue(cue) {
185 eprintln!("Invalid sync cue: {:?}", cue);
186 return Err(e);
187 }
188 }
189
190 Ok(cues)
191}
192
193pub fn parse_sync_document(body: &[u8]) -> Result<SyncDocument, HonzoError> {
195 if body.is_empty() {
196 return Ok(SyncDocument {
197 version: 1,
198 tracks: Vec::new(),
199 metadata: None,
200 });
201 }
202
203 let doc: SyncDocument = rmp_serde::from_slice(body).map_err(|e| {
204 eprintln!("Failed to deserialize sync document: {:?}", e);
205 HonzoError::Truncated
206 })?;
207
208 if let Err(e) = validate_document(&doc) {
209 eprintln!("Invalid sync document: {:?}", doc);
210 return Err(e);
211 }
212
213 Ok(doc)
214}
215
216pub fn build_sync(cues: &[SyncCue]) -> Result<Vec<u8>, HonzoError> {
218 if cues.is_empty() {
219 return Ok(Vec::new());
220 }
221
222 for cue in cues {
224 if let Err(e) = validate_cue(cue) {
225 eprintln!("Invalid sync cue during build: {:?}", cue);
226 return Err(e);
227 }
228 }
229
230 rmp_serde::to_vec_named(cues).map_err(|e| {
231 eprintln!("Failed to serialize sync cues: {:?}", e);
232 HonzoError::Truncated
233 })
234}
235
236pub fn build_sync_document(doc: &SyncDocument) -> Result<Vec<u8>, HonzoError> {
238 if let Err(e) = validate_document(doc) {
239 eprintln!("Invalid sync document during build: {:?}", doc);
240 return Err(e);
241 }
242
243 rmp_serde::to_vec_named(doc).map_err(|e| {
244 eprintln!("Failed to serialize sync document: {:?}", e);
245 HonzoError::Truncated
246 })
247}
248
249pub fn new_audio_cue(chunk_id: u32, offset: u32, timestamp_ms: u64) -> SyncCue {
251 SyncCue {
252 sync_type: SyncType::Audio,
253 chunk_id,
254 offset,
255 timestamp_ms,
256 media_id: None,
257 duration_ms: None,
258 metadata: None,
259 }
260}
261
262pub fn new_video_cue(chunk_id: u32, offset: u32, timestamp_ms: u64) -> SyncCue {
264 SyncCue {
265 sync_type: SyncType::Video,
266 chunk_id,
267 offset,
268 timestamp_ms,
269 media_id: None,
270 duration_ms: None,
271 metadata: None,
272 }
273}
274
275pub fn new_page_cue(chunk_id: u32, offset: u32, page_number: u32) -> SyncCue {
277 SyncCue {
278 sync_type: SyncType::Page,
279 chunk_id,
280 offset,
281 timestamp_ms: page_number as u64,
282 media_id: Some("page".to_string()),
283 duration_ms: None,
284 metadata: None,
285 }
286}
287
288pub fn new_media_segment_cue(
290 sync_type: SyncType,
291 chunk_id: u32,
292 offset: u32,
293 timestamp_ms: u64,
294 duration_ms: u64,
295 media_id: &str,
296) -> SyncCue {
297 SyncCue {
298 sync_type,
299 chunk_id,
300 offset,
301 timestamp_ms,
302 media_id: Some(media_id.to_string()),
303 duration_ms: Some(duration_ms),
304 metadata: None,
305 }
306}
307
308pub fn new_sync_track(
310 track_id: &str,
311 track_type: SyncType,
312 media_id: Option<&str>,
313 media_duration_ms: Option<u64>,
314) -> SyncTrack {
315 SyncTrack {
316 track_id: track_id.to_string(),
317 track_type,
318 media_id: media_id.map(|s| s.to_string()),
319 media_duration_ms,
320 cues: Vec::new(),
321 metadata: None,
322 }
323}
324
325pub fn new_sync_document() -> SyncDocument {
327 SyncDocument {
328 version: 1,
329 tracks: Vec::new(),
330 metadata: None,
331 }
332}
333
334pub fn sync_cues_to_debug_string(cues: &[SyncCue]) -> String {
336 cues.iter()
337 .map(|cue| {
338 format!(
339 "SyncCue {{ type: {:?}, chunk: {}, offset: {}, time: {}ms, media: {:?}, duration: {:?}, metadata: {:?} }}",
340 cue.sync_type,
341 cue.chunk_id,
342 cue.offset,
343 cue.timestamp_ms,
344 cue.media_id,
345 cue.duration_ms,
346 cue.metadata
347 )
348 })
349 .collect::<Vec<_>>()
350 .join("\n")
351}
352
353pub fn filter_sync_cues(cues: &[SyncCue], sync_type: SyncType) -> Vec<SyncCue> {
355 cues.iter()
356 .filter(|cue| cue.sync_type == sync_type)
357 .cloned()
358 .collect()
359}
360
361pub fn filter_sync_cues_by_media(cues: &[SyncCue], media_id: &str) -> Vec<SyncCue> {
363 cues.iter()
364 .filter(|cue| cue.media_id.as_deref() == Some(media_id))
365 .cloned()
366 .collect()
367}
368
369pub fn find_closest_cue(cues: &[SyncCue], timestamp_ms: u64) -> Option<&SyncCue> {
371 cues.iter()
372 .min_by_key(|cue| cue.timestamp_ms.abs_diff(timestamp_ms))
373}
374
375pub fn find_page_cue(cues: &[SyncCue], page_number: u32) -> Option<&SyncCue> {
377 cues.iter()
378 .find(|cue| cue.sync_type == SyncType::Page && cue.timestamp_ms == page_number as u64)
379}
380
381pub fn sort_sync_cues(cues: &mut [SyncCue]) {
383 cues.sort_by_key(|a| a.timestamp_ms);
384}
385
386pub fn merge_sync_cues(cues_sets: &[&[SyncCue]]) -> Vec<SyncCue> {
388 let mut merged = Vec::new();
389 for cues in cues_sets {
390 merged.extend_from_slice(cues);
391 }
392 sort_sync_cues(&mut merged);
393 merged
394}
395
396pub fn legacy_cues_to_document(cues: Vec<SyncCue>) -> SyncDocument {
398 let mut doc = new_sync_document();
399
400 let mut audio_cues = Vec::new();
402 let mut video_cues = Vec::new();
403 let mut page_cues = Vec::new();
404 let mut custom_cues = Vec::new();
405
406 for cue in cues {
407 match cue.sync_type {
408 SyncType::Audio => audio_cues.push(cue),
409 SyncType::Video => video_cues.push(cue),
410 SyncType::Page => page_cues.push(cue),
411 SyncType::Animation | SyncType::Custom => custom_cues.push(cue),
412 }
413 }
414
415 if !audio_cues.is_empty() {
417 let mut track = new_sync_track("audio", SyncType::Audio, None, None);
418 track.cues = audio_cues;
419 doc.tracks.push(track);
420 }
421
422 if !video_cues.is_empty() {
423 let mut track = new_sync_track("video", SyncType::Video, None, None);
424 track.cues = video_cues;
425 doc.tracks.push(track);
426 }
427
428 if !page_cues.is_empty() {
429 let mut track = new_sync_track("pages", SyncType::Page, Some("page"), None);
430 track.cues = page_cues;
431 doc.tracks.push(track);
432 }
433
434 if !custom_cues.is_empty() {
435 let mut track = new_sync_track("custom", SyncType::Custom, None, None);
436 track.cues = custom_cues;
437 doc.tracks.push(track);
438 }
439
440 doc
441}