Add native self-hosted instance connection to fluxer_desktop
Trimmed monorepo checkout (fluxer_desktop + packages/voice_engine_v2 + tools/ci) with a "Connect to a Different Server" menu item and popout that lets the desktop app switch to any self-hosted Fluxer instance, plus fixes for well-known discovery on single-domain self-hosted deployments and a false-positive ERR_ABORTED on same-origin client redirects during the switch. Defaults to chat.fluxr.chat and uses an isolated userData directory from the official build.
This commit is contained in:
@@ -0,0 +1,643 @@
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
pub fn push_json_string(out: &mut String, value: &str) {
|
||||
out.push('"');
|
||||
for ch in value.chars() {
|
||||
match ch {
|
||||
'"' => out.push_str("\\\""),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
'\u{08}' => out.push_str("\\b"),
|
||||
'\u{0C}' => out.push_str("\\f"),
|
||||
c if (c as u32) < 0x20 => {
|
||||
out.push_str(&format!("\\u{:04x}", c as u32));
|
||||
}
|
||||
c => out.push(c),
|
||||
}
|
||||
}
|
||||
out.push('"');
|
||||
}
|
||||
|
||||
pub enum JsonValue {
|
||||
Str(String),
|
||||
Raw(String),
|
||||
}
|
||||
|
||||
pub fn json_object(fields: &[(&str, JsonValue)]) -> String {
|
||||
let mut out = String::from("{");
|
||||
for (i, (key, value)) in fields.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push(',');
|
||||
}
|
||||
push_json_string(&mut out, key);
|
||||
out.push(':');
|
||||
match value {
|
||||
JsonValue::Str(s) => push_json_string(&mut out, s),
|
||||
JsonValue::Raw(r) => out.push_str(r),
|
||||
}
|
||||
}
|
||||
out.push('}');
|
||||
out
|
||||
}
|
||||
|
||||
pub fn json_string_array(items: &[String]) -> String {
|
||||
let mut out = String::from("[");
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push(',');
|
||||
}
|
||||
push_json_string(&mut out, item);
|
||||
}
|
||||
out.push(']');
|
||||
out
|
||||
}
|
||||
|
||||
pub fn json_u8_array(items: &[u8]) -> String {
|
||||
let mut out = String::from("[");
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push(',');
|
||||
}
|
||||
out.push_str(&item.to_string());
|
||||
}
|
||||
out.push(']');
|
||||
out
|
||||
}
|
||||
|
||||
pub fn json_raw_array(items: &[String]) -> String {
|
||||
let mut out = String::from("[");
|
||||
for (i, item) in items.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push(',');
|
||||
}
|
||||
out.push_str(item);
|
||||
}
|
||||
out.push(']');
|
||||
out
|
||||
}
|
||||
|
||||
pub fn json_string_map(items: &std::collections::HashMap<String, String>) -> String {
|
||||
let mut entries: Vec<(&String, &String)> = items.iter().collect();
|
||||
entries.sort_by(|left, right| left.0.cmp(right.0));
|
||||
let mut out = String::from("{");
|
||||
for (i, (key, value)) in entries.iter().enumerate() {
|
||||
if i > 0 {
|
||||
out.push(',');
|
||||
}
|
||||
push_json_string(&mut out, key);
|
||||
out.push(':');
|
||||
push_json_string(&mut out, value);
|
||||
}
|
||||
out.push('}');
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(feature = "publisher")]
|
||||
mod live {
|
||||
use super::{
|
||||
JsonValue, json_object, json_raw_array, json_string_array, json_string_map, json_u8_array,
|
||||
};
|
||||
use livekit::participant::{
|
||||
ConnectionQuality, LocalParticipant, Participant, RemoteParticipant,
|
||||
};
|
||||
use livekit::publication::{
|
||||
LocalTrackPublication, RemoteTrackPublication, SubscriptionStatus, TrackPublication,
|
||||
};
|
||||
use livekit::track::{TrackKind, TrackSource};
|
||||
use livekit::{DataPacketKind, RoomEvent};
|
||||
|
||||
pub fn track_kind_str(kind: TrackKind) -> &'static str {
|
||||
match kind {
|
||||
TrackKind::Audio => "audio",
|
||||
TrackKind::Video => "video",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn track_source_str(source: TrackSource) -> &'static str {
|
||||
match source {
|
||||
TrackSource::Unknown => "unknown",
|
||||
TrackSource::Camera => "camera",
|
||||
TrackSource::Microphone => "microphone",
|
||||
TrackSource::Screenshare => "screen_share",
|
||||
TrackSource::ScreenshareAudio => "screen_share_audio",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn connection_quality_str(quality: ConnectionQuality) -> &'static str {
|
||||
match quality {
|
||||
ConnectionQuality::Excellent => "excellent",
|
||||
ConnectionQuality::Good => "good",
|
||||
ConnectionQuality::Poor => "poor",
|
||||
ConnectionQuality::Lost => "lost",
|
||||
}
|
||||
}
|
||||
|
||||
fn s(value: impl Into<String>) -> JsonValue {
|
||||
JsonValue::Str(value.into())
|
||||
}
|
||||
|
||||
fn b(value: bool) -> JsonValue {
|
||||
JsonValue::Raw(value.to_string())
|
||||
}
|
||||
|
||||
fn subscription_status_str(status: SubscriptionStatus) -> &'static str {
|
||||
match status {
|
||||
SubscriptionStatus::Desired => "desired",
|
||||
SubscriptionStatus::Subscribed => "subscribed",
|
||||
SubscriptionStatus::Unsubscribed => "unsubscribed",
|
||||
}
|
||||
}
|
||||
|
||||
fn participant_snapshot(participant: &Participant) -> String {
|
||||
json_object(&[
|
||||
("sid", s(participant.sid().to_string())),
|
||||
("identity", s(participant.identity().to_string())),
|
||||
("name", s(participant.name())),
|
||||
])
|
||||
}
|
||||
|
||||
fn push_remote_participant_fields(
|
||||
fields: &mut Vec<(&'static str, JsonValue)>,
|
||||
participant: &RemoteParticipant,
|
||||
) {
|
||||
fields.push(("participantSid", s(participant.sid().to_string())));
|
||||
fields.push(("identity", s(participant.identity().to_string())));
|
||||
fields.push(("participantName", s(participant.name())));
|
||||
}
|
||||
|
||||
fn push_local_participant_fields(
|
||||
fields: &mut Vec<(&'static str, JsonValue)>,
|
||||
participant: &LocalParticipant,
|
||||
) {
|
||||
fields.push(("participantSid", s(participant.sid().to_string())));
|
||||
fields.push(("identity", s(participant.identity().to_string())));
|
||||
fields.push(("participantName", s(participant.name())));
|
||||
}
|
||||
|
||||
fn push_participant_fields(
|
||||
fields: &mut Vec<(&'static str, JsonValue)>,
|
||||
participant: &Participant,
|
||||
) {
|
||||
fields.push(("participantSid", s(participant.sid().to_string())));
|
||||
fields.push(("identity", s(participant.identity().to_string())));
|
||||
fields.push(("participantName", s(participant.name())));
|
||||
}
|
||||
|
||||
fn push_remote_publication_fields(
|
||||
fields: &mut Vec<(&'static str, JsonValue)>,
|
||||
publication: &RemoteTrackPublication,
|
||||
) {
|
||||
fields.push(("trackSid", s(publication.sid().to_string())));
|
||||
fields.push(("trackName", s(publication.name())));
|
||||
fields.push(("kind", s(track_kind_str(publication.kind()))));
|
||||
fields.push(("source", s(track_source_str(publication.source()))));
|
||||
fields.push(("muted", b(publication.is_muted())));
|
||||
fields.push(("subscribed", b(publication.is_subscribed())));
|
||||
fields.push((
|
||||
"subscriptionStatus",
|
||||
s(subscription_status_str(publication.subscription_status())),
|
||||
));
|
||||
}
|
||||
|
||||
fn push_local_publication_fields(
|
||||
fields: &mut Vec<(&'static str, JsonValue)>,
|
||||
publication: &LocalTrackPublication,
|
||||
) {
|
||||
fields.push(("trackSid", s(publication.sid().to_string())));
|
||||
fields.push(("trackName", s(publication.name())));
|
||||
fields.push(("kind", s(track_kind_str(publication.kind()))));
|
||||
fields.push(("source", s(track_source_str(publication.source()))));
|
||||
fields.push(("muted", b(publication.is_muted())));
|
||||
}
|
||||
|
||||
fn push_publication_fields(
|
||||
fields: &mut Vec<(&'static str, JsonValue)>,
|
||||
publication: &TrackPublication,
|
||||
) {
|
||||
fields.push(("trackSid", s(publication.sid().to_string())));
|
||||
fields.push(("trackName", s(publication.name())));
|
||||
fields.push(("kind", s(track_kind_str(publication.kind()))));
|
||||
fields.push(("source", s(track_source_str(publication.source()))));
|
||||
fields.push(("muted", b(publication.is_muted())));
|
||||
}
|
||||
|
||||
fn remote_track_payload(
|
||||
participant: &RemoteParticipant,
|
||||
publication: &RemoteTrackPublication,
|
||||
) -> String {
|
||||
let mut fields = Vec::new();
|
||||
push_remote_participant_fields(&mut fields, participant);
|
||||
push_remote_publication_fields(&mut fields, publication);
|
||||
json_object(&fields)
|
||||
}
|
||||
|
||||
const CONNECTED_ROSTER_PARTICIPANTS_MAX: usize = 1024;
|
||||
const CONNECTED_ROSTER_TRACKS_PER_PARTICIPANT_MAX: usize = 16;
|
||||
|
||||
fn connected_payload(
|
||||
participants_with_tracks: &[(RemoteParticipant, Vec<RemoteTrackPublication>)],
|
||||
) -> String {
|
||||
let participant_count = participants_with_tracks
|
||||
.len()
|
||||
.min(CONNECTED_ROSTER_PARTICIPANTS_MAX);
|
||||
let mut entries = Vec::with_capacity(participant_count);
|
||||
for (participant, publications) in participants_with_tracks
|
||||
.iter()
|
||||
.take(CONNECTED_ROSTER_PARTICIPANTS_MAX)
|
||||
{
|
||||
let track_count = publications
|
||||
.len()
|
||||
.min(CONNECTED_ROSTER_TRACKS_PER_PARTICIPANT_MAX);
|
||||
let mut tracks = Vec::with_capacity(track_count);
|
||||
for publication in publications
|
||||
.iter()
|
||||
.take(CONNECTED_ROSTER_TRACKS_PER_PARTICIPANT_MAX)
|
||||
{
|
||||
tracks.push(remote_track_payload(participant, publication));
|
||||
}
|
||||
entries.push(json_object(&[
|
||||
("sid", s(participant.sid().to_string())),
|
||||
("identity", s(participant.identity().to_string())),
|
||||
("name", s(participant.name())),
|
||||
("tracks", JsonValue::Raw(json_raw_array(&tracks))),
|
||||
]));
|
||||
}
|
||||
assert!(entries.len() <= CONNECTED_ROSTER_PARTICIPANTS_MAX);
|
||||
json_object(&[("participants", JsonValue::Raw(json_raw_array(&entries)))])
|
||||
}
|
||||
|
||||
fn local_track_payload(
|
||||
participant: &LocalParticipant,
|
||||
publication: &LocalTrackPublication,
|
||||
) -> String {
|
||||
let mut fields = Vec::new();
|
||||
push_local_participant_fields(&mut fields, participant);
|
||||
push_local_publication_fields(&mut fields, publication);
|
||||
json_object(&fields)
|
||||
}
|
||||
|
||||
pub fn map_room_event(event: &RoomEvent) -> Option<(&'static str, String)> {
|
||||
map_participant_lifecycle_room_event(event)
|
||||
.or_else(|| map_participant_profile_room_event(event))
|
||||
.or_else(|| map_track_room_event(event))
|
||||
.or_else(|| map_local_track_room_event(event))
|
||||
.or_else(|| map_connection_room_event(event))
|
||||
}
|
||||
|
||||
fn map_participant_lifecycle_room_event(event: &RoomEvent) -> Option<(&'static str, String)> {
|
||||
match event {
|
||||
RoomEvent::ParticipantConnected(p) | RoomEvent::ParticipantActive(p) => Some((
|
||||
"participantJoined",
|
||||
json_object(&[
|
||||
("sid", s(p.sid().to_string())),
|
||||
("identity", s(p.identity().to_string())),
|
||||
("name", s(p.name())),
|
||||
]),
|
||||
)),
|
||||
RoomEvent::ParticipantDisconnected(p) => Some((
|
||||
"participantLeft",
|
||||
json_object(&[
|
||||
("sid", s(p.sid().to_string())),
|
||||
("identity", s(p.identity().to_string())),
|
||||
("name", s(p.name())),
|
||||
]),
|
||||
)),
|
||||
RoomEvent::ParticipantNameChanged {
|
||||
participant,
|
||||
old_name,
|
||||
name,
|
||||
} => Some((
|
||||
"participantNameChanged",
|
||||
json_object(&[
|
||||
("sid", s(participant.sid().to_string())),
|
||||
("identity", s(participant.identity().to_string())),
|
||||
("oldName", s(old_name.to_string())),
|
||||
("name", s(name.to_string())),
|
||||
]),
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_participant_profile_room_event(event: &RoomEvent) -> Option<(&'static str, String)> {
|
||||
match event {
|
||||
RoomEvent::ParticipantMetadataChanged {
|
||||
participant,
|
||||
old_metadata,
|
||||
metadata,
|
||||
} => Some((
|
||||
"participantMetadataChanged",
|
||||
json_object(&[
|
||||
("sid", s(participant.sid().to_string())),
|
||||
("identity", s(participant.identity().to_string())),
|
||||
("name", s(participant.name())),
|
||||
("oldMetadata", s(old_metadata.to_string())),
|
||||
("metadata", s(metadata.to_string())),
|
||||
(
|
||||
"attributes",
|
||||
JsonValue::Raw(json_string_map(&participant.attributes())),
|
||||
),
|
||||
]),
|
||||
)),
|
||||
RoomEvent::ParticipantAttributesChanged {
|
||||
participant,
|
||||
changed_attributes,
|
||||
} => Some((
|
||||
"participantAttributesChanged",
|
||||
json_object(&[
|
||||
("sid", s(participant.sid().to_string())),
|
||||
("identity", s(participant.identity().to_string())),
|
||||
("name", s(participant.name())),
|
||||
(
|
||||
"attributes",
|
||||
JsonValue::Raw(json_string_map(&participant.attributes())),
|
||||
),
|
||||
(
|
||||
"changedAttributes",
|
||||
JsonValue::Raw(json_string_map(changed_attributes)),
|
||||
),
|
||||
]),
|
||||
)),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_track_room_event(event: &RoomEvent) -> Option<(&'static str, String)> {
|
||||
match event {
|
||||
RoomEvent::TrackSubscribed {
|
||||
publication,
|
||||
participant,
|
||||
..
|
||||
} => Some((
|
||||
"trackSubscribed",
|
||||
remote_track_payload(participant, publication),
|
||||
)),
|
||||
RoomEvent::TrackUnsubscribed {
|
||||
publication,
|
||||
participant,
|
||||
..
|
||||
} => Some((
|
||||
"trackUnsubscribed",
|
||||
remote_track_payload(participant, publication),
|
||||
)),
|
||||
RoomEvent::TrackSubscriptionFailed {
|
||||
participant,
|
||||
error,
|
||||
track_sid,
|
||||
} => {
|
||||
let mut fields = Vec::new();
|
||||
push_remote_participant_fields(&mut fields, participant);
|
||||
if let Some(publication) = participant.get_track_publication(track_sid) {
|
||||
push_remote_publication_fields(&mut fields, &publication);
|
||||
} else {
|
||||
fields.push(("trackSid", s(track_sid.to_string())));
|
||||
}
|
||||
fields.push(("error", s(format!("{error}"))));
|
||||
Some(("trackSubscriptionFailed", json_object(&fields)))
|
||||
}
|
||||
RoomEvent::TrackPublished {
|
||||
publication,
|
||||
participant,
|
||||
} => Some((
|
||||
"trackPublished",
|
||||
remote_track_payload(participant, publication),
|
||||
)),
|
||||
RoomEvent::TrackUnpublished {
|
||||
publication,
|
||||
participant,
|
||||
} => Some((
|
||||
"trackUnpublished",
|
||||
remote_track_payload(participant, publication),
|
||||
)),
|
||||
RoomEvent::TrackMuted {
|
||||
participant,
|
||||
publication,
|
||||
} => {
|
||||
let mut fields = Vec::new();
|
||||
push_participant_fields(&mut fields, participant);
|
||||
push_publication_fields(&mut fields, publication);
|
||||
Some(("trackMuted", json_object(&fields)))
|
||||
}
|
||||
RoomEvent::TrackUnmuted {
|
||||
participant,
|
||||
publication,
|
||||
} => {
|
||||
let mut fields = Vec::new();
|
||||
push_participant_fields(&mut fields, participant);
|
||||
push_publication_fields(&mut fields, publication);
|
||||
Some(("trackUnmuted", json_object(&fields)))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_connection_room_event(event: &RoomEvent) -> Option<(&'static str, String)> {
|
||||
match event {
|
||||
RoomEvent::ActiveSpeakersChanged { speakers } => {
|
||||
let sids: Vec<String> = speakers.iter().map(|p| p.sid().to_string()).collect();
|
||||
let participants: Vec<String> = speakers.iter().map(participant_snapshot).collect();
|
||||
Some((
|
||||
"activeSpeakers",
|
||||
json_object(&[
|
||||
("sids", JsonValue::Raw(json_string_array(&sids))),
|
||||
(
|
||||
"participants",
|
||||
JsonValue::Raw(json_raw_array(&participants)),
|
||||
),
|
||||
]),
|
||||
))
|
||||
}
|
||||
RoomEvent::ConnectionQualityChanged {
|
||||
quality,
|
||||
participant,
|
||||
} => Some((
|
||||
"connectionQuality",
|
||||
json_object(&[
|
||||
("sid", s(participant.sid().to_string())),
|
||||
("identity", s(participant.identity().to_string())),
|
||||
("name", s(participant.name())),
|
||||
("quality", s(connection_quality_str(*quality))),
|
||||
]),
|
||||
)),
|
||||
RoomEvent::DataReceived {
|
||||
payload,
|
||||
topic,
|
||||
kind,
|
||||
participant,
|
||||
} => Some((
|
||||
"dataReceived",
|
||||
data_received_payload(
|
||||
payload.as_ref().as_slice(),
|
||||
topic.as_deref(),
|
||||
kind,
|
||||
participant.as_ref(),
|
||||
),
|
||||
)),
|
||||
RoomEvent::E2eeStateChanged { participant, state } => Some((
|
||||
"e2eeState",
|
||||
json_object(&[
|
||||
("sid", s(participant.sid().to_string())),
|
||||
("identity", s(participant.identity().to_string())),
|
||||
("name", s(participant.name())),
|
||||
("state", s(format!("{state:?}").to_lowercase())),
|
||||
]),
|
||||
)),
|
||||
RoomEvent::ConnectionStateChanged(state) => Some((
|
||||
"connectionState",
|
||||
json_object(&[("state", s(format!("{state:?}").to_lowercase()))]),
|
||||
)),
|
||||
RoomEvent::Disconnected { reason } => Some((
|
||||
"disconnected",
|
||||
json_object(&[("reason", s(format!("{reason:?}").to_lowercase()))]),
|
||||
)),
|
||||
RoomEvent::Connected {
|
||||
participants_with_tracks,
|
||||
} => Some(("connected", connected_payload(participants_with_tracks))),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn data_received_payload(
|
||||
payload: &[u8],
|
||||
topic: Option<&str>,
|
||||
kind: &DataPacketKind,
|
||||
participant: Option<&RemoteParticipant>,
|
||||
) -> String {
|
||||
let mut fields = vec![
|
||||
("payloadBytes", JsonValue::Raw(json_u8_array(payload))),
|
||||
(
|
||||
"reliable",
|
||||
JsonValue::Raw(matches!(kind, DataPacketKind::Reliable).to_string()),
|
||||
),
|
||||
(
|
||||
"kind",
|
||||
s(match kind {
|
||||
DataPacketKind::Reliable => "reliable",
|
||||
DataPacketKind::Lossy => "lossy",
|
||||
}),
|
||||
),
|
||||
];
|
||||
if let Some(topic) = topic {
|
||||
fields.push(("topic", s(topic.to_string())));
|
||||
}
|
||||
if let Ok(payload_text) = std::str::from_utf8(payload) {
|
||||
fields.push(("payloadText", s(payload_text.to_string())));
|
||||
}
|
||||
if let Some(participant) = participant {
|
||||
push_remote_participant_fields(&mut fields, participant);
|
||||
}
|
||||
json_object(&fields)
|
||||
}
|
||||
|
||||
fn map_local_track_room_event(event: &RoomEvent) -> Option<(&'static str, String)> {
|
||||
match event {
|
||||
RoomEvent::LocalTrackPublished {
|
||||
publication,
|
||||
participant,
|
||||
..
|
||||
} => Some((
|
||||
"localTrackPublished",
|
||||
local_track_payload(participant, publication),
|
||||
)),
|
||||
RoomEvent::LocalTrackUnpublished {
|
||||
publication,
|
||||
participant,
|
||||
} => Some((
|
||||
"localTrackUnpublished",
|
||||
local_track_payload(participant, publication),
|
||||
)),
|
||||
RoomEvent::LocalTrackRepublished {
|
||||
previous_sid,
|
||||
publication,
|
||||
participant,
|
||||
..
|
||||
} => {
|
||||
let mut fields = Vec::new();
|
||||
push_local_participant_fields(&mut fields, participant);
|
||||
fields.push(("previousTrackSid", s(previous_sid.to_string())));
|
||||
push_local_publication_fields(&mut fields, publication);
|
||||
Some(("localTrackRepublished", json_object(&fields)))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "publisher")]
|
||||
pub use live::{map_room_event, track_kind_str, track_source_str};
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn escapes_quote_backslash_and_control_chars() {
|
||||
let mut out = String::new();
|
||||
push_json_string(&mut out, "a\"b\\c\nd\te");
|
||||
assert_eq!(out, "\"a\\\"b\\\\c\\nd\\te\"");
|
||||
|
||||
let mut ctrl = String::new();
|
||||
push_json_string(&mut ctrl, "\u{01}");
|
||||
assert_eq!(ctrl, "\"\\u0001\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn leaves_plain_ascii_and_unicode_untouched() {
|
||||
let mut out = String::new();
|
||||
push_json_string(&mut out, "PA_abc123");
|
||||
assert_eq!(out, "\"PA_abc123\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_object_preserves_order_and_mixes_str_and_raw() {
|
||||
let json = json_object(&[
|
||||
("sid", JsonValue::Str("PA_1".into())),
|
||||
(
|
||||
"sids",
|
||||
JsonValue::Raw(json_string_array(&["PA_1".into(), "PA_2".into()])),
|
||||
),
|
||||
]);
|
||||
assert_eq!(json, "{\"sid\":\"PA_1\",\"sids\":[\"PA_1\",\"PA_2\"]}");
|
||||
}
|
||||
|
||||
#[cfg(feature = "publisher")]
|
||||
#[test]
|
||||
fn track_source_strings_match_livekit_js_sources() {
|
||||
use livekit::track::TrackSource;
|
||||
|
||||
assert_eq!(track_source_str(TrackSource::Camera), "camera");
|
||||
assert_eq!(track_source_str(TrackSource::Microphone), "microphone");
|
||||
assert_eq!(track_source_str(TrackSource::Screenshare), "screen_share");
|
||||
assert_eq!(
|
||||
track_source_str(TrackSource::ScreenshareAudio),
|
||||
"screen_share_audio"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_object_and_array() {
|
||||
assert_eq!(json_object(&[]), "{}");
|
||||
assert_eq!(json_string_array(&[]), "[]");
|
||||
assert_eq!(json_u8_array(&[]), "[]");
|
||||
assert_eq!(json_raw_array(&[]), "[]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn json_u8_array_serializes_bytes_as_numbers() {
|
||||
assert_eq!(json_u8_array(&[0, 1, 127, 255]), "[0,1,127,255]");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_array_preserves_prebuilt_json_objects() {
|
||||
let items = vec![
|
||||
json_object(&[("sid", JsonValue::Str("PA_1".into()))]),
|
||||
json_object(&[("sid", JsonValue::Str("PA_2".into()))]),
|
||||
];
|
||||
assert_eq!(
|
||||
json_raw_array(&items),
|
||||
"[{\"sid\":\"PA_1\"},{\"sid\":\"PA_2\"}]"
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user