synops/maskinrommet/src/livekit.rs
vegard 445f32de69 Sanntidslyd: kommunikasjonsnode → LiveKit-rom (oppgave 11.2)
Kobler kommunikasjonsnoder til LiveKit for sanntidslyd.
Bruker sender join_communication-intensjon, maskinrommet validerer
tilgang og returnerer signert LiveKit JWT-token + rom-URL.

Nye komponenter:
- maskinrommet/src/livekit.rs: JWT token-generering (HS256-signert
  med LIVEKIT_API_SECRET, 1-times TTL, publisher/subscriber-roller)
- POST /intentions/join_communication: validerer deltaker-edge,
  genererer token, oppretter rom i STDB, oppdaterer node-metadata
- POST /intentions/leave_communication: fjerner deltaker fra STDB
- POST /intentions/close_communication: stenger rom (krever owner)
- SpacetimeDB: live_room + room_participant tabeller for sanntids
  deltakerliste (frontend abonnerer via WebSocket)

SpacetimeDB-modul publisert som synops-v2 (ny identitet etter
at den opprinnelige ikke lenger var tilgjengelig). .env oppdatert.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 23:54:40 +00:00

133 lines
3.8 KiB
Rust

// LiveKit-integrasjon — token-generering for sanntidslyd.
//
// Genererer JWT access tokens som gir brukere tilgang til LiveKit-rom.
// Tokens signeres med LIVEKIT_API_SECRET (HMAC-SHA256) og inneholder
// grants som bestemmer hva deltakeren kan gjøre (publisere, lytte, etc.).
//
// Ref: docs/concepts/møterommet.md, docs/concepts/studioet.md
use jsonwebtoken::{encode, EncodingKey, Header, Algorithm};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
/// LiveKit video grant — bestemmer hva en deltaker kan gjøre i et rom.
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct VideoGrant {
room: String,
room_join: bool,
can_publish: bool,
can_subscribe: bool,
}
/// JWT claims for LiveKit access token.
#[derive(Serialize)]
struct LiveKitClaims {
/// API Key (issuer)
iss: String,
/// Participant identity
sub: String,
/// Participant name (display)
name: String,
/// Issued at (unix timestamp)
iat: u64,
/// Not before (unix timestamp)
nbf: u64,
/// Expiration (unix timestamp)
exp: u64,
/// LiveKit video grant
video: VideoGrant,
/// Metadata (JSON string)
#[serde(skip_serializing_if = "String::is_empty")]
metadata: String,
}
/// Rolle i et LiveKit-rom. Bestemmer publiserings-rettigheter.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum RoomRole {
/// Kan publisere og lytte (host, deltaker)
Publisher,
/// Kan bare lytte (observatør)
Subscriber,
}
/// Resultat fra token-generering.
pub struct LiveKitToken {
pub room_name: String,
pub token: String,
pub identity: String,
}
/// Generer et LiveKit access token for en deltaker.
///
/// - `communication_id`: UUID for kommunikasjonsnoden (brukes til rom-navn)
/// - `user_id`: Brukerens node_id (brukes som identity)
/// - `display_name`: Visningsnavn for deltakeren
/// - `role`: Publisher (kan sende lyd) eller Subscriber (bare lytte)
/// - `ttl_secs`: Token-levetid i sekunder (typisk 3600 = 1 time)
pub fn generate_token(
communication_id: Uuid,
user_id: Uuid,
display_name: &str,
role: RoomRole,
ttl_secs: u64,
) -> Result<LiveKitToken, String> {
let api_key = std::env::var("LIVEKIT_API_KEY")
.map_err(|_| "LIVEKIT_API_KEY ikke satt".to_string())?;
let api_secret = std::env::var("LIVEKIT_API_SECRET")
.map_err(|_| "LIVEKIT_API_SECRET ikke satt".to_string())?;
let room_name = format!("communication_{communication_id}");
let identity = user_id.to_string();
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_secs();
let can_publish = role == RoomRole::Publisher;
let claims = LiveKitClaims {
iss: api_key,
sub: identity.clone(),
name: display_name.to_string(),
iat: now,
nbf: now,
exp: now + ttl_secs,
video: VideoGrant {
room: room_name.clone(),
room_join: true,
can_publish,
can_subscribe: true,
},
metadata: String::new(),
};
let header = Header::new(Algorithm::HS256);
let key = EncodingKey::from_secret(api_secret.as_bytes());
let token = encode(&header, &claims, &key)
.map_err(|e| format!("Kunne ikke generere LiveKit-token: {e}"))?;
Ok(LiveKitToken {
room_name,
token,
identity,
})
}
/// Sjekk om LiveKit-serveren er tilgjengelig.
pub async fn health_check() -> Result<bool, String> {
let url = std::env::var("LIVEKIT_URL")
.unwrap_or_else(|_| "http://localhost:7880".to_string());
match reqwest::Client::new()
.get(&url)
.timeout(std::time::Duration::from_secs(3))
.send()
.await
{
Ok(_) => Ok(true),
Err(e) => Err(format!("LiveKit utilgjengelig: {e}")),
}
}