use std::net::{SocketAddr, ToSocketAddrs};
use std::sync::{Arc, Condvar, Mutex, PoisonError};
use std::thread;
use std::time::Duration;
use rusty_esp_core::error::{Error as CoreError, Result as CoreResult};
use rusty_esp_core::media::{Codec, MediaPacket};
use rusty_esp_video_core::source::PacketSource;
use rusty_esp_video_esp::net::{MjpegHttpServer, ServeStats};
use rusty_esp_video_esp::udp_net::{RawUdpSender, RtpJpegSender, TxStats};
use crate::board::{Jpeg, Pcm};
use crate::error::{self, Error, Result};
pub const MTU: usize = 1400;
pub const MAX_JPEG: usize = 512 * 1024;
pub const SSRC: u32 = 0x4A41_4E55; pub const STREAM_STALL: Duration = Duration::from_secs(5);
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct HttpStats {
pub connections: u64,
pub streams: u64,
pub frames: u64,
pub other: u64,
}
#[derive(Debug, Default, Clone, Copy, PartialEq, Eq)]
pub struct Stats {
pub jpeg_pushed: u64,
pub pcm_pushed: u64,
pub rtp: TxStats,
pub pcm: TxStats,
pub http: HttpStats,
}
#[derive(Default)]
struct Slot {
frame: Mutex<(u64, Option<Jpeg>)>,
fresh: Condvar,
}
impl Slot {
fn publish(&self, frame: &Jpeg) {
let mut guard = self.frame.lock().unwrap_or_else(PoisonError::into_inner);
guard.0 += 1;
guard.1 = Some(frame.clone());
self.fresh.notify_all();
}
}
struct SlotSource {
slot: Arc<Slot>,
last: u64,
}
impl PacketSource for SlotSource {
fn next_packet<'b>(&mut self, scratch: &'b mut [u8]) -> CoreResult<MediaPacket<'b>> {
let mut guard = self
.slot
.frame
.lock()
.unwrap_or_else(PoisonError::into_inner);
let started = std::time::Instant::now();
while guard.0 == self.last || guard.1.is_none() {
if started.elapsed() >= STREAM_STALL {
return Err(CoreError::Timeout);
}
guard = self
.slot
.fresh
.wait_timeout(guard, Duration::from_millis(250))
.unwrap_or_else(PoisonError::into_inner)
.0;
}
self.last = guard.0;
let frame = guard.1.as_ref().ok_or(CoreError::Hardware)?;
let n = frame.bytes.len();
if scratch.len() < n {
return Err(CoreError::BufferTooSmall { needed: n });
}
scratch[..n].copy_from_slice(&frame.bytes);
let timestamp = frame.timestamp;
drop(guard);
Ok(MediaPacket::new(
Codec::Jpeg,
true,
timestamp,
&scratch[..n],
))
}
}
struct Server {
addr: SocketAddr,
slot: Arc<Slot>,
stats: Arc<Mutex<HttpStats>>,
token: Option<String>,
}
struct State {
server: Option<Server>,
rtp: Option<RtpJpegSender>,
pcm: Option<RawUdpSender>,
jpeg_pushed: u64,
pcm_pushed: u64,
}
static STATE: Mutex<State> = Mutex::new(State {
server: None,
rtp: None,
pcm: None,
jpeg_pushed: 0,
pcm_pushed: 0,
});
fn state() -> std::sync::MutexGuard<'static, State> {
STATE.lock().unwrap_or_else(PoisonError::into_inner)
}
pub fn listen(port: u16) -> bool {
error::ok(listen_inner(port, None))
}
pub fn listen_gated(port: u16) -> bool {
error::ok(
crate::identity::page_token()
.ok_or(Error::Missing(
"the page token: identity::begin comes before listen_gated",
))
.and_then(|token| listen_inner(port, Some(token))),
)
}
#[must_use]
pub fn token() -> Option<String> {
state().server.as_ref().and_then(|s| s.token.clone())
}
#[must_use]
pub fn page_url(host: &str) -> Option<String> {
let st = state();
let server = st.server.as_ref()?;
let port = if server.addr.port() == 80 {
String::new()
} else {
format!(":{}", server.addr.port())
};
Some(match &server.token {
Some(t) => format!("http://{host}{port}/?t={t}"),
None => format!("http://{host}{port}/"),
})
}
fn listen_inner(port: u16, token: Option<String>) -> Result<()> {
let mut server = MjpegHttpServer::bind(("0.0.0.0", port))?;
if let Some(t) = &token {
server = server.gated(t.clone());
}
let addr = server.local_addr()?;
let slot = Arc::new(Slot::default());
let stats = Arc::new(Mutex::new(HttpStats::default()));
let (thread_slot, thread_stats) = (Arc::clone(&slot), Arc::clone(&stats));
thread::Builder::new()
.name(format!("mjpeg-{port}"))
.stack_size(24 * 1024)
.spawn(move || {
let mut scratch = vec![0u8; MAX_JPEG];
let mut served = ServeStats::default();
loop {
let mut source = SlotSource {
slot: Arc::clone(&thread_slot),
last: 0,
};
let _ = server.serve_one(&mut source, &mut scratch, &mut served);
*thread_stats.lock().unwrap_or_else(PoisonError::into_inner) = HttpStats {
connections: served.connections,
streams: served.streams,
frames: served.frames,
other: served.other,
};
}
})?;
state().server = Some(Server {
addr,
slot,
stats,
token,
});
Ok(())
}
#[must_use]
pub fn listen_addr() -> Option<SocketAddr> {
state().server.as_ref().map(|s| s.addr)
}
pub fn rtp_to(dest: impl ToSocketAddrs) -> bool {
error::ok((|| {
let sender = RtpJpegSender::bind("0.0.0.0:0", dest, SSRC, MTU)?;
state().rtp = Some(sender);
Ok::<(), Error>(())
})())
}
pub fn pcm_to(dest: impl ToSocketAddrs) -> bool {
error::ok((|| {
let sender = RawUdpSender::bind("0.0.0.0:0", dest, MTU)?;
state().pcm = Some(sender);
Ok::<(), Error>(())
})())
}
pub fn push_jpeg(frame: &Jpeg) -> bool {
error::ok((|| {
let mut st = state();
let mut took = false;
if let Some(server) = st.server.as_ref() {
server.slot.publish(frame);
took = true;
}
if let Some(rtp) = st.rtp.as_mut() {
rtp.send_frame(&frame.bytes, frame.timestamp)?;
took = true;
}
if took {
st.jpeg_pushed += 1;
Ok(())
} else {
Err(Error::NotBegun("stream"))
}
})())
}
pub fn push_pcm(pcm: &Pcm) -> bool {
error::ok((|| {
let mut st = state();
let Some(sender) = st.pcm.as_mut() else {
return Err(Error::NotBegun("stream::pcm_to"));
};
let packet = MediaPacket::new(Codec::Pcm(pcm.format), true, pcm.timestamp, &pcm.bytes);
sender.send_packet(&packet)?;
st.pcm_pushed += 1;
Ok(())
})())
}
#[must_use]
pub fn stats() -> Stats {
let st = state();
Stats {
jpeg_pushed: st.jpeg_pushed,
pcm_pushed: st.pcm_pushed,
rtp: st.rtp.as_ref().map(|s| s.stats).unwrap_or_default(),
pcm: st.pcm.as_ref().map(|s| s.stats).unwrap_or_default(),
http: st
.server
.as_ref()
.map(|s| *s.stats.lock().unwrap_or_else(PoisonError::into_inner))
.unwrap_or_default(),
}
}
pub fn stop_senders() {
let mut st = state();
st.rtp = None;
st.pcm = None;
}