bump dioxus version & icons in desktop & logging
This commit is contained in:
+25
-48
@@ -1,13 +1,13 @@
|
||||
use crate::app::Command;
|
||||
use anyhow::Result;
|
||||
use color_eyre::eyre::Error;
|
||||
use dioxus::hooks::{UnboundedReceiver, UnboundedSender};
|
||||
use futures::io::{AsyncRead, AsyncWrite};
|
||||
use mumble_protocol::control::{ClientControlCodec, ControlPacket};
|
||||
use mumble_protocol::Serverbound;
|
||||
use mumble_web2_common::GuiConfig;
|
||||
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};
|
||||
@@ -19,60 +19,22 @@ use tokio_util::compat::{TokioAsyncReadCompatExt as _, TokioAsyncWriteCompatExt
|
||||
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");
|
||||
// TODO
|
||||
Ok(AudioSystem())
|
||||
}
|
||||
|
||||
pub fn create_player(&mut self) -> Result<AudioPlayer, Error> {
|
||||
// dbg!("todo");
|
||||
// TODO
|
||||
Ok(AudioPlayer())
|
||||
}
|
||||
}
|
||||
@@ -81,7 +43,7 @@ pub struct AudioPlayer();
|
||||
|
||||
impl AudioPlayer {
|
||||
pub fn play_opus(&mut self, payload: &[u8]) {
|
||||
// dbg!("todo");
|
||||
// TODO
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,17 +119,17 @@ pub async fn network_connect(
|
||||
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)
|
||||
.connect(address.try_into()?, 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);
|
||||
let reader = asynchronous_codec::FramedRead::new(read_server.compat(), read_codec);
|
||||
let writer = asynchronous_codec::FramedWrite::new(write_server.compat_write(), write_codec);
|
||||
|
||||
super::network_loop(username, event_rx, reader, writer).await
|
||||
crate::network_loop(username, event_rx, reader, writer).await
|
||||
}
|
||||
|
||||
pub fn set_default_username(username: &str) -> Option<()> {
|
||||
@@ -178,6 +140,21 @@ pub fn load_username() -> Option<String> {
|
||||
return None;
|
||||
}
|
||||
|
||||
pub fn load_config() -> Option<super::GuiConfig> {
|
||||
pub fn load_config() -> Option<GuiConfig> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn init_logging() {
|
||||
use tracing::level_filters::LevelFilter;
|
||||
use tracing_subscriber::filter::EnvFilter;
|
||||
|
||||
let env_filter = EnvFilter::builder()
|
||||
.with_default_directive(LevelFilter::INFO.into())
|
||||
.from_env_lossy();
|
||||
|
||||
tracing_subscriber::fmt()
|
||||
.with_target(true)
|
||||
.with_level(true)
|
||||
.with_env_filter(env_filter)
|
||||
.init();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
#[cfg(feature = "web")]
|
||||
mod web;
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
mod desktop;
|
||||
|
||||
#[cfg(all(feature = "web", not(feature = "desktop")))]
|
||||
pub use web::*;
|
||||
|
||||
#[cfg(feature = "desktop")]
|
||||
pub use desktop::*;
|
||||
+124
-112
@@ -1,27 +1,19 @@
|
||||
use crate::app::Command;
|
||||
use crate::bail;
|
||||
use crate::CONFIG;
|
||||
use color_eyre::eyre::{bail, eyre, Error};
|
||||
use dioxus::prelude::*;
|
||||
use futures::AsyncRead;
|
||||
use futures::AsyncWrite;
|
||||
use futures::{AsyncRead, 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::control::{ClientControlCodec, ControlPacket};
|
||||
use mumble_protocol::voice::{VoicePacket, VoicePacketPayload};
|
||||
use mumble_protocol::Serverbound;
|
||||
use std::fmt;
|
||||
use std::io;
|
||||
use mumble_web2_common::GuiConfig;
|
||||
use std::time::Duration;
|
||||
use tracing::{debug, error, info};
|
||||
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::js_sys::{Promise, Reflect, Uint8Array};
|
||||
use web_sys::AudioContext;
|
||||
use web_sys::AudioContextOptions;
|
||||
use web_sys::AudioData;
|
||||
@@ -44,6 +36,7 @@ use web_sys::WebTransport;
|
||||
use web_sys::WebTransportBidirectionalStream;
|
||||
use web_sys::WebTransportOptions;
|
||||
use web_sys::WorkletOptions;
|
||||
use web_sys::{console, window};
|
||||
|
||||
pub use wasm_bindgen_futures::spawn_local as spawn;
|
||||
|
||||
@@ -57,58 +50,27 @@ pub async fn sleep(d: Duration) {
|
||||
TimeoutFuture::new(d.as_millis() as u32).await
|
||||
}
|
||||
|
||||
pub struct Error(JsValue);
|
||||
trait ResultExt<T> {
|
||||
fn ey(self) -> Result<T, Error>;
|
||||
}
|
||||
|
||||
impl From<anyhow::Error> for Error {
|
||||
fn from(value: anyhow::Error) -> Self {
|
||||
Error(JsError::new(&value.to_string()).into())
|
||||
impl<T> ResultExt<T> for Result<T, JsValue> {
|
||||
fn ey(self) -> Result<T, Error> {
|
||||
match self {
|
||||
Ok(x) => Ok(x),
|
||||
Err(e) => match e.dyn_into::<js_sys::Error>() {
|
||||
Ok(e) => Err(eyre!("{}: {}", e.name(), e.message())),
|
||||
Err(e) => Err(eyre!("{:?}", e)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for Error {
|
||||
fn from(value: io::Error) -> Self {
|
||||
Error(JsError::new(&value.to_string()).into())
|
||||
impl<T> ResultExt<T> for Result<T, JsError> {
|
||||
fn ey(self) -> Result<T, Error> {
|
||||
self.map_err(|e| JsValue::from(e)).ey()
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
@@ -120,8 +82,8 @@ impl AudioSystem {
|
||||
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(node) => info!("created encoder worklet: {:?}", &node),
|
||||
Err(err) => error!("could not create encoder worklet: {err}"),
|
||||
}
|
||||
});
|
||||
|
||||
@@ -132,21 +94,25 @@ impl AudioSystem {
|
||||
let audio_context = &self.0;
|
||||
|
||||
let audio_stream_generator =
|
||||
MediaStreamTrackGenerator::new(&MediaStreamTrackGeneratorInit::new("audio"))?;
|
||||
MediaStreamTrackGenerator::new(&MediaStreamTrackGeneratorInit::new("audio")).ey()?;
|
||||
|
||||
// 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)?;
|
||||
let media_stream = MediaStream::new_with_tracks(&js_tracks).ey()?;
|
||||
|
||||
// Create MediaStreamAudioSourceNode
|
||||
let audio_source = audio_context.create_media_stream_source(&media_stream)?;
|
||||
let audio_source = audio_context
|
||||
.create_media_stream_source(&media_stream)
|
||||
.ey()?;
|
||||
// Connect output of audio_source to audio_context (browser audio)
|
||||
audio_source.connect_with_audio_node(&audio_context.destination())?;
|
||||
audio_source
|
||||
.connect_with_audio_node(&audio_context.destination())
|
||||
.ey()?;
|
||||
|
||||
// Create callback functions for AudioDecoder
|
||||
let error = Closure::wrap(Box::new(move |e: JsValue| {
|
||||
console::error_1(&e);
|
||||
let decoder_error = Closure::wrap(Box::new(move |e: JsValue| {
|
||||
error!("error decoding audio {:?}", e);
|
||||
}) as Box<dyn FnMut(JsValue)>);
|
||||
|
||||
// This knows what MediaStreamTrackGenerator to use as it closes around it
|
||||
@@ -157,29 +123,33 @@ impl AudioSystem {
|
||||
}
|
||||
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.ready()).await.ey() {
|
||||
error!("write chunk ready error {:?}", e);
|
||||
}
|
||||
if let Err(e) = JsFuture::from(writer.write_with_chunk(&audio_data)).await {
|
||||
console::error_1(&format!("write chunk error {:?}", e).into());
|
||||
if let Err(e) = JsFuture::from(writer.write_with_chunk(&audio_data))
|
||||
.await
|
||||
.ey()
|
||||
{
|
||||
error!("write chunk error {:?}", e);
|
||||
};
|
||||
writer.release_lock();
|
||||
});
|
||||
}) {
|
||||
console::error_1(&e);
|
||||
error!("error writing audio data {:?}", e);
|
||||
}
|
||||
}) as Box<dyn FnMut(AudioData)>);
|
||||
|
||||
let audio_decoder = AudioDecoder::new(&AudioDecoderInit::new(
|
||||
error.as_ref().unchecked_ref(),
|
||||
decoder_error.as_ref().unchecked_ref(),
|
||||
output.as_ref().unchecked_ref(),
|
||||
))?;
|
||||
))
|
||||
.ey()?;
|
||||
|
||||
audio_decoder.configure(&AudioDecoderConfig::new("opus", 1, 48000));
|
||||
console::log_1(&"Created Audio Decoder".into());
|
||||
info!("created audio decoder");
|
||||
|
||||
// This is required to prevent these from being deallocated
|
||||
error.forget();
|
||||
decoder_error.forget();
|
||||
output.forget();
|
||||
|
||||
Ok(AudioPlayer(audio_decoder))
|
||||
@@ -228,32 +198,41 @@ async fn create_encoder_worklet(
|
||||
let stream = window()
|
||||
.unwrap()
|
||||
.navigator()
|
||||
.media_devices()?
|
||||
.get_user_media_with_constraints(MediaStreamConstraints::new().audio(&JsValue::TRUE))?
|
||||
.media_devices()
|
||||
.ey()?
|
||||
.get_user_media_with_constraints(MediaStreamConstraints::new().audio(&JsValue::TRUE))
|
||||
.ey()?
|
||||
.into_future()
|
||||
.await?
|
||||
.await
|
||||
.ey()?
|
||||
.dyn_into()
|
||||
.map_err(|e| JsError::new(&format!("not a stream: {e:?}")))?;
|
||||
.map_err(|e| JsError::new(&format!("not a stream: {e:?}")))
|
||||
.ey()?;
|
||||
|
||||
let options = WorkletOptions::new();
|
||||
Reflect::set(
|
||||
&options,
|
||||
&"processorOptions".into(),
|
||||
&wasm_bindgen::module(),
|
||||
)?;
|
||||
)
|
||||
.ey()?;
|
||||
|
||||
let module = "/rust_mic_worklet.js";
|
||||
console::log_1(&format!("Loading mic worklet from {module:?}").into());
|
||||
let module = asset!("assets/rust_mic_worklet.js").to_string();
|
||||
info!("loading mic worklet from {module:?}");
|
||||
audio_context
|
||||
.audio_worklet()?
|
||||
.add_module_with_options(module, &options)?
|
||||
.audio_worklet()
|
||||
.ey()?
|
||||
.add_module_with_options(&module, &options)
|
||||
.ey()?
|
||||
.into_future()
|
||||
.await?;
|
||||
.await
|
||||
.ey()?;
|
||||
|
||||
let source = audio_context.create_media_stream_source(&stream)?;
|
||||
let worklet_node = AudioWorkletNode::new(audio_context, "rust_mic_worklet")?;
|
||||
let source = audio_context.create_media_stream_source(&stream).ey()?;
|
||||
let worklet_node = AudioWorkletNode::new(audio_context, "rust_mic_worklet").ey()?;
|
||||
|
||||
let error: Closure<dyn FnMut(JsValue)> = Closure::new(|e| console::error_1(&e));
|
||||
let encoder_error: Closure<dyn FnMut(JsValue)> =
|
||||
Closure::new(|e| error!("error encoding audio {:?}", e));
|
||||
|
||||
let download_buffer = std::cell::RefCell::new(Vec::new());
|
||||
|
||||
@@ -287,13 +266,13 @@ async fn create_encoder_worklet(
|
||||
});
|
||||
|
||||
let audio_encoder = AudioEncoder::new(&AudioEncoderInit::new(
|
||||
error.as_ref().unchecked_ref(),
|
||||
encoder_error.as_ref().unchecked_ref(),
|
||||
output.as_ref().unchecked_ref(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
// This is required to prevent these from being deallocated
|
||||
error.forget();
|
||||
encoder_error.forget();
|
||||
output.forget();
|
||||
let encoder_config = AudioEncoderConfig::new("opus");
|
||||
encoder_config.set_number_of_channels(1);
|
||||
@@ -301,7 +280,7 @@ async fn create_encoder_worklet(
|
||||
encoder_config.set_bitrate(72_000.0);
|
||||
|
||||
audio_encoder.configure(&encoder_config);
|
||||
console::log_1(&"Created Audio Encoder".into());
|
||||
info!("created audio encoder");
|
||||
|
||||
let download_buffer = std::cell::RefCell::new(Vec::new());
|
||||
|
||||
@@ -322,20 +301,25 @@ async fn create_encoder_worklet(
|
||||
audio_encoder.encode(&data);
|
||||
}
|
||||
Err(err) => {
|
||||
console::error_1(&err);
|
||||
console::debug_1(&event);
|
||||
error!(
|
||||
"error creating AudioData object {:?} during event {:?}",
|
||||
err, event,
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
Reflect::set(
|
||||
&Reflect::get(&worklet_node, &"port".into())?,
|
||||
&Reflect::get(&worklet_node, &"port".into()).ey()?,
|
||||
&"onmessage".into(),
|
||||
onmessage.as_ref(),
|
||||
)?;
|
||||
)
|
||||
.ey()?;
|
||||
onmessage.forget();
|
||||
|
||||
source.connect_with_audio_node(&worklet_node)?;
|
||||
worklet_node.connect_with_audio_node(&audio_context.destination())?;
|
||||
source.connect_with_audio_node(&worklet_node).ey()?;
|
||||
worklet_node
|
||||
.connect_with_audio_node(&audio_context.destination())
|
||||
.ey()?;
|
||||
|
||||
Ok(worklet_node)
|
||||
}
|
||||
@@ -345,7 +329,7 @@ pub async fn network_connect(
|
||||
username: String,
|
||||
event_rx: &mut UnboundedReceiver<Command>,
|
||||
) -> Result<(), Error> {
|
||||
console::log_1(&"Rust via WASM!".into());
|
||||
info!("Rust via WASM!");
|
||||
|
||||
let object = web_sys::js_sys::Object::new();
|
||||
|
||||
@@ -353,37 +337,38 @@ pub async fn network_connect(
|
||||
&object,
|
||||
&JsValue::from_str("algorithm"),
|
||||
&JsValue::from_str("sha-256"),
|
||||
)?;
|
||||
)
|
||||
.ey()?;
|
||||
|
||||
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)?;
|
||||
web_sys::js_sys::Reflect::set(&object, &"value".into(), &hash).ey()?;
|
||||
}
|
||||
|
||||
let array = web_sys::js_sys::Array::new();
|
||||
array.push(&object);
|
||||
|
||||
console::log_1(&object.clone().into());
|
||||
console::log_1(&"Created option object!".into());
|
||||
debug!("created option object: {:?}", &object);
|
||||
|
||||
let mut options = WebTransportOptions::new();
|
||||
options.server_certificate_hashes(&array);
|
||||
|
||||
console::log_1(&"Created WebTransportOptions!".into());
|
||||
debug!("created WebTransportOptions");
|
||||
|
||||
let transport = WebTransport::new_with_options(&address, &options)?;
|
||||
console::log_1(&"Created WebTransport connection object.".into());
|
||||
let transport = WebTransport::new_with_options(&address, &options).ey()?;
|
||||
debug!("created WebTransport connection object");
|
||||
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());
|
||||
info!("transport is ready");
|
||||
|
||||
let stream: WebTransportBidirectionalStream =
|
||||
wasm_bindgen_futures::JsFuture::from(transport.create_bidirectional_stream())
|
||||
.await?
|
||||
.await
|
||||
.ey()?
|
||||
.into();
|
||||
|
||||
let wasm_stream_readable = wasm_streams::ReadableStream::from_raw(stream.readable().into());
|
||||
@@ -397,7 +382,7 @@ pub async fn network_connect(
|
||||
let writer =
|
||||
asynchronous_codec::FramedWrite::new(wasm_stream_writable.into_async_write(), write_codec);
|
||||
|
||||
super::network_loop(username, event_rx, reader, writer).await
|
||||
crate::network_loop(username, event_rx, reader, writer).await
|
||||
}
|
||||
|
||||
pub fn set_default_username(username: &str) -> Option<()> {
|
||||
@@ -417,6 +402,33 @@ pub fn load_username() -> Option<String> {
|
||||
.ok()?
|
||||
}
|
||||
|
||||
pub fn load_config() -> Option<super::GuiConfig> {
|
||||
fn load_config_from_window() -> Option<GuiConfig> {
|
||||
serde_wasm_bindgen::from_value(Reflect::get(window()?.as_ref(), &"config".into()).ok()?).ok()
|
||||
}
|
||||
|
||||
fn load_config_from_env() -> Option<GuiConfig> {
|
||||
serde_json::from_str(option_env!("MUMBLE_WEB2_GUI_CONFIG")?).ok()?
|
||||
}
|
||||
|
||||
pub fn load_config() -> Option<GuiConfig> {
|
||||
load_config_from_window().or_else(load_config_from_env)
|
||||
}
|
||||
|
||||
pub fn init_logging() {
|
||||
// copied from tracing_web example usage
|
||||
|
||||
use tracing_subscriber::fmt::format::Pretty;
|
||||
use tracing_subscriber::prelude::*;
|
||||
use tracing_web::{performance_layer, MakeWebConsoleWriter};
|
||||
|
||||
let fmt_layer = tracing_subscriber::fmt::layer()
|
||||
.with_ansi(false) // Only partially supported across browsers
|
||||
.without_time() // std::time is not available in browsers
|
||||
.with_writer(MakeWebConsoleWriter::new()); // write events to the console
|
||||
let perf_layer = performance_layer().with_details_from_fields(Pretty::default());
|
||||
|
||||
tracing_subscriber::registry()
|
||||
.with(fmt_layer)
|
||||
.with(perf_layer)
|
||||
.init();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user