Skip to content

Commit

Permalink
support Presenter Overlay on macOS 14.2 and above; improved blacklist…
Browse files Browse the repository at this point in the history
… selector, fix some issue
  • Loading branch information
lihaoyun6 committed Apr 28, 2024
1 parent d598381 commit 9c26358
Show file tree
Hide file tree
Showing 20 changed files with 656 additions and 363 deletions.
32 changes: 27 additions & 5 deletions QuickRecorder.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,11 @@
objects = {

/* Begin PBXBuildFile section */
184CAEC72BDCCC2300D61D57 /* AVContext.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CAEC62BDCCC2300D61D57 /* AVContext.swift */; };
184CAEC92BDDEC6800D61D57 /* AppBlockSelector.swift in Sources */ = {isa = PBXBuildFile; fileRef = 184CAEC82BDDEC6800D61D57 /* AppBlockSelector.swift */; };
1862BF8D2BD5494E003ED522 /* Credits.rtf in Resources */ = {isa = PBXBuildFile; fileRef = 1862BF8F2BD5494E003ED522 /* Credits.rtf */; };
187966FA2BD5639D003DB1B2 /* MousePointer.swift in Sources */ = {isa = PBXBuildFile; fileRef = 187966F92BD5639D003DB1B2 /* MousePointer.swift */; };
189BD5E62BDE34B80056D06C /* InfoPlist.strings in Resources */ = {isa = PBXBuildFile; fileRef = 189BD5E82BDE34B80056D06C /* InfoPlist.strings */; };
18D3BDEB2BCE5DC1006CFFC0 /* QuickRecorderApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D3BDEA2BCE5DC1006CFFC0 /* QuickRecorderApp.swift */; };
18D3BDED2BCE5DC1006CFFC0 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D3BDEC2BCE5DC1006CFFC0 /* ContentView.swift */; };
18D3BDEF2BCE5DC2006CFFC0 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 18D3BDEE2BCE5DC2006CFFC0 /* Assets.xcassets */; };
Expand All @@ -27,9 +30,12 @@
/* End PBXBuildFile section */

/* Begin PBXFileReference section */
184CAEC62BDCCC2300D61D57 /* AVContext.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AVContext.swift; sourceTree = "<group>"; };
184CAEC82BDDEC6800D61D57 /* AppBlockSelector.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppBlockSelector.swift; sourceTree = "<group>"; };
1862BF902BD5494F003ED522 /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = "zh-Hans"; path = "zh-Hans.lproj/Credits.rtf"; sourceTree = "<group>"; };
1862BF912BD5495D003ED522 /* Base */ = {isa = PBXFileReference; lastKnownFileType = text.rtf; name = Base; path = Base.lproj/Credits.rtf; sourceTree = "<group>"; };
187966F92BD5639D003DB1B2 /* MousePointer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MousePointer.swift; sourceTree = "<group>"; };
189BD5E72BDE34B80056D06C /* zh-Hans */ = {isa = PBXFileReference; lastKnownFileType = text.plist.strings; name = "zh-Hans"; path = "zh-Hans.lproj/InfoPlist.strings"; sourceTree = "<group>"; };
18D3BDE72BCE5DC1006CFFC0 /* QuickRecorder.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = QuickRecorder.app; sourceTree = BUILT_PRODUCTS_DIR; };
18D3BDEA2BCE5DC1006CFFC0 /* QuickRecorderApp.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QuickRecorderApp.swift; sourceTree = "<group>"; };
18D3BDEC2BCE5DC1006CFFC0 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -63,6 +69,7 @@
18D3BDDE2BCE5DC1006CFFC0 = {
isa = PBXGroup;
children = (
189BD5E82BDE34B80056D06C /* InfoPlist.strings */,
18D3BDE92BCE5DC1006CFFC0 /* QuickRecorder */,
18D3BDE82BCE5DC1006CFFC0 /* Products */,
);
Expand All @@ -80,8 +87,9 @@
isa = PBXGroup;
children = (
18D3BDEA2BCE5DC1006CFFC0 /* QuickRecorderApp.swift */,
18D3BE032BCEC847006CFFC0 /* SCContext.swift */,
18FEBDA92BCF8200003F09BC /* RecordEngine.swift */,
18D3BE032BCEC847006CFFC0 /* SCContext.swift */,
184CAEC62BDCCC2300D61D57 /* AVContext.swift */,
18D3BE022BCEB1D4006CFFC0 /* ViewModel */,
18D3BDFD2BCE5DF5006CFFC0 /* Localizable.strings */,
1862BF8F2BD5494E003ED522 /* Credits.rtf */,
Expand Down Expand Up @@ -112,6 +120,7 @@
18F1A0E12BD3E4C000DB102C /* AreaSelector.swift */,
187966F92BD5639D003DB1B2 /* MousePointer.swift */,
18FDFC8D2BDA435F0020E685 /* ScreenMagnifier.swift */,
184CAEC82BDDEC6800D61D57 /* AppBlockSelector.swift */,
);
path = ViewModel;
sourceTree = "<group>";
Expand Down Expand Up @@ -185,6 +194,7 @@
18D3BDFB2BCE5DF5006CFFC0 /* Localizable.strings in Resources */,
18D3BDEF2BCE5DC2006CFFC0 /* Assets.xcassets in Resources */,
1862BF8D2BD5494E003ED522 /* Credits.rtf in Resources */,
189BD5E62BDE34B80056D06C /* InfoPlist.strings in Resources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand All @@ -199,6 +209,8 @@
18F1A0E22BD3E4C000DB102C /* AreaSelector.swift in Sources */,
18FDFC8E2BDA435F0020E685 /* ScreenMagnifier.swift in Sources */,
187966FA2BD5639D003DB1B2 /* MousePointer.swift in Sources */,
184CAEC92BDDEC6800D61D57 /* AppBlockSelector.swift in Sources */,
184CAEC72BDCCC2300D61D57 /* AVContext.swift in Sources */,
18D3BDFF2BCE5E4B006CFFC0 /* StatusBar.swift in Sources */,
18D3BE042BCEC847006CFFC0 /* SCContext.swift in Sources */,
18D3BDED2BCE5DC1006CFFC0 /* ContentView.swift in Sources */,
Expand All @@ -222,6 +234,14 @@
name = Credits.rtf;
sourceTree = "<group>";
};
189BD5E82BDE34B80056D06C /* InfoPlist.strings */ = {
isa = PBXVariantGroup;
children = (
189BD5E72BDE34B80056D06C /* zh-Hans */,
);
name = InfoPlist.strings;
sourceTree = "<group>";
};
18D3BDFD2BCE5DF5006CFFC0 /* Localizable.strings */ = {
isa = PBXVariantGroup;
children = (
Expand Down Expand Up @@ -359,21 +379,22 @@
CODE_SIGN_ENTITLEMENTS = QuickRecorder/QuickRecorder.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 108;
CURRENT_PROJECT_VERSION = 110;
DEVELOPMENT_ASSET_PATHS = "\"QuickRecorder/Preview Content\"";
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSCameraUsageDescription = "QuickRecorder needs this permission to record your camera alongside your display's content.";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 lihaoyun6. All rights reserved.";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "QuickRecorder needs this permission to record your microphone alongside your display's content.";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.3;
MARKETING_VERSION = 1.0.8;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.lihaoyun6.QuickRecorder;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand All @@ -389,21 +410,22 @@
CODE_SIGN_ENTITLEMENTS = QuickRecorder/QuickRecorder.entitlements;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 108;
CURRENT_PROJECT_VERSION = 110;
DEVELOPMENT_ASSET_PATHS = "\"QuickRecorder/Preview Content\"";
DEVELOPMENT_TEAM = "";
ENABLE_HARDENED_RUNTIME = YES;
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_KEY_LSApplicationCategoryType = "public.app-category.utilities";
INFOPLIST_KEY_NSCameraUsageDescription = "QuickRecorder needs this permission to record your camera alongside your display's content.";
INFOPLIST_KEY_NSHumanReadableCopyright = "Copyright © 2024 lihaoyun6. All rights reserved.";
INFOPLIST_KEY_NSMicrophoneUsageDescription = "QuickRecorder needs this permission to record your microphone alongside your display's content.";
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/../Frameworks",
);
MACOSX_DEPLOYMENT_TARGET = 12.3;
MARKETING_VERSION = 1.0.8;
MARKETING_VERSION = 1.1.0;
PRODUCT_BUNDLE_IDENTIFIER = com.lihaoyun6.QuickRecorder;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_EMIT_LOC_STRINGS = YES;
Expand Down
41 changes: 41 additions & 0 deletions QuickRecorder/AVContext.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
//
// AVContext.swift
// QuickRecorder
//
// Created by apple on 2024/4/27.
//

import Foundation
import AVFoundation

extension AppDelegate {
func recordingCamera(withName: String) {
SCContext.captureSession = AVCaptureSession()

let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera, .externalUnknown], mediaType: .video, position: .unspecified)
guard let camera = discoverySession.devices.first(where: { $0.localizedName == withName }),
let input = try? AVCaptureDeviceInput(device: camera),
SCContext.captureSession.canAddInput(input) else {
print("Failed to set up camera")
return
}
SCContext.captureSession.addInput(input)

let videoOutput = AVCaptureVideoDataOutput()
videoOutput.setSampleBufferDelegate(self, queue: .global())

if SCContext.captureSession.canAddOutput(videoOutput) {
SCContext.captureSession.addOutput(videoOutput)
}

SCContext.captureSession.startRunning()
}

func captureOutput(_ output: AVCaptureOutput, didOutput sampleBuffer: CMSampleBuffer, from connection: AVCaptureConnection) {
if !SCContext.isPaused && UserDefaults.standard.string(forKey: "recordCam") != "Disabled".local {
//if sampleBuffer.isValid { SCContext.isCameraReady = true }
//if sampleBuffer.imageBuffer != nil { SCContext.frameCache = sampleBuffer }
}
}
}

2 changes: 2 additions & 0 deletions QuickRecorder/QuickRecorder.entitlements
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,7 @@
<dict>
<key>com.apple.security.device.audio-input</key>
<true/>
<key>com.apple.security.device.camera</key>
<true/>
</dict>
</plist>
18 changes: 14 additions & 4 deletions QuickRecorder/QuickRecorderApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import UserNotifications
import KeyboardShortcuts
import ServiceManagement

var isSonoma = false
var firstRun = true
let ud = UserDefaults.standard
var statusMenu: NSMenu = NSMenu()
Expand Down Expand Up @@ -73,8 +74,11 @@ extension Scene {
}
}

class AppDelegate: NSObject, NSApplicationDelegate, SCStreamDelegate, SCStreamOutput {
class AppDelegate: NSObject, NSApplicationDelegate, SCStreamDelegate, SCStreamOutput, AVCaptureVideoDataOutputSampleBufferDelegate {
var filter: SCContentFilter?
var isCameraReady = false
var isPresenterON = false
var presenterType = "no"

func mousePointerReLocation(event: NSEvent) {
if event.type == .scrollWheel { return }
Expand Down Expand Up @@ -130,13 +134,14 @@ class AppDelegate: NSObject, NSApplicationDelegate, SCStreamDelegate, SCStreamOu
}

func applicationDidFinishLaunching(_ aNotification: Notification) {
if #available(macOS 14.2, *) { isSonoma = true }

SCContext.updateAvailableContent{ print("available content has been updated") }
lazy var userDesktop = (NSSearchPathForDirectoriesInDomains(.desktopDirectory, .userDomainMask, true) as [String]).first!
let saveDirectory = (UserDefaults(suiteName: "com.apple.screencapture")?.string(forKey: "location") ?? userDesktop) as NSString

ud.register( // default defaults (used if not set)
defaults: [
"appBlackList": "",
"audioFormat": AudioFormat.aac.rawValue,
"audioQuality": AudioQuality.high.rawValue,
"background": BackgroundType.wallpaper.rawValue,
Expand Down Expand Up @@ -176,12 +181,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, SCStreamDelegate, SCStreamOu
KeyboardShortcuts.onKeyDown(for: .screenMagnifier) { [] in SCContext.showMagnifier.toggle() }
KeyboardShortcuts.onKeyDown(for: .stop) { [] in if SCContext.stream != nil { SCContext.stopRecording() }}
KeyboardShortcuts.onKeyDown(for: .pauseResume) { [] in if SCContext.stream != nil { SCContext.pauseRecording() }}
KeyboardShortcuts.onKeyDown(for: .startWithAudio) {[self] in
for w in NSApp.windows { w.close() }
prepRecord(type: "audio", screens: SCContext.getSCDisplayWithMouse(), windows: nil, applications: nil, fastStart: true)
}
KeyboardShortcuts.onKeyDown(for: .startWithScreen) {[self] in
for w in NSApplication.shared.windows.filter({ $0.title == "QuickReader".local }) { w.close() }
for w in NSApp.windows { w.close() }
prepRecord(type: "display", screens: SCContext.getSCDisplayWithMouse(), windows: nil, applications: nil, fastStart: true)
}
KeyboardShortcuts.onKeyDown(for: .startWithWindow) { [self] in
for w in NSApplication.shared.windows.filter({ $0.title == "QuickReader".local }) { w.close() }
for w in NSApp.windows { w.close() }
let frontmostApp = NSWorkspace.shared.frontmostApplication
if let pid = frontmostApp?.processIdentifier {
let options: CGWindowListOption = .optionOnScreenOnly
Expand All @@ -203,6 +212,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, SCStreamDelegate, SCStreamOu
UNUserNotificationCenter.current().requestAuthorization(options: [.alert, .sound, .badge]) { granted, error in
if let error = error { print("Notification authorization denied: \(error.localizedDescription)") }
}

if #available(macOS 13, *) {
if firstRun && (SMAppService.mainApp.status == .enabled) {
firstRun = false
Expand Down
51 changes: 43 additions & 8 deletions QuickRecorder/RecordEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,11 +44,12 @@ extension AppDelegate {
let dockWindow = SCContext.availableContent!.windows.first(where: { $0.title == "Dock" && $0.owningApplication?.bundleIdentifier == "com.apple.dock" })
let desktopFiles = SCContext.availableContent!.windows.filter({ $0.title == "" && $0.owningApplication?.bundleIdentifier == "com.apple.finder" })
let mouseWindow = SCContext.availableContent!.windows.filter({ $0.title == "Mouse Pointer".local && $0.owningApplication?.bundleIdentifier == Bundle.main.bundleIdentifier })
let appBlackList = ud.string(forKey: "appBlackList")?.split(separator: ",").map{ $0.trimmingCharacters(in: .whitespacesAndNewlines) }
let excliudedApps = SCContext.availableContent!.applications.filter({
guard let apps = appBlackList else { return false }
return apps.contains($0.applicationName)
})
var appBlackList = [String]()
if let savedData = UserDefaults.standard.data(forKey: "hiddenApps"),
let decodedApps = try? JSONDecoder().decode([AppInfo].self, from: savedData) {
appBlackList = (decodedApps as [AppInfo]).map({ $0.bundleID })
}
let excliudedApps = SCContext.availableContent!.applications.filter({ appBlackList.contains($0.bundleIdentifier) })

if SCContext.streamType == .window || SCContext.streamType == .windows {
if var includ = SCContext.window {
Expand Down Expand Up @@ -160,6 +161,7 @@ extension AppDelegate {
try SCContext.stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: .global())
if #available(macOS 13, *) { try SCContext.stream.addStreamOutput(self, type: .audio, sampleHandlerQueue: .global()) }
if !audioOnly { initVideo(conf: conf) } else { SCContext.startTime = Date.now }
if !audioOnly && SCContext.recordCam != "Disabled".local { recordingCamera(withName: SCContext.recordCam) }
try await SCContext.stream.startCapture()
} catch {
assertionFailure("capture failed".local)
Expand Down Expand Up @@ -261,6 +263,20 @@ extension AppDelegate {
SCContext.vW.startWriting()
}

func outputVideoEffectDidStart(for stream: SCStream) {
print("[Presenter Overlay ON]")
isPresenterON = true
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.isCameraReady = true
}
}

func outputVideoEffectDidStop(for stream: SCStream) {
print("[Presenter Overlay OFF]")
isPresenterON = false
isCameraReady = false
}

func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of outputType: SCStreamOutputType) {
if SCContext.saveFrame && sampleBuffer.imageBuffer != nil {
SCContext.saveFrame = false
Expand Down Expand Up @@ -295,7 +311,7 @@ extension AppDelegate {
guard let statusRawValue = attachments[SCStreamFrameInfo.status] as? Int,
let status = SCFrameStatus(rawValue: statusRawValue),
status == .complete else { return }

if SCContext.vW != nil && SCContext.vW?.status == .writing, SCContext.startTime == nil {
SCContext.startTime = Date.now
SCContext.vW.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(SampleBuffer))
Expand All @@ -305,8 +321,27 @@ extension AppDelegate {
let dur = CMSampleBufferGetDuration(SampleBuffer)
if (dur.value > 0) { pts = CMTimeAdd(pts, dur) }
SCContext.lastPTS = pts
if SCContext.vwInput.isReadyForMoreMediaData { SCContext.vwInput.append(SampleBuffer) }

if SCContext.vwInput.isReadyForMoreMediaData {
if #available(macOS 14.2, *) {
if let rect = attachments[.presenterOverlayContentRect] as? [String: Any]{
var type = "np"
let off = (rect["X"] as! CGFloat == .infinity)
let small = (rect["X"] as! CGFloat == 0.0)
let big = (!off && !small)
if off { type = "OFF" } else if small { type = "Small" } else if big { type = "Big" }
if type != presenterType {
print("Presenter Overlay set to \"\(type)\"!")
isCameraReady = false
DispatchQueue.main.asyncAfter(deadline: .now() + 1) {
self.isCameraReady = true
}
presenterType = type
}
}
}
if isPresenterON && !isCameraReady { break }
SCContext.vwInput.append(SampleBuffer)
}
break
case .audio:
if SCContext.streamType == .systemaudio { // write directly to file if not video recording
Expand Down
17 changes: 13 additions & 4 deletions QuickRecorder/SCContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@ import ScreenCaptureKit
import UserNotifications

class SCContext {
static var recordCam = "Disabled".local
static var captureSession: AVCaptureSession!
static var frameCache: CMSampleBuffer?
static var filter: SCContentFilter?
static var audioSettings: [String : Any]!
static var frameCache: CMSampleBuffer?
static var showMagnifier = false
static var saveFrame = false
static var isPaused = false
static var isResume = false
static var isSkipFrame = false
static var lastPTS: CMTime?
static var lsatPts: CMTime?
static var timeOffset = CMTimeMake(value: 0, timescale: 0)
Expand Down Expand Up @@ -232,9 +235,8 @@ class SCContext {
if let monitor = mouseMonitor { NSEvent.removeMonitor(monitor) }

if let w = NSApplication.shared.windows.first(where: { $0.title == "Area Overlayer".local }) { w.close() }
if stream != nil {
stream.stopCapture()
}

if stream != nil { stream.stopCapture() }
stream = nil
if streamType != .systemaudio {
let dispatchGroup = DispatchGroup()
Expand All @@ -248,6 +250,7 @@ class SCContext {
}
vW.finishWriting {
startTime = nil
if let sesson = captureSession { if sesson.isRunning { sesson.stopRunning() }}
dispatchGroup.leave()
}
dispatchGroup.wait()
Expand All @@ -274,6 +277,12 @@ class SCContext {
}
}

static func getCameras() -> [String] {
let discoverySession = AVCaptureDevice.DiscoverySession(deviceTypes: [.builtInWideAngleCamera, .externalUnknown], mediaType: .video, position: .unspecified)
let cameras = ["Disabled".local] + discoverySession.devices.map { $0.localizedName }
return cameras
}

static func adjustTime(sample: CMSampleBuffer, by offset: CMTime) -> CMSampleBuffer? {
guard CMSampleBufferGetFormatDescription(sample) != nil else { return nil }

Expand Down
Loading

0 comments on commit 9c26358

Please sign in to comment.