Compare commits

...

2 Commits

Author SHA1 Message Date
Mikayla
f67815df05 better documentation for the pixel format 2024-12-17 10:56:31 -08:00
Mikayla
f2ca21ae44 Add rendering of self screen capture to test app
Fix bugs in screen share stream configuration
2024-12-15 22:47:39 -08:00
11 changed files with 84 additions and 41 deletions

View File

@@ -42,10 +42,10 @@ serde_derive.workspace = true
settings.workspace = true settings.workspace = true
util.workspace = true util.workspace = true
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(any())'.dependencies]
livekit_client_macos = { workspace = true } livekit_client_macos = { workspace = true }
[target.'cfg(not(target_os = "macos"))'.dependencies] [target.'cfg(all())'.dependencies]
livekit_client = { workspace = true } livekit_client = { workspace = true }
[dev-dependencies] [dev-dependencies]

View File

@@ -1,13 +1,13 @@
pub mod call_settings; pub mod call_settings;
#[cfg(target_os = "macos")] #[cfg(any())]
mod macos; mod macos;
#[cfg(target_os = "macos")] #[cfg(any())]
pub use macos::*; pub use macos::*;
#[cfg(not(target_os = "macos"))] #[cfg(all())]
mod cross_platform; mod cross_platform;
#[cfg(not(target_os = "macos"))] #[cfg(all())]
pub use cross_platform::*; pub use cross_platform::*;

View File

@@ -1441,7 +1441,7 @@ impl Room {
let sources = sources.await??; let sources = sources.await??;
let source = sources.first().ok_or_else(|| anyhow!("no display found"))?; let source = sources.first().ok_or_else(|| anyhow!("no display found"))?;
let (track, stream) = capture_local_video_track(&**source).await?; let (track, stream) = capture_local_video_track(&**source, None).await?;
let publication = participant let publication = participant
.publish_track( .publish_track(

View File

@@ -2078,17 +2078,7 @@ async fn test_mute_deafen(
audio_tracks_playing: participant audio_tracks_playing: participant
.audio_tracks .audio_tracks
.values() .values()
.map({ .map(|(track, _)| track.rtc_track().enabled())
#[cfg(target_os = "macos")]
{
|track| track.is_playing()
}
#[cfg(not(target_os = "macos"))]
{
|(track, _)| track.rtc_track().enabled()
}
})
.collect(), .collect(),
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()

View File

@@ -239,7 +239,7 @@ pub trait PlatformDisplay: Send + Sync + Debug {
/// A source of on-screen video content that can be captured. /// A source of on-screen video content that can be captured.
pub trait ScreenCaptureSource { pub trait ScreenCaptureSource {
/// Returns the video resolution of this source. /// Returns the video resolution of this source.
fn resolution(&self) -> Result<Size<Pixels>>; fn resolution(&self) -> Size<DevicePixels>;
/// Start capture video from this source, invoking the given callback /// Start capture video from this source, invoking the given callback
/// with each frame. /// with each frame.
@@ -253,6 +253,7 @@ pub trait ScreenCaptureSource {
pub trait ScreenCaptureStream {} pub trait ScreenCaptureStream {}
/// A frame of video captured from a screen. /// A frame of video captured from a screen.
#[derive(Clone)]
pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame); pub struct ScreenCaptureFrame(pub PlatformScreenCaptureFrame);
/// An opaque identifier for a hardware display /// An opaque identifier for a hardware display

View File

@@ -1,6 +1,6 @@
use crate::{ use crate::{
platform::{ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream}, platform::{ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream},
px, size, Pixels, Size, size, DevicePixels, Size,
}; };
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use block::ConcreteBlock; use block::ConcreteBlock;
@@ -9,6 +9,10 @@ use cocoa::{
foundation::NSArray, foundation::NSArray,
}; };
use core_foundation::base::TCFType; use core_foundation::base::TCFType;
use core_graphics::display::{
CGDirectDisplayID, CGDisplayCopyDisplayMode, CGDisplayModeGetPixelHeight,
CGDisplayModeGetPixelWidth, CGDisplayModeRelease,
};
use ctor::ctor; use ctor::ctor;
use futures::channel::oneshot; use futures::channel::oneshot;
use media::core_media::{CMSampleBuffer, CMSampleBufferRef}; use media::core_media::{CMSampleBuffer, CMSampleBufferRef};
@@ -25,6 +29,7 @@ use std::{cell::RefCell, ffi::c_void, mem, ptr, rc::Rc};
#[derive(Clone)] #[derive(Clone)]
pub struct MacScreenCaptureSource { pub struct MacScreenCaptureSource {
sc_display: id, sc_display: id,
size: Size<DevicePixels>,
} }
pub struct MacScreenCaptureStream { pub struct MacScreenCaptureStream {
@@ -43,12 +48,8 @@ const FRAME_CALLBACK_IVAR: &str = "frame_callback";
const SCStreamOutputTypeScreen: NSInteger = 0; const SCStreamOutputTypeScreen: NSInteger = 0;
impl ScreenCaptureSource for MacScreenCaptureSource { impl ScreenCaptureSource for MacScreenCaptureSource {
fn resolution(&self) -> Result<Size<Pixels>> { fn resolution(&self) -> Size<DevicePixels> {
unsafe { self.size
let width: i64 = msg_send![self.sc_display, width];
let height: i64 = msg_send![self.sc_display, height];
Ok(size(px(width as f32), px(height as f32)))
}
} }
fn stream( fn stream(
@@ -61,13 +62,21 @@ impl ScreenCaptureSource for MacScreenCaptureSource {
let configuration: id = msg_send![class!(SCStreamConfiguration), alloc]; let configuration: id = msg_send![class!(SCStreamConfiguration), alloc];
let delegate: id = msg_send![DELEGATE_CLASS, alloc]; let delegate: id = msg_send![DELEGATE_CLASS, alloc];
let output: id = msg_send![OUTPUT_CLASS, alloc]; let output: id = msg_send![OUTPUT_CLASS, alloc];
let excluded_windows = NSArray::array(nil); let excluded_windows = NSArray::array(nil);
let filter: id = msg_send![filter, initWithDisplay:self.sc_display excludingWindows:excluded_windows]; let filter: id = msg_send![filter, initWithDisplay:self.sc_display excludingWindows:excluded_windows];
let configuration: id = msg_send![configuration, init]; let configuration: id = msg_send![configuration, init];
let delegate: id = msg_send![delegate, init]; let delegate: id = msg_send![delegate, init];
let output: id = msg_send![output, init]; let output: id = msg_send![output, init];
// ASCII for '420f': https://developer.apple.com/documentation/screencapturekit/scstreamconfiguration/pixelformat?language=objc
let format = u32::from_be_bytes([52u8, 50u8, 48u8, 102u8]);
let _: () = msg_send![configuration, setShowsCursor:YES];
let _: () = msg_send![configuration, setWidth:self.size.width];
let _: () = msg_send![configuration, setHeight:self.size.height];
let _: () = msg_send![configuration, setPixelFormat:format];
let _: () = msg_send![configuration, setQueueDepth:5i32];
output.as_mut().unwrap().set_ivar( output.as_mut().unwrap().set_ivar(
FRAME_CALLBACK_IVAR, FRAME_CALLBACK_IVAR,
Box::into_raw(Box::new(frame_callback)) as *mut c_void, Box::into_raw(Box::new(frame_callback)) as *mut c_void,
@@ -94,6 +103,7 @@ impl ScreenCaptureSource for MacScreenCaptureSource {
sc_stream: stream, sc_stream: stream,
sc_stream_output: output, sc_stream_output: output,
}; };
Ok(Box::new(stream) as Box<dyn ScreenCaptureStream>) Ok(Box::new(stream) as Box<dyn ScreenCaptureStream>)
} else { } else {
let message: id = msg_send![error, localizedDescription]; let message: id = msg_send![error, localizedDescription];
@@ -159,8 +169,16 @@ pub(crate) fn get_sources() -> oneshot::Receiver<Result<Vec<Box<dyn ScreenCaptur
let mut result = Vec::new(); let mut result = Vec::new();
for i in 0..displays.count() { for i in 0..displays.count() {
let display = displays.objectAtIndex(i); let display = displays.objectAtIndex(i);
let display: id = msg_send![display, retain];
let display_id: CGDirectDisplayID = msg_send![display, displayID];
let display_mode_ref = CGDisplayCopyDisplayMode(display_id);
let width = CGDisplayModeGetPixelWidth(display_mode_ref);
let height = CGDisplayModeGetPixelHeight(display_mode_ref);
CGDisplayModeRelease(display_mode_ref);
let source = MacScreenCaptureSource { let source = MacScreenCaptureSource {
sc_display: msg_send![display, retain], sc_display: display,
size: size(DevicePixels(width as i32), DevicePixels(height as i32)),
}; };
result.push(Box::new(source) as Box<dyn ScreenCaptureSource>); result.push(Box::new(source) as Box<dyn ScreenCaptureSource>);
} }

View File

@@ -1,7 +1,8 @@
use crate::{ use crate::{
px, size, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, ForegroundExecutor, size, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
Keymap, Platform, PlatformDisplay, PlatformTextSystem, ScreenCaptureFrame, ScreenCaptureSource, ForegroundExecutor, Keymap, Platform, PlatformDisplay, PlatformTextSystem, ScreenCaptureFrame,
ScreenCaptureStream, Task, TestDisplay, TestWindow, WindowAppearance, WindowParams, ScreenCaptureSource, ScreenCaptureStream, Task, TestDisplay, TestWindow, WindowAppearance,
WindowParams,
}; };
use anyhow::Result; use anyhow::Result;
use collections::VecDeque; use collections::VecDeque;
@@ -46,8 +47,8 @@ pub struct TestScreenCaptureSource {}
pub struct TestScreenCaptureStream {} pub struct TestScreenCaptureStream {}
impl ScreenCaptureSource for TestScreenCaptureSource { impl ScreenCaptureSource for TestScreenCaptureSource {
fn resolution(&self) -> Result<crate::Size<crate::Pixels>> { fn resolution(&self) -> crate::Size<crate::DevicePixels> {
Ok(size(px(1.), px(1.))) size(DevicePixels(1), DevicePixels(1))
} }
fn stream( fn stream(

View File

@@ -49,6 +49,7 @@ livekit.workspace = true
[target.'cfg(target_os = "macos")'.dependencies] [target.'cfg(target_os = "macos")'.dependencies]
core-foundation.workspace = true core-foundation.workspace = true
coreaudio-rs = "0.12.1" coreaudio-rs = "0.12.1"
media.workspace = true
[dev-dependencies] [dev-dependencies]
collections = { workspace = true, features = ["test-support"] } collections = { workspace = true, features = ["test-support"] }

View File

@@ -3,11 +3,12 @@
// it causes compile errors. // it causes compile errors.
#![cfg_attr(target_os = "macos", allow(unused_imports))] #![cfg_attr(target_os = "macos", allow(unused_imports))]
use futures::StreamExt;
use gpui::{ use gpui::{
actions, bounds, div, point, actions, bounds, div, point,
prelude::{FluentBuilder as _, IntoElement}, prelude::{FluentBuilder as _, IntoElement},
px, rgb, size, AsyncAppContext, Bounds, InteractiveElement, KeyBinding, Menu, MenuItem, px, rgb, size, AsyncAppContext, Bounds, InteractiveElement, KeyBinding, Menu, MenuItem,
ParentElement, Pixels, Render, ScreenCaptureStream, SharedString, ParentElement, Pixels, Render, ScreenCaptureFrame, ScreenCaptureStream, SharedString,
StatefulInteractiveElement as _, Styled, Task, View, ViewContext, VisualContext, WindowBounds, StatefulInteractiveElement as _, Styled, Task, View, ViewContext, VisualContext, WindowBounds,
WindowHandle, WindowOptions, WindowHandle, WindowOptions,
}; };
@@ -22,6 +23,7 @@ use livekit_client::{
track::{LocalTrack, RemoteTrack, RemoteVideoTrack, TrackSource}, track::{LocalTrack, RemoteTrack, RemoteVideoTrack, TrackSource},
AudioStream, RemoteVideoTrackView, Room, RoomEvent, RoomOptions, AudioStream, RemoteVideoTrackView, Room, RoomEvent, RoomOptions,
}; };
use media::core_video::CVImageBuffer;
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
use postage::stream::Stream; use postage::stream::Stream;
@@ -108,6 +110,7 @@ struct LivekitWindow {
screen_share_track: Option<LocalTrackPublication>, screen_share_track: Option<LocalTrackPublication>,
microphone_stream: Option<AudioStream>, microphone_stream: Option<AudioStream>,
screen_share_stream: Option<Box<dyn ScreenCaptureStream>>, screen_share_stream: Option<Box<dyn ScreenCaptureStream>>,
latest_self_frame: Option<ScreenCaptureFrame>,
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
remote_participants: Vec<(ParticipantIdentity, ParticipantState)>, remote_participants: Vec<(ParticipantIdentity, ParticipantState)>,
_events_task: Task<()>, _events_task: Task<()>,
@@ -156,6 +159,7 @@ impl LivekitWindow {
microphone_stream: None, microphone_stream: None,
screen_share_track: None, screen_share_track: None,
screen_share_stream: None, screen_share_stream: None,
latest_self_frame: None,
remote_participants: Vec::new(), remote_participants: Vec::new(),
_events_task, _events_task,
} }
@@ -312,7 +316,25 @@ impl LivekitWindow {
cx.spawn(|this, mut cx| async move { cx.spawn(|this, mut cx| async move {
let sources = sources.await.unwrap()?; let sources = sources.await.unwrap()?;
let source = sources.into_iter().next().unwrap(); let source = sources.into_iter().next().unwrap();
let (track, stream) = capture_local_video_track(&*source).await?;
let (self_stream_tx, mut self_stream_rx) = futures::channel::mpsc::unbounded();
let (track, stream) =
capture_local_video_track(&*source, Some(self_stream_tx)).await?;
cx.spawn({
let this = this.clone();
|mut cx| async move {
while let Some(frame) = self_stream_rx.next().await {
this.update(&mut cx, |this, cx| {
this.latest_self_frame = Some(frame);
cx.notify();
})
.ok();
}
}
})
.detach();
let publication = participant let publication = participant
.publish_track( .publish_track(
LocalTrack::Video(track), LocalTrack::Video(track),
@@ -394,6 +416,11 @@ impl Render for LivekitWindow {
.on_click(cx.listener(|this, _, cx| this.toggle_screen_share(cx))), .on_click(cx.listener(|this, _, cx| this.toggle_screen_share(cx))),
]), ]),
) )
.children(
self.latest_self_frame
.as_ref()
.map(|frame| gpui::surface(frame.0.clone()).size_full()),
)
.child( .child(
div() div()
.id("remote-participants") .id("remote-participants")
@@ -403,7 +430,7 @@ impl Render for LivekitWindow {
.flex_grow() .flex_grow()
.children(self.remote_participants.iter().map(|(identity, state)| { .children(self.remote_participants.iter().map(|(identity, state)| {
div() div()
.h(px(300.0)) .size_full()
.flex() .flex()
.flex_col() .flex_col()
.m_2() .m_2()

View File

@@ -142,8 +142,9 @@ pub fn init(
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
pub async fn capture_local_video_track( pub async fn capture_local_video_track(
capture_source: &dyn ScreenCaptureSource, capture_source: &dyn ScreenCaptureSource,
show_capture: Option<futures::channel::mpsc::UnboundedSender<ScreenCaptureFrame>>,
) -> Result<(track::LocalVideoTrack, Box<dyn ScreenCaptureStream>)> { ) -> Result<(track::LocalVideoTrack, Box<dyn ScreenCaptureStream>)> {
let resolution = capture_source.resolution()?; let resolution = capture_source.resolution();
let track_source = NativeVideoSource::new(VideoResolution { let track_source = NativeVideoSource::new(VideoResolution {
width: resolution.width.0 as u32, width: resolution.width.0 as u32,
height: resolution.height.0 as u32, height: resolution.height.0 as u32,
@@ -153,6 +154,10 @@ pub async fn capture_local_video_track(
.stream({ .stream({
let track_source = track_source.clone(); let track_source = track_source.clone();
Box::new(move |frame| { Box::new(move |frame| {
if let Some(show_capture) = show_capture.as_ref() {
show_capture.unbounded_send(frame.clone()).unwrap();
}
if let Some(buffer) = video_frame_buffer_to_webrtc(frame) { if let Some(buffer) = video_frame_buffer_to_webrtc(frame) {
track_source.capture_frame(&VideoFrame { track_source.capture_frame(&VideoFrame {
rotation: VideoRotation::VideoRotation0, rotation: VideoRotation::VideoRotation0,

View File

@@ -1,11 +1,11 @@
#[cfg(target_os = "macos")] #[cfg(any())]
mod macos; mod macos;
#[cfg(target_os = "macos")] #[cfg(any())]
pub use macos::*; pub use macos::*;
#[cfg(not(target_os = "macos"))] #[cfg(all())]
mod cross_platform; mod cross_platform;
#[cfg(not(target_os = "macos"))] #[cfg(all())]
pub use cross_platform::*; pub use cross_platform::*;