1use super::{redact, Codec, VideoAu};
11use bytes::Bytes;
12use futures_util::StreamExt;
13use retina::client::{PlayOptions, SessionOptions, SetupOptions, Transport};
14use retina::codec::{CodecItem, FrameFormat};
15use std::time::{Duration, Instant};
16use tokio::sync::mpsc;
17
18const BASE_BACKOFF: Duration = Duration::from_secs(1);
19const MAX_BACKOFF: Duration = Duration::from_secs(30);
20const STABLE_SESSION: Duration = Duration::from_secs(30);
24
25pub async fn run(url: String, codec: Codec, tx: mpsc::Sender<VideoAu>) {
26 let mut backoff = BASE_BACKOFF;
27 loop {
28 let started = Instant::now();
29 let outcome = session(&url, codec, &tx).await;
30 if started.elapsed() >= STABLE_SESSION {
32 backoff = BASE_BACKOFF;
33 }
34 match outcome {
35 Ok(()) => tracing::info!(url = %redact(&url), "rtsp stream ended; reconnecting"),
36 Err(e) => {
37 tracing::warn!(url = %redact(&url), error = %redact(&e.to_string()), "rtsp input failed; retrying in {backoff:?}")
38 }
39 }
40 if tx.is_closed() {
41 return;
42 }
43 tokio::time::sleep(backoff).await;
44 backoff = (backoff * 2).min(MAX_BACKOFF);
45 }
46}
47
48pub fn resolve_credentials(url: &str) -> String {
50 let Ok(mut u) = url::Url::parse(url) else {
51 return url.to_string();
52 };
53 if !u.username().is_empty() {
54 return url.to_string();
55 }
56 if let (Ok(user), Ok(pass)) = (
57 std::env::var("SEYD_RTSP_USER"),
58 std::env::var("SEYD_RTSP_PASSWORD"),
59 ) {
60 let _ = u.set_username(&user);
61 let _ = u.set_password(Some(&pass));
62 }
63 u.to_string()
64}
65
66async fn session(url: &str, codec: Codec, tx: &mpsc::Sender<VideoAu>) -> anyhow::Result<()> {
67 let mut parsed = url::Url::parse(url)?;
68 let creds = if !parsed.username().is_empty() {
69 let c = retina::client::Credentials {
70 username: parsed.username().to_string(),
71 password: parsed.password().unwrap_or("").to_string(),
72 };
73 let _ = parsed.set_username("");
74 let _ = parsed.set_password(None);
75 Some(c)
76 } else {
77 None
78 };
79
80 let mut sess = retina::client::Session::describe(
81 parsed,
82 SessionOptions::default()
83 .creds(creds)
84 .user_agent("seydd".into()),
85 )
86 .await?;
87
88 let video_idx = sess
91 .streams()
92 .iter()
93 .position(|s| {
94 s.media() == "video"
95 && s.encoding_name()
96 .eq_ignore_ascii_case(codec.rtp_encoding_name())
97 })
98 .ok_or_else(|| anyhow::anyhow!("no H.264 video stream in RTSP DESCRIBE"))?;
99 sess.setup(
100 video_idx,
101 SetupOptions::default()
102 .transport(Transport::Tcp(Default::default()))
103 .frame_format(FrameFormat::SIMPLE),
104 )
105 .await?;
106 let playing = sess.play(PlayOptions::default()).await?;
107 let mut demuxed = playing.demuxed()?;
108 tracing::info!(url = %redact(url), "rtsp input playing");
109
110 let mut frames: u64 = 0;
111 let mut last_at = std::time::Instant::now();
112 while let Some(item) = demuxed.next().await {
113 let gap = last_at.elapsed();
114 last_at = std::time::Instant::now();
115 if gap.as_millis() > 120 {
116 tracing::debug!(gap_ms = gap.as_millis() as u64, "rtsp inter-frame gap");
117 }
118 if let CodecItem::VideoFrame(f) = item? {
119 {
120 if f.stream_id() != video_idx {
121 continue;
122 }
123 let ts = f.timestamp();
128 let capture_ts_us = (ts.elapsed().max(0) as u128 * 1_000_000
129 / ts.clock_rate().get() as u128) as u64;
130 let au = VideoAu {
131 keyframe: f.is_random_access_point(),
132 input_loss: f.loss(),
133 capture_ts_us,
134 data: Bytes::from(f.into_data()),
135 };
136 frames += 1;
137 if frames == 1 {
138 tracing::info!(
139 bytes = au.data.len(),
140 keyframe = au.keyframe,
141 "first rtsp frame"
142 );
143 }
144 if tx.send(au).await.is_err() {
145 return Ok(());
146 }
147 }
148 }
149 }
150 Ok(())
151}