1use kithara_decode::{DecodeError, Decoder};
2use kithara_platform::time::Duration;
3use kithara_stream::{MediaInfo, SourcePhase, SourceSeekAnchor};
4
5use crate::pipeline::fetch::Fetch;
6
7pub(crate) enum TrackState {
12 Decoding,
14
15 SeekRequested(SeekRequest),
17
18 WaitingForSource {
20 context: WaitContext,
21 reason: WaitingReason,
22 },
23
24 ApplyingSeek(ApplySeekState),
26
27 RecreatingDecoder(RecreateState),
29
30 AwaitingResume(ResumeState),
32
33 AtEof,
35
36 Failed(TrackFailure),
38}
39
40#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
42pub(crate) struct SeekContext {
43 pub(crate) target: Duration,
44 pub(crate) epoch: u64,
45}
46
47#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
49pub(crate) struct SeekRequest {
50 pub(crate) seek: SeekContext,
51 pub(crate) attempt: u8,
52}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub(crate) struct ApplySeekState {
57 pub(crate) mode: SeekMode,
58 pub(crate) request: SeekRequest,
59}
60
61#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
63pub(crate) struct ResumeState {
64 pub(crate) anchor_offset: Option<u64>,
67 pub(crate) skip: Option<Duration>,
68 pub(crate) seek: SeekContext,
69 pub(crate) recover_attempts: u8,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub(crate) enum RecreateNext {
75 Decode,
77 Seek(SeekRequest),
79 ApplySeek(SeekRequest),
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
85pub(crate) struct RecreateState {
86 pub(crate) media_info: MediaInfo,
87 pub(crate) cause: RecreateCause,
88 pub(crate) next: RecreateNext,
89 pub(crate) offset: u64,
90 pub(crate) attempt: u8,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
104pub(crate) enum RecreateOutcome {
105 Done,
106 SoftFailed,
107 NeedsSourceWait,
108}
109
110#[derive(Debug)]
112pub(crate) enum WaitContext {
113 Playback,
115 Seek(SeekRequest),
117 ApplySeek(ApplySeekState),
119 Recreation(RecreateState),
121 PostSeek(ResumeState),
127}
128
129#[derive(Debug, Clone, Copy, PartialEq, Eq)]
131pub enum WaitingReason {
132 Waiting,
134 WaitingDemand,
136 WaitingMetadata,
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq)]
142pub(crate) enum SeekMode {
143 Direct { target_byte: Option<u64> },
149 Anchor(SourceSeekAnchor),
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
155pub(crate) enum RecreateCause {
156 FormatBoundary,
158 VariantSwitch,
160}
161
162#[derive(Debug)]
164pub(crate) enum TrackFailure {
165 Decode(DecodeError),
167 RecreateFailed { offset: u64 },
169 SourceCancelled,
171}
172
173pub(crate) struct DecoderSession {
178 pub(crate) decoder: Box<dyn Decoder>,
179 pub(crate) media_info: Option<MediaInfo>,
180 pub(crate) base_offset: u64,
181 pub(crate) installed_at_seek_epoch: u64,
187}
188
189pub enum TrackStep<C> {
191 Produced(Fetch<C>),
193 Blocked(WaitingReason),
195 StateChanged,
197 Eof,
199 Failed,
201}
202
203#[derive(Debug, Clone, Copy, PartialEq, Eq)]
205pub enum TrackPhaseTag {
206 Decoding,
207 SeekRequested,
208 WaitingForSource,
209 ApplyingSeek,
210 RecreatingDecoder,
211 AwaitingResume,
212 AtEof,
213 Failed,
214}
215
216#[derive(Debug, Clone, Copy, PartialEq, Eq)]
218pub(crate) enum ConsumerPhase {
219 Buffering,
221 Playing,
223 SeekPending { epoch: u64 },
225 AtEof,
227 Failed,
229}
230
231impl TrackState {
232 pub(crate) fn is_terminal(&self) -> bool {
237 matches!(self, Self::Failed(_))
238 }
239}
240
241impl From<&TrackState> for TrackPhaseTag {
242 #[inline(always)]
243 fn from(state: &TrackState) -> Self {
244 match state {
245 TrackState::Decoding => Self::Decoding,
246 TrackState::SeekRequested(_) => Self::SeekRequested,
247 TrackState::WaitingForSource { .. } => Self::WaitingForSource,
248 TrackState::ApplyingSeek(_) => Self::ApplyingSeek,
249 TrackState::RecreatingDecoder(_) => Self::RecreatingDecoder,
250 TrackState::AwaitingResume(_) => Self::AwaitingResume,
251 TrackState::AtEof => Self::AtEof,
252 TrackState::Failed(_) => Self::Failed,
253 }
254 }
255}
256
257impl ConsumerPhase {
258 pub(crate) fn is_terminal(self) -> bool {
260 matches!(self, Self::AtEof | Self::Failed)
261 }
262}
263
264pub(crate) fn map_source_phase(phase: SourcePhase) -> Option<WaitingReason> {
270 match phase {
271 SourcePhase::Waiting => Some(WaitingReason::Waiting),
272 SourcePhase::WaitingDemand => Some(WaitingReason::WaitingDemand),
273 SourcePhase::WaitingMetadata => Some(WaitingReason::WaitingMetadata),
274 _ => None,
275 }
276}
277
278#[cfg(test)]
279mod tests {
280 use kithara_test_utils::kithara;
281
282 use super::*;
283
284 #[kithara::test]
285 fn is_terminal_for_each_state() {
286 let non_terminal = [
287 TrackState::Decoding,
288 TrackState::SeekRequested(SeekRequest {
289 seek: SeekContext {
290 epoch: 1,
291 target: Duration::from_secs(5),
292 },
293 ..Default::default()
294 }),
295 TrackState::WaitingForSource {
296 context: WaitContext::Playback,
297 reason: WaitingReason::Waiting,
298 },
299 TrackState::ApplyingSeek(ApplySeekState {
300 mode: SeekMode::Direct { target_byte: None },
301 request: SeekRequest {
302 seek: SeekContext {
303 epoch: 1,
304 target: Duration::from_secs(5),
305 },
306 ..Default::default()
307 },
308 }),
309 TrackState::RecreatingDecoder(RecreateState {
310 attempt: 0,
311 cause: RecreateCause::FormatBoundary,
312 media_info: MediaInfo::default(),
313 next: RecreateNext::Decode,
314 offset: 0,
315 }),
316 TrackState::AwaitingResume(ResumeState {
317 recover_attempts: 0,
318 seek: SeekContext {
319 epoch: 1,
320 target: Duration::from_secs(5),
321 },
322 anchor_offset: None,
323 skip: None,
324 }),
325 TrackState::AtEof,
326 ];
327 for state in &non_terminal {
328 assert!(
329 !state.is_terminal(),
330 "expected non-terminal for {:?}",
331 TrackPhaseTag::from(state)
332 );
333 }
334
335 assert!(TrackState::Failed(TrackFailure::SourceCancelled).is_terminal());
336 }
337
338 #[kithara::test]
339 fn phase_tag_preserves_discriminant() {
340 assert_eq!(
341 TrackPhaseTag::from(&TrackState::Decoding),
342 TrackPhaseTag::Decoding
343 );
344 assert_eq!(
345 TrackPhaseTag::from(&TrackState::SeekRequested(SeekRequest {
346 seek: SeekContext {
347 epoch: 1,
348 target: Duration::ZERO,
349 },
350 ..Default::default()
351 })),
352 TrackPhaseTag::SeekRequested
353 );
354 assert_eq!(
355 TrackPhaseTag::from(&TrackState::WaitingForSource {
356 context: WaitContext::Playback,
357 reason: WaitingReason::WaitingDemand,
358 }),
359 TrackPhaseTag::WaitingForSource
360 );
361 assert_eq!(
362 TrackPhaseTag::from(&TrackState::ApplyingSeek(ApplySeekState {
363 mode: SeekMode::Direct { target_byte: None },
364 request: SeekRequest {
365 seek: SeekContext {
366 epoch: 1,
367 target: Duration::ZERO,
368 },
369 ..Default::default()
370 },
371 })),
372 TrackPhaseTag::ApplyingSeek
373 );
374 assert_eq!(
375 TrackPhaseTag::from(&TrackState::RecreatingDecoder(RecreateState {
376 attempt: 1,
377 cause: RecreateCause::VariantSwitch,
378 media_info: MediaInfo::default(),
379 next: RecreateNext::ApplySeek(SeekRequest {
380 attempt: 1,
381 seek: SeekContext {
382 epoch: 1,
383 target: Duration::from_secs(10),
384 },
385 }),
386 offset: 100,
387 })),
388 TrackPhaseTag::RecreatingDecoder
389 );
390 assert_eq!(
391 TrackPhaseTag::from(&TrackState::AwaitingResume(ResumeState {
392 recover_attempts: 0,
393 seek: SeekContext {
394 epoch: 1,
395 target: Duration::from_secs(10),
396 },
397 anchor_offset: None,
398 skip: None,
399 })),
400 TrackPhaseTag::AwaitingResume
401 );
402 assert_eq!(
403 TrackPhaseTag::from(&TrackState::AtEof),
404 TrackPhaseTag::AtEof
405 );
406 assert_eq!(
407 TrackPhaseTag::from(&TrackState::Failed(TrackFailure::SourceCancelled)),
408 TrackPhaseTag::Failed
409 );
410 }
411
412 #[kithara::test]
413 fn map_source_phase_table() {
414 assert_eq!(
415 map_source_phase(SourcePhase::Waiting),
416 Some(WaitingReason::Waiting)
417 );
418 assert_eq!(
419 map_source_phase(SourcePhase::WaitingDemand),
420 Some(WaitingReason::WaitingDemand)
421 );
422 assert_eq!(
423 map_source_phase(SourcePhase::WaitingMetadata),
424 Some(WaitingReason::WaitingMetadata)
425 );
426
427 assert_eq!(map_source_phase(SourcePhase::Ready), None);
428 assert_eq!(map_source_phase(SourcePhase::Eof), None);
429 assert_eq!(map_source_phase(SourcePhase::Seeking), None);
430 assert_eq!(map_source_phase(SourcePhase::Cancelled), None);
431 }
432
433 #[kithara::test]
434 fn consumer_phase_terminal() {
435 assert!(!ConsumerPhase::Buffering.is_terminal());
436 assert!(!ConsumerPhase::Playing.is_terminal());
437 assert!(!ConsumerPhase::SeekPending { epoch: 1 }.is_terminal());
438 assert!(ConsumerPhase::AtEof.is_terminal());
439 assert!(ConsumerPhase::Failed.is_terminal());
440 }
441
442 #[kithara::test]
443 fn seek_context_copy_and_eq() {
444 let ctx = SeekContext {
445 epoch: 42,
446 target: Duration::from_millis(500),
447 };
448 let copy = ctx;
449 assert_eq!(ctx, copy);
450 assert_eq!(copy.epoch, 42);
451 assert_eq!(copy.target, Duration::from_millis(500));
452 }
453
454 #[kithara::test]
455 fn at_eof_allows_seek_transition() {
456 let state = TrackState::AtEof;
457 assert!(!state.is_terminal());
458 assert_eq!(TrackPhaseTag::from(&state), TrackPhaseTag::AtEof);
459 }
460}