[MIRA-137] Select Display (#34)
This commit is contained in:
Generated
+8
-1
@@ -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"
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
@@ -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>>;
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
@@ -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
@@ -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,5 +1,5 @@
|
||||
mod d3d;
|
||||
pub mod display;
|
||||
mod display;
|
||||
mod wgc_capture;
|
||||
|
||||
pub use wgc_capture::WGCScreenCapture;
|
||||
|
||||
+103
-35
@@ -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();
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ use windows::{
|
||||
Win32::Graphics::{Direct3D::*, Direct3D11::*, Dxgi::Common::*},
|
||||
};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct YuvConverter {
|
||||
device: Arc<ID3D11Device>,
|
||||
device_context: Arc<ID3D11DeviceContext>,
|
||||
|
||||
@@ -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,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
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user