feat: rtc peer connection offer

This commit is contained in:
Ahmet Kaan Gümüş 2025-04-11 04:58:16 +03:00
parent 7434d131c4
commit 0aa65f0f60
17 changed files with 389 additions and 59 deletions

View file

@ -1,8 +1,11 @@
[workspace] [workspace]
resolver = "3"
members = [ members = [
"client", "client", "protocol",
"server", "server",
] ]
[workspace.dependencies] [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"

View file

@ -1,5 +1,5 @@
[package] [package]
name = "rust_communication" name = "rust_communication_client"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
@ -7,7 +7,31 @@ edition = "2024"
console_error_panic_hook = "0.1.7" console_error_panic_hook = "0.1.7"
leptos = { version = "0.7.8", features = ["csr"] } leptos = { version = "0.7.8", features = ["csr"] }
wasm-bindgen-futures = "0.4.50" 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] [profile]

25
client/assets/main.css Normal file
View file

@ -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;
}

View file

@ -1,6 +1,11 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head></head>
<head>
<link data-trunk rel="css" href="assets/main.css" />
</head>
<h1>"Hello"</h1> <h1>"Hello"</h1>
<body></body> <body></body>
</html> </html>

View file

@ -1,20 +1,20 @@
use leptos::{ use leptos::{
IntoView, ev, IntoView,
html::{ElementChild, button}, attr::Value,
ev,
html::{ElementChild, button, form, input},
logging::log, logging::log,
prelude::{OnAttribute, Read, Show, ShowProps, ToChildren}, prelude::{BindAttribute, Get, OnAttribute, Read, Show, ShowProps, ToChildren, signal},
server::LocalResource, server::LocalResource,
task::spawn_local, task::spawn_local,
}; };
use wasm_bindgen_futures::JsFuture; use wasm_bindgen_futures::JsFuture;
use web_sys::{ use web_sys::HtmlAudioElement;
HtmlAudioElement, MediaStream, MediaStreamConstraints, MediaStreamTrack, MediaTrackConstraints,
wasm_bindgen::{JsCast, JsValue}, use crate::{media::audio, rtc::offer, signal::start_signalling};
window,
};
pub fn app() -> impl IntoView { pub fn app() -> impl IntoView {
let audio_stream = LocalResource::new(|| media()); let audio_stream = LocalResource::new(|| audio());
let props = ShowProps::builder() let props = ShowProps::builder()
.when(move || audio_stream.read().is_some()) .when(move || audio_stream.read().is_some())
.children(ToChildren::to_children(move || { .children(ToChildren::to_children(move || {
@ -38,47 +38,42 @@ pub fn app() -> impl IntoView {
})) }))
.fallback(|| button().child("Sad Button")) .fallback(|| button().child("Sad Button"))
.build(); .build();
Show(props) (Show(props), signalling(), rtc())
} }
async fn media() -> MediaStream { fn signalling() -> impl IntoView {
let media_devices = window().unwrap().navigator().media_devices().unwrap(); let signalling_server_input_data = signal(String::new());
let media_stream_constraints = MediaStreamConstraints::new(); let signalling_trigger = move || {
let media_track_constraints = MediaTrackConstraints::new(); 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); let signalling_submit_button = button()
.on(ev::click, move |_| signalling_trigger())
media_track_constraints.set_echo_cancellation(&JsValue::FALSE); .child("Signal");
media_track_constraints.set_noise_suppression(&JsValue::FALSE); (signalling_server_input, signalling_submit_button)
media_track_constraints.set_auto_gain_control(&JsValue::FALSE); }
let media_stream_promise = media_devices fn rtc() -> impl IntoView {
.get_user_media_with_constraints(&media_stream_constraints) let rtc_trigger = || {
.unwrap(); spawn_local(offer());
let media_stream = JsFuture::from(media_stream_promise) };
.await
.unwrap() let rtc_start_button = button()
.dyn_into::<MediaStream>() .on(ev::click, move |_| rtc_trigger())
.unwrap(); .child("RTC Offer");
let audio_stream_tracks = media_stream.get_audio_tracks(); rtc_start_button
let audio_stream_tracks = audio_stream_tracks
.iter()
.map(|audio_track| audio_track.dyn_into::<MediaStreamTrack>().unwrap())
.collect::<Vec<_>>();
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
} }

View file

@ -1 +1,4 @@
pub mod gui; pub mod gui;
pub mod media;
pub mod rtc;
pub mod signal;

View file

@ -1,5 +1,5 @@
use leptos::mount::mount_to_body; use leptos::mount::mount_to_body;
use rust_communication::gui::app; use rust_communication_client::gui::app;
fn main() { fn main() {
println!("Hello, world!"); println!("Hello, world!");

49
client/src/media.rs Normal file
View file

@ -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::<MediaStream>()
.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::<MediaStreamTrack>().unwrap())
.collect::<Vec<_>>();
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
}

38
client/src/rtc.rs Normal file
View file

@ -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::<Array>();
let ice_server = RtcIceServer::new();
ice_server.set_urls(&JsValue::from(ice_server_addresses));
let ice_servers = vec![ice_server].into_iter().collect::<Array>();
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::<RtcSessionDescriptionInit>();
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() {}

26
client/src/signal.rs Normal file
View file

@ -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() {}

8
protocol/Cargo.toml Normal file
View file

@ -0,0 +1,8 @@
[package]
name = "protocol"
version = "0.1.0"
edition = "2024"
[dependencies]
serde = { workspace = true }
chrono = { workspace = true }

8
protocol/src/lib.rs Normal file
View file

@ -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<Utc>,
}

View file

@ -1,6 +1,13 @@
[package] [package]
name = "server" name = "rust_communication_server"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [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" }

74
server/src/lib.rs Normal file
View file

@ -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<ServerConfig> = 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")
}
}
}

View file

@ -1,3 +1,8 @@
fn main() { use rust_communication_server::signal::start_signalling;
#[tokio::main]
async fn main() {
println!("Hello, world!"); println!("Hello, world!");
tokio::spawn(start_signalling()).await.unwrap();
} }

36
server/src/signal.rs Normal file
View file

@ -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<Arc<RwLock<Vec<Signal>>>> =
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<Signal>) -> impl IntoResponse {
SIGNALS.write().unwrap().push(signal);
StatusCode::OK
}

24
server/src/utils.rs Normal file
View file

@ -0,0 +1,24 @@
use std::{collections::VecDeque, fs::File, io::Read};
pub fn naive_toml_parser(file_location: &str) -> (String, VecDeque<String>) {
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::<VecDeque<&str>>();
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)
}