@ -21,7 +21,8 @@ final class ActiveOutput {
let receiver : NDIReceiver
let window : OutputWindow
let displayID : CGDirectDisplayID
let originalMode : CGDisplayMode ?
// / M o d e b e f o r e M u s u b i s w i t c h e d t o 5 0 H z ; r e s t o r e d w h e n t h e o u t p u t e n d s .
var originalMode : CGDisplayMode ?
var snapshot : NDIReceiver . Snapshot
init ( source : Source , receiver : NDIReceiver , window : OutputWindow , displayID : CGDirectDisplayID , originalMode : CGDisplayMode ? ) {
@ -57,7 +58,8 @@ final class OutputController {
init ( ) {
refreshScreens ( )
NotificationCenter . default . addObserver ( forName : NSApplication . didChangeScreenParametersNotification , object : nil , queue : . main ) { [ weak self ] _ in
MainActor . assumeIsolated { self ? . screensChanged ( ) }
// D e f e r r e d : t h e n o t i f i c a t i o n c a n a r r i v e s y n c h r o n o u s l y i n s i d e a d i s p l a y m o d e s w i t c h .
DispatchQueue . main . async { MainActor . assumeIsolated { self ? . screensChanged ( ) } }
}
timer = Timer . scheduledTimer ( withTimeInterval : 1 , repeats : true ) { [ weak self ] _ in
MainActor . assumeIsolated { self ? . updateSnapshots ( ) }
@ -98,8 +100,25 @@ final class OutputController {
onChange ? ( )
}
private var reconciling = false
private var reconcileAgain = false
// / S t a r t s a n d s t o p s o u t p u t s s o t h a t c o n n e c t e d s c r e e n s m a t c h ` d e s i r e d ` .
// / N e v e r n e s t e d : d i s p l a y m o d e s w i t c h e s p o s t s c r e e n - c h a n g e n o t i f i c a t i o n s w h i l e w e a r e i n h e r e .
private func reconcile ( ) {
guard ! reconciling else {
reconcileAgain = true
return
}
reconciling = true
defer { reconciling = false }
repeat {
reconcileAgain = false
reconcileOnce ( )
} while reconcileAgain
}
private func reconcileOnce ( ) {
let screens = NSScreen . screens
let resolved = ScreenMatching . resolve ( desired , in : screens . map ( \ . localizedName ) )
let wanted = Dictionary ( uniqueKeysWithValues : resolved . map { ( screens [ $0 . key ] . displayID , $0 . value ) } )
@ -136,22 +155,32 @@ final class OutputController {
private func start ( _ source : Source , on displayID : CGDirectDisplayID ) {
guard let screen = NSScreen . screens . first ( where : { $0 . displayID = = displayID } ) else { return }
halt ( displayID )
// S w i t c h i n g s o u r c e s o n a s c r e e n k e e p s i t s 5 0 H z m o d e i n s t e a d o f b o u n c i n g t o 6 0 H z a n d b a c k .
let previous = outputs . removeValue ( forKey : displayID )
previous ? . receiver . stop ( )
let window = OutputWindow ( screen : screen )
let sink = window . sink
let receiverName = " Musubi \( screen . localizedName ) "
guard let receiver = NDIReceiver ( source : source , receiverName : receiverName , onFrame : { sink . display ( $0 ) } ) else {
Log . ndi . error ( " Could not create receiver for \( source . ndiName , privacy : . public ) " )
previous ? . window . orderOut ( nil )
if let original = previous ? . originalMode { DisplayModeSwitcher . restore ( original , on : displayID ) }
return
}
let mode = DisplayModeSwitcher . setRefreshRate ( Self . targetRefreshRate , on : displayID )
window . onCancel = { [ weak self ] in self ? . stop ( displayID ) }
window . setFrame ( screen . frame , display : true )
window . orderFrontRegardless ( )
previous ? . window . orderOut ( nil )
// R e g i s t e r b e f o r e t h e m o d e s w i t c h : i t p o s t s a s c r e e n - c h a n g e n o t i f i c a t i o n .
let output = ActiveOutput ( source : source , receiver : receiver , window : window , displayID : displayID , originalMode : previous ? . originalMode )
outputs [ displayID ] = output
let mode = DisplayModeSwitcher . setRefreshRate ( Self . targetRefreshRate , on : displayID )
if output . originalMode = = nil { output . originalMode = mode . original }
window . setFrame ( screen . frame , display : true )
outputs [ displayID ] = ActiveOutput ( source : source , receiver : receiver , window : window , displayID : displayID , originalMode : mode . original )
Log . ndi . info ( " Output \( source . ndiName , privacy : . public ) → \( screen . localizedName , privacy : . public ) @ \( mode . refreshRate ) Hz " )
updateSleepBlocker ( )
refreshScreens ( )