feat: server sdk (#47)

* New webrtc build scripts (Still not integrated within the SDK)
* New livekit-api crate (Integrate the server sdk protocol of livekit)
* Moved livekit-utils to livekit-protocol
This commit is contained in:
Théo Monnom
2023-04-19 12:33:39 +02:00
committed by GitHub
parent 459a42bf12
commit 180864e953
62 changed files with 2621 additions and 635 deletions
+15
View File
@@ -0,0 +1,15 @@
[package]
name = "livekit-api"
version = "0.1.0"
edition = "2021"
[dependencies]
jsonwebtoken = {version = "8", default-features = false }
thiserror = "1.0"
serde = {version = "1.0", features = ["derive"] }
serde_json = "1.0"
livekit-protocol = { path = "../livekit-protocol", version = "0.1.0" }
sha2 = "0.10.6"
reqwest = { version = "0.11", features=["json"] }
url = "2.3.1"
prost = "0.11.9"
+260
View File
@@ -0,0 +1,260 @@
use crate::get_env_keys;
use jsonwebtoken::{self, DecodingKey, EncodingKey, Header};
use serde::{Deserialize, Serialize};
use std::env;
use std::fmt::Debug;
use std::ops::Add;
use std::time::Duration;
use std::time::{SystemTime, UNIX_EPOCH};
use thiserror::Error;
pub const DEFAULT_TTL: Duration = Duration::from_secs(3600 * 6); // 6 hours
#[derive(Debug, Error)]
pub enum AccessTokenError {
#[error("Invalid API Key or Secret Key")]
InvalidKeys,
#[error("Invalid environment")]
InvalidEnv(#[from] env::VarError),
#[error("invalid claims: {0}")]
InvalidClaims(&'static str),
#[error("failed to encode jwt")]
Encoding(#[from] jsonwebtoken::errors::Error),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VideoGrants {
// actions on rooms
pub room_create: bool,
pub room_list: bool,
pub room_record: bool,
// actions on a particular room
pub room_admin: bool,
pub room_join: bool,
pub room: String,
// permissions within a room
pub can_publish: bool,
pub can_subscribe: bool,
pub can_publish_data: bool,
// TrackSource types that a participant may publish.
// When set, it supercedes CanPublish. Only sources explicitly set here can be published
pub can_publish_sources: Vec<String>, // keys keep track of each source
// by default, a participant is not allowed to update its own metadata
pub can_update_own_metadata: bool,
// actions on ingresses
pub ingress_admin: bool, // applies to all ingress
// participant is not visible to other participants (useful when making bots)
pub hidden: bool,
// indicates to the room that current participant is a recorder
pub recorder: bool,
}
impl Default for VideoGrants {
fn default() -> Self {
Self {
room_create: false,
room_list: false,
room_record: false,
room_admin: false,
room_join: false,
room: "".to_string(),
can_publish: true,
can_subscribe: true,
can_publish_data: true,
can_publish_sources: Vec::default(),
can_update_own_metadata: false,
ingress_admin: false,
hidden: false,
recorder: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Claims {
pub exp: usize, // Expiration
pub iss: String, // ApiKey
pub nbf: usize,
pub sub: String, // Identity
pub name: String,
pub video: VideoGrants,
pub sha256: String, // Used to verify the integrity of the message body
pub metadata: String,
}
#[derive(Clone)]
pub struct AccessToken {
api_key: String,
api_secret: String,
claims: Claims,
}
impl Debug for AccessToken {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
// Don't show api_secret here
f.debug_struct("AccessToken")
.field("api_key", &self.api_key)
.field("claims", &self.claims)
.finish()
}
}
impl AccessToken {
pub fn with_api_key(api_key: &str, api_secret: &str) -> Self {
let now = SystemTime::now().duration_since(UNIX_EPOCH).unwrap();
Self {
api_key: api_key.to_owned(),
api_secret: api_secret.to_owned(),
claims: Claims {
exp: now.add(DEFAULT_TTL).as_secs() as usize,
iss: api_key.to_owned(),
nbf: now.as_secs() as usize,
sub: Default::default(),
name: Default::default(),
video: VideoGrants::default(),
sha256: Default::default(),
metadata: Default::default(),
},
}
}
pub fn new() -> Result<Self, AccessTokenError> {
// Try to get the API Key and the Secret Key from the environment
let (api_key, api_secret) = get_env_keys()?;
Ok(Self::with_api_key(&api_key, &api_secret))
}
pub fn with_ttl(mut self, ttl: Duration) -> Self {
let time = SystemTime::now().duration_since(UNIX_EPOCH).unwrap() + ttl;
self.claims.exp = time.as_secs() as usize;
self
}
pub fn with_grants(mut self, grants: VideoGrants) -> Self {
self.claims.video = grants;
self
}
pub fn with_identity(mut self, identity: &str) -> Self {
self.claims.sub = identity.to_owned();
self
}
pub fn with_name(mut self, name: &str) -> Self {
self.claims.name = name.to_owned();
self
}
pub fn with_metadata(mut self, metadata: &str) -> Self {
self.claims.metadata = metadata.to_owned();
self
}
pub fn with_sha256(mut self, sha256: &str) -> Self {
self.claims.sha256 = sha256.to_owned();
self
}
pub fn to_jwt(self) -> Result<String, AccessTokenError> {
if self.api_key.is_empty() || self.api_secret.is_empty() {
return Err(AccessTokenError::InvalidKeys);
}
if self.claims.video.room_join && self.claims.sub.is_empty() {
return Err(AccessTokenError::InvalidClaims(
"token grants room_join but doesn't have an identity",
));
}
Ok(jsonwebtoken::encode(
&Header::new(jsonwebtoken::Algorithm::HS256),
&self.claims,
&EncodingKey::from_secret(self.api_secret.as_ref()),
)?)
}
}
#[derive(Clone)]
pub struct TokenVerifier {
api_key: String,
api_secret: String,
}
impl Debug for TokenVerifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TokenVerifier")
.field("api_key", &self.api_key)
.finish()
}
}
impl TokenVerifier {
pub fn with_api_key(api_key: &str, api_secret: &str) -> Self {
Self {
api_key: api_key.to_owned(),
api_secret: api_secret.to_owned(),
}
}
pub fn new() -> Result<Self, AccessTokenError> {
let (api_key, api_secret) = get_env_keys()?;
Ok(Self::with_api_key(&api_key, &api_secret))
}
pub fn verify(&self, token: &str) -> Result<Claims, AccessTokenError> {
let mut validation = jsonwebtoken::Validation::new(jsonwebtoken::Algorithm::HS256);
validation.validate_exp = true;
validation.validate_nbf = true;
validation.set_issuer(&[&self.api_key]);
let token = jsonwebtoken::decode::<Claims>(
token,
&DecodingKey::from_secret(self.api_secret.as_ref()),
&validation,
)?;
Ok(token.claims)
}
}
#[cfg(test)]
mod tests {
use super::{AccessToken, TokenVerifier, VideoGrants};
use std::time::Duration;
const TEST_API_KEY: &str = "myapikey";
const TEST_API_SECRET: &str = "thiskeyistotallyunsafe";
#[test]
fn test_access_token() {
let token = AccessToken::with_api_key(TEST_API_KEY, TEST_API_SECRET)
.with_ttl(Duration::from_secs(60))
.with_identity("test")
.with_name("test")
.with_grants(VideoGrants::default())
.to_jwt()
.unwrap();
let verifier = TokenVerifier::with_api_key(TEST_API_KEY, TEST_API_SECRET);
let claims = verifier.verify(&token).unwrap();
assert_eq!(claims.sub, "test");
assert_eq!(claims.name, "test");
assert_eq!(claims.iss, TEST_API_KEY);
let incorrect_issuer = TokenVerifier::with_api_key("incorrect", TEST_API_SECRET);
assert!(incorrect_issuer.verify(&token).is_err());
let incorrect_token = TokenVerifier::with_api_key(TEST_API_KEY, "incorrect");
assert!(incorrect_token.verify(&token).is_err());
}
}
+9
View File
@@ -0,0 +1,9 @@
pub mod access_token;
pub mod services;
pub mod webhook_receiver;
pub(crate) fn get_env_keys() -> Result<(String, String), std::env::VarError> {
let api_key = std::env::var("LIVEKIT_API_KEY")?;
let api_secret = std::env::var("LIVEKIT_API_SECRET")?;
Ok((api_key, api_secret))
}
+423
View File
@@ -0,0 +1,423 @@
use super::{ServiceBase, ServiceResult, LIVEKIT_PACKAGE};
use crate::services::twirp_client::TwirpClient;
use crate::{access_token::VideoGrants, get_env_keys};
use livekit_protocol as proto;
#[derive(Default, Clone, Debug)]
pub struct RoomCompositeOptions {
pub layout: String,
pub encoding: encoding::EncodingOptions,
pub audio_only: bool,
pub video_only: bool,
pub custom_base_url: String,
}
#[derive(Default, Clone, Debug)]
pub struct WebOptions {
pub encoding: encoding::EncodingOptions,
pub audio_only: bool,
pub video_only: bool,
}
#[derive(Default, Clone, Debug)]
pub struct TrackCompositeOptions {
pub encoding: encoding::EncodingOptions,
pub audio_track_id: String,
pub video_track_id: String,
}
#[derive(Debug, Clone)]
pub enum EgressOutput {
File(proto::EncodedFileOutput),
Stream(proto::StreamOutput),
Segments(proto::SegmentedFileOutput),
}
#[derive(Debug, Clone)]
pub enum TrackEgressOutput {
File(proto::DirectFileOutput),
WebSocket(String),
}
#[derive(Debug, Clone)]
pub enum EgressListFilter {
All,
Egress(String),
Room(String),
}
#[derive(Debug, Clone)]
pub struct EgressListOptions {
pub filter: EgressListFilter,
pub active: bool,
}
const SVC: &'static str = "Egress";
#[derive(Debug)]
pub struct EgressClient {
base: ServiceBase,
client: TwirpClient,
}
impl EgressClient {
pub fn with_api_key(host: &str, api_key: &str, api_secret: &str) -> Self {
Self {
base: ServiceBase::with_api_key(api_key, api_secret),
client: TwirpClient::new(host, LIVEKIT_PACKAGE, None),
}
}
pub fn new(host: &str) -> ServiceResult<Self> {
let (api_key, api_secret) = get_env_keys()?;
Ok(Self::with_api_key(host, &api_key, &api_secret))
}
pub async fn start_room_composite_egress(
&self,
room: &str,
outputs: Vec<EgressOutput>,
options: RoomCompositeOptions,
) -> ServiceResult<proto::EgressInfo> {
let (file_outputs, stream_outputs, segment_outputs) = get_outputs(outputs);
self.client
.request(
SVC,
"StartRoomCompositeEgress",
proto::RoomCompositeEgressRequest {
room_name: room.to_string(),
layout: options.layout,
audio_only: options.audio_only,
video_only: options.video_only,
options: Some(proto::room_composite_egress_request::Options::Advanced(
options.encoding.into(),
)),
custom_base_url: options.custom_base_url,
file_outputs,
stream_outputs,
segment_outputs,
output: None, // Deprecated
},
self.base.auth_header(VideoGrants {
room_record: true,
..Default::default()
})?,
)
.await
.map_err(Into::into)
}
pub async fn start_web_egress(
&self,
url: &str,
outputs: Vec<EgressOutput>,
options: WebOptions,
) -> ServiceResult<proto::EgressInfo> {
let (file_outputs, stream_outputs, segment_outputs) = get_outputs(outputs);
self.client
.request(
SVC,
"StartWebEgress",
proto::WebEgressRequest {
url: url.to_string(),
options: Some(proto::web_egress_request::Options::Advanced(
options.encoding.into(),
)),
audio_only: options.audio_only,
video_only: options.video_only,
file_outputs,
stream_outputs,
segment_outputs,
output: None, // Deprecated
},
self.base.auth_header(VideoGrants {
room_record: true,
..Default::default()
})?,
)
.await
.map_err(Into::into)
}
pub async fn start_track_composite_egress(
&self,
room: &str,
outputs: Vec<EgressOutput>,
options: TrackCompositeOptions,
) -> ServiceResult<proto::EgressInfo> {
let (file_outputs, stream_outputs, segment_outputs) = get_outputs(outputs);
self.client
.request(
SVC,
"StartTrackCompositeEgress",
proto::TrackCompositeEgressRequest {
room_name: room.to_string(),
options: Some(proto::track_composite_egress_request::Options::Advanced(
options.encoding.into(),
)),
audio_track_id: options.audio_track_id,
video_track_id: options.video_track_id,
file_outputs,
stream_outputs,
segment_outputs,
output: None, // Deprecated
},
self.base.auth_header(VideoGrants {
room_record: true,
..Default::default()
})?,
)
.await
.map_err(Into::into)
}
pub async fn start_track_egress(
&self,
room: &str,
output: TrackEgressOutput,
track_id: &str,
) -> ServiceResult<proto::EgressInfo> {
self.client
.request(
SVC,
"StartTrackEgress",
proto::TrackEgressRequest {
room_name: room.to_string(),
output: match output {
TrackEgressOutput::File(f) => {
Some(proto::track_egress_request::Output::File(f))
}
TrackEgressOutput::WebSocket(url) => {
Some(proto::track_egress_request::Output::WebsocketUrl(url))
}
},
track_id: track_id.to_string(),
},
self.base.auth_header(VideoGrants {
room_record: true,
..Default::default()
})?,
)
.await
.map_err(Into::into)
}
pub async fn update_layout(
&self,
egress_id: &str,
layout: &str,
) -> ServiceResult<proto::EgressInfo> {
self.client
.request(
SVC,
"UpdateLayout",
proto::UpdateLayoutRequest {
egress_id: egress_id.to_owned(),
layout: layout.to_owned(),
},
self.base.auth_header(VideoGrants {
room_record: true,
..Default::default()
})?,
)
.await
.map_err(Into::into)
}
pub async fn update_stream(
&self,
egress_id: &str,
add_output_urls: Vec<String>,
remove_output_urls: Vec<String>,
) -> ServiceResult<proto::EgressInfo> {
self.client
.request(
SVC,
"UpdateStream",
proto::UpdateStreamRequest {
egress_id: egress_id.to_owned(),
add_output_urls,
remove_output_urls,
},
self.base.auth_header(VideoGrants {
room_record: true,
..Default::default()
})?,
)
.await
.map_err(Into::into)
}
pub async fn list_egress(
&self,
options: EgressListOptions,
) -> ServiceResult<Vec<proto::EgressInfo>> {
let mut room_name = String::default();
let mut egress_id = String::default();
match options.filter {
EgressListFilter::Room(room) => room_name = room,
EgressListFilter::Egress(egress) => egress_id = egress,
_ => {}
}
let resp: proto::ListEgressResponse = self
.client
.request(
SVC,
"ListEgress",
proto::ListEgressRequest {
room_name,
egress_id,
active: options.active,
},
self.base.auth_header(VideoGrants {
room_record: true,
..Default::default()
})?,
)
.await?;
Ok(resp.items)
}
pub async fn stop_egress(&self, egress_id: &str) -> ServiceResult<proto::EgressInfo> {
self.client
.request(
SVC,
"StopEgress",
proto::StopEgressRequest {
egress_id: egress_id.to_owned(),
},
self.base.auth_header(VideoGrants {
room_record: true,
..Default::default()
})?,
)
.await
.map_err(Into::into)
}
}
fn get_outputs(
outputs: Vec<EgressOutput>,
) -> (
Vec<proto::EncodedFileOutput>,
Vec<proto::StreamOutput>,
Vec<proto::SegmentedFileOutput>,
) {
let mut file_outputs = Vec::new();
let mut stream_outputs = Vec::new();
let mut segment_outputs = Vec::new();
for output in outputs {
match output {
EgressOutput::File(f) => file_outputs.push(f),
EgressOutput::Stream(s) => stream_outputs.push(s),
EgressOutput::Segments(s) => segment_outputs.push(s),
}
}
(file_outputs, stream_outputs, segment_outputs)
}
pub mod encoding {
use super::*;
#[derive(Clone, Debug)]
pub struct EncodingOptions {
pub width: i32,
pub height: i32,
pub depth: i32,
pub framerate: i32,
pub audio_codec: proto::AudioCodec,
pub audio_bitrate: i32,
pub audio_frequency: i32,
pub video_codec: proto::VideoCodec,
pub video_bitrate: i32,
pub keyframe_interval: f64,
}
impl From<EncodingOptions> for proto::EncodingOptions {
fn from(opts: EncodingOptions) -> Self {
Self {
width: opts.width,
height: opts.height,
depth: opts.depth,
framerate: opts.framerate,
audio_codec: opts.audio_codec as i32,
audio_bitrate: opts.audio_bitrate,
audio_frequency: opts.audio_frequency,
video_codec: opts.video_codec as i32,
video_bitrate: opts.video_bitrate,
key_frame_interval: opts.keyframe_interval,
}
}
}
impl EncodingOptions {
const fn new() -> Self {
Self {
width: 1920,
height: 1080,
depth: 24,
framerate: 30,
audio_codec: proto::AudioCodec::Opus,
audio_bitrate: 128,
audio_frequency: 44100,
video_codec: proto::VideoCodec::H264Main,
video_bitrate: 4500,
keyframe_interval: 0.0,
}
}
}
impl Default for EncodingOptions {
fn default() -> Self {
Self::new()
}
}
pub const H264_720P_30: EncodingOptions = EncodingOptions {
width: 1280,
height: 720,
video_bitrate: 3000,
..EncodingOptions::new()
};
pub const H264_720P_60: EncodingOptions = EncodingOptions {
width: 1280,
height: 720,
framerate: 60,
..EncodingOptions::new()
};
pub const H264_1080P_30: EncodingOptions = EncodingOptions::new();
pub const H264_1080P_60: EncodingOptions = EncodingOptions {
framerate: 60,
video_bitrate: 6000,
..EncodingOptions::new()
};
pub const PORTRAIT_H264_720P_30: EncodingOptions = EncodingOptions {
width: 720,
height: 1280,
video_bitrate: 3000,
..EncodingOptions::new()
};
pub const PORTRAIT_H264_720P_60: EncodingOptions = EncodingOptions {
width: 720,
height: 1280,
framerate: 60,
..EncodingOptions::new()
};
pub const PORTRAIT_H264_1080P_30: EncodingOptions = EncodingOptions {
width: 1080,
height: 1920,
..EncodingOptions::new()
};
pub const PORTRAIT_H264_1080P_60: EncodingOptions = EncodingOptions {
width: 1080,
height: 1920,
framerate: 60,
video_bitrate: 6000,
..EncodingOptions::new()
};
}
+138
View File
@@ -0,0 +1,138 @@
use super::{ServiceBase, ServiceResult, LIVEKIT_PACKAGE};
use crate::services::twirp_client::TwirpClient;
use crate::{access_token::VideoGrants, get_env_keys};
use livekit_protocol as proto;
#[derive(Default, Clone, Debug)]
pub struct IngressOptions {
pub name: String,
pub room_name: String,
pub participant_identity: String,
pub participant_name: String,
pub audio: proto::IngressAudioOptions,
pub video: proto::IngressVideoOptions,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IngressListFilter {
All,
Room(String),
}
const SVC: &'static str = "Ingress";
#[derive(Debug)]
pub struct IngressClient {
base: ServiceBase,
client: TwirpClient,
}
impl IngressClient {
pub fn with_api_key(host: &str, api_key: &str, api_secret: &str) -> Self {
Self {
base: ServiceBase::with_api_key(api_key, api_secret),
client: TwirpClient::new(host, LIVEKIT_PACKAGE, None),
}
}
pub fn new(host: &str) -> ServiceResult<Self> {
let (api_key, api_secret) = get_env_keys()?;
Ok(Self::with_api_key(host, &api_key, &api_secret))
}
pub async fn create_ingress(
&self,
input_type: proto::IngressInput,
options: IngressOptions,
) -> ServiceResult<proto::IngressInfo> {
self.client
.request(
SVC,
"CreateIngress",
proto::CreateIngressRequest {
input_type: input_type as i32,
name: options.name,
room_name: options.room_name,
participant_identity: options.participant_identity,
participant_name: options.participant_name,
audio: Some(options.audio),
video: Some(options.video),
},
self.base.auth_header(VideoGrants {
ingress_admin: true,
..Default::default()
})?,
)
.await
.map_err(Into::into)
}
pub async fn update_ingress(
&self,
ingress_id: &str,
options: IngressOptions,
) -> ServiceResult<proto::IngressInfo> {
self.client
.request(
SVC,
"UpdateIngress",
proto::UpdateIngressRequest {
ingress_id: ingress_id.to_owned(),
name: options.name,
room_name: options.room_name,
participant_identity: options.participant_identity,
participant_name: options.participant_name,
audio: Some(options.audio),
video: Some(options.video),
},
self.base.auth_header(VideoGrants {
ingress_admin: true,
..Default::default()
})?,
)
.await
.map_err(Into::into)
}
pub async fn list_ingress(
&self,
filter: IngressListFilter,
) -> ServiceResult<Vec<proto::IngressInfo>> {
let resp: proto::ListIngressResponse = self
.client
.request(
SVC,
"ListIngress",
proto::ListIngressRequest {
room_name: match filter{
IngressListFilter::All => Default::default(),
IngressListFilter::Room(room) => room,
},
},
self.base.auth_header(VideoGrants {
ingress_admin: true,
..Default::default()
})?,
)
.await?;
Ok(resp.items)
}
pub async fn delete_ingress(&self, ingress_id: &str) -> ServiceResult<proto::IngressInfo> {
self.client
.request(
SVC,
"DeleteIngress",
proto::DeleteIngressRequest {
ingress_id: ingress_id.to_owned(),
},
self.base.auth_header(VideoGrants {
ingress_admin: true,
..Default::default()
})?,
)
.await
.map_err(Into::into)
}
}
+59
View File
@@ -0,0 +1,59 @@
use crate::access_token::{AccessToken, AccessTokenError, VideoGrants};
use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION};
use std::fmt::Debug;
use thiserror::Error;
pub mod room;
pub mod egress;
pub mod ingress;
mod twirp_client;
pub const LIVEKIT_PACKAGE: &'static str = "livekit";
#[derive(Debug, Error)]
pub enum ServiceError {
#[error("invalid environment: {0}")]
Env(#[from] std::env::VarError),
#[error("invalid access token: {0}")]
AccessToken(#[from] AccessTokenError),
#[error("twirp error: {0}")]
Twirp(#[from] twirp_client::TwirpError),
}
pub type ServiceResult<T> = Result<T, ServiceError>;
struct ServiceBase {
api_key: String,
api_secret: String,
}
impl Debug for ServiceBase {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ServiceBase")
.field("api_key", &self.api_key)
.finish()
}
}
impl ServiceBase {
pub fn with_api_key(api_key: &str, api_secret: &str) -> Self {
Self {
api_key: api_key.to_owned(),
api_secret: api_secret.to_owned(),
}
}
pub fn auth_header(&self, grants: VideoGrants) -> Result<HeaderMap, AccessTokenError> {
let token = AccessToken::with_api_key(&self.api_key, &self.api_secret)
.with_grants(grants)
.to_jwt()?;
let mut headers = HeaderMap::new();
headers.insert(
AUTHORIZATION,
HeaderValue::from_str(&format!("Bearer {}", token)).unwrap(),
);
Ok(headers)
}
}
+308
View File
@@ -0,0 +1,308 @@
use super::{ServiceBase, ServiceResult, LIVEKIT_PACKAGE};
use crate::services::twirp_client::TwirpClient;
use crate::{access_token::VideoGrants, get_env_keys};
use livekit_protocol as proto;
const SVC: &'static str = "RoomService";
#[derive(Debug, Clone, Default)]
pub struct CreateRoomOptions {
pub empty_timeout: u32,
pub max_participants: u32,
pub node_id: String,
pub metadata: String,
pub egress: Option<proto::RoomEgress>, // TODO(theomonnom): Better API?
}
#[derive(Debug, Clone, Default)]
pub struct UpdateParticipantOptions {
pub metadata: String,
pub permission: Option<proto::ParticipantPermission>,
pub name: String, // No effect if left empty
}
#[derive(Debug, Clone, Default)]
pub struct SendDataOptions {
pub kind: proto::data_packet::Kind,
pub destination_sids: Vec<String>,
pub topic: Option<String>,
}
#[derive(Debug)]
pub struct RoomClient {
base: ServiceBase,
client: TwirpClient,
}
impl RoomClient {
pub fn with_api_key(host: &str, api_key: &str, api_secret: &str) -> Self {
Self {
base: ServiceBase::with_api_key(api_key, api_secret),
client: TwirpClient::new(host, LIVEKIT_PACKAGE, None),
}
}
pub fn new(host: &str) -> ServiceResult<Self> {
let (api_key, api_secret) = get_env_keys()?;
Ok(Self::with_api_key(host, &api_key, &api_secret))
}
pub async fn create_room(
&self,
name: &str,
options: CreateRoomOptions,
) -> ServiceResult<proto::Room> {
self.client
.request(
SVC,
"CreateRoom",
proto::CreateRoomRequest {
name: name.to_owned(),
empty_timeout: options.empty_timeout,
max_participants: options.max_participants,
node_id: options.node_id,
metadata: options.metadata,
egress: options.egress,
},
self.base.auth_header(VideoGrants {
room_create: true,
..Default::default()
})?,
)
.await
.map_err(Into::into)
}
pub async fn list_rooms(&self, names: Vec<String>) -> ServiceResult<Vec<proto::Room>> {
let resp: proto::ListRoomsResponse = self
.client
.request(
SVC,
"ListRooms",
proto::ListRoomsRequest { names },
self.base.auth_header(VideoGrants {
room_list: true,
..Default::default()
})?,
)
.await?;
Ok(resp.rooms)
}
pub async fn delete_room(&self, room: &str) -> ServiceResult<()> {
self.client
.request(
SVC,
"DeleteRoom",
proto::DeleteRoomRequest {
room: room.to_owned(),
},
self.base.auth_header(VideoGrants {
room_create: true,
..Default::default()
})?,
)
.await
.map_err(Into::into)
}
pub async fn update_room_metadata(
&self,
room: &str,
metadata: &str,
) -> ServiceResult<proto::Room> {
self.client
.request(
SVC,
"UpdateRoomMetadata",
proto::UpdateRoomMetadataRequest {
room: room.to_owned(),
metadata: metadata.to_owned(),
},
self.base.auth_header(VideoGrants {
room_admin: true,
room: room.to_owned(),
..Default::default()
})?,
)
.await
.map_err(Into::into)
}
pub async fn list_participants(
&self,
room: &str,
) -> ServiceResult<Vec<proto::ParticipantInfo>> {
let resp: proto::ListParticipantsResponse = self
.client
.request(
SVC,
"ListParticipants",
proto::ListParticipantsRequest {
room: room.to_owned(),
},
self.base.auth_header(VideoGrants {
room_admin: true,
room: room.to_owned(),
..Default::default()
})?,
)
.await?;
Ok(resp.participants)
}
pub async fn get_participant(
&self,
room: &str,
identity: &str,
) -> ServiceResult<proto::ParticipantInfo> {
self.client
.request(
SVC,
"GetParticipant",
proto::RoomParticipantIdentity {
room: room.to_owned(),
identity: identity.to_owned(),
},
self.base.auth_header(VideoGrants {
room_admin: true,
room: room.to_owned(),
..Default::default()
})?,
)
.await
.map_err(Into::into)
}
pub async fn remove_participant(&self, room: &str, identity: &str) -> ServiceResult<()> {
self.client
.request(
SVC,
"RemoveParticipant",
proto::RoomParticipantIdentity {
room: room.to_owned(),
identity: identity.to_owned(),
},
self.base.auth_header(VideoGrants {
room_admin: true,
room: room.to_owned(),
..Default::default()
})?,
)
.await
.map_err(Into::into)
}
pub async fn mute_published_track(
&self,
room: &str,
identity: &str,
track_sid: &str,
muted: bool,
) -> ServiceResult<proto::TrackInfo> {
let resp: proto::MuteRoomTrackResponse = self
.client
.request(
SVC,
"MutePublishedTrack",
proto::MuteRoomTrackRequest {
room: room.to_owned(),
identity: identity.to_owned(),
track_sid: track_sid.to_owned(),
muted,
},
self.base.auth_header(VideoGrants {
room_admin: true,
room: room.to_owned(),
..Default::default()
})?,
)
.await?;
Ok(resp.track.unwrap())
}
pub async fn update_participant(
&self,
room: &str,
identity: &str,
options: UpdateParticipantOptions,
) -> ServiceResult<proto::ParticipantInfo> {
self.client
.request(
SVC,
"UpdateParticipant",
proto::UpdateParticipantRequest {
room: room.to_owned(),
identity: identity.to_owned(),
permission: options.permission,
metadata: options.metadata,
name: options.name,
},
self.base.auth_header(VideoGrants {
room_admin: true,
room: room.to_owned(),
..Default::default()
})?,
)
.await
.map_err(Into::into)
}
pub async fn update_subscriptions(
&self,
room: &str,
identity: &str,
track_sids: Vec<String>,
subscribe: bool,
) -> ServiceResult<()> {
self.client
.request(
SVC,
"UpdateSubscriptions",
proto::UpdateSubscriptionsRequest {
room: room.to_owned(),
identity: identity.to_owned(),
track_sids,
subscribe,
..Default::default()
},
self.base.auth_header(VideoGrants {
room_admin: true,
room: room.to_owned(),
..Default::default()
})?,
)
.await
.map_err(Into::into)
}
pub async fn send_data(
&self,
room: &str,
data: Vec<u8>,
options: SendDataOptions,
) -> ServiceResult<()> {
self.client
.request(
SVC,
"SendData",
proto::SendDataRequest {
room: room.to_owned(),
data,
destination_sids: options.destination_sids,
topic: options.topic,
kind: options.kind as i32,
},
self.base.auth_header(VideoGrants {
room_admin: true,
room: room.to_owned(),
..Default::default()
})?,
)
.await
.map_err(Into::into)
}
}
+109
View File
@@ -0,0 +1,109 @@
use reqwest::{
header::{HeaderMap, HeaderValue, CONTENT_TYPE},
StatusCode,
};
use serde::Deserialize;
use std::fmt::Display;
use thiserror::Error;
pub const DEFAULT_PREFIX: &str = "/twirp";
#[derive(Debug, Error)]
pub enum TwirpError {
#[error("failed to execute the request: {0}")]
Request(#[from] reqwest::Error),
#[error("twirp error: {0}")]
Twirp(TwirpErrorCode),
#[error("url error: {0}")]
Url(#[from] url::ParseError),
#[error("prost error: {0}")]
Prost(#[from] prost::DecodeError),
}
#[derive(Debug, Deserialize)]
pub struct TwirpErrorCode {
pub code: String,
pub msg: String,
}
impl TwirpErrorCode {
pub const CANCELED: &'static str = "canceled";
pub const UNKNOWN: &'static str = "unknown";
pub const INVALID_ARGUMENT: &'static str = "invalid_argument";
pub const MALFORMED: &'static str = "malformed";
pub const DEADLINE_EXCEEDED: &'static str = "deadline_exceeded";
pub const NOT_FOUND: &'static str = "not_found";
pub const BAD_ROUTE: &'static str = "bad_route";
pub const ALREADY_EXISTS: &'static str = "already_exists";
pub const PERMISSION_DENIED: &'static str = "permission_denied";
pub const UNAUTHENTICATED: &'static str = "unauthenticated";
pub const RESOURCE_EXHAUSTED: &'static str = "resource_exhausted";
pub const FAILED_PRECONDITION: &'static str = "failed_precondition";
pub const ABORTED: &'static str = "aborted";
pub const OUT_OF_RANGE: &'static str = "out_of_range";
pub const UNIMPLEMENTED: &'static str = "unimplemented";
pub const INTERNAL: &'static str = "internal";
pub const UNAVAILABLE: &'static str = "unavailable";
pub const DATA_LOSS: &'static str = "dataloss";
}
impl Display for TwirpErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.code, self.msg)
}
}
pub type TwirpResult<T> = Result<T, TwirpError>;
#[derive(Debug)]
pub struct TwirpClient {
host: String,
pkg: String,
prefix: String,
client: reqwest::Client,
}
impl TwirpClient {
pub fn new(host: &str, pkg: &str, prefix: Option<&str>) -> Self {
Self {
host: host.to_owned(),
pkg: pkg.to_owned(),
prefix: prefix.unwrap_or(DEFAULT_PREFIX).to_owned(),
client: reqwest::Client::new(),
}
}
pub async fn request<D: prost::Message, R: prost::Message + Default>(
&self,
service: &str,
method: &str,
data: D,
mut headers: HeaderMap,
) -> TwirpResult<R> {
let mut url = url::Url::parse(&self.host)?;
url.set_path(&format!(
"{}/{}.{}/{}",
self.prefix, self.pkg, service, method
));
headers.insert(
CONTENT_TYPE,
HeaderValue::from_static("application/protobuf"),
);
let resp = self
.client
.post(url)
.headers(headers)
.body(data.encode_to_vec())
.send()
.await?;
if resp.status() == StatusCode::OK {
Ok(R::decode(resp.bytes().await?)?)
} else {
let err: TwirpErrorCode = resp.json().await?;
Err(TwirpError::Twirp(err))
}
}
}
+55
View File
@@ -0,0 +1,55 @@
// Webhooks are not yet integrated into the Rust SDK.
// Our webhooks protocol use protojson which isn't supported by Prost
/*
use crate::access_token::{AccessTokenError, TokenVerifier};
use livekit_protocol as proto;
use serde_json;
use sha2::{Digest, Sha256};
use thiserror::Error;
#[derive(Debug, Error)]
pub enum WebhookError {
#[error("invalid signature")]
InvalidSignature,
#[error("failed to verify the authorization: {0}")]
InvalidAuth(#[from] AccessTokenError),
#[error("invalid body, failed to decode: {0}")]
InvalidData(#[from] serde_json::Error),
}
#[derive(Clone, Debug)]
pub struct WebhookReceiver {
token_verifier: TokenVerifier,
}
impl WebhookReceiver {
pub fn new(token_verifier: TokenVerifier) -> Self {
Self { token_verifier }
}
pub fn receive(
&self,
body: &str,
auth_token: &str,
) -> Result<proto::WebhookEvent, WebhookError> {
let claims = self.token_verifier.verify(auth_token)?;
let mut hasher = Sha256::new();
hasher.update(body);
let hash = hasher.finalize();
let hex: Result<Vec<u8>, std::num::ParseIntError> = (0..claims.sha256.len())
.step_by(2)
.map(|i| u8::from_str_radix(&claims.sha256[i..i + 2], 16))
.collect();
let hex = hex.map_err(|_| WebhookError::InvalidSignature)?; // Failed to parse
if &hex[..] != &hash[..] {
return Err(WebhookError::InvalidSignature);
}
Ok(serde_json::from_str(body)?)
}
}*/