use crate::{Result, SyphonError};
use crate::directory::ServerInfo;
#[cfg(target_os = "macos")]
use core_foundation::base::TCFType;
#[cfg(target_os = "macos")]
use objc::runtime::{Class, Object};
#[cfg(target_os = "macos")]
use objc::{msg_send, sel, sel_impl};
#[cfg(target_os = "macos")]
use objc_id::ShareId;
pub struct Frame {
#[cfg(target_os = "macos")]
pub(crate) surface: io_surface::IOSurface,
#[cfg(target_os = "macos")]
frame_texture: *mut objc::runtime::Object,
pub width: u32,
pub height: u32,
}
#[cfg(target_os = "macos")]
unsafe impl Send for Frame {}
#[cfg(target_os = "macos")]
unsafe impl Sync for Frame {}
impl Drop for Frame {
fn drop(&mut self) {
#[cfg(target_os = "macos")]
unsafe {
if !self.frame_texture.is_null() {
let _: () = objc::msg_send![self.frame_texture, release];
}
}
}
}
impl Frame {
#[cfg(target_os = "macos")]
pub fn iosurface_id(&self) -> io_surface::IOSurfaceID {
self.surface.get_id()
}
#[cfg(target_os = "macos")]
pub fn iosurface(&self) -> &io_surface::IOSurface {
&self.surface
}
#[cfg(target_os = "macos")]
pub fn metal_texture_ptr(&self) -> *mut objc::runtime::Object {
self.frame_texture
}
#[cfg(target_os = "macos")]
pub fn iosurface_ref(&self) -> io_surface::IOSurfaceRef {
self.surface.as_concrete_TypeRef()
}
#[cfg(target_os = "macos")]
pub fn lock(&mut self) -> Result<(*mut u8, u32)> {
use crate::iosurface_ext::{IOSurfaceLock, kIOSurfaceLockReadOnly, kIOSurfaceLockAvoidSync};
unsafe {
let surface_ref = self.surface.as_CFTypeRef() as io_surface::IOSurfaceRef;
let mut seed = 0u32;
let result = IOSurfaceLock(surface_ref, kIOSurfaceLockReadOnly, &mut seed);
if result != 0 {
let result2 = IOSurfaceLock(
surface_ref,
kIOSurfaceLockReadOnly | kIOSurfaceLockAvoidSync,
&mut seed,
);
if result2 != 0 {
return Err(SyphonError::LockFailed);
}
}
let addr = crate::iosurface_ext::IOSurfaceGetBaseAddress(surface_ref);
if addr.is_null() {
let _ = self.unlock(seed);
return Err(SyphonError::LockFailed);
}
Ok((addr as *mut u8, seed))
}
}
#[cfg(target_os = "macos")]
pub fn unlock(&mut self, seed: u32) -> Result<()> {
use crate::iosurface_ext::IOSurfaceUnlock;
unsafe {
let surface_ref = self.surface.as_CFTypeRef() as io_surface::IOSurfaceRef;
let mut seed_copy = seed;
let result = IOSurfaceUnlock(surface_ref, 0, &mut seed_copy);
if result != 0 {
log::trace!("[Frame] IOSurfaceUnlock failed ({}), ignoring", result);
return Err(SyphonError::LockFailed);
}
Ok(())
}
}
#[cfg(target_os = "macos")]
pub fn bytes_per_row(&self) -> usize {
use crate::iosurface_ext::IOSurfaceGetBytesPerRow;
unsafe {
IOSurfaceGetBytesPerRow(self.surface.as_CFTypeRef() as io_surface::IOSurfaceRef)
}
}
#[cfg(target_os = "macos")]
pub fn to_vec(&mut self) -> Result<Vec<u8>> {
use std::slice;
let (addr, seed) = self.lock()?;
let height = self.height as usize;
let stride = self.bytes_per_row();
unsafe {
let data = slice::from_raw_parts(addr, height * stride).to_vec();
let _ = self.unlock(seed);
Ok(data)
}
}
}
#[cfg(target_os = "macos")]
struct FrameHandlerBlock(
block::RcBlock<(*mut objc::runtime::Object,), ()>,
);
#[cfg(target_os = "macos")]
unsafe impl Send for FrameHandlerBlock {}
#[cfg(target_os = "macos")]
unsafe impl Sync for FrameHandlerBlock {}
pub struct SyphonClient {
#[cfg(target_os = "macos")]
inner: ShareId<Object>,
info: ServerInfo,
#[cfg(target_os = "macos")]
_handler_block: Option<Box<dyn std::any::Any>>,
}
#[cfg(target_os = "macos")]
unsafe impl Send for SyphonClient {}
#[cfg(target_os = "macos")]
unsafe impl Sync for SyphonClient {}
impl SyphonClient {
pub fn connect(server_name: &str) -> Result<Self> {
#[cfg(target_os = "macos")]
{
unsafe { objc::rc::autoreleasepool(|| Self::connect_by_name_macos(server_name)) }
}
#[cfg(not(target_os = "macos"))]
{ Err(SyphonError::NotAvailable) }
}
pub fn connect_by_info(info: &ServerInfo) -> Result<Self> {
#[cfg(target_os = "macos")]
{
unsafe { objc::rc::autoreleasepool(|| Self::connect_by_info_macos(info)) }
}
#[cfg(not(target_os = "macos"))]
{ Err(SyphonError::NotAvailable) }
}
pub fn connect_with_channel(
server_name: &str,
) -> Result<(Self, std::sync::mpsc::Receiver<()>)> {
let (tx, rx) = std::sync::mpsc::sync_channel(1);
#[cfg(target_os = "macos")]
{
let client = unsafe {
objc::rc::autoreleasepool(|| Self::connect_by_name_with_tx(server_name, Some(tx)))
}?;
Ok((client, rx))
}
#[cfg(not(target_os = "macos"))]
{ Err(SyphonError::NotAvailable) }
}
pub fn connect_by_info_with_channel(
info: &ServerInfo,
) -> Result<(Self, std::sync::mpsc::Receiver<()>)> {
let (tx, rx) = std::sync::mpsc::sync_channel(1);
#[cfg(target_os = "macos")]
{
let client = unsafe {
objc::rc::autoreleasepool(|| Self::connect_by_info_with_tx(info, Some(tx)))
}?;
Ok((client, rx))
}
#[cfg(not(target_os = "macos"))]
{ Err(SyphonError::NotAvailable) }
}
#[cfg(target_os = "macos")]
unsafe fn shared_dir() -> Result<*mut Object> {
Class::get("SyphonServerDirectory")
.map(|cls| { let dir: *mut Object = msg_send![cls, sharedDirectory]; dir })
.ok_or_else(|| SyphonError::FrameworkNotFound(
"SyphonServerDirectory not found".to_string()
))
}
#[cfg(target_os = "macos")]
unsafe fn ensure_servers_populated(dir: *mut Object) {
let servers: *mut Object = msg_send![dir, servers];
let count: usize = msg_send![servers, count];
if count > 0 { return; }
let _: () = msg_send![dir, requestServerAnnounce];
}
#[cfg(target_os = "macos")]
unsafe fn connect_by_name_macos(name: &str) -> Result<Self> {
Self::connect_by_name_with_tx(name, None)
}
#[cfg(target_os = "macos")]
unsafe fn connect_by_info_macos(info: &ServerInfo) -> Result<Self> {
Self::connect_by_info_with_tx(info, None)
}
#[cfg(target_os = "macos")]
unsafe fn connect_by_name_with_tx(
name: &str,
tx: Option<std::sync::mpsc::SyncSender<()>>,
) -> Result<Self> {
let dir = Self::shared_dir()?;
Self::ensure_servers_populated(dir);
let servers: *mut Object = msg_send![dir, servers];
let count: usize = msg_send![servers, count];
let mut first_match: *mut Object = std::ptr::null_mut();
let mut first_info: Option<ServerInfo> = None;
let mut match_count = 0usize;
for i in 0..count {
let desc: *mut Object = msg_send![servers, objectAtIndex: i];
let n = Self::str_from_desc(desc, "SyphonServerDescriptionNameKey");
let a = Self::str_from_desc(desc, "SyphonServerDescriptionAppNameKey");
if n == name || a == name {
match_count += 1;
if first_match.is_null() {
let u = Self::str_from_desc(desc, "SyphonServerDescriptionUUIDKey");
let b = Self::str_from_desc(desc, "SyphonServerDescriptionAppBundleIdentifierKey");
let _: () = msg_send![desc, retain];
first_match = desc;
first_info = Some(ServerInfo { name: n, uuid: u, app_name: a, bundle_id: b });
}
}
}
if first_match.is_null() {
return Err(SyphonError::ServerNotFound(name.to_string()));
}
if match_count > 1 {
let _: () = msg_send![first_match, release];
return Err(SyphonError::AmbiguousServerName(format!(
"{} servers match name '{}'. \
List servers with SyphonServerDirectory::servers() and call connect_by_info().",
match_count, name
)));
}
let info = first_info.unwrap();
log::info!("[SyphonClient] Connecting to '{}' (uuid={})", info.display_name(), info.uuid);
let result = Self::create_client(first_match, info, tx);
let _: () = msg_send![first_match, release];
result
}
#[cfg(target_os = "macos")]
unsafe fn connect_by_info_with_tx(
info: &ServerInfo,
tx: Option<std::sync::mpsc::SyncSender<()>>,
) -> Result<Self> {
let dir = Self::shared_dir()?;
Self::ensure_servers_populated(dir);
let servers: *mut Object = msg_send![dir, servers];
let count: usize = msg_send![servers, count];
let mut found: *mut Object = std::ptr::null_mut();
for i in 0..count {
let desc: *mut Object = msg_send![servers, objectAtIndex: i];
let u = Self::str_from_desc(desc, "SyphonServerDescriptionUUIDKey");
if u == info.uuid {
let _: () = msg_send![desc, retain];
found = desc;
break;
}
}
if found.is_null() {
return Err(SyphonError::ServerNotFound(format!("uuid={}", info.uuid)));
}
log::info!("[SyphonClient] Connecting to '{}' (uuid={})", info.display_name(), info.uuid);
let result = Self::create_client(found, info.clone(), tx);
let _: () = msg_send![found, release];
result
}
#[cfg(target_os = "macos")]
unsafe fn create_client(
server_desc: *mut Object,
info: ServerInfo,
tx: Option<std::sync::mpsc::SyncSender<()>>,
) -> Result<Self> {
let device = crate::metal_device::default_device()
.map(|d| d.raw_device)
.ok_or_else(|| SyphonError::FrameworkNotFound("Metal not available".to_string()))?;
let cls = Class::get("SyphonMetalClient")
.ok_or_else(|| SyphonError::FrameworkNotFound(
"SyphonMetalClient class not found".to_string()
))?;
let (handler_ptr, handler_block): (*mut Object, Option<Box<dyn std::any::Any>>) =
match tx {
Some(sender) => {
let blk = block::ConcreteBlock::new(move |_client: *mut Object| {
let _ = sender.try_send(());
}).copy();
let ptr = &*blk as *const _ as *mut Object;
(ptr, Some(Box::new(FrameHandlerBlock(blk))))
}
None => (std::ptr::null_mut(), None),
};
let init_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
let obj: *mut Object = msg_send![cls, alloc];
let obj: *mut Object = msg_send![
obj,
initWithServerDescription: server_desc
device: device
options: std::ptr::null_mut::<Object>()
newFrameHandler: handler_ptr
];
obj
}));
let obj = match init_result {
Ok(o) => o,
Err(_) => return Err(SyphonError::CreateFailed(
"SyphonMetalClient init threw an Objective-C exception".to_string()
)),
};
if obj.is_null() {
return Err(SyphonError::CreateFailed("SyphonMetalClient returned nil".to_string()));
}
let is_valid: bool = msg_send![obj, isValid];
if !is_valid {
return Err(SyphonError::CreateFailed(
"SyphonMetalClient.isValid is false — server may have stopped".to_string()
));
}
Ok(Self { inner: ShareId::from_ptr(obj), info, _handler_block: handler_block })
}
#[cfg(target_os = "macos")]
unsafe fn str_from_desc(desc: *mut Object, key: &str) -> String {
use crate::utils::{to_nsstring, from_nsstring};
let k = match to_nsstring(key) { Ok(k) => k, Err(_) => return String::new() };
let v: *mut Object = msg_send![desc, objectForKey: k];
if v.is_null() { String::new() } else { from_nsstring(v) }
}
#[cfg(target_os = "macos")]
pub fn try_receive(&self) -> Result<Option<Frame>> {
unsafe {
objc::rc::autoreleasepool(|| {
let has_new: bool = msg_send![&*self.inner, hasNewFrame];
if !has_new { return Ok(None); }
let frame_texture: *mut Object = msg_send![&*self.inner, newFrameImage];
if frame_texture.is_null() { return Ok(None); }
let width: u64 = msg_send![frame_texture, width];
let height: u64 = msg_send![frame_texture, height];
let ios_ref: io_surface::IOSurfaceRef = msg_send![frame_texture, iosurface];
if ios_ref.is_null() { return Ok(None); }
let surface = io_surface::IOSurface::wrap_under_get_rule(ios_ref);
Ok(Some(Frame { surface, frame_texture, width: width as u32, height: height as u32 }))
})
}
}
#[cfg(target_os = "macos")]
pub fn has_new_frame(&self) -> bool {
unsafe {
objc::rc::autoreleasepool(|| msg_send![&*self.inner, hasNewFrame])
}
}
#[cfg(target_os = "macos")]
pub fn receive(&self) -> Result<Frame> {
loop {
if let Some(frame) = self.try_receive()? { return Ok(frame); }
std::thread::yield_now();
}
}
#[cfg(target_os = "macos")]
pub fn is_connected(&self) -> bool {
unsafe {
objc::rc::autoreleasepool(|| msg_send![&*self.inner, isValid])
}
}
pub fn server_info(&self) -> &ServerInfo {
&self.info
}
pub fn server_name(&self) -> &str {
self.info.display_name()
}
pub fn server_app(&self) -> &str {
&self.info.app_name
}
pub fn stop(&self) {
#[cfg(target_os = "macos")]
unsafe {
objc::rc::autoreleasepool(|| { let _: () = msg_send![&*self.inner, stop]; });
}
}
}
impl Drop for SyphonClient {
fn drop(&mut self) {
self.stop();
log::debug!("[SyphonClient] dropped (server='{}')", self.info.display_name());
}
}