1use std::collections::VecDeque;
4use std::time::Duration;
5
6use broadcast_smooth::{SmoothManifest, StreamIndex, StreamType};
7use transmux::dash::rfc6381_track_codec_string;
8use transmux::pipeline::build_init_segment;
9use transmux::smooth_fragment::{
10 FragmentState, fragment_state, fragment_track_id, manifest_declares_protection,
11};
12use transmux::{TrackId, TrackSpec, track_spec_from_quality_level};
13use url::Url;
14
15use broadcast_common::backoff::{RetrySchedule, mix_seed};
16use broadcast_common::url_ref;
17
18use super::action::{Action, FetchFailure, RequestId, StreamId};
19use super::config::{SYNTHETIC_MOVIE_TIMESCALE, SmoothConfig};
20use super::output::{Dropped, ManifestFault, Output, QualityInfo};
21use super::stream::{Chunk, Cursor, HeldFragment, Stream, demux_fragment, live_join};
22use crate::error::SmoothError;
23use broadcast_common::units::MICROS_PER_SEC;
24
25const MIN_BACKOFF_MS: u64 = 1;
27const REFRESH_DIVISOR: u64 = 2;
28const FIRST_TRACK_ID: u32 = 1;
29const FOURCC_AVC: [&str; 2] = ["H264", "AVC1"];
30const FOURCC_HEVC: [&str; 3] = ["H265", "HEVC", "HVC1"];
31const FOURCC_HEVC_ALT: &str = "HEV1";
32
33fn codec_family(four_cc: &str) -> String {
34 let upper = four_cc.to_ascii_uppercase();
35 if FOURCC_AVC.contains(&upper.as_str()) {
36 FOURCC_AVC[0].to_string()
37 } else if FOURCC_HEVC.contains(&upper.as_str()) || upper == FOURCC_HEVC_ALT {
38 FOURCC_HEVC[0].to_string()
39 } else {
40 upper
41 }
42}
43
44fn timescale_of(manifest: &SmoothManifest, index: &StreamIndex) -> Option<u32> {
45 u32::try_from(index.timescale.unwrap_or(manifest.timescale))
46 .ok()
47 .filter(|&t| t != 0)
48}
49
50fn primary_video(manifest: &SmoothManifest) -> Option<&StreamIndex> {
51 manifest.streams.iter().find(|s| {
52 s.stream_type == StreamType::Video
53 && !s.qualities.is_empty()
54 && timescale_of(manifest, s).is_some()
55 })
56}
57
58#[derive(Debug, Clone, Copy)]
59enum ManifestCursor {
60 Idle,
61 InFlight(RequestId),
62}
63
64pub struct SmoothClient {
66 config: SmoothConfig,
67 manifest_url: Url,
68 streams: Vec<Stream>,
69 started: bool,
70 announced: bool,
71 is_live: bool,
72 ended: bool,
73 fatal: bool,
74 video_quality_count: usize,
75 manifest: ManifestCursor,
76 manifest_at: Option<u64>,
77 manifest_failures: u32,
78 retry: RetrySchedule,
79 next_request: u64,
80 actions: VecDeque<Action>,
81 outputs: VecDeque<Output>,
82}
83
84impl SmoothClient {
85 pub fn new(manifest_url: Url, config: SmoothConfig) -> Self {
87 let retry = RetrySchedule::from_steps(
88 &config
89 .backoff_ms
90 .iter()
91 .map(|ms| Duration::from_millis((*ms).max(MIN_BACKOFF_MS)))
92 .collect::<Vec<_>>(),
93 Duration::from_millis(config.backoff_cap_ms.max(MIN_BACKOFF_MS)),
94 mix_seed(config.jitter_seed),
95 );
96 Self {
97 config,
98 manifest_url,
99 streams: Vec::new(),
100 started: false,
101 announced: false,
102 is_live: false,
103 ended: false,
104 fatal: false,
105 video_quality_count: 0,
106 manifest: ManifestCursor::Idle,
107 manifest_at: Some(0),
108 manifest_failures: 0,
109 retry,
110 next_request: 0,
111 actions: VecDeque::new(),
112 outputs: VecDeque::new(),
113 }
114 }
115
116 pub fn set_manifest_url(&mut self, url: Url) {
118 self.manifest_url = url;
119 }
120
121 pub fn is_started(&self) -> bool {
123 self.started
124 }
125
126 pub fn is_live(&self) -> bool {
128 self.is_live && !self.ended
129 }
130
131 pub fn is_ended(&self) -> bool {
133 self.ended
134 }
135
136 pub fn is_fatal(&self) -> bool {
138 self.fatal
139 }
140
141 pub fn video_quality_count(&self) -> usize {
143 self.video_quality_count
144 }
145
146 pub fn select_video_quality(&mut self, index: usize) -> bool {
155 let Some(si) = self
156 .streams
157 .iter()
158 .position(|s| s.index.stream_type == StreamType::Video)
159 else {
160 return false;
161 };
162 let Some(quality) = self.streams[si].index.qualities.get(index).cloned() else {
163 return false;
164 };
165 if self.announced && !self.switchable(si, &quality) {
166 return false;
167 }
168 self.streams[si].quality = quality;
169 true
170 }
171
172 fn switchable(&self, si: usize, candidate: &broadcast_smooth::QualityLevel) -> bool {
173 let stream = &self.streams[si];
174 codec_family(&stream.applied.four_cc) == codec_family(&candidate.four_cc)
175 && track_spec_from_quality_level(
176 TrackId::new(FIRST_TRACK_ID),
177 stream.timescale,
178 stream.index.stream_type,
179 candidate,
180 )
181 .is_ok()
182 }
183
184 pub fn next_deadline(&self) -> Option<u64> {
187 if self.fatal || self.ended {
188 return None;
189 }
190 let backoffs = self.streams.iter().filter_map(|s| match s.cursor {
191 Cursor::Backoff { until, .. } => Some(until),
192 _ => None,
193 });
194 backoffs.chain(self.manifest_at).min()
195 }
196
197 pub fn poll(&mut self, now: u64) -> Option<Action> {
199 if !self.fatal && !self.ended {
200 self.fire_timers(now);
201 self.schedule();
202 self.advance(now);
203 }
204 self.actions.pop_front()
205 }
206
207 pub fn next_output(&mut self) -> Option<Output> {
209 self.outputs.pop_front()
210 }
211
212 pub fn on_manifest(&mut self, now: u64, id: RequestId, bytes: &[u8]) {
214 if !self.take_manifest(id) {
215 return;
216 }
217 let Ok(text) = std::str::from_utf8(bytes) else {
218 return self.manifest_failed(now, ManifestFault::Utf8, 0);
219 };
220 if manifest_declares_protection(text) {
221 return self.fail(SmoothError::ManifestProtected);
222 }
223 match SmoothManifest::parse(text) {
224 Ok(manifest) => self.accept_manifest(&manifest),
225 Err(_) => self.manifest_failed(now, ManifestFault::Parse, 0),
226 }
227 self.advance(now);
228 }
229
230 pub fn on_fragment(&mut self, now: u64, id: RequestId, bytes: &[u8]) {
232 let Some((si, chunk)) = self.take_fragment(id) else {
233 return;
234 };
235 match fragment_state(bytes) {
236 FragmentState::Clear => {}
237 FragmentState::Encrypted => return self.fail(SmoothError::FragmentEncrypted),
238 _ => return self.fragment_failed(now, si, chunk, None, 0),
239 }
240 if self.streams[si].local_track_id.is_none() {
241 match fragment_track_id(bytes) {
242 Ok(tid) => self.streams[si].local_track_id = Some(tid),
243 Err(_) => return self.fragment_failed(now, si, chunk, None, 0),
244 }
245 }
246 self.streams[si].failures = 0;
247 self.streams[si].cursor = Cursor::Idle;
248 let quality = self.streams[si].fetched.clone();
249 if self.announced {
250 self.emit_fragment(si, chunk.0, bytes, &quality);
251 } else {
252 self.streams[si].held.push(HeldFragment {
253 start: chunk.0,
254 body: bytes.to_vec(),
255 quality,
256 });
257 self.try_announce();
258 }
259 self.advance(now);
260 }
261
262 pub fn on_failure(&mut self, now: u64, id: RequestId, failure: FetchFailure) {
264 if self.take_manifest(id) {
265 if failure.is_not_found() {
266 self.outputs.push_back(Output::ManifestNotFound);
267 }
268 return self.manifest_failed(now, ManifestFault::Fetch, failure.retry_after_us);
269 }
270 if let Some((si, chunk)) = self.take_fragment(id) {
271 self.fragment_failed(now, si, chunk, failure.status, failure.retry_after_us);
272 self.advance(now);
273 }
274 }
275
276 fn mint(&mut self) -> RequestId {
277 self.next_request += 1;
278 RequestId(self.next_request)
279 }
280
281 fn fail(&mut self, error: SmoothError) {
282 self.fatal = true;
283 self.outputs.push_back(Output::Fatal(error));
284 }
285
286 fn take_manifest(&mut self, id: RequestId) -> bool {
287 if matches!(self.manifest, ManifestCursor::InFlight(m) if m == id) {
288 self.manifest = ManifestCursor::Idle;
289 return !self.fatal;
290 }
291 false
292 }
293
294 fn take_fragment(&mut self, id: RequestId) -> Option<(usize, Chunk)> {
295 if self.fatal {
296 return None;
297 }
298 self.streams
299 .iter()
300 .position(|s| matches!(s.cursor, Cursor::InFlight { id: i, .. } if i == id))
301 .and_then(|si| match self.streams[si].cursor {
302 Cursor::InFlight { chunk, .. } => Some((si, chunk)),
303 _ => None,
304 })
305 }
306
307 fn backoff_us(&self, attempt: u32) -> u64 {
311 let delay = self.retry.delay_for_attempt(attempt.saturating_sub(1));
312 u64::try_from(delay.as_micros()).unwrap_or(u64::MAX)
313 }
314
315 fn fire_timers(&mut self, now: u64) {
316 let due: Vec<usize> = self
317 .streams
318 .iter()
319 .enumerate()
320 .filter(|(_, s)| matches!(s.cursor, Cursor::Backoff { until, .. } if until <= now))
321 .map(|(i, _)| i)
322 .collect();
323 for si in due {
324 if let Cursor::Backoff { chunk, .. } = self.streams[si].cursor {
325 self.issue_fragment(si, chunk);
326 }
327 }
328 if self.manifest_at.is_some_and(|t| t <= now)
329 && matches!(self.manifest, ManifestCursor::Idle)
330 {
331 self.manifest_at = None;
332 let id = self.mint();
333 self.manifest = ManifestCursor::InFlight(id);
334 self.actions.push_back(Action::FetchManifest {
335 id,
336 url: self.manifest_url.clone(),
337 });
338 }
339 }
340
341 fn schedule(&mut self) {
342 if !self.started {
343 return;
344 }
345 for si in 0..self.streams.len() {
346 let stream = &mut self.streams[si];
347 if stream.abandoned || !matches!(stream.cursor, Cursor::Idle) {
348 continue;
349 }
350 if let Some(chunk) = stream.plan.pop_front() {
351 self.issue_fragment(si, chunk);
352 }
353 }
354 }
355
356 fn issue_fragment(&mut self, si: usize, chunk: Chunk) {
357 let stream = &self.streams[si];
358 let relative = stream
359 .index
360 .resolve_fragment_url(stream.quality.bitrate, chunk.0);
361 let Ok(url) = url_ref::resolve(&self.manifest_url, &relative) else {
362 self.streams[si].cursor = Cursor::Idle;
363 self.outputs
364 .push_back(Output::Dropped(Dropped::FragmentUrl {
365 stream: StreamId(si),
366 }));
367 return;
368 };
369 let id = self.mint();
370 self.streams[si].fetched = self.streams[si].quality.clone();
371 self.streams[si].cursor = Cursor::InFlight { id, chunk };
372 self.actions.push_back(Action::FetchFragment {
373 id,
374 stream: StreamId(si),
375 start: chunk.0,
376 duration: chunk.1,
377 url,
378 });
379 }
380
381 fn manifest_failed(&mut self, now: u64, fault: ManifestFault, wait_us: u64) {
382 self.manifest_failures += 1;
383 if self.manifest_failures >= self.config.max_manifest_failures {
384 return self.fail(SmoothError::Manifest(fault));
385 }
386 self.outputs
387 .push_back(Output::Dropped(Dropped::Manifest(fault)));
388 self.manifest_at =
389 Some(now.saturating_add(self.backoff_us(self.manifest_failures).max(wait_us)));
390 }
391
392 fn fragment_failed(
393 &mut self,
394 now: u64,
395 si: usize,
396 chunk: Chunk,
397 status: Option<u16>,
398 wait_us: u64,
399 ) {
400 self.streams[si].failures += 1;
401 if self.streams[si].failures >= self.config.max_fragment_failures {
402 if !self.is_live {
403 return self.fail(SmoothError::Fragment { status });
404 }
405 self.streams[si].failures = 0;
406 self.streams[si].cursor = Cursor::Idle;
407 self.outputs.push_back(Output::Dropped(Dropped::Fragment {
408 stream: StreamId(si),
409 status,
410 }));
411 self.outputs.push_back(Output::Discontinuity);
412 return;
413 }
414 let missing = FetchFailure {
415 status,
416 retry_after_us: 0,
417 }
418 .is_not_found();
419 if self.is_live && missing && matches!(self.manifest, ManifestCursor::Idle) {
420 self.manifest_at = Some(now);
421 }
422 let until = now.saturating_add(self.backoff_us(self.streams[si].failures).max(wait_us));
423 self.streams[si].cursor = Cursor::Backoff { until, chunk };
424 }
425
426 fn accept_manifest(&mut self, manifest: &SmoothManifest) {
427 self.manifest_failures = 0;
428 if self.started {
429 return self.refresh_streams(manifest);
430 }
431 match self.build_streams(manifest) {
432 Ok(true) => {
433 self.started = true;
434 self.is_live = manifest.is_live;
435 self.offer_qualities(manifest);
436 }
437 Ok(false) => self.fail(SmoothError::NoUsableStream),
438 Err(e) => self.fail(e),
439 }
440 }
441
442 fn build_streams(&mut self, manifest: &SmoothManifest) -> Result<bool, SmoothError> {
443 for index in &manifest.streams {
444 if !matches!(index.stream_type, StreamType::Video | StreamType::Audio) {
445 continue;
446 }
447 let pick = if primary_video(manifest).is_some_and(|v| std::ptr::eq(v, index)) {
448 self.choose_video_quality(manifest, index)
449 } else {
450 0
451 };
452 let Some(quality) = index.qualities.get(pick).cloned() else {
453 continue;
454 };
455 let Some(timescale) = timescale_of(manifest, index) else {
456 continue;
457 };
458 let chunks = match index.enumerate_chunks() {
459 Ok(chunks) => chunks,
460 Err(e) => {
461 return Err(SmoothError::Timeline {
462 stream: self.streams.len(),
463 reason: e.to_string(),
464 });
465 }
466 };
467 let chunks = if manifest.is_live {
468 live_join(chunks, manifest, self.config.live_join_fragments)
469 } else {
470 chunks
471 };
472 let mut stream = Stream::new(index.clone(), timescale, quality);
473 stream.plan_from(chunks);
474 self.streams.push(stream);
475 }
476 Ok(!self.streams.is_empty())
477 }
478
479 fn quality_infos(manifest: &SmoothManifest, index: &StreamIndex) -> Vec<QualityInfo> {
480 let timescale = timescale_of(manifest, index);
481 index
482 .qualities
483 .iter()
484 .map(|q| {
485 let codecs = timescale
486 .and_then(|ts| {
487 track_spec_from_quality_level(
488 TrackId::new(FIRST_TRACK_ID),
489 ts,
490 index.stream_type,
491 q,
492 )
493 .ok()
494 })
495 .and_then(|spec| rfc6381_track_codec_string(&spec).ok());
496 QualityInfo {
497 bitrate: q.bitrate,
498 resolution: q.width.zip(q.height),
499 codecs,
500 }
501 })
502 .collect()
503 }
504
505 fn choose_video_quality(&mut self, manifest: &SmoothManifest, index: &StreamIndex) -> usize {
506 let infos = Self::quality_infos(manifest, index);
507 self.video_quality_count = infos.len();
508 self.config.video_quality.choose(&infos)
509 }
510
511 fn offer_qualities(&mut self, manifest: &SmoothManifest) {
512 let Some(video) = primary_video(manifest) else {
513 return;
514 };
515 let qualities = Self::quality_infos(manifest, video);
516 let chosen = self.config.video_quality.choose(&qualities);
517 self.outputs
518 .push_back(Output::Qualities { qualities, chosen });
519 }
520
521 fn refresh_streams(&mut self, manifest: &SmoothManifest) {
522 self.is_live = manifest.is_live;
523 let mut discontinuity = false;
524 for si in 0..self.streams.len() {
525 if self.streams[si].abandoned {
526 continue;
527 }
528 let found = manifest
529 .streams
530 .iter()
531 .find(|s| s.same_stream(&self.streams[si].index));
532 let Some(found) = found else {
533 self.streams[si].refresh_misses += 1;
534 if self.streams[si].refresh_misses >= self.config.max_refresh_misses {
535 self.streams[si].abandoned = true;
536 self.streams[si].plan.clear();
537 self.outputs.push_back(Output::Dropped(Dropped::StreamLost {
538 stream: StreamId(si),
539 }));
540 }
541 continue;
542 };
543 self.streams[si].refresh_misses = 0;
544 match found.enumerate_chunks() {
545 Ok(chunks) => discontinuity |= self.streams[si].merge_refresh(chunks),
546 Err(_) => {
547 self.outputs
548 .push_back(Output::Dropped(Dropped::Manifest(ManifestFault::Timeline)));
549 }
550 }
551 self.streams[si].index = found.clone();
552 }
553 if discontinuity {
554 self.outputs.push_back(Output::Discontinuity);
555 }
556 }
557
558 fn try_announce(&mut self) {
559 let live: Vec<usize> = (0..self.streams.len())
560 .filter(|&i| !self.streams[i].abandoned)
561 .collect();
562 if self.announced || live.is_empty() {
563 return;
564 }
565 if live
566 .iter()
567 .any(|&i| self.streams[i].local_track_id.is_none())
568 {
569 return;
570 }
571 let mut specs = Vec::new();
572 for (position, &si) in live.iter().enumerate() {
573 let global = TrackId::new(
574 FIRST_TRACK_ID.saturating_add(u32::try_from(position).unwrap_or(u32::MAX)),
575 );
576 let local = self.streams[si].local_track_id.unwrap_or(FIRST_TRACK_ID);
577 let quality = self.streams[si]
578 .held
579 .first()
580 .map_or_else(|| self.streams[si].quality.clone(), |h| h.quality.clone());
581 match self.setup_track(si, local, global, &quality) {
582 Ok(spec) => {
583 self.streams[si].track = Some(global);
584 self.streams[si].spec = Some(spec.clone());
585 specs.push(spec);
586 }
587 Err(reason) => return self.fail(SmoothError::TrackSetup { reason }),
588 }
589 }
590 self.announced = true;
591 self.outputs.push_back(Output::Tracks(specs));
592 for &si in &live {
593 for held in std::mem::take(&mut self.streams[si].held) {
594 self.emit_fragment(si, held.start, &held.body, &held.quality);
595 }
596 }
597 }
598
599 fn setup_track(
600 &mut self,
601 si: usize,
602 local: u32,
603 global: TrackId,
604 quality: &broadcast_smooth::QualityLevel,
605 ) -> Result<TrackSpec, String> {
606 let stream = &self.streams[si];
607 let mut spec = track_spec_from_quality_level(
608 TrackId::new(local),
609 stream.timescale,
610 stream.index.stream_type,
611 quality,
612 )
613 .map_err(|e| e.to_string())?;
614 let init = build_init_segment(std::slice::from_ref(&spec), SYNTHETIC_MOVIE_TIMESCALE)
615 .map_err(|e| e.to_string())?;
616 self.streams[si].init = Some(init);
617 self.streams[si].applied = quality.clone();
618 spec.track_id = global;
619 Ok(spec)
620 }
621
622 fn emit_fragment(
623 &mut self,
624 si: usize,
625 start: u64,
626 body: &[u8],
627 quality: &broadcast_smooth::QualityLevel,
628 ) {
629 if self.streams[si].applied != *quality && !self.reconfigure(si, quality) {
630 return self.outputs.push_back(Output::Dropped(Dropped::Fragment {
631 stream: StreamId(si),
632 status: None,
633 }));
634 }
635 self.emit_samples(si, start, body);
636 }
637
638 fn reconfigure(&mut self, si: usize, quality: &broadcast_smooth::QualityLevel) -> bool {
639 let (Some(local), Some(global)) = (self.streams[si].local_track_id, self.streams[si].track)
640 else {
641 return false;
642 };
643 let Ok(spec) = self.setup_track(si, local, global, quality) else {
644 return false;
645 };
646 if self.streams[si].spec.as_ref() != Some(&spec) {
647 self.streams[si].spec = Some(spec);
648 let specs = self.streams.iter().filter_map(|s| s.spec.clone()).collect();
649 self.outputs.push_back(Output::TracksChanged(specs));
650 }
651 true
652 }
653
654 fn emit_samples(&mut self, si: usize, start: u64, body: &[u8]) {
655 let stream = &self.streams[si];
656 let (Some(init), Some(local), Some(track)) =
657 (stream.init.as_deref(), stream.local_track_id, stream.track)
658 else {
659 return;
660 };
661 match demux_fragment(init, local, body, start) {
662 Ok(samples) => {
663 for sample in samples {
664 self.outputs.push_back(Output::Sample { track, sample });
665 }
666 }
667 Err(_) => self.outputs.push_back(Output::Dropped(Dropped::Fragment {
668 stream: StreamId(si),
669 status: None,
670 })),
671 }
672 }
673
674 fn advance(&mut self, now: u64) {
675 if self.fatal || self.ended || !self.announced {
676 return;
677 }
678 if !self.streams.iter().all(Stream::is_drained) {
679 return;
680 }
681 if !self.is_live {
682 self.ended = true;
683 self.outputs.push_back(Output::Ended);
684 } else if self.manifest_at.is_none() && matches!(self.manifest, ManifestCursor::Idle) {
685 self.manifest_at = Some(now.saturating_add(self.refresh_wait_us()));
686 }
687 }
688
689 fn refresh_wait_us(&self) -> u64 {
690 let last = self.streams.iter().find(|s| !s.abandoned).and_then(|s| {
691 s.index
692 .enumerate_chunks()
693 .ok()?
694 .last()
695 .map(|c| (c.1, s.timescale))
696 });
697 let half = last.map_or(self.config.refresh_max_us, |(d, ts)| {
698 d.saturating_mul(MICROS_PER_SEC) / u64::from(ts).max(1) / REFRESH_DIVISOR
699 });
700 half.clamp(self.config.refresh_min_us, self.config.refresh_max_us)
701 }
702}