Skip to main content

discord_stream_rs/
utils.rs

1/// Ported from `utils.ts`.
2
3use thiserror::Error;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum StreamType {
7    Guild,
8    Call,
9}
10
11impl std::fmt::Display for StreamType {
12    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
13        match self {
14            StreamType::Guild => write!(f, "guild"),
15            StreamType::Call => write!(f, "call"),
16        }
17    }
18}
19
20#[derive(Debug, Clone)]
21pub struct StreamKey {
22    pub kind: StreamType,
23    pub guild_id: Option<String>,
24    pub channel_id: String,
25    pub user_id: String,
26}
27
28#[derive(Debug, Error)]
29pub enum StreamKeyError {
30    #[error("invalid stream key type: {0}")]
31    InvalidType(String),
32    #[error("stream key too short: {0}")]
33    TooShort(String),
34}
35
36/// Parse a Discord stream key.  
37/// Format: `guild:<guild_id>:<channel_id>:<user_id>` or `call:<channel_id>:<user_id>`
38pub fn parse_stream_key(key: &str) -> Result<StreamKey, StreamKeyError> {
39    let mut parts: Vec<&str> = key.splitn(5, ':').collect();
40    let kind_str = parts.remove(0);
41    let kind = match kind_str {
42        "guild" => StreamType::Guild,
43        "call" => StreamType::Call,
44        other => return Err(StreamKeyError::InvalidType(other.to_owned())),
45    };
46
47    match kind {
48        StreamType::Guild => {
49            if parts.len() < 3 {
50                return Err(StreamKeyError::TooShort(key.to_owned()));
51            }
52            Ok(StreamKey {
53                kind,
54                guild_id: Some(parts[0].to_owned()),
55                channel_id: parts[1].to_owned(),
56                user_id: parts[2].to_owned(),
57            })
58        }
59        StreamType::Call => {
60            if parts.len() < 2 {
61                return Err(StreamKeyError::TooShort(key.to_owned()));
62            }
63            Ok(StreamKey {
64                kind,
65                guild_id: None,
66                channel_id: parts[0].to_owned(),
67                user_id: parts[1].to_owned(),
68            })
69        }
70    }
71}
72
73/// Generate a Discord stream key.
74pub fn generate_stream_key(
75    kind: StreamType,
76    guild_id: Option<&str>,
77    channel_id: &str,
78    user_id: &str,
79) -> String {
80    match kind {
81        StreamType::Guild => {
82            format!(
83                "guild:{}:{}:{}",
84                guild_id.unwrap_or(""),
85                channel_id,
86                user_id
87            )
88        }
89        StreamType::Call => {
90            format!("call:{}:{}", channel_id, user_id)
91        }
92    }
93}
94
95// ---------------------------------------------------------------------------
96// Video codec helpers
97// ---------------------------------------------------------------------------
98
99#[derive(Debug, Clone, Copy, PartialEq, Eq)]
100pub enum VideoCodec {
101    H264,
102    H265,
103    Vp8,
104    Vp9,
105    Av1,
106}
107
108impl std::fmt::Display for VideoCodec {
109    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110        match self {
111            VideoCodec::H264 => write!(f, "H264"),
112            VideoCodec::H265 => write!(f, "H265"),
113            VideoCodec::Vp8 => write!(f, "VP8"),
114            VideoCodec::Vp9 => write!(f, "VP9"),
115            VideoCodec::Av1 => write!(f, "AV1"),
116        }
117    }
118}
119
120#[derive(Debug, Error)]
121#[error("unknown codec: {0}")]
122pub struct UnknownCodecError(pub String);
123
124/// Normalize a codec string to a [`VideoCodec`].
125/// Mirrors `normalizeVideoCodec()` in `utils.ts`.
126pub fn normalize_video_codec(s: &str) -> Result<VideoCodec, UnknownCodecError> {
127    let upper = s.to_uppercase();
128    if upper.contains("264") || upper.contains("AVC") {
129        return Ok(VideoCodec::H264);
130    }
131    if upper.contains("265") || upper.contains("HEVC") {
132        return Ok(VideoCodec::H265);
133    }
134    if upper.contains("VP8") {
135        return Ok(VideoCodec::Vp8);
136    }
137    if upper.contains("VP9") {
138        return Ok(VideoCodec::Vp9);
139    }
140    if upper.contains("AV1") {
141        return Ok(VideoCodec::Av1);
142    }
143    Err(UnknownCodecError(s.to_owned()))
144}
145
146// ---------------------------------------------------------------------------
147// Encryption mode constants  (SupportedEncryptionModes in utils.ts)
148// ---------------------------------------------------------------------------
149
150pub const ENCRYPTION_AES256: &str = "aead_aes256_gcm_rtpsize";
151pub const ENCRYPTION_XCHACHA20: &str = "aead_xchacha20_poly1305_rtpsize";
152
153// Hardcoded simulcast descriptor sent in IDENTIFY (STREAMS_SIMULCAST in utils.ts)
154pub const STREAMS_SIMULCAST_RID: &str = "100";
155pub const STREAMS_SIMULCAST_QUALITY: u8 = 100;
156
157pub const MAX_INT16: u32 = 1 << 16;
158pub const MAX_INT32: u64 = 1 << 32;