use crate::events::UiCmd; use crate::video_renderer::VideoRenderer; use crate::{events::AsyncCmd, video_grid::VideoGrid}; use egui::{Rounding, Stroke}; use egui_wgpu::WgpuConfiguration; use livekit::room::track::remote_track::RemoteTrackHandle; use parking_lot::Mutex; use std::sync::{ atomic::{AtomicBool, Ordering}, Arc, }; use tokio::sync::{mpsc, oneshot}; use tracing::error; use livekit::room::room_session::ConnectionState; use livekit::room::{Room, RoomEvent, RoomEvents, SimulateScenario}; // Useful default constants for developing const DEFAULT_URL: &str = "ws://localhost:7880"; const DEFAULT_TOKEN : &str = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE5MDY2MTMyODgsImlzcyI6IkFQSVRzRWZpZFpqclFvWSIsIm5hbWUiOiJuYXRpdmUiLCJuYmYiOjE2NzI2MTMyODgsInN1YiI6Im5hdGl2ZSIsInZpZGVvIjp7InJvb20iOiJ0ZXN0Iiwicm9vbUFkbWluIjp0cnVlLCJyb29tQ3JlYXRlIjp0cnVlLCJyb29tSm9pbiI6dHJ1ZSwicm9vbUxpc3QiOnRydWV9fQ.uSNIangMRu8jZD5mnRYoCHjcsQWCrJXgHCs0aNIgBFY"; // eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE5MDY2MTM0MzcsImlzcyI6IkFQSVRzRWZpZFpqclFvWSIsIm5hbWUiOiJ3ZWIiLCJuYmYiOjE2NzI2MTM0MzcsInN1YiI6IndlYiIsInZpZGVvIjp7InJvb20iOiJ0ZXN0Iiwicm9vbUFkbWluIjp0cnVlLCJyb29tQ3JlYXRlIjp0cnVlLCJyb29tSm9pbiI6dHJ1ZSwicm9vbUxpc3QiOnRydWV9fQ.DFTXt60n1kzGq4cSuOhbFBTQW2nd3rlcXKQ54sXsP8s use winit::{ event::*, event_loop::{ControlFlow, EventLoop}, window::{WindowBuilder, WindowId}, }; struct AppState { room: Mutex)>>, connecting: AtomicBool, } struct App { state: Arc, video_renderers: Vec, egui_context: egui::Context, egui_state: egui_winit::State, egui_painter: egui_wgpu::winit::Painter, window: winit::window::Window, cmd_tx: mpsc::UnboundedSender, cmd_rx: mpsc::UnboundedReceiver, // UI State lk_url: String, lk_token: String, connection_failure: Option, room_state: ConnectionState, } pub fn run(rt: tokio::runtime::Runtime) { rt.block_on(async { let event_loop = EventLoop::new(); let window = WindowBuilder::new() .with_title("LiveKit - NativeSDK") .build(&event_loop) .unwrap(); let egui_context = egui::Context::default(); let egui_state = egui_winit::State::new(&event_loop); let mut egui_painter = egui_wgpu::winit::Painter::new(WgpuConfiguration::default(), 1, 32); unsafe { egui_painter.set_window(Some(&window)); } let (async_cmd_tx, mut async_cmd_rx) = mpsc::unbounded_channel::(); let (ui_cmd_tx, ui_cmd_rx) = mpsc::unbounded_channel::(); let state = Arc::new(AppState { room: Default::default(), connecting: AtomicBool::new(false), }); let mut app = App { state: state.clone(), video_renderers: Vec::default(), egui_context, egui_state, egui_painter, window, cmd_tx: async_cmd_tx, cmd_rx: ui_cmd_rx, lk_url: DEFAULT_URL.to_owned(), lk_token: DEFAULT_TOKEN.to_owned(), connection_failure: None, room_state: ConnectionState::Connected, }; // Async event loop tokio::spawn(async move { while let Some(event) = async_cmd_rx.recv().await { match event { AsyncCmd::RoomConnect { url, token } => { if let Some((room, close_tx)) = state.room.lock().take() { // Close the Room if already connected let _ = room.close().await; let _ = close_tx.send(()); } state.connecting.store(true, Ordering::SeqCst); let res = Room::connect(&url, &token).await; match res { Ok((room, room_events)) => { let (close_tx, close_rx) = oneshot::channel(); state.room.lock().replace((room, close_tx)); tokio::spawn(room_task( state.clone(), room_events, close_rx, ui_cmd_tx.clone(), )); let _ = ui_cmd_tx.send(UiCmd::ConnectResult { result: Ok(()) }); } Err(err) => { let _ = ui_cmd_tx.send(UiCmd::ConnectResult { result: Err(err) }); } } state.connecting.store(false, Ordering::SeqCst); } AsyncCmd::SimulateScenario { scenario } => { if let Some((room, _)) = state.room.lock().as_ref() { let _ = room.session().simulate_scenario(scenario).await; } } } } }); tokio::task::block_in_place(move || loop { // UI/Main Thread event_loop.run(move |event, _, control_flow| { app.update(event, control_flow); }); }); }); } async fn room_task( _app_state: Arc, mut room_events: RoomEvents, mut close_rx: oneshot::Receiver<()>, ui_cmd_tx: mpsc::UnboundedSender, ) { loop { tokio::select! { Some(event) = room_events.recv() => { let _ = ui_cmd_tx.send(UiCmd::RoomEvent{event}); } _ = &mut close_rx => { //break; } } } } impl App { fn update(&mut self, event: Event<'_, T>, control_flow: &mut ControlFlow) { if let Ok(cmd) = self.cmd_rx.try_recv() { match cmd { UiCmd::ConnectResult { result } => { if let Err(err) = result { self.connection_failure = Some(err.to_string()); } else { self.connection_failure = None } } UiCmd::RoomEvent { event } => { match event { RoomEvent::TrackSubscribed { track, .. } => { match track { RemoteTrackHandle::Video(video_track) => { // Create a new VideoRenderer let video_renderer = VideoRenderer::new( self.egui_painter.render_state().clone().unwrap(), video_track.rtc_track(), ); self.video_renderers.push(video_renderer); } RemoteTrackHandle::Audio(_) => { // The demo doesn't support Audio rendering at the moment. } }; } _ => {} } } } } match event { Event::WindowEvent { window_id, event } => { if let Some(flow) = self.on_window_event(window_id, event) { *control_flow = flow; } } Event::RedrawRequested(window_id) if window_id == self.window.id() => { self.render(); } Event::RedrawEventsCleared => { self.window.request_redraw(); } _ => {} }; } fn on_window_event( &mut self, _window_id: WindowId, event: WindowEvent<'_>, ) -> Option { if self .egui_state .on_event(&self.egui_context, &event) .consumed { return None; } match event { WindowEvent::CloseRequested => Some(ControlFlow::Exit), WindowEvent::Resized(inner_size) => { self.egui_painter .on_window_resized(inner_size.width, inner_size.height); None } WindowEvent::ScaleFactorChanged { new_inner_size, .. } => { self.egui_painter .on_window_resized(new_inner_size.width, new_inner_size.height); None } _ => None, } } fn ui(&mut self, ui: &mut egui::Ui) { egui::TopBottomPanel::top("top_panel").show(ui.ctx(), |ui| { egui::menu::bar(ui, |ui| { ui.menu_button("Tools", |ui| { if ui.button("Logs").clicked() {} if ui.button("Profiler").clicked() {} if ui.button("WebRTC Stats").clicked() {} if ui.button("Events").clicked() {} }); ui.menu_button("Simulate", |ui| { if ui.button("SignalReconnect").clicked() { let _ = self.cmd_tx.send(AsyncCmd::SimulateScenario { scenario: SimulateScenario::SignalReconnect, }); } if ui.button("Speaker").clicked() { let _ = self.cmd_tx.send(AsyncCmd::SimulateScenario { scenario: SimulateScenario::Speaker, }); } if ui.button("NodeFailure").clicked() { let _ = self.cmd_tx.send(AsyncCmd::SimulateScenario { scenario: SimulateScenario::NodeFailure, }); } if ui.button("ServerLeave").clicked() { let _ = self.cmd_tx.send(AsyncCmd::SimulateScenario { scenario: SimulateScenario::ServerLeave, }); } if ui.button("Migration").clicked() { let _ = self.cmd_tx.send(AsyncCmd::SimulateScenario { scenario: SimulateScenario::Migration, }); } if ui.button("ForceTcp").clicked() { let _ = self.cmd_tx.send(AsyncCmd::SimulateScenario { scenario: SimulateScenario::ForceTcp, }); } if ui.button("ForceTls").clicked() { let _ = self.cmd_tx.send(AsyncCmd::SimulateScenario { scenario: SimulateScenario::ForceTls, }); } }); }); }); egui::SidePanel::right("room_panel") .default_width(256.0) .show(ui.ctx(), |ui| { ui.heading("Livekit - Connect to a room"); ui.separator(); ui.horizontal(|ui| { ui.label("URL: "); ui.text_edit_singleline(&mut self.lk_url); }); ui.horizontal(|ui| { ui.label("Token: "); ui.text_edit_singleline(&mut self.lk_token); }); ui.horizontal(|ui| { let connecting = self.state.connecting.load(Ordering::SeqCst); ui.set_enabled(!connecting); if ui.button("Connect").clicked() { self.connection_failure = None; let _ = self.cmd_tx.send(AsyncCmd::RoomConnect { url: self.lk_url.clone(), token: self.lk_token.clone(), }); } if connecting { ui.spinner(); } }); if let Some(err) = &self.connection_failure { ui.colored_label(egui::Color32::RED, err); } ui.separator(); }); egui::CentralPanel::default().show(ui.ctx(), |ui| { egui::ScrollArea::vertical().show(ui, |ui| { VideoGrid::new("default_grid") .max_columns(6) .show(ui, |ui| { if self.room_state == ConnectionState::Disconnected { for _ in 0..20 { ui.video_frame(|ui| { egui::Frame::none().fill(egui::Color32::DARK_GRAY).show( ui, |ui| { ui.allocate_space(ui.available_size()); }, ); }); } } else { // Render participant videos for video_renderer in &self.video_renderers { ui.video_frame(|ui| { ui.painter().rect( ui.available_rect_before_wrap(), Rounding::none(), egui::Color32::BLUE, Stroke::NONE ); if let Some(tex) = video_renderer.texture_id() { ui.painter().image( tex, ui.available_rect_before_wrap(), egui::Rect::from_min_max( egui::pos2(0.0, 0.0), egui::pos2(1.0, 1.0), ), egui::Color32::WHITE, ); } }); } } }); }); }); } fn render(&mut self) { self.egui_state .set_pixels_per_point(egui_winit::native_pixels_per_point(&self.window)); let raw_inputs = self.egui_state.take_egui_input(&self.window); let full_output = self.egui_context.clone().run(raw_inputs, |ctx| { egui::CentralPanel::default().show(ctx, |ui| { self.ui(ui); }); }); let clipped_primitives = self.egui_context.tessellate(full_output.shapes); self.egui_painter.paint_and_update_textures( egui_winit::native_pixels_per_point(&self.window), egui::Rgba::BLACK, &clipped_primitives, &full_output.textures_delta, ); self.egui_state.handle_platform_output( &self.window, &self.egui_context, full_output.platform_output, ); } }