use nojson::{DisplayJson, JsonFormatter, RawJsonOwned};
use shiguredo_http11::uri::Uri;
use crate::error::{Error, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignalingType {
WebSocket,
DataChannel,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SignalingDirection {
Sent,
Received,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Role {
SendOnly,
RecvOnly,
SendRecv,
}
impl Role {
pub fn parse(value: &str) -> Result<Self> {
match value {
"sendonly" => Ok(Self::SendOnly),
"recvonly" => Ok(Self::RecvOnly),
"sendrecv" => Ok(Self::SendRecv),
_ => Err(Error::InvalidRole {
value: value.to_string(),
}),
}
}
pub fn as_sora_role(self) -> &'static str {
match self {
Role::SendOnly => "sendonly",
Role::RecvOnly => "recvonly",
Role::SendRecv => "sendrecv",
}
}
pub fn wants_send(self) -> bool {
matches!(self, Role::SendOnly | Role::SendRecv)
}
pub fn wants_recv(self) -> bool {
matches!(self, Role::RecvOnly | Role::SendRecv)
}
}
#[derive(Clone, PartialEq, Eq, Default)]
pub struct ProxyInfo {
pub url: String,
pub username: Option<String>,
pub password: Option<String>,
pub user_agent: Option<String>,
}
impl std::fmt::Debug for ProxyInfo {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let masked_url = mask_url_userinfo(&self.url);
f.debug_struct("ProxyInfo")
.field("url", &masked_url)
.field("username", &self.username.as_ref().map(|_| "<redacted>"))
.field("password", &self.password.as_ref().map(|_| "<redacted>"))
.field("user_agent", &self.user_agent)
.finish()
}
}
fn mask_url_userinfo(url: &str) -> std::borrow::Cow<'_, str> {
let Ok(uri) = Uri::parse(url) else {
return std::borrow::Cow::Borrowed(url);
};
let Some(authority) = uri.authority() else {
return std::borrow::Cow::Borrowed(url);
};
let Some(at_pos) = authority.rfind('@') else {
return std::borrow::Cow::Borrowed(url);
};
let mut masked = String::with_capacity(url.len() + 10);
if let Some(scheme) = uri.scheme() {
masked.push_str(scheme);
masked.push_str("://");
}
masked.push_str("<redacted>@");
masked.push_str(&authority[at_pos + 1..]);
masked.push_str(uri.path());
if let Some(query) = uri.query() {
masked.push('?');
masked.push_str(query);
}
if let Some(fragment) = uri.fragment() {
masked.push('#');
masked.push_str(fragment);
}
std::borrow::Cow::Owned(masked)
}
#[derive(Debug, Clone)]
pub struct JsonString {
raw: RawJsonOwned,
}
impl std::str::FromStr for JsonString {
type Err = Error;
fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
RawJsonOwned::parse(value)
.map(Self::from)
.map_err(Error::from)
}
}
impl std::fmt::Display for JsonString {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&self.raw.to_string())
}
}
impl From<RawJsonOwned> for JsonString {
fn from(raw: RawJsonOwned) -> Self {
Self { raw }
}
}
impl DisplayJson for JsonString {
fn fmt(&self, f: &mut JsonFormatter<'_, '_>) -> std::fmt::Result {
DisplayJson::fmt(&self.raw, f)
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AudioOpusParams {
pub channels: Option<u32>,
pub maxplaybackrate: Option<u32>,
pub minptime: Option<u32>,
pub ptime: Option<u32>,
pub stereo: Option<bool>,
pub sprop_stereo: Option<bool>,
pub useinbandfec: Option<bool>,
pub usedtx: Option<bool>,
}
impl DisplayJson for AudioOpusParams {
fn fmt(&self, f: &mut JsonFormatter<'_, '_>) -> std::fmt::Result {
f.object(|f| {
if let Some(channels) = self.channels {
f.member("channels", channels)?;
}
if let Some(maxplaybackrate) = self.maxplaybackrate {
f.member("maxplaybackrate", maxplaybackrate)?;
}
if let Some(minptime) = self.minptime {
f.member("minptime", minptime)?;
}
if let Some(ptime) = self.ptime {
f.member("ptime", ptime)?;
}
if let Some(stereo) = self.stereo {
f.member("stereo", stereo)?;
}
if let Some(sprop_stereo) = self.sprop_stereo {
f.member("sprop_stereo", sprop_stereo)?;
}
if let Some(useinbandfec) = self.useinbandfec {
f.member("useinbandfec", useinbandfec)?;
}
if let Some(usedtx) = self.usedtx {
f.member("usedtx", usedtx)?;
}
Ok(())
})
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VideoCodecType {
Vp8,
Vp9,
H264,
H265,
Av1,
}
impl DisplayJson for VideoCodecType {
fn fmt(&self, f: &mut JsonFormatter<'_, '_>) -> std::fmt::Result {
match self {
VideoCodecType::Vp8 => f.value("VP8"),
VideoCodecType::Vp9 => f.value("VP9"),
VideoCodecType::H264 => f.value("H264"),
VideoCodecType::H265 => f.value("H265"),
VideoCodecType::Av1 => f.value("AV1"),
}
}
}
impl From<VideoCodecType> for shiguredo_webrtc::VideoCodecType {
fn from(value: VideoCodecType) -> Self {
match value {
VideoCodecType::Vp8 => shiguredo_webrtc::VideoCodecType::Vp8,
VideoCodecType::Vp9 => shiguredo_webrtc::VideoCodecType::Vp9,
VideoCodecType::H264 => shiguredo_webrtc::VideoCodecType::H264,
VideoCodecType::H265 => shiguredo_webrtc::VideoCodecType::H265,
VideoCodecType::Av1 => shiguredo_webrtc::VideoCodecType::Av1,
}
}
}
impl TryFrom<shiguredo_webrtc::VideoCodecType> for VideoCodecType {
type Error = String;
fn try_from(value: shiguredo_webrtc::VideoCodecType) -> std::result::Result<Self, Self::Error> {
match value {
shiguredo_webrtc::VideoCodecType::Vp8 => Ok(VideoCodecType::Vp8),
shiguredo_webrtc::VideoCodecType::Vp9 => Ok(VideoCodecType::Vp9),
shiguredo_webrtc::VideoCodecType::H264 => Ok(VideoCodecType::H264),
shiguredo_webrtc::VideoCodecType::H265 => Ok(VideoCodecType::H265),
shiguredo_webrtc::VideoCodecType::Av1 => Ok(VideoCodecType::Av1),
other => Err(format!(
"変換できません: shiguredo_webrtc::VideoCodecType::{other:?} → sora_sdk::VideoCodecType"
)),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AudioCodecType {
Opus,
}
impl DisplayJson for AudioCodecType {
fn fmt(&self, f: &mut JsonFormatter<'_, '_>) -> std::fmt::Result {
match self {
AudioCodecType::Opus => f.value("OPUS"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Audio {
Bool(bool),
Opus {
bit_rate: Option<u32>,
params: Option<AudioOpusParams>,
},
}
impl Audio {
pub fn new_bool(enabled: bool) -> Self {
Self::Bool(enabled)
}
pub fn new_opus(bit_rate: Option<u32>, params: Option<AudioOpusParams>) -> Self {
Self::Opus { bit_rate, params }
}
}
impl DisplayJson for Audio {
fn fmt(&self, f: &mut JsonFormatter<'_, '_>) -> std::fmt::Result {
match self {
Audio::Bool(b) => f.value(*b),
Audio::Opus { bit_rate, params } => f.object(|f| {
f.member("codec_type", AudioCodecType::Opus)?;
if let Some(bit_rate) = bit_rate {
f.member("bit_rate", bit_rate)?;
}
if let Some(params) = params {
f.member("opus_params", params)?;
}
Ok(())
}),
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct VideoVP9Params {
pub profile_id: Option<u32>,
}
impl DisplayJson for VideoVP9Params {
fn fmt(&self, f: &mut JsonFormatter<'_, '_>) -> std::fmt::Result {
f.object(|f| {
if let Some(profile_id) = self.profile_id {
f.member("profile_id", profile_id)?;
}
Ok(())
})
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct VideoH264Params {
pub profile_level_id: Option<String>,
pub b_frame: Option<bool>,
}
impl DisplayJson for VideoH264Params {
fn fmt(&self, f: &mut JsonFormatter<'_, '_>) -> std::fmt::Result {
f.object(|f| {
if let Some(profile_level_id) = self.profile_level_id.as_ref() {
f.member("profile_level_id", profile_level_id)?;
}
if let Some(b_frame) = self.b_frame {
f.member("b_frame", b_frame)?;
}
Ok(())
})
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct VideoH265Params {
pub level_id: Option<String>,
pub profile_id: Option<u32>,
pub tier_flag: Option<u32>,
pub tx_mode: Option<String>,
pub b_frame: Option<bool>,
}
impl DisplayJson for VideoH265Params {
fn fmt(&self, f: &mut JsonFormatter<'_, '_>) -> std::fmt::Result {
f.object(|f| {
if let Some(level_id) = self.level_id.as_ref() {
f.member("level_id", level_id)?;
}
if let Some(profile_id) = self.profile_id {
f.member("profile_id", profile_id)?;
}
if let Some(tier_flag) = self.tier_flag {
f.member("tier_flag", tier_flag)?;
}
if let Some(tx_mode) = self.tx_mode.as_ref() {
f.member("tx_mode", tx_mode)?;
}
if let Some(b_frame) = self.b_frame {
f.member("b_frame", b_frame)?;
}
Ok(())
})
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct VideoAV1Params {
pub profile: Option<u32>,
pub level_idx: Option<u32>,
pub tier: Option<u32>,
}
impl DisplayJson for VideoAV1Params {
fn fmt(&self, f: &mut JsonFormatter<'_, '_>) -> std::fmt::Result {
f.object(|f| {
if let Some(profile) = self.profile {
f.member("profile", profile)?;
}
if let Some(level_idx) = self.level_idx {
f.member("level_idx", level_idx)?;
}
if let Some(tier) = self.tier {
f.member("tier", tier)?;
}
Ok(())
})
}
}
#[derive(Debug, Clone)]
pub struct ConnectDataChannel {
pub label: String,
pub direction: String,
pub ordered: Option<bool>,
pub max_packet_life_time: Option<i32>,
pub max_retransmits: Option<i32>,
pub protocol: Option<String>,
pub compress: Option<bool>,
pub header: Option<Vec<JsonString>>,
}
impl DisplayJson for ConnectDataChannel {
fn fmt(&self, f: &mut JsonFormatter<'_, '_>) -> std::fmt::Result {
f.object(|f| {
f.member("label", &self.label)?;
f.member("direction", &self.direction)?;
if let Some(ordered) = self.ordered {
f.member("ordered", ordered)?;
}
if let Some(max_packet_life_time) = self.max_packet_life_time {
f.member("max_packet_life_time", max_packet_life_time)?;
}
if let Some(max_retransmits) = self.max_retransmits {
f.member("max_retransmits", max_retransmits)?;
}
if let Some(protocol) = &self.protocol {
f.member("protocol", protocol)?;
}
if let Some(compress) = self.compress {
f.member("compress", compress)?;
}
if let Some(header) = &self.header {
f.member("header", header)?;
}
Ok(())
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ForwardingFilterRule {
pub field: String,
pub operator: String,
pub values: Vec<String>,
}
impl DisplayJson for ForwardingFilterRule {
fn fmt(&self, f: &mut JsonFormatter<'_, '_>) -> std::fmt::Result {
f.object(|f| {
f.member("field", &self.field)?;
f.member("operator", &self.operator)?;
f.member("values", &self.values)
})
}
}
#[derive(Debug, Clone)]
pub struct ForwardingFilter {
pub name: Option<String>,
pub priority: Option<i32>,
pub action: Option<String>,
pub rules: Vec<Vec<ForwardingFilterRule>>,
pub version: Option<String>,
pub metadata: Option<JsonString>,
}
impl DisplayJson for ForwardingFilter {
fn fmt(&self, f: &mut JsonFormatter<'_, '_>) -> std::fmt::Result {
f.object(|f| {
if let Some(name) = &self.name {
f.member("name", name)?;
}
if let Some(priority) = self.priority {
f.member("priority", priority)?;
}
if let Some(action) = &self.action {
f.member("action", action)?;
}
f.member("rules", &self.rules)?;
if let Some(version) = &self.version {
f.member("version", version)?;
}
if let Some(metadata) = &self.metadata {
f.member("metadata", metadata)?;
}
Ok(())
})
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Video {
Bool(bool),
Vp8 {
bit_rate: Option<u32>,
},
Vp9 {
bit_rate: Option<u32>,
params: Option<VideoVP9Params>,
},
H264 {
bit_rate: Option<u32>,
params: Option<VideoH264Params>,
},
H265 {
bit_rate: Option<u32>,
params: Option<VideoH265Params>,
},
Av1 {
bit_rate: Option<u32>,
params: Option<VideoAV1Params>,
},
}
impl Video {
pub fn new_bool(enabled: bool) -> Self {
Self::Bool(enabled)
}
pub fn new_vp8(bit_rate: Option<u32>) -> Self {
Self::Vp8 { bit_rate }
}
pub fn new_vp9(bit_rate: Option<u32>, params: Option<VideoVP9Params>) -> Self {
Self::Vp9 { bit_rate, params }
}
pub fn new_av1(bit_rate: Option<u32>, params: Option<VideoAV1Params>) -> Self {
Self::Av1 { bit_rate, params }
}
pub fn new_h264(bit_rate: Option<u32>, params: Option<VideoH264Params>) -> Self {
Self::H264 { bit_rate, params }
}
pub fn new_h265(bit_rate: Option<u32>, params: Option<VideoH265Params>) -> Self {
Self::H265 { bit_rate, params }
}
}
impl DisplayJson for Video {
fn fmt(&self, f: &mut JsonFormatter<'_, '_>) -> std::fmt::Result {
match self {
Video::Bool(b) => f.value(*b),
Video::Vp8 { bit_rate } => f.object(|f| {
f.member("codec_type", VideoCodecType::Vp8)?;
if let Some(bit_rate) = bit_rate {
f.member("bit_rate", bit_rate)?;
}
Ok(())
}),
Video::Vp9 { bit_rate, params } => f.object(|f| {
f.member("codec_type", VideoCodecType::Vp9)?;
if let Some(bit_rate) = bit_rate {
f.member("bit_rate", bit_rate)?;
}
if let Some(params) = params {
f.member("vp9_params", params)?;
}
Ok(())
}),
Video::Av1 { bit_rate, params } => f.object(|f| {
f.member("codec_type", VideoCodecType::Av1)?;
if let Some(bit_rate) = bit_rate {
f.member("bit_rate", bit_rate)?;
}
if let Some(params) = params {
f.member("av1_params", params)?;
}
Ok(())
}),
Video::H264 { bit_rate, params } => f.object(|f| {
f.member("codec_type", VideoCodecType::H264)?;
if let Some(bit_rate) = bit_rate {
f.member("bit_rate", bit_rate)?;
}
if let Some(params) = params {
f.member("h264_params", params)?;
}
Ok(())
}),
Video::H265 { bit_rate, params } => f.object(|f| {
f.member("codec_type", VideoCodecType::H265)?;
if let Some(bit_rate) = bit_rate {
f.member("bit_rate", bit_rate)?;
}
if let Some(params) = params {
f.member("h265_params", params)?;
}
Ok(())
}),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use nojson::Json;
#[test]
fn audio_new_opus_serializes_opus_params() {
let opus_params = AudioOpusParams {
channels: Some(2),
maxplaybackrate: Some(48_000),
minptime: Some(10),
ptime: Some(20),
stereo: Some(true),
sprop_stereo: Some(true),
useinbandfec: Some(true),
usedtx: Some(false),
};
let audio = Audio::new_opus(Some(64_000), Some(opus_params));
let json = Json(&audio).to_string();
assert_eq!(
json,
r#"{"codec_type":"OPUS","bit_rate":64000,"opus_params":{"channels":2,"maxplaybackrate":48000,"minptime":10,"ptime":20,"stereo":true,"sprop_stereo":true,"useinbandfec":true,"usedtx":false}}"#
);
}
#[test]
fn video_new_vp9_serializes_vp9_params() {
let vp9_params = VideoVP9Params {
profile_id: Some(2),
};
let video = Video::new_vp9(Some(512_000), Some(vp9_params));
let json = Json(&video).to_string();
assert_eq!(
json,
r#"{"codec_type":"VP9","bit_rate":512000,"vp9_params":{"profile_id":2}}"#
);
}
#[test]
fn video_new_av1_serializes_av1_params() {
let av1_params = VideoAV1Params {
profile: Some(1),
level_idx: Some(10),
tier: Some(1),
};
let video = Video::new_av1(Some(768_000), Some(av1_params));
let json = Json(&video).to_string();
assert_eq!(
json,
r#"{"codec_type":"AV1","bit_rate":768000,"av1_params":{"profile":1,"level_idx":10,"tier":1}}"#
);
}
#[test]
fn video_new_h264_serializes_h264_params() {
let h264_params = VideoH264Params {
profile_level_id: Some("42e01f".to_string()),
b_frame: Some(true),
};
let video = Video::new_h264(Some(1_000_000), Some(h264_params));
let json = Json(&video).to_string();
assert_eq!(
json,
r#"{"codec_type":"H264","bit_rate":1000000,"h264_params":{"profile_level_id":"42e01f","b_frame":true}}"#
);
}
#[test]
fn video_new_h265_serializes_h265_params() {
let h265_params = VideoH265Params {
level_id: Some("120".to_string()),
profile_id: Some(1),
tier_flag: Some(0),
tx_mode: Some("MRST".to_string()),
b_frame: Some(false),
};
let video = Video::new_h265(Some(1_200_000), Some(h265_params));
let json = Json(&video).to_string();
assert_eq!(
json,
r#"{"codec_type":"H265","bit_rate":1200000,"h265_params":{"level_id":"120","profile_id":1,"tier_flag":0,"tx_mode":"MRST","b_frame":false}}"#
);
}
#[test]
fn mask_url_userinfo_masks_userinfo() {
assert_eq!(
mask_url_userinfo("http://user:pass@host"),
"http://<redacted>@host"
);
}
#[test]
fn mask_url_userinfo_masks_userinfo_with_path() {
assert_eq!(
mask_url_userinfo("http://user:pass@host/path"),
"http://<redacted>@host/path"
);
}
#[test]
fn mask_url_userinfo_no_userinfo() {
assert_eq!(mask_url_userinfo("http://host"), "http://host");
}
#[test]
fn mask_url_userinfo_query_at_sign_not_masked() {
assert_eq!(mask_url_userinfo("http://host?q=x@y"), "http://host?q=x@y");
}
#[test]
fn mask_url_userinfo_fragment_at_sign_not_masked() {
assert_eq!(
mask_url_userinfo("http://host#frag@evil"),
"http://host#frag@evil"
);
}
#[test]
fn mask_url_userinfo_user_only() {
assert_eq!(
mask_url_userinfo("http://user@host"),
"http://<redacted>@host"
);
}
#[test]
fn mask_url_userinfo_query_bug_case() {
assert_eq!(
mask_url_userinfo("http://host?token=abc@evil.com"),
"http://host?token=abc@evil.com"
);
}
#[test]
fn mask_url_userinfo_with_port() {
assert_eq!(
mask_url_userinfo("http://user:pass@host:8080/path"),
"http://<redacted>@host:8080/path"
);
}
#[test]
fn mask_url_userinfo_non_http_scheme() {
assert_eq!(
mask_url_userinfo("rtsp://admin:12345@camera.example.com:554/stream"),
"rtsp://<redacted>@camera.example.com:554/stream"
);
}
#[test]
fn mask_url_userinfo_no_scheme() {
assert_eq!(mask_url_userinfo("no-scheme-url"), "no-scheme-url");
}
#[test]
fn mask_url_userinfo_invalid_url() {
assert_eq!(mask_url_userinfo("not a valid url"), "not a valid url");
}
#[test]
fn mask_url_userinfo_ipv6_address() {
assert_eq!(
mask_url_userinfo("http://user:pass@[::1]:8080/path"),
"http://<redacted>@[::1]:8080/path"
);
}
}