feat: video publishing (#42)
- Prepare webrtc abstraction ( for future wasm support ) - Added track publish support for videos - Added LogoTrack example to simple_room demo - Lot of cleanup - There are compiler warnings I'll solve on our v1 release
This commit is contained in:
Generated
+566
-110
File diff suppressed because it is too large
Load Diff
@@ -16,3 +16,5 @@ parking_lot = "0.12.1"
|
||||
egui = "0.20.1"
|
||||
egui-wgpu = { version = "0.20.0", features = ["winit"] }
|
||||
egui-winit = "0.20.1"
|
||||
|
||||
image = "0.24.5"
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
use crate::events::UiCmd;
|
||||
use crate::logo_track::LogoTrack;
|
||||
use crate::video_renderer::VideoRenderer;
|
||||
use crate::{events::AsyncCmd, video_grid::VideoGrid};
|
||||
use egui::{Rounding, Stroke};
|
||||
use egui_wgpu::WgpuConfiguration;
|
||||
use image::ImageFormat;
|
||||
use livekit::options::{TrackPublishOptions, VideoCaptureOptions};
|
||||
use livekit::prelude::*;
|
||||
use livekit::webrtc::native::yuv_helper;
|
||||
use livekit::webrtc::video_frame::native::I420BufferExt;
|
||||
use livekit::webrtc::video_frame::{I420Buffer, VideoFrame, VideoRotation};
|
||||
use livekit::webrtc::video_source::native::NativeVideoSource;
|
||||
use livekit::SimulateScenario;
|
||||
use parking_lot::Mutex;
|
||||
use std::collections::HashMap;
|
||||
@@ -11,6 +18,7 @@ use std::sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc,
|
||||
};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::{mpsc, oneshot};
|
||||
|
||||
// Useful default constants for developing
|
||||
@@ -25,16 +33,22 @@ use winit::{
|
||||
window::{WindowBuilder, WindowId},
|
||||
};
|
||||
|
||||
struct Session {
|
||||
room: Room,
|
||||
logo_track: LogoTrack,
|
||||
close_tx: oneshot::Sender<()>,
|
||||
handle: tokio::task::JoinHandle<()>,
|
||||
}
|
||||
|
||||
struct AppState {
|
||||
room: Mutex<Option<Room>>,
|
||||
close_tx: Mutex<Option<oneshot::Sender<()>>>,
|
||||
session: Mutex<Option<Session>>,
|
||||
connecting: AtomicBool,
|
||||
}
|
||||
|
||||
struct App {
|
||||
state: Arc<AppState>,
|
||||
|
||||
video_renderers: HashMap<(ParticipantSid, TrackSid), VideoRenderer>,
|
||||
|
||||
egui_context: egui::Context,
|
||||
egui_state: egui_winit::State,
|
||||
egui_painter: egui_wgpu::winit::Painter,
|
||||
@@ -69,8 +83,7 @@ pub fn run(rt: tokio::runtime::Runtime) {
|
||||
let (ui_cmd_tx, ui_cmd_rx) = mpsc::unbounded_channel::<UiCmd>();
|
||||
|
||||
let state = Arc::new(AppState {
|
||||
room: Default::default(),
|
||||
close_tx: Default::default(),
|
||||
session: Default::default(),
|
||||
connecting: AtomicBool::new(false),
|
||||
});
|
||||
|
||||
@@ -94,45 +107,53 @@ pub fn run(rt: tokio::runtime::Runtime) {
|
||||
while let Some(event) = async_cmd_rx.recv().await {
|
||||
match event {
|
||||
AsyncCmd::RoomConnect { url, token } => {
|
||||
if let Some(close_tx) = state.close_tx.lock().take() {
|
||||
let _ = state.room.lock().take().unwrap().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);
|
||||
state.close_tx.lock().replace(close_tx);
|
||||
if let Ok((room, room_events)) = res {
|
||||
let (close_tx, close_rx) = oneshot::channel();
|
||||
let logo_track = LogoTrack::new(room.session());
|
||||
let handle = tokio::spawn(room_task(
|
||||
state.clone(),
|
||||
room_events,
|
||||
close_rx,
|
||||
ui_cmd_tx.clone(),
|
||||
));
|
||||
|
||||
tokio::spawn(room_task(
|
||||
state.clone(),
|
||||
room_events,
|
||||
close_rx,
|
||||
ui_cmd_tx.clone(),
|
||||
));
|
||||
*state.session.lock() = Some(Session {
|
||||
room,
|
||||
logo_track,
|
||||
close_tx,
|
||||
handle,
|
||||
});
|
||||
|
||||
let _ = ui_cmd_tx.send(UiCmd::ConnectResult { result: Ok(()) });
|
||||
}
|
||||
Err(err) => {
|
||||
let _ = ui_cmd_tx.send(UiCmd::ConnectResult { result: Err(err) });
|
||||
}
|
||||
let _ = ui_cmd_tx.send(UiCmd::ConnectResult { result: Ok(()) });
|
||||
} else if let Err(err) = res {
|
||||
let _ = ui_cmd_tx.send(UiCmd::ConnectResult { result: Err(err) });
|
||||
}
|
||||
|
||||
state.connecting.store(false, Ordering::SeqCst);
|
||||
}
|
||||
AsyncCmd::RoomDisconnect => {
|
||||
if let Some(close_tx) = state.close_tx.lock().take() {
|
||||
let _ = state.room.lock().take().unwrap().close().await;
|
||||
let _ = close_tx.send(());
|
||||
if let Some(session) = state.session.lock().take() {
|
||||
let _ = session.room.close().await;
|
||||
let _ = session.close_tx.send(());
|
||||
let _ = session.handle.await;
|
||||
}
|
||||
}
|
||||
AsyncCmd::SimulateScenario { scenario } => {
|
||||
if let Some(room) = state.room.lock().as_ref() {
|
||||
let _ = room.session().simulate_scenario(scenario).await;
|
||||
if let Some(session) = state.session.lock().as_ref() {
|
||||
let _ = session.room.session().simulate_scenario(scenario).await;
|
||||
}
|
||||
}
|
||||
AsyncCmd::ToggleLogo => {
|
||||
if let Some(session) = state.session.lock().as_mut() {
|
||||
let logo_track = &mut session.logo_track;
|
||||
if !logo_track.is_published() {
|
||||
logo_track.publish().await.unwrap();
|
||||
} else {
|
||||
logo_track.unpublish().await.unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,7 +161,7 @@ pub fn run(rt: tokio::runtime::Runtime) {
|
||||
});
|
||||
|
||||
tokio::task::block_in_place(move || loop {
|
||||
// UI/Main Thread
|
||||
// ui/main thread
|
||||
event_loop.run(move |event, _, control_flow| {
|
||||
app.update(event, control_flow);
|
||||
});
|
||||
@@ -183,7 +204,7 @@ impl App {
|
||||
track, participant, ..
|
||||
} => {
|
||||
match track.clone() {
|
||||
RemoteTrackHandle::Video(video_track) => {
|
||||
RemoteTrack::Video(video_track) => {
|
||||
// Create a new VideoRenderer
|
||||
let video_renderer = VideoRenderer::new(
|
||||
self.egui_painter.render_state().clone().unwrap(),
|
||||
@@ -192,7 +213,7 @@ impl App {
|
||||
self.video_renderers
|
||||
.insert((participant.sid(), track.sid()), video_renderer);
|
||||
}
|
||||
RemoteTrackHandle::Audio(_) => {
|
||||
RemoteTrack::Audio(_) => {
|
||||
// The demo doesn't support Audio rendering at the moment.
|
||||
}
|
||||
};
|
||||
@@ -300,6 +321,12 @@ impl App {
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
ui.menu_button("Publish", |ui| {
|
||||
if ui.button("CustomTrack - LK Logo").clicked() {
|
||||
let _ = self.cmd_tx.send(AsyncCmd::ToggleLogo);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -321,9 +348,9 @@ impl App {
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
let connecting = self.state.connecting.load(Ordering::SeqCst);
|
||||
let session = self.state.session.lock();
|
||||
|
||||
let room = self.state.room.lock();
|
||||
ui.add_enabled_ui(!connecting && room.is_none(), |ui| {
|
||||
ui.add_enabled_ui(!connecting && session.is_none(), |ui| {
|
||||
if ui.button("Connect").clicked() {
|
||||
self.connection_failure = None;
|
||||
let _ = self.cmd_tx.send(AsyncCmd::RoomConnect {
|
||||
@@ -337,7 +364,7 @@ impl App {
|
||||
ui.spinner();
|
||||
}
|
||||
|
||||
if room.is_some() {
|
||||
if session.is_some() {
|
||||
if ui.button("Disconnect").clicked() {
|
||||
let _ = self.cmd_tx.send(AsyncCmd::RoomDisconnect);
|
||||
}
|
||||
@@ -352,17 +379,16 @@ impl App {
|
||||
|
||||
{
|
||||
// Room Info
|
||||
let room = self.state.room.lock();
|
||||
if let Some(room) = room.as_ref() {
|
||||
ui.label(format!("Name: {}", room.session().name()));
|
||||
ui.label(format!("SID: {}", room.session().sid()));
|
||||
if let Some(session) = self.state.session.lock().as_ref() {
|
||||
ui.label(format!("Name: {}", session.room.session().name()));
|
||||
ui.label(format!("SID: {}", session.room.session().sid()));
|
||||
ui.label(format!(
|
||||
"ConnectionState: {:?}",
|
||||
room.session().connection_state()
|
||||
session.room.session().connection_state()
|
||||
));
|
||||
ui.label(format!(
|
||||
"ParticipantCount: {:?}",
|
||||
room.session().participants().len() + 1
|
||||
session.room.session().participants().len() + 1
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -408,12 +434,15 @@ impl App {
|
||||
);
|
||||
}
|
||||
|
||||
let name = self.state.room.lock().as_ref().and_then(|room| {
|
||||
room.session()
|
||||
.participants()
|
||||
.get(participant_sid)
|
||||
.map(|p| p.name())
|
||||
});
|
||||
let name =
|
||||
self.state.session.lock().as_ref().and_then(|session| {
|
||||
session
|
||||
.room
|
||||
.session()
|
||||
.participants()
|
||||
.get(participant_sid)
|
||||
.map(|p| p.name())
|
||||
});
|
||||
|
||||
if let Some(name) = name {
|
||||
ui.painter().text(
|
||||
|
||||
@@ -6,6 +6,7 @@ pub enum AsyncCmd {
|
||||
RoomConnect { url: String, token: String },
|
||||
RoomDisconnect,
|
||||
SimulateScenario { scenario: SimulateScenario },
|
||||
ToggleLogo, // Unpublish/Publish a logo track
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
|
||||
@@ -0,0 +1,206 @@
|
||||
use image::ImageFormat;
|
||||
use image::RgbaImage;
|
||||
use livekit::options::{TrackPublishOptions, VideoCaptureOptions};
|
||||
use livekit::prelude::*;
|
||||
use livekit::webrtc::{
|
||||
native::yuv_helper,
|
||||
video_frame::native::I420BufferExt,
|
||||
video_frame::{I420Buffer, VideoFrame, VideoRotation},
|
||||
video_source::native::NativeVideoSource,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::task::JoinHandle;
|
||||
|
||||
// The logo must not be bigger than the framebuffer
|
||||
const PIXEL_SIZE: usize = 4;
|
||||
const FRAME_RATE: u64 = 30;
|
||||
const MOVE_SPEED: i32 = 16;
|
||||
const FB_WIDTH: usize = 1280;
|
||||
const FB_HEIGHT: usize = 720;
|
||||
|
||||
#[derive(Clone)]
|
||||
struct FrameData {
|
||||
image: Arc<RgbaImage>,
|
||||
framebuffer: Arc<Mutex<Vec<u8>>>,
|
||||
video_frame: Arc<Mutex<VideoFrame<I420Buffer>>>,
|
||||
pos: (u32, u32),
|
||||
direction: (i32, i32),
|
||||
}
|
||||
|
||||
struct TrackHandle {
|
||||
close_tx: oneshot::Sender<()>,
|
||||
track: LocalVideoTrack,
|
||||
task: JoinHandle<()>,
|
||||
}
|
||||
|
||||
pub struct LogoTrack {
|
||||
rtc_source: NativeVideoSource,
|
||||
session: RoomSession,
|
||||
handle: Option<TrackHandle>,
|
||||
}
|
||||
|
||||
impl LogoTrack {
|
||||
pub fn new(session: RoomSession) -> Self {
|
||||
Self {
|
||||
rtc_source: NativeVideoSource::default(),
|
||||
session,
|
||||
handle: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_published(&self) -> bool {
|
||||
self.handle.is_some()
|
||||
}
|
||||
|
||||
pub async fn publish(&mut self) -> Result<(), RoomError> {
|
||||
self.unpublish().await;
|
||||
|
||||
let (close_tx, close_rx) = oneshot::channel();
|
||||
let track = LocalVideoTrack::create_video_track(
|
||||
"livekit_logo",
|
||||
VideoCaptureOptions::default(),
|
||||
self.rtc_source.clone(),
|
||||
);
|
||||
|
||||
let task = tokio::spawn(Self::track_task(close_rx, self.rtc_source.clone()));
|
||||
|
||||
self.session
|
||||
.local_participant()
|
||||
.publish_track(
|
||||
LocalTrack::Video(track.clone()),
|
||||
TrackPublishOptions {
|
||||
source: TrackSource::Camera,
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
let handle = TrackHandle {
|
||||
close_tx,
|
||||
task,
|
||||
track,
|
||||
};
|
||||
|
||||
self.handle = Some(handle);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn unpublish(&mut self) -> Result<(), RoomError> {
|
||||
if let Some(handle) = self.handle.take() {
|
||||
let _ = handle.close_tx.send(());
|
||||
let _ = handle.task.await;
|
||||
|
||||
self.session
|
||||
.local_participant()
|
||||
.unpublish_track(handle.track.sid(), true)
|
||||
.await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn track_task(mut close_rx: oneshot::Receiver<()>, rtc_source: NativeVideoSource) {
|
||||
let mut interval = tokio::time::interval(Duration::from_millis(1000 / FRAME_RATE));
|
||||
|
||||
let image = tokio::task::spawn_blocking(|| {
|
||||
image::load_from_memory_with_format(include_bytes!("moving-logo.png"), ImageFormat::Png)
|
||||
.unwrap()
|
||||
.to_rgba8()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut data = FrameData {
|
||||
image: Arc::new(image),
|
||||
framebuffer: Arc::new(Mutex::new(vec![0u8; (FB_WIDTH * FB_HEIGHT * 4) as usize])),
|
||||
video_frame: Arc::new(Mutex::new(VideoFrame {
|
||||
rotation: VideoRotation::VideoRotation0,
|
||||
buffer: I420Buffer::new(FB_WIDTH as u32, FB_HEIGHT as u32),
|
||||
timestamp: 0,
|
||||
})),
|
||||
pos: (0, 0),
|
||||
direction: (1, 1),
|
||||
};
|
||||
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = &mut close_rx => {
|
||||
break;
|
||||
}
|
||||
_ = interval.tick() => {}
|
||||
}
|
||||
|
||||
data.pos.0 = (data.pos.0 as i32 + data.direction.0 * MOVE_SPEED) as u32;
|
||||
data.pos.1 = (data.pos.1 as i32 + data.direction.1 * MOVE_SPEED) as u32;
|
||||
|
||||
if data.pos.0 >= (FB_WIDTH - data.image.width() as usize) as u32 {
|
||||
data.direction.0 = -1;
|
||||
} else if data.pos.0 <= 0 {
|
||||
data.direction.0 = 1;
|
||||
}
|
||||
|
||||
if data.pos.1 >= (FB_HEIGHT - data.image.height() as usize) as u32 {
|
||||
data.direction.1 = -1;
|
||||
} else if data.pos.1 <= 0 {
|
||||
data.direction.1 = 1;
|
||||
}
|
||||
|
||||
tokio::task::spawn_blocking({
|
||||
let data = data.clone();
|
||||
let source = rtc_source.clone();
|
||||
move || {
|
||||
let image = data.image.as_raw();
|
||||
let mut framebuffer = data.framebuffer.lock();
|
||||
let mut video_frame = data.video_frame.lock();
|
||||
let i420_buffer = &mut video_frame.buffer;
|
||||
|
||||
let stride_y = i420_buffer.stride_y();
|
||||
let stride_u = i420_buffer.stride_u();
|
||||
let stride_v = i420_buffer.stride_v();
|
||||
let (data_y, data_u, data_v) = i420_buffer.data_mut();
|
||||
|
||||
framebuffer.fill(0);
|
||||
for i in 0..data.image.height() as usize {
|
||||
let x = data.pos.0 as usize;
|
||||
let y = data.pos.1 as usize;
|
||||
let frame_width = data.image.width() as usize;
|
||||
let logo_stride = frame_width * PIXEL_SIZE;
|
||||
let row_start = (x + ((i + y) * FB_WIDTH)) * PIXEL_SIZE;
|
||||
let row_end = row_start + logo_stride;
|
||||
|
||||
framebuffer[row_start..row_end].copy_from_slice(
|
||||
&image[i * logo_stride..i * logo_stride + logo_stride],
|
||||
);
|
||||
}
|
||||
|
||||
yuv_helper::abgr_to_i420(
|
||||
&framebuffer,
|
||||
(FB_WIDTH * PIXEL_SIZE) as i32,
|
||||
data_y,
|
||||
stride_y,
|
||||
data_u,
|
||||
stride_u,
|
||||
data_v,
|
||||
stride_v,
|
||||
FB_WIDTH as i32,
|
||||
FB_HEIGHT as i32,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
source.capture_frame(&*video_frame);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for LogoTrack {
|
||||
fn drop(&mut self) {
|
||||
if let Some(handle) = self.handle.take() {
|
||||
let _ = handle.close_tx.send(());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
mod app;
|
||||
mod events;
|
||||
mod logo_track;
|
||||
mod video_grid;
|
||||
mod video_renderer;
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 7.0 KiB |
@@ -1,18 +1,17 @@
|
||||
use livekit::prelude::*;
|
||||
use livekit::webrtc::video_frame_buffer::*;
|
||||
use livekit::webrtc::yuv_helper;
|
||||
use std::convert::TryInto;
|
||||
use futures::StreamExt;
|
||||
use livekit::webrtc::native::yuv_helper;
|
||||
use livekit::webrtc::prelude::*;
|
||||
use livekit::webrtc::video_stream::native::NativeVideoStream;
|
||||
use std::num::NonZeroU32;
|
||||
use std::{
|
||||
ops::DerefMut,
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
use tracing::debug_span;
|
||||
use tracing::{error, warn};
|
||||
|
||||
pub struct VideoRenderer {
|
||||
internal: Arc<Mutex<RendererInternal>>,
|
||||
rtc_track: Arc<VideoTrack>,
|
||||
rtc_track: RtcVideoTrack,
|
||||
}
|
||||
|
||||
struct RendererInternal {
|
||||
@@ -86,7 +85,7 @@ impl RendererInternal {
|
||||
}
|
||||
|
||||
impl VideoRenderer {
|
||||
pub fn new(render_state: egui_wgpu::RenderState, rtc_track: Arc<VideoTrack>) -> Self {
|
||||
pub fn new(render_state: egui_wgpu::RenderState, rtc_track: RtcVideoTrack) -> Self {
|
||||
let internal = Arc::new(Mutex::new(RendererInternal {
|
||||
render_state,
|
||||
width: 0,
|
||||
@@ -97,62 +96,73 @@ impl VideoRenderer {
|
||||
egui_texture: None,
|
||||
}));
|
||||
|
||||
rtc_track.on_frame({
|
||||
let mut video_sink = NativeVideoStream::new(rtc_track.clone());
|
||||
|
||||
tokio::spawn({
|
||||
let internal = internal.clone();
|
||||
async move {
|
||||
while let Some(frame) = video_sink.next().await {
|
||||
let internal = internal.clone();
|
||||
// Process the frame
|
||||
let _ = tokio::task::spawn_blocking(move || {
|
||||
let span = debug_span!("texture_upload");
|
||||
let _enter = span.enter();
|
||||
|
||||
Box::new(move |_frame, buffer| {
|
||||
let span = debug_span!("texture_upload");
|
||||
let _enter = span.enter();
|
||||
let mut internal = internal.lock().unwrap();
|
||||
let buffer = frame.buffer.to_i420();
|
||||
|
||||
let mut internal = internal.lock().unwrap();
|
||||
let buffer = buffer.to_i420();
|
||||
let width: u32 = buffer.width().try_into().unwrap();
|
||||
let height: u32 = buffer.height().try_into().unwrap();
|
||||
|
||||
let width: u32 = buffer.width().try_into().unwrap();
|
||||
let height: u32 = buffer.height().try_into().unwrap();
|
||||
internal.ensure_texture_size(width, height);
|
||||
|
||||
internal.ensure_texture_size(width, height);
|
||||
let rgba_ptr = internal.rgba_data.deref_mut();
|
||||
let rgba_stride = buffer.width() * 4;
|
||||
|
||||
let rgba_ptr = internal.rgba_data.deref_mut();
|
||||
let rgba_stride = buffer.width() * 4;
|
||||
let (data_y, data_u, data_v) = buffer.data();
|
||||
|
||||
yuv_helper::i420_to_abgr(
|
||||
buffer.data_y(),
|
||||
buffer.stride_y(),
|
||||
buffer.data_u(),
|
||||
buffer.stride_u(),
|
||||
buffer.data_v(),
|
||||
buffer.stride_v(),
|
||||
rgba_ptr,
|
||||
rgba_stride,
|
||||
buffer.width(),
|
||||
buffer.height(),
|
||||
);
|
||||
yuv_helper::i420_to_abgr(
|
||||
data_y,
|
||||
buffer.stride_y(),
|
||||
data_u,
|
||||
buffer.stride_u(),
|
||||
data_v,
|
||||
buffer.stride_v(),
|
||||
rgba_ptr,
|
||||
rgba_stride,
|
||||
buffer.width(),
|
||||
buffer.height(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let copy_desc = wgpu::ImageCopyTexture {
|
||||
texture: internal.texture.as_ref().unwrap(),
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::default(),
|
||||
aspect: wgpu::TextureAspect::default(),
|
||||
};
|
||||
let copy_desc = wgpu::ImageCopyTexture {
|
||||
texture: internal.texture.as_ref().unwrap(),
|
||||
mip_level: 0,
|
||||
origin: wgpu::Origin3d::default(),
|
||||
aspect: wgpu::TextureAspect::default(),
|
||||
};
|
||||
|
||||
let copy_layout = wgpu::ImageDataLayout {
|
||||
bytes_per_row: Some(NonZeroU32::new(width * 4).unwrap()),
|
||||
..Default::default()
|
||||
};
|
||||
let copy_layout = wgpu::ImageDataLayout {
|
||||
bytes_per_row: Some(NonZeroU32::new(width * 4).unwrap()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let copy_size = wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
..Default::default()
|
||||
};
|
||||
let copy_size = wgpu::Extent3d {
|
||||
width,
|
||||
height,
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
internal.render_state.queue.write_texture(
|
||||
copy_desc,
|
||||
&internal.rgba_data,
|
||||
copy_layout,
|
||||
copy_size,
|
||||
);
|
||||
})
|
||||
internal.render_state.queue.write_texture(
|
||||
copy_desc,
|
||||
&internal.rgba_data,
|
||||
copy_layout,
|
||||
copy_size,
|
||||
);
|
||||
})
|
||||
.await;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
@@ -165,9 +175,3 @@ impl VideoRenderer {
|
||||
self.internal.lock().unwrap().egui_texture.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for VideoRenderer {
|
||||
fn drop(&mut self) {
|
||||
self.rtc_track.on_frame(Box::new(|_, _| {}));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user