//! Shared types used across moqbox modules. use std::fmt; use chrono::{DateTime, Utc}; use serde::Serialize; use tokio::sync::{broadcast, mpsc, watch}; /// Typed identifier for a queued / active / history track. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] #[serde(transparent)] pub(crate) struct TrackId(pub(crate) uuid::Uuid); impl TrackId { pub(crate) fn new() -> Self { Self(uuid::Uuid::now_v7()) } /// Sentinel value: no specific track. Used in PersistMetadata effects /// generated by URL-keyed operations where the effect targets media_cache, /// not a particular track row. pub(crate) fn nil() -> Self { Self(uuid::Uuid::nil()) } pub(crate) fn as_str_buf(&self) -> String { self.0.to_string() } } impl fmt::Display for TrackId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } /// Typed identifier for a perpetual playlist entry. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] #[serde(transparent)] pub(crate) struct PlaylistEntryId(pub(crate) uuid::Uuid); impl PlaylistEntryId { pub(crate) fn new() -> Self { Self(uuid::Uuid::now_v7()) } pub(crate) fn parse(s: &str) -> Option { uuid::Uuid::parse_str(s).ok().map(Self) } pub(crate) fn as_str_buf(&self) -> String { self.0.to_string() } } impl fmt::Display for PlaylistEntryId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } /// Typed identifier for a chat message. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] #[serde(transparent)] pub(crate) struct ChatMessageId(pub(crate) uuid::Uuid); impl ChatMessageId { pub(crate) fn new() -> Self { Self(uuid::Uuid::now_v7()) } pub(crate) fn as_str_buf(&self) -> String { self.0.to_string() } } impl fmt::Display for ChatMessageId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } /// Opaque room identifier backed by a UUIDv7. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] #[serde(transparent)] pub(crate) struct RoomId(pub(crate) uuid::Uuid); impl RoomId { pub(crate) fn new() -> Self { Self(uuid::Uuid::now_v7()) } pub(crate) fn as_str_buf(&self) -> String { self.0.to_string() } /// Parse a UUID string into a `RoomId`. Returns `None` if invalid. pub(crate) fn parse(s: &str) -> Option { uuid::Uuid::parse_str(s).ok().map(Self) } } impl fmt::Display for RoomId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } /// Snapshot of the currently playing track for state publishing. #[derive(Debug, Clone, Serialize)] pub(crate) struct TrackState { pub(crate) id: TrackId, /// Stable hash of the media URL, used for file serving. pub(crate) hash: String, pub(crate) title: String, pub(crate) url: String, pub(crate) duration: String, pub(crate) thumbnail: Option, pub(crate) started_at: i64, } /// Summary of a queued track for state publishing. #[derive(Debug, Clone, Serialize)] pub(crate) struct QueueSummary { pub(crate) id: TrackId, /// Stable hash of the media URL, used for file serving. pub(crate) hash: String, pub(crate) title: String, pub(crate) url: String, pub(crate) duration: String, pub(crate) thumbnail: Option, /// True while metadata is being extracted / download in progress. #[serde(skip_serializing_if = "is_false")] pub(crate) pending: bool, } fn is_false(b: &bool) -> bool { !*b } #[derive(Debug, Clone, Serialize)] pub(crate) struct HistoryEntry { pub(crate) id: TrackId, pub(crate) hash: String, pub(crate) title: String, pub(crate) url: String, pub(crate) duration: String, pub(crate) thumbnail: Option, pub(crate) played_at: String, } /// A chat message returned from send/recent operations. #[derive(Debug, Clone, Serialize)] pub(crate) struct ChatMessage { pub(crate) id: ChatMessageId, pub(crate) user_name: String, pub(crate) content: String, pub(crate) msg_type: String, pub(crate) created_at: String, } /// WebSocket publishers for a room. /// /// Replaces the old MoQ `TrackPublishers`. Provides a `watch` channel for /// state snapshots and a `broadcast` channel for chat messages. #[derive(Clone)] pub(crate) struct RoomPublishers { pub(crate) state_tx: watch::Sender, pub(crate) chat_tx: broadcast::Sender, } impl RoomPublishers { pub(crate) fn new() -> Self { let (state_tx, _) = watch::channel(serde_json::Value::Null); let (chat_tx, _) = broadcast::channel(256); Self { state_tx, chat_tx } } } /// Re-exported from [`crate::room`] for convenience. pub(crate) use crate::room::RoomCommand; /// Ephemeral info about the active track, stored in-memory only. #[derive(Debug, Clone, PartialEq, Eq)] pub(crate) struct ActiveTrackInfo { pub(crate) id: TrackId, pub(crate) title: String, pub(crate) url: String, pub(crate) duration: String, pub(crate) thumbnail: Option, /// Epoch ms when the track started, for client-side elapsed computation. pub(crate) started_at_wall: i64, } /// Public handle to a room: allows sending commands and reading publishers. #[derive(Clone)] pub(crate) struct RoomHandle { pub(crate) cmd_tx: mpsc::Sender, pub(crate) publishers: RoomPublishers, pub(crate) last_active: watch::Receiver>, } #[cfg(test)] mod tests { use super::*; #[test] fn test_room_id_display() { let id = RoomId::new(); assert!( id.to_string().contains('-'), "RoomId should be UUID with hyphens" ); } }