move into gui folder for monorepo
This commit is contained in:
@@ -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()
|
||||
}
|
||||
Reference in New Issue
Block a user