[MIRA-137] Select Display (#34)

This commit is contained in:
Mark Wang
2023-07-07 18:29:55 -04:00
committed by GitHub
parent 8ce9400beb
commit bd2f4f5370
24 changed files with 843 additions and 311 deletions
Generated
+8 -1
View File
@@ -2302,7 +2302,7 @@ dependencies = [
"libc",
"libloading 0.7.4",
"thiserror",
"widestring",
"widestring 0.5.1",
"winapi",
]
@@ -3119,6 +3119,7 @@ dependencies = [
"url",
"uuid",
"webrtc",
"widestring 1.0.2",
"windows 0.48.0",
]
@@ -5921,6 +5922,12 @@ version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17882f045410753661207383517a6f62ec3dbeb6a4ed2acce01f0728238d1983"
[[package]]
name = "widestring"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8"
[[package]]
name = "winapi"
version = "0.3.9"
+3
View File
@@ -41,6 +41,7 @@ apple-sys = { version = "0.2.0", features = ["AVFAudio", "CoreMedia", "ScreenCap
objc = "0.2.3"
[target.'cfg(target_os = "windows")'.dependencies]
widestring = "1.0.2"
[dependencies.windows]
version = "0.48.0"
features = [
@@ -66,6 +67,8 @@ features = [
"Graphics_DirectX_Direct3D11",
"Graphics_Imaging",
"Win32_System_WinRT_Direct3D11",
"Win32_Devices",
"Win32_Devices_Display",
]
[build-dependencies]
+107 -64
View File
@@ -4,13 +4,15 @@ use clap::Parser;
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
use crate::capture::{Display, DisplayInfo, ScreenCapture, ScreenCaptureImpl};
#[allow(unused_imports)]
use crate::capture::audio::AudioCapture;
use crate::capture::display::DisplaySelector;
use crate::capture::{ScreenCapture, ScreenCaptureImpl};
use crate::config::Config;
use crate::encoder;
use crate::inputs::InputHandler;
use crate::output::{FileOutput, OutputSink, WebRTCOutput};
use crate::performance_profiler::PerformanceProfiler;
use crate::result::Result;
use crate::signaller::{Signaller, WebSocketSignaller};
#[derive(Parser, Debug, Clone)]
@@ -39,16 +41,18 @@ pub struct Capturer {
shutdown_token_opt: Option<CancellationToken>,
signaller: Arc<Mutex<Option<Arc<dyn Signaller + Send + Sync>>>>,
notify_update: Arc<dyn Fn() + Send + Sync>,
capture: Arc<Mutex<ScreenCaptureImpl>>,
}
impl Capturer {
pub fn new(args: Args, config: Config, notify_update: Arc<dyn Fn() + Send + Sync>) -> Self {
Self {
args,
config,
config: config.clone(),
shutdown_token_opt: None,
signaller: Arc::new(Mutex::new(None)),
notify_update,
capture: Arc::new(Mutex::new(ScreenCaptureImpl::new(config.clone()).unwrap())),
}
}
@@ -57,24 +61,8 @@ impl Capturer {
let config = self.config.clone();
let shutdown_token = CancellationToken::new();
self.shutdown_token_opt = Some(shutdown_token.clone());
let signaller_opt = self.signaller.clone();
let notify_update = self.notify_update.clone();
tokio::spawn(async move {
let signaller_url = config.signaller_url.clone();
let signaller = Arc::new(
WebSocketSignaller::new(&signaller_url, notify_update)
.await
.unwrap(),
);
signaller_opt.lock().await.replace(signaller.clone());
tokio::select! {
_ = start_capture(args, config, signaller, shutdown_token.clone()) => {}
_ = shutdown_token.cancelled() => {}
}
});
self.shutdown_token_opt.replace(shutdown_token.clone());
self.capture(args, config, shutdown_token.clone());
}
pub fn shutdown(&mut self) {
@@ -97,48 +85,103 @@ impl Capturer {
}
pub fn get_room_id(&self) -> Option<String> {
self.signaller
.clone()
.try_lock()
.unwrap() // TODO:Fix
.as_ref()
.map_or(None, |s| s.get_room_id())
match self.signaller.try_lock() {
Ok(signaller) => signaller.as_ref().map(|s| s.get_room_id()).flatten(),
Err(e) => {
error!("Failed to get room id: {}", e);
None
}
}
}
pub fn available_displays(&self) -> Vec<<ScreenCaptureImpl as DisplaySelector>::Display> {
match self.capture.try_lock() {
Ok(mut capturer) => capturer.available_displays().unwrap(),
Err(e) => {
error!("Failed to get available displays: {}", e);
Vec::new()
}
}
}
pub fn selected_display(&self) -> Option<<ScreenCaptureImpl as DisplaySelector>::Display> {
match self.capture.try_lock() {
Ok(capturer) => capturer.selected_display().unwrap(),
Err(e) => {
error!("Failed to get selected display: {}", e);
None
}
}
}
pub fn select_display(&self, display: <ScreenCaptureImpl as DisplaySelector>::Display) {
match self.capture.try_lock() {
Ok(mut capturer) => capturer.select_display(&display).unwrap(),
Err(e) => {
error!("Failed to select display: {}", e);
}
}
}
fn capture(
&mut self,
args: Args,
config: Config,
#[allow(unused_variables)] shutdown_token: CancellationToken,
) {
let profiler = PerformanceProfiler::new(args.profiler, config.max_fps);
let signaller_opt = self.signaller.clone();
let notify_update = self.notify_update.clone();
let capture = self.capture.clone();
tokio::spawn(async move {
{
let mut capture = capture.lock().await;
let signaller_url = config.signaller_url.clone();
let signaller = Arc::new(
WebSocketSignaller::new(&signaller_url, notify_update.clone())
.await
.unwrap(),
);
signaller_opt.lock().await.replace(signaller.clone());
let resolution = capture.display().resolution();
let mut encoder =
encoder::FfmpegEncoder::new(resolution.0, resolution.1, &config.encoder);
let input_handler = Arc::new(InputHandler::new(
args.disable_control,
capture.display().dpi_conversion_factor(),
));
let output: Arc<Mutex<dyn OutputSink + Send>> = if let Some(path) = args.file {
Arc::new(Mutex::new(FileOutput::new(&path)))
} else {
WebRTCOutput::new(
signaller,
&mut encoder.force_idr,
input_handler.clone(),
&config,
)
.await
.unwrap()
};
#[cfg(target_os = "windows")]
AudioCapture::capture(output.clone(), shutdown_token.clone()).unwrap();
capture
.start_capture(encoder, output, profiler, shutdown_token.clone())
.await
.unwrap();
}
notify_update(); // Update when capture starts
shutdown_token.cancelled().await;
// Cleanup
capture.lock().await.stop_capture().await.unwrap();
signaller_opt.lock().await.take();
notify_update(); // Update when capture stops
});
}
}
async fn start_capture(
args: Args,
config: Config,
signaller: Arc<dyn Signaller + Send + Sync>,
#[allow(unused_variables)] shutdown_token: CancellationToken,
) -> Result<()> {
let display = Display::online().unwrap()[args.display].select()?;
let dpi_conversion_factor = display.dpi_conversion_factor();
let profiler = PerformanceProfiler::new(args.profiler, config.max_fps);
let resolution = display.resolution();
let mut capture = ScreenCaptureImpl::new(display, &config)?;
let mut encoder = encoder::FfmpegEncoder::new(resolution.0, resolution.1, &config.encoder);
let input_handler = Arc::new(InputHandler::new(
args.disable_control,
dpi_conversion_factor,
));
info!("Resolution: {:?}", resolution);
let output: Arc<Mutex<dyn OutputSink + Send>> = if let Some(path) = args.file {
Arc::new(Mutex::new(FileOutput::new(&path)))
} else {
WebRTCOutput::new(
signaller,
&mut encoder.force_idr,
input_handler.clone(),
&config,
)
.await?
};
#[cfg(target_os = "windows")]
AudioCapture::capture(output.clone(), shutdown_token)?;
capture.capture(encoder, output, profiler).await?;
Ok(())
}
+11
View File
@@ -0,0 +1,11 @@
use crate::result::Result;
pub trait DisplaySelector {
type Display: ToString + Eq + Send;
fn available_displays(&mut self) -> Result<Vec<Self::Display>>;
fn select_display(&mut self, display: &Self::Display) -> Result<()>;
fn selected_display(&self) -> Result<Option<Self::Display>>;
}
+75 -40
View File
@@ -1,58 +1,93 @@
use anyhow::format_err;
use apple_sys::CoreMedia::{
CGDisplayCopyDisplayMode, CGDisplayModeGetPixelHeight, CGDisplayModeGetPixelWidth,
CGDisplayPixelsHigh, CGError_kCGErrorSuccess, CGGetOnlineDisplayList,
use apple_sys::ScreenCaptureKit::{
INSDictionary, INSNumber, INSScreen, ISCDisplay, NSDictionary, NSNumber, NSScreen, NSString,
NSString_NSStringDeprecated, SCDisplay,
};
use crate::capture::macos::ffi::{from_nsarray, from_nsstring, FromNSArray};
use crate::capture::DisplayInfo;
use crate::result::Result;
#[derive(PartialEq, Eq, Debug, Clone, Copy)]
#[repr(C)]
pub struct Display(u32);
#[derive(Clone, Debug)]
pub struct Display {
pub sc_display: SCDisplay,
scale_factor: usize,
name: String,
}
unsafe impl Send for Display {}
// TODO
impl Display {
pub fn online() -> Result<Vec<Display>> {
unsafe {
let mut displays = Vec::with_capacity(16);
let mut len: u32 = 0;
#[allow(non_upper_case_globals)]
match CGGetOnlineDisplayList(16, displays.as_mut_ptr(), &mut len) {
CGError_kCGErrorSuccess => (),
x => return Err(format_err!("CGGetOnlineDisplayList failed: {:?}", x)),
}
displays.set_len(len as usize);
Ok(displays.iter().map(|it| Display(*it)).collect())
pub fn new(sc_display: SCDisplay) -> Self {
let ns_screen = unsafe { try_get_ns_screen(sc_display) };
let scale_factor = ns_screen
.as_ref()
.map(|screen| unsafe { screen.backingScaleFactor() as usize })
.unwrap_or(2);
Self {
sc_display,
scale_factor,
name: unsafe { get_name(sc_display, scale_factor, ns_screen) },
}
}
}
pub fn select(self) -> Result<Self> {
Ok(self)
}
pub fn id(self) -> u32 {
self.0
}
pub fn width(self) -> usize {
unsafe { CGDisplayModeGetPixelWidth(CGDisplayCopyDisplayMode(self.id())) }
}
pub fn height(self) -> usize {
// unsafe { CGDisplayPixelsHigh(self.id()) }
unsafe { CGDisplayModeGetPixelHeight(CGDisplayCopyDisplayMode(self.id())) }
impl ToString for Display {
fn to_string(&self) -> String {
self.name.clone()
}
}
impl PartialEq<Self> for Display {
fn eq(&self, other: &Self) -> bool {
unsafe { self.sc_display.displayID() == other.sc_display.displayID() }
}
}
impl Eq for Display {}
impl DisplayInfo for Display {
fn resolution(&self) -> (u32, u32) {
(self.width() as u32, self.height() as u32)
unsafe {
(
self.sc_display.width() as u32 * self.scale_factor as u32,
self.sc_display.height() as u32 * self.scale_factor as u32,
)
}
}
fn dpi_conversion_factor(&self) -> f64 {
self.height() as f64 / unsafe { CGDisplayPixelsHigh(self.id()) } as f64
self.scale_factor as f64
}
}
unsafe fn try_get_ns_screen(display: SCDisplay) -> Option<NSScreen> {
from_nsarray!(NSScreen, NSScreen::screens())
.iter()
.find_map(|screen| {
let screen_dictionary = screen.deviceDescription();
if screen_dictionary.0.is_null() {
return None;
}
let screen_id = NSNumber(
<NSDictionary as INSDictionary<NSString, NSNumber>>::objectForKey_(
&screen_dictionary,
NSString::alloc().initWithCString_(b"NSScreenNumber\0".as_ptr() as *const _),
),
);
if screen_id.unsignedIntValue() == display.displayID() {
Some(screen.clone())
} else {
None
}
})
}
unsafe fn get_name(display: SCDisplay, scale_factor: usize, ns_screen: Option<NSScreen>) -> String {
let id = display.displayID();
let width = display.width() as usize * scale_factor;
let height = display.height() as usize * scale_factor;
let name = ns_screen
.map(|screen| from_nsstring!(screen.localizedName()).to_string())
.unwrap_or(format!("Display {}", id));
format!("{} ({} x {})", name, width, height)
}
+2 -1
View File
@@ -1,5 +1,5 @@
use apple_sys::ScreenCaptureKit::{
INSArray, NSArray, NSArray_NSExtendedArray, SCDisplay, SCRunningApplication, SCWindow,
INSArray, NSArray, NSArray_NSExtendedArray, NSScreen, SCDisplay, SCRunningApplication, SCWindow,
};
#[macro_export]
@@ -75,3 +75,4 @@ macro_rules! impl_from_to_nsarray_for {
impl_from_to_nsarray_for!(SCRunningApplication);
impl_from_to_nsarray_for!(SCDisplay);
impl_from_to_nsarray_for!(SCWindow);
impl_from_to_nsarray_for!(NSScreen);
+118 -78
View File
@@ -5,105 +5,145 @@ use ac_ffmpeg::codec::audio::{AudioEncoder, AudioFrameMut, ChannelLayout};
use ac_ffmpeg::codec::Encoder;
use async_trait::async_trait;
use bytes::Bytes;
use tokio::select;
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
use crate::capture::display::DisplaySelector;
use crate::capture::macos::pcm_buffer::PCMBuffer;
use crate::capture::macos::screen_recorder::ScreenRecorder;
use crate::capture::Display;
use crate::capture::{DisplayInfo, ScreenCaptureImpl, YUVFrame};
use crate::config::Config;
use crate::encoder::{FfmpegEncoder, FrameData};
use crate::performance_profiler::PerformanceProfiler;
use crate::result::Result;
use crate::{OutputSink, ScreenCapture};
pub struct MacOSCapture<'a> {
config: &'a Config,
}
unsafe impl Send for MacOSCapture<'_> {}
pub type GraphicsCaptureItem = Display;
impl<'a> MacOSCapture<'a> {
pub fn new(_display: GraphicsCaptureItem, config: &'a Config) -> Result<Self> {
// TODO select display
// TODO hot-update config
Ok(Self { config })
}
pub struct MacOSCapture {
config: Config,
recorder: ScreenRecorder,
}
#[async_trait]
impl ScreenCapture for MacOSCapture<'_> {
async fn capture(
impl ScreenCapture for MacOSCapture {
fn new(config: Config) -> Result<ScreenCaptureImpl> {
// TODO hot-reload config
let mut recorder = ScreenRecorder::new();
recorder.set_max_fps(config.max_fps as u8);
recorder.monitor_available_content();
Ok(Self { config, recorder })
}
fn display(&self) -> &dyn DisplayInfo {
&self.recorder
}
async fn start_capture(
&mut self,
mut encoder: FfmpegEncoder,
output: Arc<Mutex<impl OutputSink + Send + ?Sized>>,
mut profiler: PerformanceProfiler,
shutdown_token: CancellationToken,
) -> Result<()> {
let (video_tx, mut video_rx) = tokio::sync::mpsc::channel(1);
let (audio_tx, mut audio_rx) = tokio::sync::mpsc::channel(1);
let mut audio_encoder = None;
let mut recorder = ScreenRecorder::new();
recorder.set_max_fps(self.config.max_fps as u8);
recorder.start(video_tx, audio_tx).await;
let output_audio_clone = output.clone();
tokio::spawn(async move {
while let Some(pcm_buffer) = audio_rx.recv().await {
let audio_encoder = audio_encoder.get_or_insert_with(|| {
AudioEncoder::builder("libopus")
.unwrap()
.sample_rate(pcm_buffer.sample_rate as _)
.channel_layout(
ChannelLayout::from_channels(pcm_buffer.channels as u32).unwrap(),
)
.sample_format(pcm_buffer.sample_format())
.set_option("frame_duration", pcm_buffer.frame_duration)
.build()
.unwrap()
});
let mut audio_frame = AudioFrameMut::silence(
audio_encoder.codec_parameters().channel_layout(),
audio_encoder.codec_parameters().sample_format(),
audio_encoder.codec_parameters().sample_rate(),
pcm_buffer.sample_size,
);
pcm_buffer.write_samples_into(&mut audio_frame);
audio_encoder.push(audio_frame.freeze()).unwrap();
let mut ret: Vec<u8> = Vec::new();
while let Some(packet) = audio_encoder.take().unwrap() {
ret.extend(packet.data());
}
output_audio_clone
.lock()
.await
.write_audio(
Bytes::from(ret),
Duration::from_millis(pcm_buffer.frame_duration as u64),
)
.await
.unwrap();
}
});
let (video_tx, mut video_rx) = tokio::sync::mpsc::channel::<YUVFrame>(1);
let (audio_tx, mut audio_rx) = tokio::sync::mpsc::channel::<PCMBuffer>(1);
let output_audio = output.clone();
let mut ticker =
tokio::time::interval(Duration::from_millis((1000 / self.config.max_fps) as u64));
while let Some(frame) = video_rx.recv().await {
let frame_time = frame.display_time as f64;
profiler.accept_frame(frame_time as i64);
profiler.done_preprocessing();
let encoded = encoder
.encode(FrameData::NV12(&frame), frame_time as i64)
.unwrap();
let encoded_len = encoded.len();
profiler.done_encoding();
output.lock().await.write(encoded).await.unwrap();
profiler.done_processing(encoded_len);
ticker.tick().await;
}
let cancel_audio = shutdown_token.clone();
tokio::spawn(async move {
let mut audio_encoder_opt = None;
loop {
select! {
Some(pcm_buffer) = audio_rx.recv() => {
let audio_encoder = audio_encoder_opt.get_or_insert_with(|| {
AudioEncoder::builder("libopus")
.unwrap()
.sample_rate(pcm_buffer.sample_rate as _)
.channel_layout(
ChannelLayout::from_channels(pcm_buffer.channels as u32).unwrap(),
)
.sample_format(pcm_buffer.sample_format())
.set_option("frame_duration", pcm_buffer.frame_duration)
.build()
.unwrap()
});
let mut audio_frame = AudioFrameMut::silence(
audio_encoder.codec_parameters().channel_layout(),
audio_encoder.codec_parameters().sample_format(),
audio_encoder.codec_parameters().sample_rate(),
pcm_buffer.sample_size,
);
pcm_buffer.write_samples_into(&mut audio_frame);
audio_encoder.push(audio_frame.freeze()).unwrap();
let mut ret: Vec<u8> = Vec::new();
while let Some(packet) = audio_encoder.take().unwrap() {
ret.extend(packet.data());
}
output_audio
.lock()
.await
.write_audio(
Bytes::from(ret),
Duration::from_millis(pcm_buffer.frame_duration as u64),
)
.await
.unwrap();
}
_ = cancel_audio.cancelled() => {}
}
}
});
let cancel_video = shutdown_token.clone();
tokio::spawn(async move {
loop {
select! {
Some(frame) = video_rx.recv() => {
let frame_time = frame.display_time as f64;
profiler.accept_frame(frame_time as i64);
profiler.done_preprocessing();
let encoded = encoder
.encode(FrameData::NV12(&frame), frame_time as i64)
.unwrap();
let encoded_len = encoded.len();
profiler.done_encoding();
output.lock().await.write(encoded).await.unwrap();
profiler.done_processing(encoded_len);
ticker.tick().await;
}
_ = cancel_video.cancelled() => {}
}
}
});
self.recorder.start(video_tx, audio_tx);
Ok(())
}
async fn stop_capture(&mut self) -> Result<()> {
self.recorder.stop();
Ok(())
}
}
impl DisplaySelector for MacOSCapture {
type Display = <ScreenRecorder as DisplaySelector>::Display;
fn available_displays(&mut self) -> Result<Vec<Self::Display>> {
self.recorder.available_displays()
}
fn select_display(&mut self, display: &Self::Display) -> Result<()> {
self.recorder.select_display(display)
}
fn selected_display(&self) -> Result<Option<Self::Display>> {
self.recorder.selected_display()
}
}
+2 -2
View File
@@ -1,8 +1,8 @@
pub use macos_capture::MacOSCapture;
mod capture_engine;
pub mod display;
mod display;
mod ffi;
pub mod macos_capture;
mod pcm_buffer;
pub mod screen_recorder;
mod screen_recorder;
+123 -61
View File
@@ -2,23 +2,26 @@ extern crate libc;
use std::ffi::c_void;
use anyhow::anyhow;
use apple_sys::ScreenCaptureKit::{
id, CGSize, CMTime, INSBundle, INSError, INSObject, INSScreen, ISCContentFilter, ISCDisplay,
ISCRunningApplication, ISCShareableContent, ISCStreamConfiguration, ISCWindow, NSArray,
NSBundle, NSError, NSScreen, NSString_NSStringDeprecated, PNSObject, SCContentFilter,
SCDisplay, SCRunningApplication, SCShareableContent, SCStreamConfiguration, SCWindow,
id, CGSize, CMTime, INSBundle, INSError, INSObject, INSProcessInfo, ISCContentFilter,
ISCDisplay, ISCRunningApplication, ISCShareableContent, ISCStreamConfiguration, ISCWindow,
NSArray, NSBundle, NSError, NSProcessInfo, NSString_NSStringDeprecated, PNSObject,
SCContentFilter, SCDisplay, SCRunningApplication, SCShareableContent, SCStreamConfiguration,
SCWindow,
};
use block::Block;
use itertools::Itertools;
use tokio::sync::mpsc;
use tokio::sync::mpsc::Sender;
use crate::capture::display::DisplaySelector;
use crate::capture::macos::capture_engine::CaptureEngine;
use crate::capture::macos::display::Display;
use crate::capture::macos::ffi::{
from_nsarray, from_nsstring, new_nsarray, objc_closure, FromNSArray, ToNSArray, UnsafeSendable,
};
use crate::capture::macos::pcm_buffer::PCMBuffer;
use crate::capture::YUVFrame;
use crate::capture::{DisplayInfo, YUVFrame};
use crate::result::Result;
#[allow(dead_code)]
enum CaptureType {
@@ -30,15 +33,14 @@ enum CaptureType {
pub struct ScreenRecorder {
is_running: bool,
capture_type: CaptureType,
selected_display: Option<SCDisplay>,
selected_display: Option<Display>,
selected_window: Option<SCWindow>,
is_app_excluded: bool,
content_size: CGSize,
scale_factor: usize,
max_fps: u8,
available_content: Option<SCShareableContent>,
available_apps: Vec<SCRunningApplication>,
available_displays: Vec<SCDisplay>,
available_displays: Vec<Display>,
available_windows: Vec<SCWindow>,
is_audio_capture_enabled: bool,
is_app_audio_excluded: bool,
@@ -74,14 +76,6 @@ impl ScreenRecorder {
width: 1.,
height: 1.,
},
scale_factor: {
let screen = unsafe { NSScreen::mainScreen() };
if screen.0.is_null() {
2
} else {
(unsafe { screen.backingScaleFactor() }) as usize
}
},
max_fps: 60,
available_content: None,
available_apps: Vec::new(),
@@ -98,30 +92,34 @@ impl ScreenRecorder {
self.max_fps = fps;
}
pub async fn can_record() -> bool {
let (tx, mut rx) = mpsc::channel::<bool>(1);
pub fn can_record() -> bool {
let (tx, rx) = std::sync::mpsc::channel::<bool>();
unsafe {
SCShareableContent::getShareableContentExcludingDesktopWindows_onScreenWindowsOnly_completionHandler_(
false,
true,
objc_closure!(move |_content: id, error: id| {
let result = error.is_null();
tx.blocking_send(result).unwrap();
tx.send(result).unwrap();
}),
);
}
rx.recv().await.unwrap()
rx.recv().unwrap()
}
pub async fn monitor_available_content(&mut self) {
pub fn monitor_available_content(&mut self) {
if self.is_setup {
return;
}
self.refresh_available_content().await;
self.refresh_available_content();
}
/// Starts capturing screen content.
pub async fn start(&mut self, video_tx: Sender<YUVFrame>, audio_tx: Sender<PCMBuffer>) {
pub fn start(
&mut self,
video_tx: tokio::sync::mpsc::Sender<YUVFrame>,
audio_tx: tokio::sync::mpsc::Sender<PCMBuffer>,
) {
// Exit early if already running.
if self.is_running {
return;
@@ -129,7 +127,7 @@ impl ScreenRecorder {
if !self.is_setup {
// Starting polling for available screen content.
self.monitor_available_content().await;
self.monitor_available_content();
self.is_setup = true;
}
@@ -163,14 +161,24 @@ impl ScreenRecorder {
unsafe {
match self.capture_type {
CaptureType::Display => {
if let Some(display) = self.selected_display {
// Exclude the Sharer app itself by matching its bundle identifier.
if let Some(display) = &self.selected_display {
info!("Capturing display: {}", display.sc_display.displayID());
// Exclude the Sharer app itself.
let excluded_apps: NSArray = if self.is_app_excluded {
self.available_apps
.clone()
.into_iter()
.filter(|app| match NSBundle::mainBundle().bundleIdentifier() {
this_bundle if this_bundle.0.is_null() => false,
this_bundle if this_bundle.0.is_null() => {
let app_name = from_nsstring!(app.applicationName());
let this_name = {
from_nsstring!(
NSProcessInfo::processInfo().processName()
)
};
app_name == this_name
}
bundle => {
let app_bundle = from_nsstring!(app.bundleIdentifier());
let this_bundle = from_nsstring!(bundle);
@@ -186,7 +194,7 @@ impl ScreenRecorder {
SCContentFilter(
SCContentFilter::alloc()
.initWithDisplay_excludingApplications_exceptingWindows_(
display,
display.sc_display,
excluded_apps,
new_nsarray::<SCWindow>(),
),
@@ -214,22 +222,9 @@ impl ScreenRecorder {
config.setCapturesAudio_(self.is_audio_capture_enabled);
config.setExcludesCurrentProcessAudio_(self.is_app_audio_excluded);
match self.capture_type {
CaptureType::Display => {
if let Some(display) = self.selected_display {
// Configure the display content width and height.
config.setWidth_(display.width() as usize * self.scale_factor);
config.setHeight_(display.height() as usize * self.scale_factor);
}
}
CaptureType::Window => {
if let Some(window) = self.selected_window {
// Configure the display content width and height.
config.setWidth_(window.frame().size.width as usize * 2);
config.setHeight_(window.frame().size.height as usize * 2);
}
}
}
let (width, height) = self.resolution();
config.setWidth_(width as _);
config.setHeight_(height as _);
config.setMinimumFrameInterval_(CMTime {
value: 1,
@@ -256,8 +251,8 @@ impl ScreenRecorder {
}
}
async fn refresh_available_content(&mut self) {
let (result_tx, mut result_rx) = mpsc::channel(1);
fn refresh_available_content(&mut self) {
let (result_tx, result_rx) = std::sync::mpsc::channel();
unsafe {
SCShareableContent::getShareableContentExcludingDesktopWindows_onScreenWindowsOnly_completionHandler_(
false,
@@ -269,11 +264,11 @@ impl ScreenRecorder {
} else {
let available_content = SCShareableContent(content);
available_content.retain();
result_tx.blocking_send(UnsafeSendable(available_content)).unwrap();
result_tx.send(UnsafeSendable(available_content)).unwrap();
}
}),
);
let available_content = result_rx.recv().await.unwrap().0;
let available_content = result_rx.recv().unwrap().0;
let available_displays = from_nsarray!(SCDisplay, available_content.displays());
let available_windows = ScreenRecorder::filter_windows(from_nsarray!(
SCWindow,
@@ -285,37 +280,41 @@ impl ScreenRecorder {
// Release later.
let old_content = self.available_content.replace(available_content);
self.available_displays = available_displays
.iter()
.map(|display| Display::new(display.clone()))
.collect();
self.available_windows = available_windows;
self.available_apps = available_apps;
self.selected_display = self
.selected_display
.as_ref()
.map(
// Use the currently selected display if it is still available.
|selected_display| {
available_displays
self.available_displays
.iter()
.find(|display| display.0 == selected_display.0)
.find(|display| display == &selected_display)
.cloned()
},
)
.flatten()
.or(available_displays.first().cloned());
.or(self.available_displays.first().cloned());
self.selected_window = self
.selected_window
.map(
// Use the currently selected window if it is still available.
|selected_window| {
available_windows
self.available_windows
.iter()
.find(|window| window.0 == selected_window.0)
.find(|window| window.windowID() == selected_window.windowID())
.cloned()
},
)
.flatten()
.or(available_windows.first().cloned());
self.available_displays = available_displays;
self.available_windows = available_windows;
self.available_apps = available_apps;
.or(self.available_windows.first().cloned());
if let Some(old_content) = old_content {
old_content.release();
@@ -354,7 +353,12 @@ impl ScreenRecorder {
// Remove this app's window from the list.
.filter(|window| unsafe {
match NSBundle::mainBundle().bundleIdentifier() {
this_bundle if this_bundle.0.is_null() => true,
this_bundle if this_bundle.0.is_null() => {
let window_name =
from_nsstring!(window.owningApplication().applicationName());
let this_name = from_nsstring!(NSProcessInfo::processInfo().processName());
window_name != this_name
}
bundle => {
let this_bundle = from_nsstring!(bundle);
let window_bundle =
@@ -366,3 +370,61 @@ impl ScreenRecorder {
.collect()
}
}
impl DisplayInfo for ScreenRecorder {
fn resolution(&self) -> (u32, u32) {
match self.capture_type {
CaptureType::Display => self
.selected_display
.as_ref()
.unwrap_or_else(|| panic!("No display is selected."))
.resolution(),
CaptureType::Window => {
if let Some(window) = self.selected_window {
unsafe {
(
window.frame().size.width as u32 * self.dpi_conversion_factor() as u32,
window.frame().size.height as u32 * self.dpi_conversion_factor() as u32,
)
}
} else {
panic!("No window is selected.")
}
}
}
}
fn dpi_conversion_factor(&self) -> f64 {
match self.capture_type {
CaptureType::Display => self
.selected_display
.as_ref()
.unwrap_or_else(|| panic!("No display is selected."))
.dpi_conversion_factor(),
CaptureType::Window => 2.0,
}
}
}
impl DisplaySelector for ScreenRecorder {
type Display = Display;
fn available_displays(&mut self) -> Result<Vec<Self::Display>> {
self.refresh_available_content();
Ok(self.available_displays.clone())
}
fn select_display(&mut self, display: &Self::Display) -> Result<()> {
match self.available_displays.iter().find(|d| d == &display) {
Some(display) => {
self.selected_display = Some(display.clone());
Ok(())
}
None => Err(anyhow!("Display is not available.")),
}
}
fn selected_display(&self) -> Result<Option<Self::Display>> {
Ok(self.selected_display.clone())
}
}
+11 -5
View File
@@ -2,15 +2,23 @@ use crate::{OutputSink, Result};
use async_trait::async_trait;
use std::sync::Arc;
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
#[async_trait]
pub trait ScreenCapture {
async fn capture(
fn new(config: Config) -> Result<ScreenCaptureImpl>;
fn display(&self) -> &dyn DisplayInfo;
async fn start_capture(
&mut self,
encoder: FfmpegEncoder,
output: Arc<Mutex<impl OutputSink + Send + ?Sized>>,
profiler: PerformanceProfiler,
shutdown_token: CancellationToken,
) -> Result<()>;
async fn stop_capture(&mut self) -> Result<()>;
}
pub trait DisplayInfo {
@@ -26,8 +34,6 @@ use crate::performance_profiler::PerformanceProfiler;
#[cfg(target_os = "windows")]
mod wgc;
#[cfg(target_os = "windows")]
pub use wgc::display::Display;
#[cfg(target_os = "windows")]
pub use wgc::WGCScreenCapture as ScreenCaptureImpl;
pub mod capturer;
@@ -37,11 +43,11 @@ mod macos;
pub use frame::YUVFrame;
#[cfg(target_os = "macos")]
pub use macos::display::Display;
#[cfg(target_os = "macos")]
pub use macos::MacOSCapture as ScreenCaptureImpl;
mod audio;
pub mod display;
mod yuv_convert;
use crate::config::Config;
pub use yuv_convert::YuvConverter;
+129 -5
View File
@@ -1,13 +1,27 @@
use crate::capture::DisplayInfo;
use crate::result::Result;
use std::ffi::CStr;
use std::mem::size_of;
use windows::Graphics::Capture::GraphicsCaptureItem;
use windows::Win32::Devices::Display::{
DisplayConfigGetDeviceInfo, GetDisplayConfigBufferSizes, QueryDisplayConfig,
DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME, DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME,
DISPLAYCONFIG_DEVICE_INFO_HEADER, DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE,
DISPLAYCONFIG_MODE_INFO_TYPE_TARGET, DISPLAYCONFIG_SOURCE_DEVICE_NAME,
DISPLAYCONFIG_TARGET_DEVICE_NAME, QDC_ONLY_ACTIVE_PATHS,
};
use windows::Win32::Foundation::{BOOL, LPARAM, RECT};
use windows::Win32::Graphics::Gdi::{EnumDisplayMonitors, HDC, HMONITOR};
use windows::Win32::Graphics::Gdi::{
EnumDisplayMonitors, GetMonitorInfoA, HDC, HMONITOR, MONITORINFO, MONITORINFOEXA,
};
use windows::Win32::System::WinRT::Graphics::Capture::IGraphicsCaptureItemInterop;
#[derive(Clone)]
use crate::capture::DisplayInfo;
use crate::result::Result;
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Display {
pub handle: HMONITOR,
pub name: String,
}
impl Display {
@@ -20,7 +34,10 @@ impl Display {
}
pub fn new(handle: HMONITOR) -> Result<Self> {
Ok(Self { handle })
Ok(Self {
handle,
name: unsafe { get_display_name(handle) },
})
}
pub fn select(&self) -> Result<GraphicsCaptureItem> {
@@ -29,6 +46,113 @@ impl Display {
}
}
unsafe fn get_display_name(handle: HMONITOR) -> String {
let (device_name, width, height) = {
let info = MONITORINFOEXA {
monitorInfo: MONITORINFO {
cbSize: size_of::<MONITORINFOEXA>() as u32,
..Default::default()
},
szDevice: [0; 32],
};
GetMonitorInfoA(handle, &info as *const _ as *mut _);
(
CStr::from_ptr(info.szDevice.as_ptr() as _)
.to_str()
.unwrap()
.to_string(),
info.monitorInfo.rcMonitor.right - info.monitorInfo.rcMonitor.left,
info.monitorInfo.rcMonitor.bottom - info.monitorInfo.rcMonitor.top,
)
};
let name = try_get_user_friendly_name(device_name.clone()).unwrap_or(device_name);
format!("{} ({} x {})", name, width, height)
}
unsafe fn try_get_user_friendly_name(device_name: String) -> Option<String> {
let mut num_path_array_elements = 0;
let mut num_mode_info_array_elements = 0;
GetDisplayConfigBufferSizes(
QDC_ONLY_ACTIVE_PATHS,
&mut num_path_array_elements,
&mut num_mode_info_array_elements,
);
let mut path_info_array = vec![Default::default(); num_path_array_elements as usize];
let mut mode_info_array = vec![Default::default(); num_mode_info_array_elements as usize];
QueryDisplayConfig(
QDC_ONLY_ACTIVE_PATHS,
&mut num_path_array_elements,
path_info_array.as_mut_ptr(),
&mut num_mode_info_array_elements,
mode_info_array.as_mut_ptr(),
None,
);
mode_info_array
.iter()
.filter(|source_mode| source_mode.infoType == DISPLAYCONFIG_MODE_INFO_TYPE_SOURCE)
.find_map(|source_mode| {
let source_device_name = DISPLAYCONFIG_SOURCE_DEVICE_NAME {
header: DISPLAYCONFIG_DEVICE_INFO_HEADER {
adapterId: source_mode.adapterId,
id: source_mode.id,
size: size_of::<DISPLAYCONFIG_SOURCE_DEVICE_NAME>() as u32,
r#type: DISPLAYCONFIG_DEVICE_INFO_GET_SOURCE_NAME,
},
..Default::default()
};
DisplayConfigGetDeviceInfo(&source_device_name.header as *const _ as *mut _);
let gdi_device_name =
widestring::U16CString::from_ptr_str(source_device_name.viewGdiDeviceName.as_ptr())
.to_string()
.ok()?;
if gdi_device_name == device_name {
let target_mode = {
let id = path_info_array
.iter()
.find(|path_info| path_info.sourceInfo.id == source_mode.id)?
.targetInfo
.id;
mode_info_array
.iter()
.find(|target_mode| target_mode.id == id)?
};
let target_device_name = DISPLAYCONFIG_TARGET_DEVICE_NAME {
header: DISPLAYCONFIG_DEVICE_INFO_HEADER {
adapterId: target_mode.adapterId,
id: target_mode.id,
size: size_of::<DISPLAYCONFIG_TARGET_DEVICE_NAME>() as u32,
r#type: DISPLAYCONFIG_DEVICE_INFO_GET_TARGET_NAME,
},
..Default::default()
};
DisplayConfigGetDeviceInfo(&target_device_name.header as *const _ as *mut _);
let user_friendly_name = widestring::U16CString::from_ptr_str(
target_device_name.monitorFriendlyDeviceName.as_ptr(),
)
.to_string()
.ok()?;
if user_friendly_name.is_empty() {
None
} else {
Some(user_friendly_name)
}
} else {
None
}
})
}
impl ToString for Display {
fn to_string(&self) -> String {
self.name.clone()
}
}
// callback function for EnumDisplayMonitors
extern "system" fn enum_monitor(monitor: HMONITOR, _: HDC, _: *mut RECT, state: LPARAM) -> BOOL {
unsafe {
+1 -1
View File
@@ -1,5 +1,5 @@
mod d3d;
pub mod display;
mod display;
mod wgc_capture;
pub use wgc_capture::WGCScreenCapture;
+103 -35
View File
@@ -1,34 +1,44 @@
use async_trait::async_trait;
use std::sync::Arc;
use std::time::Duration;
use tokio::select;
use windows::core::IInspectable;
use windows::Foundation::TypedEventHandler;
use windows::Graphics::Capture::{
Direct3D11CaptureFrame, Direct3D11CaptureFramePool, GraphicsCaptureItem,
Direct3D11CaptureFrame, Direct3D11CaptureFramePool, GraphicsCaptureItem, GraphicsCaptureSession,
};
use windows::Graphics::DirectX::DirectXPixelFormat;
use crate::capture::display::DisplaySelector;
use crate::capture::wgc::d3d;
use crate::capture::YuvConverter;
use crate::capture::wgc::display::Display;
use crate::capture::{DisplayInfo, ScreenCaptureImpl, YuvConverter};
use crate::config::Config;
use crate::encoder::{FfmpegEncoder, FrameData};
use crate::performance_profiler::PerformanceProfiler;
use crate::result::Result;
use crate::{OutputSink, ScreenCapture};
use tokio::sync::Mutex;
use tokio_util::sync::CancellationToken;
pub struct WGCScreenCapture<'a> {
pub struct WGCScreenCapture {
config: Config,
engine: CaptureEngine,
selected_display: Display,
session: Option<GraphicsCaptureSession>,
}
struct CaptureEngine {
item: GraphicsCaptureItem,
frame_pool: Direct3D11CaptureFramePool,
config: &'a Config,
duplicator: YuvConverter,
}
impl<'a> WGCScreenCapture<'a> {
pub fn new(item: GraphicsCaptureItem, config: &'a Config) -> Result<Self> {
let item_size = item.Size()?;
let (device, d3d_device, d3d_context) = d3d::create_direct3d_devices_and_context()?;
impl CaptureEngine {
fn new(item: GraphicsCaptureItem) -> Self {
let item_size = item.Size().unwrap();
let (device, d3d_device, d3d_context) = d3d::create_direct3d_devices_and_context().unwrap();
let device = Arc::new(device);
let d3d_context = Arc::new(d3d_context);
let frame_pool = Direct3D11CaptureFramePool::CreateFreeThreaded(
@@ -36,34 +46,55 @@ impl<'a> WGCScreenCapture<'a> {
DirectXPixelFormat::B8G8R8A8UIntNormalized,
1,
item_size,
)?;
)
.unwrap();
let duplicator = YuvConverter::new(
device,
d3d_context,
(item_size.Width as u32, item_size.Height as u32),
)?;
Ok(Self {
)
.unwrap();
Self {
item,
frame_pool,
config,
duplicator,
})
}
}
}
#[async_trait]
impl ScreenCapture for WGCScreenCapture<'_> {
async fn capture(
impl ScreenCapture for WGCScreenCapture {
fn new(config: Config) -> Result<ScreenCaptureImpl> {
let selected_display = Display::online().unwrap()[0].clone();
let item = selected_display.select()?;
let engine = CaptureEngine::new(item);
Ok(Self {
config,
engine,
selected_display,
session: None,
})
}
fn display(&self) -> &dyn DisplayInfo {
&self.engine.item
}
async fn start_capture(
&mut self,
mut encoder: FfmpegEncoder,
output: Arc<Mutex<impl OutputSink + Send + ?Sized>>,
mut profiler: PerformanceProfiler,
shutdown_token: CancellationToken,
) -> Result<()> {
let session = self.frame_pool.CreateCaptureSession(&self.item)?;
let session = self
.engine
.frame_pool
.CreateCaptureSession(&self.engine.item)?;
let (sender, mut receiver) = tokio::sync::mpsc::channel::<Direct3D11CaptureFrame>(1);
self.frame_pool.FrameArrived(&TypedEventHandler::<
self.engine.frame_pool.FrameArrived(&TypedEventHandler::<
Direct3D11CaptureFramePool,
IInspectable,
>::new({
@@ -78,33 +109,70 @@ impl ScreenCapture for WGCScreenCapture<'_> {
}))?;
session.StartCapture()?;
self.session.replace(session);
let mut duplicator = self.engine.duplicator.clone();
let mut ticker =
tokio::time::interval(Duration::from_millis((1000 / self.config.max_fps) as u64));
while let Some(frame) = receiver.recv().await {
let frame_time = frame.SystemRelativeTime()?.Duration;
profiler.accept_frame(frame.SystemRelativeTime()?.Duration);
let yuv_frame = {
self.duplicator
.capture(d3d::get_d3d_interface_from_object(&frame.Surface()?)?)?
};
profiler.done_preprocessing();
let encoded = encoder
.encode(FrameData::NV12(&yuv_frame), frame_time)
.unwrap();
let encoded_len = encoded.len();
profiler.done_encoding();
output.lock().await.write(encoded).await.unwrap();
profiler.done_processing(encoded_len);
ticker.tick().await;
tokio::spawn(async move {
loop {
select! {
Some(frame) = receiver.recv() => {
let frame_time = frame.SystemRelativeTime().unwrap().Duration;
profiler.accept_frame(frame.SystemRelativeTime().unwrap().Duration);
let yuv_frame = {
duplicator
.capture(d3d::get_d3d_interface_from_object(&frame.Surface().unwrap()).unwrap()).unwrap()
};
profiler.done_preprocessing();
let encoded = encoder
.encode(FrameData::NV12(&yuv_frame), frame_time)
.unwrap();
let encoded_len = encoded.len();
profiler.done_encoding();
output.lock().await.write(encoded).await.unwrap();
profiler.done_processing(encoded_len);
ticker.tick().await;
}
_ = shutdown_token.cancelled() => {
break;
}
}
}
});
Ok(())
}
async fn stop_capture(&mut self) -> Result<()> {
if let Some(session) = self.session.take() {
session.Close()?;
}
session.Close()?;
Ok(())
}
}
impl Drop for WGCScreenCapture<'_> {
impl DisplaySelector for WGCScreenCapture {
type Display = Display;
fn available_displays(&mut self) -> Result<Vec<Display>> {
Display::online()
}
fn select_display(&mut self, display: &Display) -> Result<()> {
self.engine = CaptureEngine::new(display.select()?);
self.selected_display = display.clone();
Ok(())
}
fn selected_display(&self) -> Result<Option<Self::Display>> {
Ok(Some(self.selected_display.clone()))
}
}
impl Drop for CaptureEngine {
fn drop(&mut self) {
self.frame_pool.Close().unwrap();
}
+1
View File
@@ -19,6 +19,7 @@ use windows::{
Win32::Graphics::{Direct3D::*, Direct3D11::*, Dxgi::Common::*},
};
#[derive(Clone)]
pub struct YuvConverter {
device: Arc<ID3D11Device>,
device_context: Arc<ID3D11DeviceContext>,
+6 -4
View File
@@ -31,6 +31,8 @@ pub enum FrameData<'a> {
impl FfmpegEncoder {
pub fn new(w: u32, h: u32, encoder_config: &EncoderConfig) -> Self {
let w = if w % 2 == 0 { w } else { w + 1 } as usize;
let h = if h % 2 == 0 { h } else { h + 1 } as usize;
let time_base = TimeBase::new(1, 90_000);
let pixel_format = video::frame::get_pixel_format(&encoder_config.pixel_format);
@@ -38,8 +40,8 @@ impl FfmpegEncoder {
let mut encoder = VideoEncoder::builder(&encoder_config.encoder)
.unwrap()
.pixel_format(pixel_format)
.width(w as _)
.height(h as _)
.width(w)
.height(h)
.time_base(time_base);
for option in &encoder_config.options {
@@ -53,8 +55,8 @@ impl FfmpegEncoder {
pixel_format: encoder_config.pixel_format.clone(),
frame_pool: FramePool::new(w, h, time_base, pixel_format),
force_idr: Arc::new(AtomicBool::new(false)),
w: w as usize,
h: h as usize,
w,
h,
}
}
+4 -5
View File
@@ -4,14 +4,14 @@ use std::collections::VecDeque;
pub(crate) struct FramePool {
frames: VecDeque<VideoFrame>,
w: u32,
h: u32,
w: usize,
h: usize,
time_base: TimeBase,
pixel_format: PixelFormat,
}
impl FramePool {
pub fn new(w: u32, h: u32, time_base: TimeBase, pixel_format: PixelFormat) -> Self {
pub fn new(w: usize, h: usize, time_base: TimeBase, pixel_format: PixelFormat) -> Self {
Self {
frames: VecDeque::new(),
w,
@@ -35,7 +35,6 @@ impl FramePool {
}
}
VideoFrameMut::black(self.pixel_format, self.w as _, self.h as _)
.with_time_base(self.time_base)
VideoFrameMut::black(self.pixel_format, self.w, self.h).with_time_base(self.time_base)
}
}
+3 -1
View File
@@ -99,7 +99,9 @@ impl Application for App {
invite_link: self.capturer.get_invite_link().unwrap_or_default(),
})
} else {
self.start_page.view(())
self.start_page.view(start::ViewProps {
capturer: &self.capturer,
})
}]
.spacing(12),]
.align_items(Center)
+36 -8
View File
@@ -1,7 +1,11 @@
use iced::alignment::{Horizontal, Vertical};
use iced::widget::{container, pick_list, vertical_space};
use iced::Alignment::Center;
use iced::Length::Fill;
use crate::capture::capturer::Capturer;
use crate::capture::display::DisplaySelector;
use crate::capture::ScreenCaptureImpl;
use crate::column_iced;
use crate::gui::app;
use crate::gui::component::Component;
@@ -15,6 +19,7 @@ pub struct StartPage {}
#[derive(Clone, Debug)]
pub enum Message {
Start,
SelectDisplay(<ScreenCaptureImpl as DisplaySelector>::Display),
}
impl From<Message> for app::Message {
@@ -27,10 +32,14 @@ pub struct UpdateProps<'a> {
pub capturer: &'a mut Capturer,
}
pub struct ViewProps<'a> {
pub capturer: &'a Capturer,
}
impl<'a> Component<'a> for StartPage {
type Message = Message;
type UpdateProps = UpdateProps<'a>;
type ViewProps = ();
type ViewProps = ViewProps<'a>;
fn update(
&mut self,
@@ -41,18 +50,37 @@ impl<'a> Component<'a> for StartPage {
Message::Start => {
props.capturer.run();
}
Message::SelectDisplay(display) => {
props.capturer.select_display(display);
}
}
iced::Command::none()
}
fn view(&self, _params: Self::ViewProps) -> Element<'_, app::Message> {
column_iced![FAB::new("Start Sharing", Icon::PlayCircle)
.style(button::Style::Primary)
.build()
.on_press(Message::Start.into()),]
.align_items(Center)
.padding(16)
fn view(&self, params: Self::ViewProps) -> Element<'_, app::Message> {
container(
column_iced![
pick_list(
params.capturer.available_displays(),
params.capturer.selected_display(),
move |message| app::Message::Start(Message::SelectDisplay(message))
)
.width(Fill),
vertical_space(32),
FAB::new("Start Sharing", Icon::PlayCircle)
.style(button::Style::Primary)
.build()
.on_press(Message::Start.into()),
]
.align_items(Center)
.padding(16)
.width(Fill)
.max_width(400),
)
.width(Fill)
.height(Fill)
.align_x(Horizontal::Center)
.align_y(Vertical::Center)
.into()
}
}
+18
View File
@@ -0,0 +1,18 @@
use crate::gui::theme::Theme;
use iced::overlay::menu::{Appearance, StyleSheet};
impl StyleSheet for Theme {
type Style = ();
fn appearance(&self, _style: &Self::Style) -> Appearance {
Appearance {
text_color: self.palette().on_surface,
background: self.palette().surface.into(),
border_width: 1.,
border_radius: 4.,
border_color: self.palette().outline,
selected_text_color: self.palette().on_primary,
selected_background: self.palette().primary.into(),
}
}
}
+3
View File
@@ -6,6 +6,9 @@ pub mod button;
pub mod color;
pub mod container;
pub mod icon;
pub mod menu;
pub mod picklist;
pub mod scrollable;
pub mod svg;
pub mod tab;
pub mod text;
+23
View File
@@ -0,0 +1,23 @@
use crate::gui::theme::Theme;
use iced::widget::pick_list::{Appearance, StyleSheet};
impl StyleSheet for Theme {
type Style = ();
fn active(&self, _style: &<Self as StyleSheet>::Style) -> Appearance {
let palette = self.palette();
Appearance {
text_color: palette.on_surface,
placeholder_color: palette.on_surface,
handle_color: palette.on_surface,
background: palette.surface.into(),
border_width: 1.,
border_radius: 4.,
border_color: palette.outline,
}
}
fn hovered(&self, style: &<Self as StyleSheet>::Style) -> Appearance {
self.active(style)
}
}
+26
View File
@@ -0,0 +1,26 @@
use crate::gui::theme::Theme;
use iced::widget::scrollable::{Scrollbar, Scroller, StyleSheet};
impl StyleSheet for Theme {
type Style = ();
fn active(&self, _style: &Self::Style) -> Scrollbar {
let palette = self.palette();
Scrollbar {
background: palette.surface.into(),
border_radius: 4.,
border_width: 1.,
border_color: palette.outline,
scroller: Scroller {
color: palette.on_surface,
border_radius: 4.,
border_width: 1.,
border_color: palette.outline,
},
}
}
fn hovered(&self, style: &Self::Style, _is_mouse_over_scrollbar: bool) -> Scrollbar {
self.active(style)
}
}
+2
View File
@@ -11,9 +11,11 @@ pub trait OutputSink: Send + Sync + 'static {
}
mod file_output;
mod noop_output;
mod webrtc_output;
mod webrtc_peer;
pub use file_output::FileOutput;
pub use noop_output::NoOpOutput;
pub use webrtc_output::WebRTCOutput;
pub use webrtc_peer::WebRTCPeer;
+28
View File
@@ -0,0 +1,28 @@
use std::time::Duration;
use async_trait::async_trait;
use bytes::Bytes;
use crate::OutputSink;
use crate::Result;
/// Voids outputs, for testing.
pub struct NoOpOutput;
impl NoOpOutput {
#[allow(dead_code)]
pub fn new() -> Self {
Self {}
}
}
#[async_trait]
impl OutputSink for NoOpOutput {
async fn write(&mut self, _input: Bytes) -> Result<()> {
Ok(())
}
async fn write_audio(&mut self, _input: Bytes, _duration: Duration) -> Result<()> {
Ok(())
}
}