use std::time::Duration;
use anyhow::{Context, anyhow};
use retina::client::{
Credentials, PacketItem, PlayOptions, Session, SessionOptions, SetupOptions,
TcpTransportOptions, Transport,
};
use tokio_stream::StreamExt;
use crate::tapo_vendor::error::Error;
pub(crate) async fn grab_mjpeg_frame(
url: &str,
creds: Credentials,
timeout: Duration,
) -> Result<Vec<u8>, Error> {
tokio::time::timeout(timeout, grab_frame(url, creds))
.await
.map_err(|_| anyhow!("RTSP snapshot timed out after {timeout:?}"))?
.map_err(Into::into)
}
async fn grab_frame(url: &str, creds: Credentials) -> anyhow::Result<Vec<u8>> {
let parsed_url = reqwest::Url::parse(url).context("parse RTSP URL")?;
let opts = SessionOptions::default()
.creds(Some(creds))
.user_agent("tapo".into());
let mut session = Session::describe(parsed_url, opts)
.await
.context("RTSP DESCRIBE")?;
let video_idx = session
.streams()
.iter()
.position(|s| (s.media() == "video" || s.media() == "image") && s.encoding_name() == "jpeg")
.ok_or_else(|| anyhow!("no MJPEG video stream in RTSP SDP"))?;
session
.setup(
video_idx,
SetupOptions::default().transport(Transport::Tcp(TcpTransportOptions::default())),
)
.await
.context("RTSP SETUP")?;
let mut session = session
.play(PlayOptions::default())
.await
.context("RTSP PLAY")?;
let mut frame: Vec<u8> = Vec::with_capacity(64 * 1024);
let mut have_start = false;
loop {
let item = session
.next()
.await
.ok_or_else(|| anyhow!("RTSP stream ended before a frame arrived"))?
.context("RTSP packet")?;
let pkt = match item {
PacketItem::Rtp(p) => p,
_ => continue,
};
let mark = pkt.mark();
let payload = pkt.payload();
if payload.len() < 8 {
continue;
}
let frag_offset = u32::from_be_bytes([0, payload[1], payload[2], payload[3]]);
let type_field = payload[4];
let q = payload[5];
let mut body_offset = 8usize;
if type_field >= 64 {
if payload.len() < body_offset + 4 {
continue;
}
body_offset += 4;
}
if frag_offset == 0 && q >= 128 {
if payload.len() < body_offset + 4 {
continue;
}
let length =
((payload[body_offset + 2] as usize) << 8) | (payload[body_offset + 3] as usize);
body_offset += 4 + length;
if payload.len() < body_offset {
continue;
}
}
if frag_offset == 0 {
frame.clear();
have_start = true;
} else if !have_start {
continue;
}
frame.extend_from_slice(&payload[body_offset..]);
if mark && have_start {
let n = frame.len();
let has_soi = n >= 4 && frame[0] == 0xff && frame[1] == 0xd8;
let has_eoi = n >= 4 && frame[n - 2] == 0xff && frame[n - 1] == 0xd9;
if has_soi && has_eoi {
return Ok(frame);
}
frame.clear();
have_start = false;
}
}
}