Files
mumble-web2/src/lib.rs
T
2024-09-28 17:34:59 -06:00

748 lines
26 KiB
Rust

pub mod app;
use app::ChannelId;
use app::Chat;
use app::Command;
use app::ConnectionState;
use dioxus::prelude::*;
use app::STATE;
use futures::select;
use futures::FutureExt;
use futures::SinkExt;
use futures::StreamExt;
use futures_channel::mpsc::UnboundedSender;
use gloo_timers::future::TimeoutFuture;
use manganis::{file, mg};
use markdown;
use mumble_protocol::control::ControlPacket;
use mumble_protocol::control::{msgs, ClientControlCodec};
use mumble_protocol::voice::VoicePacket;
use mumble_protocol::voice::VoicePacketPayload;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use std::fmt;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::spawn_local as spawn;
use wasm_bindgen_futures::{future_to_promise, 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;
mod ass {
use byteorder::{ByteOrder, LittleEndian};
use ogg::PacketWriter;
const VER: &str = "ballz";
const fn to_samples<const S_PS: u32>(ms: u32) -> usize {
((S_PS * ms) / 1000) as usize
}
const fn calc_sr_u64(val: u64, from: u32, to: u32) -> u64 {
(val * to as u64) / from as u64
}
pub fn encode(pre_encoded_frames: Vec<Vec<u8>>, frame_size: usize, skip: u16) -> Vec<u8> {
let mut buffer: Vec<u8> = Vec::new();
let mut packet_writer = PacketWriter::new(&mut buffer);
// Hardcoded serial number
let serial = 12345;
let skip_48 = calc_sr_u64(skip.into(), 48000, 48000);
let opus_head: [u8; 19] = [
b'O', b'p', b'u', b's', b'H', b'e', b'a', b'd', 1, 1, // NUM_CHANNELS = 1
0, 0, 0, 0, 0, 0, 0, 0, 0,
];
let mut head = opus_head;
LittleEndian::write_u16(&mut head[10..12], skip_48 as u16);
LittleEndian::write_u32(&mut head[12..16], 48000);
let mut opus_tags: Vec<u8> = Vec::with_capacity(60);
let vendor_str = format!("ogg-opus {}", VER);
opus_tags.extend(b"OpusTags");
let mut len_bf = [0u8; 4];
LittleEndian::write_u32(&mut len_bf, vendor_str.len() as u32);
opus_tags.extend(&len_bf);
opus_tags.extend(vendor_str.bytes());
opus_tags.extend(&[0u8; 4]);
let _ = packet_writer.write_packet(&head, serial, ogg::PacketWriteEndInfo::EndPage, 0);
let _ = packet_writer.write_packet(&opus_tags, serial, ogg::PacketWriteEndInfo::EndPage, 0);
for (i, frame) in pre_encoded_frames.iter().enumerate() {
let is_last = i == pre_encoded_frames.len() - 1;
//let granule_pos = 0;
let granule_pos =
calc_sr_u64((skip as usize + (i + 1) * frame_size) as u64, 48000, 48000);
let _ = packet_writer.write_packet(
frame.clone(),
serial,
if is_last {
ogg::PacketWriteEndInfo::EndStream
} else {
ogg::PacketWriteEndInfo::NormalPacket
},
granule_pos,
);
}
buffer
}
}
// Function to download data as a file
pub fn download_data(data: Vec<u8>, filename: &str) -> Result<(), JsValue> {
use wasm_bindgen::prelude::*;
use web_sys::{window, Blob, HtmlAnchorElement, Url};
// Create a new Blob from the data
let array = web_sys::js_sys::Uint8Array::from(&data[..]);
let blob = Blob::new_with_u8_array_sequence(&vec![array].into())?;
// Create a URL for the Blob
let url = Url::create_object_url_with_blob(&blob)?;
// Create an anchor element and set its href to the Blob URL
let document = window().unwrap().document().unwrap();
let a = document
.create_element("a")?
.dyn_into::<HtmlAnchorElement>()?;
a.set_href(&url);
a.set_download(filename);
// Append the anchor to the document body, click it, and remove it
document.body().unwrap().append_child(&a)?;
a.click();
document.body().unwrap().remove_child(&a)?;
// Revoke the object URL to free resources
Url::revoke_object_url(&url)?;
Ok(())
}
// 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, JsValue> {
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 mut 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_array(&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 mut 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_array(&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)
}
fn create_decoder(audio_context: &AudioContext) -> Result<AudioDecoder, JsValue> {
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(audio_decoder)
}
pub async fn network_entrypoint(mut event_rx: UnboundedReceiver<Command>) {
loop {
let Some(Command::Connect {
address,
username,
hash,
}) = event_rx.next().await
else {
panic!("Did not receive connect command")
};
if let Err(error) = network_connect(address, username, &mut event_rx).await {
console::error_1(&error);
}
}
}
macro_rules! bail {
($($x:tt)*) => {
return Err(wasm_bindgen::JsError::new(&format!($($x)*)).into());
};
}
async fn network_connect(
address: String,
username: String,
event_rx: &mut UnboundedReceiver<Command>,
) -> Result<(), JsValue> {
*STATE.server.write() = Default::default();
*STATE.status.write() = ConnectionState::Connecting;
console::log_1(&"Rust via WASM!".into());
let Ok(server_hash): Result<Vec<u8>, _> = include_str!("../server_hash.txt")
.trim()
.trim_matches(&['[', ']'])
.split(',')
.map(|x| x.trim().parse())
.collect()
else {
bail!("could not parse server hash");
};
let hash = web_sys::js_sys::Uint8Array::from(server_hash.as_slice());
let object = web_sys::js_sys::Object::new();
Reflect::set(
&object,
&JsValue::from_str("algorithm"),
&JsValue::from_str("sha-256"),
)?;
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 {
console::log_1(&e.into());
panic!();
}
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 mut reader =
asynchronous_codec::FramedRead::new(wasm_stream_readable.into_async_read(), read_codec);
let mut writer =
asynchronous_codec::FramedWrite::new(wasm_stream_writable.into_async_write(), write_codec);
let (mut send_chan, mut writer_recv_chan) = futures_channel::mpsc::unbounded();
// Spawn worker to send packets.
spawn(async move {
while let Some(msg) = writer_recv_chan.next().await {
if !matches!(msg, ControlPacket::Ping(_) | ControlPacket::UDPTunnel(_)) {
console::log_1(&format!("sending {:#?}", msg).into());
}
if let Err(e) = writer.send(msg).await {
console::error_1(&e.to_string().into());
break;
}
}
});
// Get version packet
let version = match reader.next().await {
Some(Ok(v)) => v,
Some(Err(err)) => bail!("bad version packet: {err:?}"),
None => bail!("no version was recieved"),
};
console::log_1(&"Got version packet".into());
console::log_1(&format!("{:#?}", version).into());
// Send version packet
let mut msg = msgs::Version::new();
msg.set_version(0x000010204);
msg.set_release(format!("{} {}", "mumbleweb2", "6.9.0"));
//msg.set_os("Chrome".to_string());
send_chan.send(msg.into()).await.unwrap();
// Send authenticate packet
let mut msg = msgs::Authenticate::new();
msg.set_username(username);
msg.set_opus(true);
send_chan.send(msg.into()).await.unwrap();
// Spawn worker to send pings
{
let mut send_chan = send_chan.clone();
spawn(async move {
loop {
if let Err(e) = send_chan.send(msgs::Ping::new().into()).await {
console::log_1(&"could not ping".into());
console::log_1(&e.to_string().into());
break;
}
TimeoutFuture::new(3000).await;
}
});
}
// 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();
let packet_sender_worklet = send_chan.clone();
spawn(async move {
match create_encoder_worklet(&audio_context_worklet, packet_sender_worklet).await {
Ok(node) => console::log_2(&"Created audio worklet:".into(), &node),
Err(err) => console::error_1(&err),
}
});
// Create map of session_id -> AudioDecoder
let mut decoder_map = HashMap::new();
let mut reader_future = reader.next().fuse();
let mut command_future = event_rx.next();
loop {
select! {
packet = reader_future => {
reader_future = reader.next().fuse();
match packet {
Some(Ok(msg)) => {
let res =accept_packet(msg, &audio_context, &mut decoder_map);
if let Err(err) = res {
console::error_1(&err.into());
}
},
Some(Err(err)) => console::error_1(&err.to_string().into()),
None => break,
}
}
command = command_future => {
command_future = event_rx.next();
if let Some(command) = &command {
console::log_1(&format!("commanding {:#?}", command).into());
}
match command {
Some(Command::Disconnect) => break,
Some(command) => {
let res = accept_command(command, &mut send_chan);
if let Err(err) = res {
console::error_1(&err.into());
}
}
None => continue,
}
}
}
}
let _ = send_chan.close();
*STATE.status.write() = ConnectionState::Disconnected;
Ok(())
}
fn accept_command(
command: Command,
send_chan: &mut UnboundedSender<ControlPacket<mumble_protocol::Serverbound>>,
) -> Result<(), JsValue> {
match command {
Command::SendChat { markdown, channels } => {
use markdown::*;
let blocks = tokenize(&markdown);
let html_text = match blocks.as_slice() {
[Block::Paragraph(par)] => match par.as_slice() {
[Span::Text(text)] => text.to_string(),
_ => to_html(&markdown)
.trim()
.strip_prefix("<p>")
.unwrap()
.strip_suffix("</p>")
.unwrap()
.to_string(),
},
_ => to_html(&markdown).trim().to_string(),
};
{
let mut server = STATE.server.write();
let Some(me) = server.session else {
bail!("not signed in with a session id")
};
server.chat.push(Chat {
raw: markdown,
dangerous_html: html_text.clone(),
sender: Some(me),
})
}
let mut u = msgs::TextMessage::new();
u.set_message(html_text.to_string());
u.set_channel_id(channels);
let _ = send_chan.unbounded_send(u.into());
}
_ => (),
}
Ok(())
}
fn accept_packet(
msg: ControlPacket<mumble_protocol::Clientbound>,
audio_context: &AudioContext,
decoder_map: &mut HashMap<u32, AudioDecoder>,
) -> Result<(), JsValue> {
if !matches!(msg, ControlPacket::UDPTunnel(_) | ControlPacket::Ping(_)) {
console::log_1(&format!("receiving {:#?}", msg).into());
}
match msg {
ControlPacket::UDPTunnel(u) => {
match *u.clone() {
mumble_protocol::voice::VoicePacket::Audio {
_dst,
target,
session_id,
seq_num,
payload,
position_info,
} => {
// Get or create audio decoder for this user
let audio_decoder = match decoder_map.entry(session_id) {
Entry::Occupied(occupied_entry) => occupied_entry.into_mut(),
Entry::Vacant(vacant_entry) => {
vacant_entry.insert(create_decoder(&audio_context)?)
}
};
// This will over time (as users join and leave) leak
// AudioDecoders, MediaStreamTrackGenerators, MediaStreams, and MediaStreamAudioSourceNodes.
// A better way to handle this would be to delete and create all the audio
// infra on channel join and update it as new users join the channel, dropping
// any audio packets that come in the meantime.
if let VoicePacketPayload::Opus(audio_payload, end_bit) = payload {
let js_audio_payload = Uint8Array::from(audio_payload.as_ref());
let _ = audio_decoder.decode(
&EncodedAudioChunk::new(&EncodedAudioChunkInit::new(
&js_audio_payload.into(),
0.0,
EncodedAudioChunkType::Key,
))
.unwrap(),
);
//console::log_1(&"Oueued audio chunk for decoding".into());
}
}
_ => {
unreachable!("TCP tunnels UDP packets should not contain pings");
// I think?
}
}
}
ControlPacket::ChannelState(u) => {
let mut server = STATE.server.write();
let id = u.get_channel_id();
let state = server.channels.entry(id).or_default();
let new_parent = if u.has_parent() {
if let Some(parent) = state.parent.and_then(|p| server.channels.get_mut(&p)) {
parent.children.remove(&id);
}
let parent_id = u.get_parent();
let parent = server.channels.entry(parent_id).or_default();
if u.has_position() && u.get_position() as usize <= parent.children.len() {
// TODO: what if positions are received out of order? we need to sort afterwards?
parent.children.insert_before(u.get_position() as usize, id);
} else {
parent.children.insert(id);
}
Some(parent_id)
} else {
None
};
let state = server.channels.entry(id).or_default();
state.parent = new_parent;
if u.has_name() {
state.name = u.get_name().to_string();
}
}
ControlPacket::ChannelRemove(u) => {
let mut server = STATE.server.write();
let id = u.get_channel_id();
if let Some(channel) = server.channels.remove(&id) {
if let Some(parent) = channel.parent.and_then(|p| server.channels.get_mut(&p)) {
parent.children.remove(&id);
}
}
}
ControlPacket::UserState(u) => {
let mut server = STATE.server.write();
let server = &mut *server;
let id = u.get_session();
let state = server.users.entry(id).or_default();
if u.has_channel_id() {
if let Some(parent) = server.channels.get_mut(&state.channel) {
parent.users.remove(&id);
}
let channel_id = u.get_channel_id();
server
.channels
.entry(channel_id)
.or_default()
.users
.insert(id);
state.channel = channel_id;
}
if u.has_name() {
state.name = u.get_name().to_string();
}
}
ControlPacket::UserRemove(u) => {
let mut server = STATE.server.write();
let id = u.get_session();
if let Some(state) = server.users.remove(&id) {
if let Some(parent) = server.channels.get_mut(&state.channel) {
parent.users.remove(&id);
}
}
}
ControlPacket::TextMessage(u) => {
let mut server = STATE.server.write();
if u.has_message() {
let text = u.get_message().to_string();
server.chat.push(Chat {
sender: if u.has_actor() {
Some(u.get_actor())
} else {
None
},
dangerous_html: html_purifier::purifier(&text, Default::default()),
raw: text,
});
}
}
ControlPacket::ServerSync(u) => {
*STATE.status.write() = ConnectionState::Connected;
let mut server = STATE.server.write();
if u.has_welcome_text() {
let text = u.get_welcome_text().to_string();
server.chat.push(Chat {
sender: None,
dangerous_html: html_purifier::purifier(&text, Default::default()),
raw: text,
});
}
if u.has_session() {
server.session = Some(u.get_session());
}
}
_ => {}
}
Ok(())
}