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)
+}