You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
89 lines
2.8 KiB
89 lines
2.8 KiB
import AppKit
|
|
import AVFoundation
|
|
|
|
/// A borderless window covering one screen, above the menu bar, that shows a video layer.
|
|
@MainActor
|
|
public final class OutputWindow: NSWindow {
|
|
public let sink: VideoSink
|
|
/// Called when the operator presses Esc on the output.
|
|
public var onCancel: (() -> Void)?
|
|
|
|
public init(screen: NSScreen) {
|
|
let view = VideoView()
|
|
sink = VideoSink(renderer: view.displayLayer.sampleBufferRenderer)
|
|
super.init(contentRect: screen.frame, styleMask: .borderless, backing: .buffered, defer: false)
|
|
|
|
contentView = view
|
|
setFrame(screen.frame, display: false)
|
|
level = .screenSaver
|
|
collectionBehavior = [.canJoinAllSpaces, .stationary, .ignoresCycle, .fullScreenNone]
|
|
backgroundColor = .black
|
|
isOpaque = true
|
|
hasShadow = false
|
|
isReleasedWhenClosed = false
|
|
animationBehavior = .none
|
|
}
|
|
|
|
public override var canBecomeKey: Bool { true }
|
|
|
|
public override func orderFrontRegardless() {
|
|
super.orderFrontRegardless()
|
|
(contentView as? VideoView)?.hideCursorIfInside()
|
|
}
|
|
|
|
public override func orderOut(_ sender: Any?) {
|
|
(contentView as? VideoView)?.showCursor()
|
|
super.orderOut(sender)
|
|
}
|
|
|
|
public override func cancelOperation(_ sender: Any?) {
|
|
onCancel?()
|
|
}
|
|
}
|
|
|
|
/// Layer-hosting view backed by `AVSampleBufferDisplayLayer`; hides the cursor while it is inside.
|
|
@MainActor
|
|
final class VideoView: NSView {
|
|
let displayLayer = AVSampleBufferDisplayLayer()
|
|
private var cursorHidden = false
|
|
|
|
init() {
|
|
super.init(frame: .zero)
|
|
displayLayer.videoGravity = .resizeAspect
|
|
displayLayer.backgroundColor = NSColor.black.cgColor
|
|
layer = displayLayer
|
|
wantsLayer = true
|
|
}
|
|
|
|
@available(*, unavailable)
|
|
required init?(coder: NSCoder) { fatalError() }
|
|
|
|
override func updateTrackingAreas() {
|
|
super.updateTrackingAreas()
|
|
trackingAreas.forEach(removeTrackingArea)
|
|
addTrackingArea(NSTrackingArea(rect: .zero, options: [.mouseEnteredAndExited, .activeAlways, .inVisibleRect], owner: self))
|
|
}
|
|
|
|
override func mouseEntered(with event: NSEvent) { hideCursor() }
|
|
override func mouseExited(with event: NSEvent) { showCursor() }
|
|
|
|
/// The cursor may already rest on the screen when the output opens; no enter event comes then.
|
|
func hideCursorIfInside() {
|
|
guard let window, window.frame.contains(NSEvent.mouseLocation) else { return }
|
|
hideCursor()
|
|
}
|
|
|
|
// NSCursor.hide/unhide are counted, so keep them balanced.
|
|
private func hideCursor() {
|
|
guard !cursorHidden else { return }
|
|
NSCursor.hide()
|
|
cursorHidden = true
|
|
}
|
|
|
|
func showCursor() {
|
|
guard cursorHidden else { return }
|
|
NSCursor.unhide()
|
|
cursorHidden = false
|
|
}
|
|
}
|