Skip to main content

netget/server/srt/
streamid.rs

1//! SRT stream IDs: the access-control syntax (`#!::r=live/cam,m=publish,u=alice`, SRT
2//! AccessControl.md) and the `publish:path` / `read:path` form MediaMTX uses.
3
4#[derive(Debug, Clone, PartialEq, Eq)]
5pub struct Target {
6    /// The resource, e.g. live/cam.
7    pub resource: String,
8    /// "publish" or "request".
9    pub mode: String,
10    pub user: Option<String>,
11}
12
13pub fn parse(stream_id: &str) -> Result<Target, String> {
14    if stream_id.len() > 512 || crate::utils::sanitize::has_controls(&stream_id) {
15        return Err("stream ID is over 512 characters or holds a control character".into());
16    }
17    if let Some(list) = stream_id.strip_prefix("#!::") {
18        let mut t = Target {
19            resource: String::new(),
20            mode: "request".into(),
21            user: None,
22        };
23        for item in list.split(',').filter(|i| !i.is_empty()) {
24            let (k, v) = item
25                .split_once('=')
26                .ok_or_else(|| format!("{item:?} is not key=value"))?;
27            match k {
28                "r" => t.resource = v.to_owned(),
29                "m" => {
30                    t.mode = match v {
31                        "publish" => "publish",
32                        "request" => "request",
33                        "bidirectional" => return Err("bidirectional mode is not supported".into()),
34                        other => return Err(format!("unknown mode {other:?}")),
35                    }
36                    .into()
37                }
38                "u" => t.user = Some(v.to_owned()),
39                _ => {}
40            }
41        }
42        return Ok(t);
43    }
44    for (prefix, mode) in [("publish:", "publish"), ("read:", "request")] {
45        if let Some(rest) = stream_id.strip_prefix(prefix) {
46            // publish:path[:user:pass][?query]
47            let mut parts = rest.split('?').next().unwrap_or_default().split(':');
48            let path = parts.next().unwrap_or_default();
49            let user = parts.next().filter(|u| !u.is_empty()).map(str::to_owned);
50            return Ok(Target {
51                resource: path.to_owned(),
52                mode: mode.into(),
53                user,
54            });
55        }
56    }
57    Ok(Target {
58        resource: stream_id.to_owned(),
59        mode: "request".into(),
60        user: None,
61    })
62}