Harden LA publishers and add multi-relay guide
This commit is contained in:
parent
5d6f77f868
commit
cfc4902016
13 changed files with 1430 additions and 402 deletions
|
|
@ -38,6 +38,31 @@ struct DirectoryList {
|
|||
entries: Vec<DirectoryEntry>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct PublicStreamRelay {
|
||||
relay_url: String,
|
||||
broadcast_name: String,
|
||||
track_name: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
struct PublicStreamEntry {
|
||||
stream_id: String,
|
||||
title: String,
|
||||
relay_url: String,
|
||||
broadcast_name: String,
|
||||
track_name: String,
|
||||
relays: Vec<PublicStreamRelay>,
|
||||
updated_ms: u64,
|
||||
expires_ms: u64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
struct PublicStreamList {
|
||||
now_ms: u64,
|
||||
entries: Vec<PublicStreamEntry>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
struct HealthResp {
|
||||
ok: bool,
|
||||
|
|
@ -69,10 +94,29 @@ struct AnswerGetReq {
|
|||
stream_id: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
struct StreamUpsertReq {
|
||||
stream_id: String,
|
||||
title: String,
|
||||
relay_url: Option<String>,
|
||||
broadcast_name: Option<String>,
|
||||
track_name: Option<String>,
|
||||
relays: Option<Vec<PublicStreamRelay>>,
|
||||
expires_ms: Option<u64>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
struct StreamUpsertResp {
|
||||
ok: bool,
|
||||
ttl_ms: u64,
|
||||
entry: PublicStreamEntry,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct State {
|
||||
entries: HashMap<String, DirectoryEntry>,
|
||||
answers: HashMap<String, AnswerEntry>,
|
||||
streams: HashMap<String, PublicStreamEntry>,
|
||||
}
|
||||
|
||||
fn now_ms() -> u64 {
|
||||
|
|
@ -100,6 +144,7 @@ fn json_headers() -> HeaderMap {
|
|||
fn prune_state(state: &mut State, now: u64) {
|
||||
state.entries.retain(|_, v| v.expires_ms > now);
|
||||
state.answers.retain(|_, v| v.expires_ms > now);
|
||||
state.streams.retain(|_, v| v.expires_ms > now);
|
||||
|
||||
// Cap growth defensively. This is not spam-resistant; it's a bootstrap rendezvous.
|
||||
if state.entries.len() > 200 {
|
||||
|
|
@ -114,6 +159,12 @@ fn prune_state(state: &mut State, now: u64) {
|
|||
items.truncate(500);
|
||||
state.answers = items.into_iter().map(|e| (e.stream_id.clone(), e)).collect();
|
||||
}
|
||||
if state.streams.len() > 1000 {
|
||||
let mut items = state.streams.values().cloned().collect::<Vec<_>>();
|
||||
items.sort_by_key(|e| std::cmp::Reverse(e.updated_ms));
|
||||
items.truncate(1000);
|
||||
state.streams = items.into_iter().map(|e| (e.stream_id.clone(), e)).collect();
|
||||
}
|
||||
}
|
||||
|
||||
async fn health() -> impl IntoResponse {
|
||||
|
|
@ -129,6 +180,118 @@ async fn directory(state: axum::extract::State<Arc<RwLock<State>>>) -> impl Into
|
|||
(json_headers(), Json(DirectoryList { now_ms: now, entries }))
|
||||
}
|
||||
|
||||
fn push_stream_relay(relays: &mut Vec<PublicStreamRelay>, relay: PublicStreamRelay) {
|
||||
if relay.relay_url.is_empty() || relay.broadcast_name.is_empty() {
|
||||
return;
|
||||
}
|
||||
if relays.iter().any(|existing| {
|
||||
existing.relay_url == relay.relay_url
|
||||
&& existing.broadcast_name == relay.broadcast_name
|
||||
&& existing.track_name == relay.track_name
|
||||
}) {
|
||||
return;
|
||||
}
|
||||
if relays.len() < 16 {
|
||||
relays.push(relay);
|
||||
}
|
||||
}
|
||||
|
||||
fn normalize_stream_relays(body: &StreamUpsertReq) -> Vec<PublicStreamRelay> {
|
||||
let mut relays = Vec::new();
|
||||
if let (Some(relay_url), Some(broadcast_name)) = (&body.relay_url, &body.broadcast_name) {
|
||||
push_stream_relay(
|
||||
&mut relays,
|
||||
PublicStreamRelay {
|
||||
relay_url: clamp_str(relay_url.clone(), 512),
|
||||
broadcast_name: clamp_str(broadcast_name.clone(), 256),
|
||||
track_name: clamp_str(
|
||||
body.track_name
|
||||
.clone()
|
||||
.unwrap_or_else(|| "video0.m4s".to_string()),
|
||||
256,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
if let Some(body_relays) = &body.relays {
|
||||
for relay in body_relays {
|
||||
push_stream_relay(
|
||||
&mut relays,
|
||||
PublicStreamRelay {
|
||||
relay_url: clamp_str(relay.relay_url.clone(), 512),
|
||||
broadcast_name: clamp_str(relay.broadcast_name.clone(), 256),
|
||||
track_name: clamp_str(
|
||||
if relay.track_name.is_empty() {
|
||||
"video0.m4s".to_string()
|
||||
} else {
|
||||
relay.track_name.clone()
|
||||
},
|
||||
256,
|
||||
),
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
relays
|
||||
}
|
||||
|
||||
async fn public_streams(state: axum::extract::State<Arc<RwLock<State>>>) -> impl IntoResponse {
|
||||
let now = now_ms();
|
||||
let mut guard = state.write().await;
|
||||
prune_state(&mut guard, now);
|
||||
let mut entries = guard.streams.values().cloned().collect::<Vec<_>>();
|
||||
entries.sort_by_key(|e| std::cmp::Reverse(e.updated_ms));
|
||||
(json_headers(), Json(PublicStreamList { now_ms: now, entries }))
|
||||
}
|
||||
|
||||
async fn stream_upsert(
|
||||
state: axum::extract::State<Arc<RwLock<State>>>,
|
||||
Json(body): Json<StreamUpsertReq>,
|
||||
) -> impl IntoResponse {
|
||||
let now = now_ms();
|
||||
let relays = normalize_stream_relays(&body);
|
||||
|
||||
if body.stream_id.is_empty()
|
||||
|| body.title.is_empty()
|
||||
|| body.relay_url.as_deref().unwrap_or_default().is_empty()
|
||||
|| body.broadcast_name.as_deref().unwrap_or_default().is_empty()
|
||||
{
|
||||
let resp =
|
||||
serde_json::json!({ "error": "missing stream_id/title/relay_url/broadcast_name" });
|
||||
return (StatusCode::BAD_REQUEST, json_headers(), Json(resp)).into_response();
|
||||
}
|
||||
|
||||
let requested_expires = body.expires_ms.unwrap_or(now + 20_000);
|
||||
let requested_ttl = requested_expires.saturating_sub(now);
|
||||
let ttl_ms = requested_ttl.clamp(5_000, 60_000);
|
||||
let primary = relays[0].clone();
|
||||
|
||||
let entry = PublicStreamEntry {
|
||||
stream_id: clamp_str(body.stream_id, 256),
|
||||
title: clamp_str(body.title, 128),
|
||||
relay_url: primary.relay_url,
|
||||
broadcast_name: primary.broadcast_name,
|
||||
track_name: primary.track_name,
|
||||
relays,
|
||||
updated_ms: now,
|
||||
expires_ms: now + ttl_ms,
|
||||
};
|
||||
|
||||
let mut guard = state.write().await;
|
||||
prune_state(&mut guard, now);
|
||||
guard.streams.insert(entry.stream_id.clone(), entry.clone());
|
||||
|
||||
(
|
||||
json_headers(),
|
||||
Json(StreamUpsertResp {
|
||||
ok: true,
|
||||
ttl_ms,
|
||||
entry,
|
||||
}),
|
||||
)
|
||||
.into_response()
|
||||
}
|
||||
|
||||
async fn announce(
|
||||
state: axum::extract::State<Arc<RwLock<State>>>,
|
||||
Json(body): Json<AnnounceReq>,
|
||||
|
|
@ -233,6 +396,8 @@ async fn main() -> anyhow::Result<()> {
|
|||
let app = Router::new()
|
||||
.route("/api/health", get(health))
|
||||
.route("/api/directory", get(directory))
|
||||
.route("/api/public-streams", get(public_streams))
|
||||
.route("/api/stream-upsert", post(stream_upsert))
|
||||
.route("/api/announce", post(announce))
|
||||
.route("/api/answer", post(post_answer).get(get_answer))
|
||||
.with_state(state)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue