move into gui folder for monorepo

This commit is contained in:
2024-11-11 12:24:51 -07:00
parent 7308a210e2
commit 30a94323b3
14 changed files with 810 additions and 459 deletions
+183
View File
@@ -0,0 +1,183 @@
use crate::app::Command;
use anyhow::Result;
use dioxus::hooks::{UnboundedReceiver, UnboundedSender};
use futures::io::{AsyncRead, AsyncWrite};
use mumble_protocol::control::{ClientControlCodec, ControlPacket};
use mumble_protocol::Serverbound;
use std::net::ToSocketAddrs;
use std::{fmt, io, sync::Arc};
use tokio::net::TcpStream;
use tokio::task::LocalSet;
use tokio_rustls::rustls;
use tokio_rustls::rustls::client::danger::{HandshakeSignatureValid, ServerCertVerifier};
use tokio_rustls::rustls::pki_types::{CertificateDer, ServerName, UnixTime};
use tokio_rustls::rustls::ClientConfig;
use tokio_rustls::rustls::DigitallySignedStruct;
use tokio_rustls::TlsConnector;
use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt as _};
pub use tokio::task::spawn;
pub use tokio::time::sleep;
pub struct Error(anyhow::Error);
pub trait ImpRead: AsyncRead + Unpin + Send + 'static {}
impl<T: AsyncRead + Unpin + Send + 'static> ImpRead for T {}
pub trait ImpWrite: AsyncWrite + Unpin + Send + 'static {}
impl<T: AsyncWrite + Unpin + Send + 'static> ImpWrite for T {}
impl From<anyhow::Error> for Error {
fn from(value: anyhow::Error) -> Self {
Error(value)
}
}
impl From<io::Error> for Error {
fn from(value: io::Error) -> Self {
Error(value.into())
}
}
impl Error {
pub fn new(text: String) -> Self {
Self(anyhow::Error::msg(text))
}
pub fn log(&self) {
eprintln!("{}", self.0);
}
}
impl std::error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
}
}
pub struct AudioSystem();
impl AudioSystem {
pub fn new(sender: UnboundedSender<ControlPacket<Serverbound>>) -> Result<Self, Error> {
// dbg!("todo");
Ok(AudioSystem())
}
pub fn create_player(&mut self) -> Result<AudioPlayer, Error> {
// dbg!("todo");
Ok(AudioPlayer())
}
}
pub struct AudioPlayer();
impl AudioPlayer {
pub fn play_opus(&mut self, payload: &[u8]) {
// dbg!("todo");
}
}
#[derive(Debug)]
struct NoCertificateVerification;
impl ServerCertVerifier for NoCertificateVerification {
fn verify_server_cert(
&self,
_end_entity: &CertificateDer<'_>,
_intermediates: &[CertificateDer<'_>],
_server_name: &ServerName<'_>,
_ocsp: &[u8],
_now: UnixTime,
) -> Result<rustls::client::danger::ServerCertVerified, rustls::Error> {
Ok(rustls::client::danger::ServerCertVerified::assertion())
}
fn verify_tls12_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn verify_tls13_signature(
&self,
_message: &[u8],
_cert: &CertificateDer<'_>,
_dss: &DigitallySignedStruct,
) -> Result<HandshakeSignatureValid, rustls::Error> {
Ok(HandshakeSignatureValid::assertion())
}
fn supported_verify_schemes(&self) -> Vec<rustls::SignatureScheme> {
vec![
rustls::SignatureScheme::RSA_PKCS1_SHA1,
rustls::SignatureScheme::ECDSA_SHA1_Legacy,
rustls::SignatureScheme::RSA_PKCS1_SHA256,
rustls::SignatureScheme::ECDSA_NISTP256_SHA256,
rustls::SignatureScheme::RSA_PKCS1_SHA384,
rustls::SignatureScheme::ECDSA_NISTP384_SHA384,
rustls::SignatureScheme::RSA_PKCS1_SHA512,
rustls::SignatureScheme::ECDSA_NISTP521_SHA512,
rustls::SignatureScheme::RSA_PSS_SHA256,
rustls::SignatureScheme::RSA_PSS_SHA384,
rustls::SignatureScheme::RSA_PSS_SHA512,
rustls::SignatureScheme::ED25519,
rustls::SignatureScheme::ED448,
]
}
}
pub async fn network_connect(
address: String,
username: String,
event_rx: &mut UnboundedReceiver<Command>,
) -> Result<(), Error> {
let config = ClientConfig::builder()
.dangerous()
.with_custom_certificate_verifier(Arc::new(NoCertificateVerification))
.with_no_client_auth();
let connector = TlsConnector::from(Arc::new(config));
let addr = format!("{}:{}", address, 64738)
.to_socket_addrs()?
.next()
.unwrap();
let server_tcp = TcpStream::connect(addr).await?;
let server_stream = connector
//.connect("127.0.0.1".try_into()?, server_tcp)
.connect(address.try_into().map_err(anyhow::Error::from)?, server_tcp)
.await?;
let (read_server, write_server) = tokio::io::split(server_stream);
let read_codec = ClientControlCodec::new();
let write_codec = ClientControlCodec::new();
let mut reader = asynchronous_codec::FramedRead::new(read_server.compat(), read_codec);
let mut writer = asynchronous_codec::FramedWrite::new(write_server.compat_write(), write_codec);
super::network_loop(username, event_rx, reader, writer).await
}
pub fn set_default_username(username: &str) -> Option<()> {
None
}
pub fn load_username() -> Option<String> {
return None;
}
pub fn load_config() -> Option<super::GuiConfig> {
None
}
+422
View File
@@ -0,0 +1,422 @@
use crate::app::Command;
use crate::bail;
use crate::CONFIG;
use dioxus::prelude::*;
use futures::AsyncRead;
use futures::AsyncWrite;
use futures_channel::mpsc::UnboundedSender;
use gloo_timers::future::TimeoutFuture;
use manganis::mg;
use mumble_protocol::control::ClientControlCodec;
use mumble_protocol::control::ControlPacket;
use mumble_protocol::voice::VoicePacket;
use mumble_protocol::voice::VoicePacketPayload;
use mumble_protocol::Serverbound;
use std::fmt;
use std::io;
use std::time::Duration;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::JsFuture;
use web_sys::console;
use web_sys::js_sys::Promise;
use web_sys::js_sys::Reflect;
use web_sys::js_sys::Uint8Array;
use web_sys::window;
use web_sys::AudioContext;
use web_sys::AudioContextOptions;
use web_sys::AudioData;
use web_sys::AudioDecoder;
use web_sys::AudioDecoderConfig;
use web_sys::AudioDecoderInit;
use web_sys::AudioEncoder;
use web_sys::AudioEncoderConfig;
use web_sys::AudioEncoderInit;
use web_sys::AudioWorkletNode;
use web_sys::EncodedAudioChunk;
use web_sys::EncodedAudioChunkInit;
use web_sys::EncodedAudioChunkType;
use web_sys::MediaStream;
use web_sys::MediaStreamConstraints;
use web_sys::MediaStreamTrackGenerator;
use web_sys::MediaStreamTrackGeneratorInit;
use web_sys::MessageEvent;
use web_sys::WebTransport;
use web_sys::WebTransportBidirectionalStream;
use web_sys::WebTransportOptions;
use web_sys::WorkletOptions;
pub use wasm_bindgen_futures::spawn_local as spawn;
pub trait ImpRead: AsyncRead + Unpin + 'static {}
impl<T: AsyncRead + Unpin + 'static> ImpRead for T {}
pub trait ImpWrite: AsyncWrite + Unpin + 'static {}
impl<T: AsyncWrite + Unpin + 'static> ImpWrite for T {}
pub async fn sleep(d: Duration) {
TimeoutFuture::new(d.as_millis() as u32).await
}
pub struct Error(JsValue);
impl From<anyhow::Error> for Error {
fn from(value: anyhow::Error) -> Self {
Error(JsError::new(&value.to_string()).into())
}
}
impl From<io::Error> for Error {
fn from(value: io::Error) -> Self {
Error(JsError::new(&value.to_string()).into())
}
}
impl From<JsValue> for Error {
fn from(value: JsValue) -> Self {
Error(value)
}
}
impl From<JsError> for Error {
fn from(value: JsError) -> Self {
Error(JsError::from(value).into())
}
}
impl Error {
pub fn new(text: String) -> Self {
wasm_bindgen::JsError::new(&text).into()
}
pub fn log(&self) {
console::error_1(&self.0);
}
}
impl std::error::Error for Error {}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text: String = js_sys::Object::from(self.0.clone()).to_string().into();
f.write_str(&text)
}
}
impl fmt::Debug for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let text: String = js_sys::Object::from(self.0.clone()).to_string().into();
f.write_str(&text)
}
}
pub struct AudioSystem(AudioContext);
impl AudioSystem {
pub fn new(sender: UnboundedSender<ControlPacket<Serverbound>>) -> Result<Self, Error> {
// Create MediaStreams to playback decoded audio
// The audio context is used to reproduce audio.
let audio_context = configure_audio_context();
let audio_context_worklet = audio_context.clone();
spawn(async move {
match create_encoder_worklet(&audio_context_worklet, sender).await {
Ok(node) => console::log_2(&"Created audio worklet:".into(), &node),
Err(err) => err.log(),
}
});
Ok(AudioSystem(audio_context))
}
pub fn create_player(&mut self) -> Result<AudioPlayer, Error> {
let audio_context = &self.0;
let audio_stream_generator =
MediaStreamTrackGenerator::new(&MediaStreamTrackGeneratorInit::new("audio"))?;
// Create MediaStream from MediaStreamTrackGenerator
let js_tracks = web_sys::js_sys::Array::new();
js_tracks.push(&audio_stream_generator);
let media_stream = MediaStream::new_with_tracks(&js_tracks)?;
// Create MediaStreamAudioSourceNode
let audio_source = audio_context.create_media_stream_source(&media_stream)?;
// Connect output of audio_source to audio_context (browser audio)
audio_source.connect_with_audio_node(&audio_context.destination())?;
// Create callback functions for AudioDecoder
let error = Closure::wrap(Box::new(move |e: JsValue| {
console::error_1(&e);
}) as Box<dyn FnMut(JsValue)>);
// This knows what MediaStreamTrackGenerator to use as it closes around it
let output = Closure::wrap(Box::new(move |audio_data: AudioData| {
let writable = audio_stream_generator.writable();
if writable.locked() {
return;
}
if let Err(e) = writable.get_writer().map(|writer| {
spawn(async move {
if let Err(e) = JsFuture::from(writer.ready()).await {
console::error_1(&format!("write chunk ready error {:?}", e).into());
}
if let Err(e) = JsFuture::from(writer.write_with_chunk(&audio_data)).await {
console::error_1(&format!("write chunk error {:?}", e).into());
};
writer.release_lock();
});
}) {
console::error_1(&e);
}
}) as Box<dyn FnMut(AudioData)>);
let audio_decoder = AudioDecoder::new(&AudioDecoderInit::new(
error.as_ref().unchecked_ref(),
output.as_ref().unchecked_ref(),
))?;
audio_decoder.configure(&AudioDecoderConfig::new("opus", 1, 48000));
console::log_1(&"Created Audio Decoder".into());
// This is required to prevent these from being deallocated
error.forget();
output.forget();
Ok(AudioPlayer(audio_decoder))
}
}
pub struct AudioPlayer(AudioDecoder);
impl AudioPlayer {
pub fn play_opus(&mut self, payload: &[u8]) {
let js_audio_payload = Uint8Array::from(payload);
let _ = self.0.decode(
&EncodedAudioChunk::new(&EncodedAudioChunkInit::new(
&js_audio_payload.into(),
0.0,
EncodedAudioChunkType::Key,
))
.unwrap(),
);
}
}
// Borrowed from
// https://github.com/security-union/videocall-rs/blob/main/videocall-client/src/decode/config.rs#L6
fn configure_audio_context() -> AudioContext {
let mut audio_context_options = AudioContextOptions::new();
audio_context_options.sample_rate(48000 as f32);
let audio_context = AudioContext::new_with_context_options(&audio_context_options).unwrap();
audio_context
}
trait PromiseExt {
fn into_future(self) -> JsFuture;
}
impl PromiseExt for Promise {
fn into_future(self) -> JsFuture {
self.into()
}
}
async fn create_encoder_worklet(
audio_context: &AudioContext,
packets: UnboundedSender<ControlPacket<mumble_protocol::Serverbound>>,
) -> Result<AudioWorkletNode, Error> {
let stream = window()
.unwrap()
.navigator()
.media_devices()?
.get_user_media_with_constraints(MediaStreamConstraints::new().audio(&JsValue::TRUE))?
.into_future()
.await?
.dyn_into()
.map_err(|e| JsError::new(&format!("not a stream: {e:?}")))?;
let options = WorkletOptions::new();
Reflect::set(
&options,
&"processorOptions".into(),
&wasm_bindgen::module(),
)?;
let module = "/rust_mic_worklet.js";
console::log_1(&format!("Loading mic worklet from {module:?}").into());
audio_context
.audio_worklet()?
.add_module_with_options(module, &options)?
.into_future()
.await?;
let source = audio_context.create_media_stream_source(&stream)?;
let worklet_node = AudioWorkletNode::new(audio_context, "rust_mic_worklet")?;
let error: Closure<dyn FnMut(JsValue)> = Closure::new(|e| console::error_1(&e));
let download_buffer = std::cell::RefCell::new(Vec::new());
// This knows what MediaStreamTrackGenerator to use as it closes around it
let mut sequence_num = 0;
let output: Closure<dyn FnMut(EncodedAudioChunk)> =
Closure::new(move |audio_data: EncodedAudioChunk| {
let mut array = vec![0u8; audio_data.byte_length() as usize];
audio_data.copy_to_with_u8_slice(&mut array);
download_buffer.borrow_mut().push(array.clone());
if download_buffer.borrow().len() > 200 {
//download_data(download_buffer.borrow().to_vec(), "download_buffer.opus");
//download_data(
// ass::encode(download_buffer.borrow().to_vec(), 960, 0),
// "download_buffer.opus",
//);
download_buffer.borrow_mut().clear();
}
let _ =
packets.unbounded_send(ControlPacket::UDPTunnel(Box::new(VoicePacket::Audio {
_dst: std::marker::PhantomData,
target: 0,
session_id: (),
seq_num: sequence_num,
payload: VoicePacketPayload::Opus(array.into(), false),
position_info: None,
})));
sequence_num = sequence_num.wrapping_add(2);
});
let audio_encoder = AudioEncoder::new(&AudioEncoderInit::new(
error.as_ref().unchecked_ref(),
output.as_ref().unchecked_ref(),
))
.unwrap();
// This is required to prevent these from being deallocated
error.forget();
output.forget();
let encoder_config = AudioEncoderConfig::new("opus");
encoder_config.set_number_of_channels(1);
encoder_config.set_sample_rate(48000);
encoder_config.set_bitrate(72_000.0);
audio_encoder.configure(&encoder_config);
console::log_1(&"Created Audio Encoder".into());
let download_buffer = std::cell::RefCell::new(Vec::new());
let onmessage: Closure<dyn FnMut(MessageEvent)> = Closure::new(move |event: MessageEvent| {
match AudioData::new(event.data().unchecked_ref()) {
Ok(data) => {
let x = web_sys::AudioDataCopyToOptions::new(0);
x.set_format(web_sys::AudioSampleFormat::F32);
let mut sub_buffer = vec![0; data.allocation_size(&x).unwrap() as usize];
data.copy_to_with_u8_slice(&mut sub_buffer, &x);
download_buffer.borrow_mut().append(&mut sub_buffer);
if download_buffer.borrow().len() > 48000 * 10 * 4 {
//pub fn download_data(data: Vec<u8>, filename: &str) -> Result<(), JsValue> {
//download_data(download_buffer.borrow().to_vec(), "download_buffer.pcm32");
download_buffer.borrow_mut().clear();
}
audio_encoder.encode(&data);
}
Err(err) => {
console::error_1(&err);
console::debug_1(&event);
}
}
});
Reflect::set(
&Reflect::get(&worklet_node, &"port".into())?,
&"onmessage".into(),
onmessage.as_ref(),
)?;
onmessage.forget();
source.connect_with_audio_node(&worklet_node)?;
worklet_node.connect_with_audio_node(&audio_context.destination())?;
Ok(worklet_node)
}
pub async fn network_connect(
address: String,
username: String,
event_rx: &mut UnboundedReceiver<Command>,
) -> Result<(), Error> {
console::log_1(&"Rust via WASM!".into());
let object = web_sys::js_sys::Object::new();
Reflect::set(
&object,
&JsValue::from_str("algorithm"),
&JsValue::from_str("sha-256"),
)?;
if let Some(server_hash) = &CONFIG.cert_hash {
let hash = web_sys::js_sys::Uint8Array::from(server_hash.as_slice());
web_sys::js_sys::Reflect::set(&object, &"value".into(), &hash)?;
}
let array = web_sys::js_sys::Array::new();
array.push(&object);
console::log_1(&object.clone().into());
console::log_1(&"Created option object!".into());
let mut options = WebTransportOptions::new();
options.server_certificate_hashes(&array);
console::log_1(&"Created WebTransportOptions!".into());
let transport = WebTransport::new_with_options(&address, &options)?;
console::log_1(&"Created WebTransport connection object.".into());
console::log_1(&transport.clone().into());
if let Err(e) = wasm_bindgen_futures::JsFuture::from(transport.ready()).await {
bail!("could not connect to transport: {e:?}");
}
console::log_1(&"Transport is ready.".into());
let stream: WebTransportBidirectionalStream =
wasm_bindgen_futures::JsFuture::from(transport.create_bidirectional_stream())
.await?
.into();
let wasm_stream_readable = wasm_streams::ReadableStream::from_raw(stream.readable().into());
let wasm_stream_writable = wasm_streams::WritableStream::from_raw(stream.writable().into());
let read_codec = ClientControlCodec::new();
let write_codec = ClientControlCodec::new();
let reader =
asynchronous_codec::FramedRead::new(wasm_stream_readable.into_async_read(), read_codec);
let writer =
asynchronous_codec::FramedWrite::new(wasm_stream_writable.into_async_write(), write_codec);
super::network_loop(username, event_rx, reader, writer).await
}
pub fn set_default_username(username: &str) -> Option<()> {
web_sys::window()?
.local_storage()
.ok()??
.set_item("username", username)
.ok()
}
pub fn load_username() -> Option<String> {
web_sys::window()
.unwrap()
.local_storage()
.ok()??
.get_item("username")
.ok()?
}
pub fn load_config() -> Option<super::GuiConfig> {
serde_wasm_bindgen::from_value(Reflect::get(window()?.as_ref(), &"config".into()).ok()?).ok()
}