1extern crate rustc_serialize;
2
3use rand::Rng;
4use rustc_serialize::hex::ToHex;
5use serde::{Deserialize, Serialize};
6use std::fmt;
7use std::time::{SystemTime, UNIX_EPOCH};
8use thiserror::Error;
9
10mod http_client;
11
12static SERVER_URL: &str = "https://api.opentok.com";
13static API_ENDPOINT_PATH_START: &str = "/v2/project/";
14
15pub type SessionId = String;
17
18#[derive(Debug, Error, PartialEq)]
20pub enum OpenTokError {
21 #[error("Bad request {0}")]
22 BadRequest(String),
23 #[error("Cannot encode request")]
24 EncodingError,
25 #[error("OpenTok server error {0}")]
26 ServerError(String),
27 #[error("Unexpected response {0}")]
28 UnexpectedResponse(String),
29 #[error("Unknown error")]
30 __Unknown,
31}
32
33impl From<surf::Error> for OpenTokError {
34 fn from(error: surf::Error) -> OpenTokError {
35 match error.status().into() {
36 400..=499 => OpenTokError::BadRequest(error.to_string()),
37 500..=599 => OpenTokError::ServerError(error.to_string()),
38 _ => OpenTokError::__Unknown,
39 }
40 }
41}
42
43#[derive(Debug, PartialEq)]
46pub enum MediaMode {
47 Relayed,
49 Routed,
51}
52
53impl fmt::Display for MediaMode {
54 fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
55 write!(formatter, "{}", format!("{:?}", self).to_lowercase())
56 }
57}
58
59#[derive(Debug)]
62pub enum ArchiveMode {
63 Always,
65 Manual,
68}
69
70impl fmt::Display for ArchiveMode {
71 fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
72 write!(formatter, "{}", format!("{:?}", self).to_lowercase())
73 }
74}
75
76#[derive(Default)]
78pub struct SessionOptions<'a> {
79 pub location: Option<&'a str>,
83 pub media_mode: Option<MediaMode>,
89 pub archive_mode: Option<ArchiveMode>,
94}
95
96#[derive(Serialize)]
97#[serde(rename_all = "camelCase")]
98struct CreateSessionBody<'a> {
99 archive_mode: String,
100 location: Option<&'a str>,
101 #[serde(rename = "p2p.preference")]
102 p2p_preference: &'a str,
103}
104
105impl<'a> From<SessionOptions<'a>> for CreateSessionBody<'a> {
106 fn from(options: SessionOptions) -> CreateSessionBody {
107 CreateSessionBody {
108 archive_mode: options
109 .archive_mode
110 .map(|mode| mode.to_string())
111 .unwrap_or_else(|| "manual".into()),
112 location: options.location,
113 p2p_preference: options
114 .media_mode
115 .map(|mode| {
116 if mode == MediaMode::Relayed {
117 "enabled"
118 } else {
119 "disabled"
120 }
121 })
122 .unwrap_or("disabled"),
123 }
124 }
125}
126
127#[derive(Deserialize)]
128struct CreateSessionResponse {
129 session_id: String,
130}
131
132#[derive(Debug)]
133pub enum TokenRole {
134 Publisher,
135 Subscriber,
136 Moderator,
137}
138
139impl fmt::Display for TokenRole {
140 fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
141 write!(formatter, "{}", format!("{:?}", self).to_lowercase())
142 }
143}
144
145#[derive(Debug)]
146struct TokenData<'a> {
147 session_id: &'a str,
148 create_time: u64,
149 expire_time: u64,
150 nonce: u64,
151 role: TokenRole,
152}
153
154impl<'a> TokenData<'a> {
155 pub fn new(session_id: &'a str, role: TokenRole) -> Self {
156 let now = SystemTime::now()
157 .duration_since(UNIX_EPOCH)
158 .expect("Time went backwards, Doc!")
159 .as_secs();
160 let mut rng = rand::thread_rng();
161 Self {
162 session_id,
163 create_time: now,
164 expire_time: now + (60 * 60 * 24),
165 nonce: rng.gen::<u64>(),
166 role,
167 }
168 }
169}
170
171impl<'a> fmt::Display for TokenData<'a> {
172 fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
173 write!(
174 formatter,
175 "session_id={}&create_time={}&expire_time={}&nonce={}&role={}",
176 self.session_id, self.create_time, self.expire_time, self.nonce, self.role,
177 )
178 }
179}
180
181#[derive(Debug, Deserialize)]
182#[serde(rename_all = "lowercase")]
183pub enum VideoType {
184 Camera,
185 Screen,
186 Custom,
187}
188
189impl fmt::Display for VideoType {
190 fn fmt(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
191 write!(formatter, "{}", format!("{:?}", self).to_lowercase())
192 }
193}
194
195#[derive(Debug, Deserialize)]
196#[serde(rename_all = "camelCase")]
197#[allow(dead_code)]
198pub struct StreamInfo {
199 id: String,
200 video_type: VideoType,
201 name: String,
202 layout_class_list: Vec<String>,
203}
204
205pub struct OpenTok {
209 api_key: String,
210 api_secret: String,
211}
212
213impl OpenTok {
214 pub fn new(api_key: String, api_secret: String) -> Self {
218 Self {
219 api_key,
220 api_secret,
221 }
222 }
223
224 pub async fn create_session<'a>(
227 &self,
228 options: SessionOptions<'a>,
229 ) -> Result<String, OpenTokError> {
230 let body: CreateSessionBody = options.into();
231 let endpoint = format!("{}{}", SERVER_URL, "/session/create");
232 let mut response =
233 http_client::post(&endpoint, &self.api_key, &self.api_secret, &body).await?;
234 let response_str = response.body_string().await?;
235 let mut response: Vec<CreateSessionResponse> =
236 serde_json::from_str::<Vec<CreateSessionResponse>>(&response_str)
237 .map_err(|_| OpenTokError::UnexpectedResponse(response_str.clone()))?;
238 assert_eq!(response.len(), 1);
239 match response.pop() {
240 Some(session) => Ok(session.session_id),
241 None => Err(OpenTokError::UnexpectedResponse(response_str)),
242 }
243 }
244
245 pub fn generate_token(&self, session_id: &str, role: TokenRole) -> String {
246 let token_data = TokenData::new(session_id, role);
247 let signed = hmacsha1::hmac_sha1(
248 self.api_secret.as_bytes(),
249 token_data.to_string().as_bytes(),
250 )
251 .to_hex();
252 let decoded = format!("partner_id={}&sig={}:{}", self.api_key, signed, token_data);
253 let encoded = base64::encode(decoded);
254 format!("T1=={}", encoded)
255 }
256
257 pub async fn get_stream_info(
258 &self,
259 session_id: &str,
260 stream_id: &str,
261 ) -> Result<StreamInfo, OpenTokError> {
262 let endpoint = format!(
263 "{}{}{}/session/{}/stream/{}",
264 SERVER_URL, API_ENDPOINT_PATH_START, self.api_key, session_id, stream_id
265 );
266 let mut response = http_client::get(&endpoint, &self.api_key, &self.api_secret).await?;
267 let response_str = response.body_string().await?;
268 serde_json::from_str::<StreamInfo>(&response_str)
269 .map_err(|_| OpenTokError::UnexpectedResponse(response_str.clone()))
270 }
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 use futures::executor::LocalPool;
278 use opentok::utils::common::Credentials;
279 use opentok::utils::publisher::Publisher;
280 use std::env;
281
282 #[test]
283 fn test_create_session_invalid_credentials() {
284 let opentok = OpenTok::new("sancho".into(), "quijote".into());
285 let mut pool = LocalPool::new();
286 assert!(pool
287 .run_until(opentok.create_session(SessionOptions::default()))
288 .is_err());
289 }
290
291 #[test]
292 fn test_create_session() {
293 let api_key = env::var("OPENTOK_KEY").unwrap();
294 let api_secret = env::var("OPENTOK_SECRET").unwrap();
295 let opentok = OpenTok::new(api_key, api_secret);
296 let mut pool = LocalPool::new();
297 let session_id = pool
298 .run_until(opentok.create_session(SessionOptions::default()))
299 .unwrap();
300 assert!(!session_id.is_empty());
301 }
302
303 #[test]
304 fn test_generate_token() {
305 let api_key = env::var("OPENTOK_KEY").unwrap();
306 let api_secret = env::var("OPENTOK_SECRET").unwrap();
307 let opentok = OpenTok::new(api_key, api_secret);
308 let mut pool = LocalPool::new();
309 let session_id = pool
310 .run_until(opentok.create_session(SessionOptions::default()))
311 .unwrap();
312 assert!(!session_id.is_empty());
313 let token = opentok.generate_token(&session_id, TokenRole::Publisher);
314 assert!(!token.is_empty());
315 }
316
317 #[test]
318 fn test_get_stream_info() {
319 let api_key = env::var("OPENTOK_KEY").unwrap();
320 let api_secret = env::var("OPENTOK_SECRET").unwrap();
321 let opentok = OpenTok::new(api_key.clone(), api_secret);
322 let mut pool = LocalPool::new();
323 let session_id = pool
324 .run_until(opentok.create_session(SessionOptions::default()))
325 .unwrap();
326 assert!(!session_id.is_empty());
327 let token = opentok.generate_token(&session_id, TokenRole::Publisher);
328 assert!(!token.is_empty());
329
330 opentok::init().unwrap();
331
332 let publisher = Publisher::new(
333 Credentials {
334 api_key,
335 session_id: session_id.clone(),
336 token,
337 },
338 Some(Box::new(move |publisher, stream_id| {
339 let mut pool = LocalPool::new();
340 let stream_info = pool
341 .run_until(opentok.get_stream_info(&session_id, &stream_id))
342 .unwrap();
343 assert_eq!(stream_info.id, stream_id);
344 publisher.stop();
345 })),
346 None,
347 );
348
349 publisher.run().unwrap();
350
351 opentok::deinit().unwrap();
352 }
353}