diff --git a/Cargo.toml b/Cargo.toml index 42a3da9..8ca816f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,11 @@ [workspace] +resolver = "3" members = [ - "client", + "client", "protocol", "server", ] [workspace.dependencies] -serde = "1.0.219" +chrono = { version = "0.4.40", features = ["serde"] } +serde = { version = "1.0.219", features = ["derive"]} +serde_json = "1.0.140" diff --git a/client/Cargo.toml b/client/Cargo.toml index e85a801..9ad9448 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -1,5 +1,5 @@ [package] -name = "rust_communication" +name = "rust_communication_client" version = "0.1.0" edition = "2024" @@ -7,7 +7,31 @@ edition = "2024" console_error_panic_hook = "0.1.7" leptos = { version = "0.7.8", features = ["csr"] } wasm-bindgen-futures = "0.4.50" -web-sys = { version = "0.3.77", features = ["AudioBuffer", "AudioBufferSourceNode", "AudioContext", "HtmlAudioElement","MediaDevices", "MediaStream", "MediaStreamConstraints", "MediaStreamTrack", "MediaTrackConstraints", "MediaTrackConstraintSet", "Navigator", "Window"] } +reqwest = { version = "0.12.15", features = ["json"] } +web-sys = { version = "0.3.77", features = [ + "AudioBuffer", + "AudioBufferSourceNode", + "AudioContext", + "HtmlAudioElement", + "MediaDevices", + "MediaStream", + "MediaStreamConstraints", + "MediaStreamTrack", + "MediaTrackConstraints", + "MediaTrackConstraintSet", + "Navigator", + "RtcConfiguration", + "RtcIceServer", + "RtcPeerConnection", + # "RtcSdpType", + "RtcSessionDescription", + "RtcSessionDescriptionInit", + "Window", +] } +protocol = { path = "../protocol" } +serde = { workspace = true } +serde_json = { workspace = true } +chrono = { workspace = true } [profile] diff --git a/client/assets/main.css b/client/assets/main.css new file mode 100644 index 0000000..9305cc6 --- /dev/null +++ b/client/assets/main.css @@ -0,0 +1,25 @@ +* { + box-sizing: border-box; + margin: 0; + border: none; + /* font-family: 'Trebuchet MS', 'Lucida Sans Unicode', 'Lucida Grande', 'Lucida Sans', Arial, sans-serif; */ +} + +body { + margin: 5%; + background-color: rgba(0, 0, 0, 0.98); + color: silver; +} + +button { + margin: 2%; + width: 7cap; + height: 3cap; + border-radius: 10%; +} + +input { + border-radius: 1%; + width: 10cap; + height: 2cap; +} diff --git a/client/index.html b/client/index.html index dd7d8d2..9dcc2e0 100644 --- a/client/index.html +++ b/client/index.html @@ -1,6 +1,11 @@ - -

"Hello"

- + + + + +

"Hello"

+ + + diff --git a/client/src/gui.rs b/client/src/gui.rs index bf27b70..2ffa0aa 100644 --- a/client/src/gui.rs +++ b/client/src/gui.rs @@ -1,20 +1,20 @@ use leptos::{ - IntoView, ev, - html::{ElementChild, button}, + IntoView, + attr::Value, + ev, + html::{ElementChild, button, form, input}, logging::log, - prelude::{OnAttribute, Read, Show, ShowProps, ToChildren}, + prelude::{BindAttribute, Get, OnAttribute, Read, Show, ShowProps, ToChildren, signal}, server::LocalResource, task::spawn_local, }; use wasm_bindgen_futures::JsFuture; -use web_sys::{ - HtmlAudioElement, MediaStream, MediaStreamConstraints, MediaStreamTrack, MediaTrackConstraints, - wasm_bindgen::{JsCast, JsValue}, - window, -}; +use web_sys::HtmlAudioElement; + +use crate::{media::audio, rtc::offer, signal::start_signalling}; pub fn app() -> impl IntoView { - let audio_stream = LocalResource::new(|| media()); + let audio_stream = LocalResource::new(|| audio()); let props = ShowProps::builder() .when(move || audio_stream.read().is_some()) .children(ToChildren::to_children(move || { @@ -38,47 +38,42 @@ pub fn app() -> impl IntoView { })) .fallback(|| button().child("Sad Button")) .build(); - Show(props) + (Show(props), signalling(), rtc()) } -async fn media() -> MediaStream { - let media_devices = window().unwrap().navigator().media_devices().unwrap(); - let media_stream_constraints = MediaStreamConstraints::new(); - let media_track_constraints = MediaTrackConstraints::new(); +fn signalling() -> impl IntoView { + let signalling_server_input_data = signal(String::new()); + let signalling_trigger = move || { + spawn_local(start_signalling( + "Zurna Dürüm".to_string(), + signalling_server_input_data.0.get(), + )) + }; + let signalling_server_input = form() + .child( + input() + .bind(Value, signalling_server_input_data) + .placeholder("0.0.0.0:4546") + .r#type("text"), + ) + .on(ev::submit, move |event| { + event.prevent_default(); + signalling_trigger() + }); - media_stream_constraints.set_audio(&JsValue::TRUE); - - media_track_constraints.set_echo_cancellation(&JsValue::FALSE); - media_track_constraints.set_noise_suppression(&JsValue::FALSE); - media_track_constraints.set_auto_gain_control(&JsValue::FALSE); - - let media_stream_promise = media_devices - .get_user_media_with_constraints(&media_stream_constraints) - .unwrap(); - let media_stream = JsFuture::from(media_stream_promise) - .await - .unwrap() - .dyn_into::() - .unwrap(); - let audio_stream_tracks = media_stream.get_audio_tracks(); - let audio_stream_tracks = audio_stream_tracks - .iter() - .map(|audio_track| audio_track.dyn_into::().unwrap()) - .collect::>(); - log!( - "{:#?}\n audio_stream_track_count = {}", - audio_stream_tracks, - audio_stream_tracks.len() - ); - let audio_stream_track = audio_stream_tracks.first().unwrap(); - let audio_stream_track_apply_constraints_promise = audio_stream_track - .apply_constraints_with_constraints(&media_track_constraints) - .unwrap(); - JsFuture::from(audio_stream_track_apply_constraints_promise) - .await - .unwrap(); - let audio_stream = MediaStream::new().unwrap(); - log!("{:#?}", audio_stream_track.get_constraints()); - audio_stream.add_track(audio_stream_track); - audio_stream + let signalling_submit_button = button() + .on(ev::click, move |_| signalling_trigger()) + .child("Signal"); + (signalling_server_input, signalling_submit_button) +} + +fn rtc() -> impl IntoView { + let rtc_trigger = || { + spawn_local(offer()); + }; + + let rtc_start_button = button() + .on(ev::click, move |_| rtc_trigger()) + .child("RTC Offer"); + rtc_start_button } diff --git a/client/src/lib.rs b/client/src/lib.rs index 14c39ab..07c2648 100644 --- a/client/src/lib.rs +++ b/client/src/lib.rs @@ -1 +1,4 @@ pub mod gui; +pub mod media; +pub mod rtc; +pub mod signal; diff --git a/client/src/main.rs b/client/src/main.rs index 8f1dd90..971b3e2 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -1,5 +1,5 @@ use leptos::mount::mount_to_body; -use rust_communication::gui::app; +use rust_communication_client::gui::app; fn main() { println!("Hello, world!"); diff --git a/client/src/media.rs b/client/src/media.rs new file mode 100644 index 0000000..b550478 --- /dev/null +++ b/client/src/media.rs @@ -0,0 +1,49 @@ +use leptos::logging::log; +use wasm_bindgen_futures::JsFuture; +use web_sys::{ + MediaStream, MediaStreamConstraints, MediaStreamTrack, MediaTrackConstraints, + wasm_bindgen::{JsCast, JsValue}, + window, +}; + +pub async fn audio() -> MediaStream { + let media_devices = window().unwrap().navigator().media_devices().unwrap(); + let media_stream_constraints = MediaStreamConstraints::new(); + let media_track_constraints = MediaTrackConstraints::new(); + + media_stream_constraints.set_audio(&JsValue::TRUE); + + media_track_constraints.set_echo_cancellation(&JsValue::FALSE); + media_track_constraints.set_noise_suppression(&JsValue::FALSE); + media_track_constraints.set_auto_gain_control(&JsValue::FALSE); + + let media_stream_promise = media_devices + .get_user_media_with_constraints(&media_stream_constraints) + .unwrap(); + let media_stream = JsFuture::from(media_stream_promise) + .await + .unwrap() + .dyn_into::() + .unwrap(); + let audio_stream_tracks = media_stream.get_audio_tracks(); + let audio_stream_tracks = audio_stream_tracks + .iter() + .map(|audio_track| audio_track.dyn_into::().unwrap()) + .collect::>(); + log!( + "{:#?}\n audio_stream_track_count = {}", + audio_stream_tracks, + audio_stream_tracks.len() + ); + let audio_stream_track = audio_stream_tracks.first().unwrap(); + let audio_stream_track_apply_constraints_promise = audio_stream_track + .apply_constraints_with_constraints(&media_track_constraints) + .unwrap(); + JsFuture::from(audio_stream_track_apply_constraints_promise) + .await + .unwrap(); + let audio_stream = MediaStream::new().unwrap(); + log!("{:#?}", audio_stream_track.get_constraints()); + audio_stream.add_track(audio_stream_track); + audio_stream +} diff --git a/client/src/rtc.rs b/client/src/rtc.rs new file mode 100644 index 0000000..bb2c2ce --- /dev/null +++ b/client/src/rtc.rs @@ -0,0 +1,38 @@ +use leptos::logging::log; +use wasm_bindgen_futures::JsFuture; +use web_sys::{ + RtcConfiguration, RtcIceServer, RtcPeerConnection, RtcSessionDescriptionInit, + js_sys::{Array, Reflect}, + wasm_bindgen::{JsCast, JsValue}, +}; + +pub async fn offer() { + let ice_server_addresses = vec![JsValue::from("stun:stun.l.google.com:19302")] + .into_iter() + .collect::(); + let ice_server = RtcIceServer::new(); + ice_server.set_urls(&JsValue::from(ice_server_addresses)); + let ice_servers = vec![ice_server].into_iter().collect::(); + let rtc_configuration = RtcConfiguration::new(); + rtc_configuration.set_ice_servers(&ice_servers); + let peer_connection = RtcPeerConnection::new_with_configuration(&rtc_configuration).unwrap(); + let peer_connection_create_offer_promise = peer_connection.create_offer(); + let rtc_session_offer = JsFuture::from(peer_connection_create_offer_promise) + .await + .unwrap(); + log!("{:#?}", rtc_session_offer); + let rtc_session_offer = rtc_session_offer + .as_ref() + .unchecked_ref::(); + log!("{:#?}", rtc_session_offer); + JsFuture::from(peer_connection.set_local_description(rtc_session_offer)) + .await + .unwrap(); + let rtc_session_offer = Reflect::get(&rtc_session_offer, &JsValue::from_str("sdp")) + .unwrap() + .as_string() + .unwrap(); + log!("{}", rtc_session_offer); +} + +pub async fn answer() {} diff --git a/client/src/signal.rs b/client/src/signal.rs new file mode 100644 index 0000000..3f77d9b --- /dev/null +++ b/client/src/signal.rs @@ -0,0 +1,26 @@ +use chrono::DateTime; +use leptos::logging::log; +use protocol::Signal; +use serde_json::json; + +pub async fn start_signalling(username: String, signal_address: String) { + log!("Start Signalling"); + log!("{}\n{}", username, signal_address); + let request_client = reqwest::Client::new(); + let signal = Signal { + username, + time: DateTime::default(), + }; + let body = json!(signal); + match request_client.post(signal_address).json(&body).send().await { + Ok(signal_response) => log!("{:#?}", signal_response), + Err(err_val) => { + log!("Error: Signal Post | {}", err_val); + } + } +} + +pub async fn send_offer() {} +pub async fn receive_offer() {} +pub async fn send_answer() {} +pub async fn receive_answer() {} diff --git a/protocol/Cargo.toml b/protocol/Cargo.toml new file mode 100644 index 0000000..63f4f2f --- /dev/null +++ b/protocol/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "protocol" +version = "0.1.0" +edition = "2024" + +[dependencies] +serde = { workspace = true } +chrono = { workspace = true } diff --git a/protocol/src/lib.rs b/protocol/src/lib.rs new file mode 100644 index 0000000..8baaeea --- /dev/null +++ b/protocol/src/lib.rs @@ -0,0 +1,8 @@ +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct Signal { + pub username: String, + pub time: DateTime, +} diff --git a/server/Cargo.toml b/server/Cargo.toml index ff06670..774fdf9 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -1,6 +1,13 @@ [package] -name = "server" +name = "rust_communication_server" version = "0.1.0" edition = "2024" [dependencies] +axum = { version = "0.8.3", features = ["json"] } +axum-macros = "0.5.0" +tokio = "1.42.1" +webrtc = "0.12.0" +serde = { workspace = true } +chrono = { workspace = true } +protocol = { path = "../protocol" } diff --git a/server/src/lib.rs b/server/src/lib.rs new file mode 100644 index 0000000..9ba444c --- /dev/null +++ b/server/src/lib.rs @@ -0,0 +1,74 @@ +use std::sync::LazyLock; + +use utils::naive_toml_parser; + +pub mod signal; +pub mod utils; + +const SERVER_CONFIG_FILE_LOCATION: &str = "./configs/server_config.toml"; +const DATABASE_CONFIG_FILE_LOCATION: &str = "./configs/database_config.toml"; + +pub static SERVER_CONFIG: LazyLock = LazyLock::new(ServerConfig::default); + +#[derive(Debug)] +pub struct DatabaseConfig { + pub address: String, + pub username: String, + pub password: String, + pub database: String, + pub backend: String, + pub connection_pool_size: u32, +} +impl Default for DatabaseConfig { + fn default() -> Self { + let (header, mut database_configs) = naive_toml_parser(DATABASE_CONFIG_FILE_LOCATION); + + if header == "[database_config]" { + Self { + address: database_configs.pop_front().unwrap().parse().unwrap(), + username: database_configs.pop_front().unwrap().parse().unwrap(), + password: database_configs.pop_front().unwrap().parse().unwrap(), + database: database_configs.pop_front().unwrap().parse().unwrap(), + backend: database_configs.pop_front().unwrap().parse().unwrap(), + connection_pool_size: database_configs.pop_front().unwrap().parse().unwrap(), + } + } else { + panic!("Database Config File Must Include [database_config] at the First Line") + } + } +} + +#[derive(Debug)] +pub struct ServerConfig { + pub address: String, + pub otp_time_limit: usize, + pub login_token_expiration_time_limit: usize, + pub login_token_refresh_time_limit: usize, + pub concurrency_limit: usize, +} + +impl Default for ServerConfig { + fn default() -> Self { + let (header, mut server_configs) = naive_toml_parser(SERVER_CONFIG_FILE_LOCATION); + let value_or_max = |value: String| value.parse().map_or(usize::MAX, |value| value); + let value_or_semaphore_max = |value: String| { + value + .parse() + .map_or(tokio::sync::Semaphore::MAX_PERMITS, |value| value) + }; + + if header == "[server_config]" { + Self { + address: server_configs.pop_front().unwrap().parse().unwrap(), + otp_time_limit: value_or_max(server_configs.pop_front().unwrap()), + login_token_expiration_time_limit: value_or_max( + server_configs.pop_front().unwrap(), + ), + login_token_refresh_time_limit: value_or_max(server_configs.pop_front().unwrap()), + concurrency_limit: value_or_semaphore_max(server_configs.pop_front().unwrap()), + } + } else { + panic!("Server Config File Must Include [server_config] at the First Line") + } + } +} diff --git a/server/src/main.rs b/server/src/main.rs index e7a11a9..6c4d798 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,3 +1,8 @@ -fn main() { +use rust_communication_server::signal::start_signalling; + +#[tokio::main] +async fn main() { println!("Hello, world!"); + + tokio::spawn(start_signalling()).await.unwrap(); } diff --git a/server/src/signal.rs b/server/src/signal.rs new file mode 100644 index 0000000..e18ef29 --- /dev/null +++ b/server/src/signal.rs @@ -0,0 +1,36 @@ +use std::sync::{Arc, LazyLock, RwLock}; + +use axum::{ + Json, Router, + http::StatusCode, + response::IntoResponse, + routing::{get, post}, +}; +use axum_macros::debug_handler; +use protocol::Signal; +use tokio::net::TcpListener; + +static SIGNALS: LazyLock>>> = + LazyLock::new(|| Arc::new(RwLock::new(vec![]))); + +pub async fn start_signalling() { + let route = route(); + let listener = TcpListener::bind("0.0.0.0:4546").await.unwrap(); + axum::serve(listener, route).await.unwrap(); +} + +fn route() -> Router { + Router::new() + .route("/alive", get(alive)) + .route("/", post(signal)) +} + +async fn alive() -> impl IntoResponse { + StatusCode::OK +} + +#[debug_handler] +async fn signal(Json(signal): Json) -> impl IntoResponse { + SIGNALS.write().unwrap().push(signal); + StatusCode::OK +} diff --git a/server/src/utils.rs b/server/src/utils.rs new file mode 100644 index 0000000..17f83c8 --- /dev/null +++ b/server/src/utils.rs @@ -0,0 +1,24 @@ +use std::{collections::VecDeque, fs::File, io::Read}; + +pub fn naive_toml_parser(file_location: &str) -> (String, VecDeque) { + let mut toml_file = File::open(file_location).unwrap(); + let mut toml_ingredients = String::default(); + toml_file.read_to_string(&mut toml_ingredients).unwrap(); + let mut toml_ingredients = toml_ingredients.lines().collect::>(); + + let header = toml_ingredients.pop_front().unwrap().trim_end().to_string(); + let parsed = toml_ingredients + .iter() + .map(|ingredient| { + ingredient + .split_once('=') + .unwrap() + .1 + .replace('"', "") + .trim() + .to_string() + }) + .collect(); + + (header, parsed) +}