diff --git a/Azayaka.xcodeproj/project.pbxproj b/Azayaka.xcodeproj/project.pbxproj index 3013d94..a83bf96 100644 --- a/Azayaka.xcodeproj/project.pbxproj +++ b/Azayaka.xcodeproj/project.pbxproj @@ -101,7 +101,7 @@ attributes = { BuildIndependentTargetsInParallel = 1; LastSwiftUpdateCheck = 1410; - LastUpgradeCheck = 1410; + LastUpgradeCheck = 1500; TargetAttributes = { 17322D462958D07E00185BB6 = { CreatedOnToolsVersion = 14.1; @@ -159,6 +159,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -188,9 +189,11 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -219,6 +222,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; @@ -248,9 +252,11 @@ CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; COPY_PHASE_STRIP = NO; + DEAD_CODE_STRIPPING = YES; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = YES; GCC_C_LANGUAGE_STANDARD = gnu11; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -277,7 +283,8 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 20; + CURRENT_PROJECT_VERSION = 31; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 35BSP6SUG9; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -291,7 +298,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.1; PRODUCT_BUNDLE_IDENTIFIER = dev.mnpn.Azayaka; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -308,7 +315,8 @@ "CODE_SIGN_IDENTITY[sdk=macosx*]" = "Apple Development"; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 20; + CURRENT_PROJECT_VERSION = 31; + DEAD_CODE_STRIPPING = YES; DEVELOPMENT_TEAM = 35BSP6SUG9; ENABLE_HARDENED_RUNTIME = YES; GENERATE_INFOPLIST_FILE = YES; @@ -322,7 +330,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.0; + MARKETING_VERSION = 1.1; PRODUCT_BUNDLE_IDENTIFIER = dev.mnpn.Azayaka; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/Azayaka/AppDelegate.swift b/Azayaka/AppDelegate.swift index 6c67e99..5b06a8d 100644 --- a/Azayaka/AppDelegate.swift +++ b/Azayaka/AppDelegate.swift @@ -27,7 +27,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, SCStreamDelegate, SCStreamOu var screen: SCDisplay? var window: SCWindow? - let excludedWindows = ["", "com.apple.dock", "com.apple.controlcenter", "com.apple.notificationcenterui", "dev.mnpn.Azayaka", "com.gaosun.eul"] + let excludedWindows = ["", "com.apple.dock", "com.apple.controlcenter", "com.apple.notificationcenterui", "com.apple.systemuiserver", "com.apple.WindowManager", "dev.mnpn.Azayaka", "com.gaosun.eul", "com.pointum.hazeover", "net.matthewpalmer.Vanilla"] var statusItem: NSStatusItem! var menu = NSMenu() @@ -53,6 +53,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, SCStreamDelegate, SCStreamOu statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength) updateIcon() statusItem.menu = menu + menu.minimumWidth = 250 updateAvailableContent(buildMenu: true) } diff --git a/Azayaka/Menu.swift b/Azayaka/Menu.swift index 80be7a6..5586da4 100644 --- a/Azayaka/Menu.swift +++ b/Azayaka/Menu.swift @@ -11,34 +11,28 @@ extension AppDelegate: NSMenuDelegate { func createMenu() { menu.removeAllItems() menu.delegate = self - let centreText = NSMutableParagraphStyle() - centreText.alignment = .center - let title = NSMenuItem(title: "Title", action: nil, keyEquivalent: "") if isRecording { var typeText = "" if screen != nil { - typeText = "DISPLAY " + String((availableContent?.displays.firstIndex(where: { $0.displayID == screen?.displayID }))!+1) + typeText = "Display " + String((availableContent?.displays.firstIndex(where: { $0.displayID == screen?.displayID }))!+1) } else if window != nil { - typeText = window?.owningApplication?.applicationName.uppercased() ?? "A WINDOW" + typeText = window?.owningApplication?.applicationName.uppercased() ?? "A window" } else { - typeText = "SYSTEM AUDIO" + typeText = "System Audio" } - title.attributedTitle = NSMutableAttributedString(string: "RECORDING " + typeText, attributes: [.font: NSFont.systemFont(ofSize: 12, weight: .heavy), .paragraphStyle: centreText]) - menu.addItem(title) + menu.addItem(header("Recording " + typeText, size: 12)) + menu.addItem(NSMenuItem(title: "Stop Recording", action: #selector(stopRecording), keyEquivalent: "")) menu.addItem(info) } else { - title.attributedTitle = NSAttributedString(string: "SELECT CONTENT TO RECORD", attributes: [.font: NSFont.systemFont(ofSize: 12, weight: .heavy), .paragraphStyle: centreText]) - menu.addItem(title) + menu.addItem(header("Audio-only")) let audio = NSMenuItem(title: "System Audio", action: #selector(prepRecord), keyEquivalent: "") audio.identifier = NSUserInterfaceItemIdentifier(rawValue: "audio") menu.addItem(audio) - let displays = NSMenuItem(title: "Displays", action: nil, keyEquivalent: "") - displays.attributedTitle = NSAttributedString(string: "DISPLAYS", attributes: [.font: NSFont.systemFont(ofSize: 10, weight: .heavy)]) - menu.addItem(displays) + menu.addItem(header("Displays")) for (i, display) in availableContent!.displays.enumerated() { let displayItem = NSMenuItem(title: "Mondai", action: #selector(prepRecord), keyEquivalent: "") @@ -48,9 +42,8 @@ extension AppDelegate: NSMenuDelegate { menu.addItem(displayItem) } - let windows = NSMenuItem(title: "Windows", action: nil, keyEquivalent: "") - windows.attributedTitle = NSAttributedString(string: "WINDOWS", attributes: [NSAttributedString.Key.font: NSFont.systemFont(ofSize: 10, weight: .heavy)]) - menu.addItem(windows) + menu.addItem(header("Windows")) + noneAvailable.isHidden = true menu.addItem(noneAvailable) } @@ -81,17 +74,18 @@ extension AppDelegate: NSMenuDelegate { } func refreshWindows() { - noneAvailable.isHidden = true - let validWindows = availableContent!.windows.filter { !excludedWindows.contains($0.owningApplication!.bundleIdentifier) && !$0.title!.contains("Item-0") && !$0.title!.isEmpty } + DispatchQueue.main.async { self.noneAvailable.isHidden = true } + // in sonoma, there is a new new purple thing overlaying the traffic lights, I don't really want this to show up. + // its title is simply "Window", but its bundle id is the same as the parent, so this seems like a strange bodge.. + let validWindows = availableContent!.windows.filter { !excludedWindows.contains($0.owningApplication!.bundleIdentifier) && !$0.title!.contains("Item-0") && !$0.title!.isEmpty && $0.title != "Window" } let programIDs = validWindows.compactMap { $0.windowID.description } for window in menu.items.filter({ !programIDs.contains($0.title) && $0.identifier?.rawValue == "window" }) { menu.removeItem(window) } - usleep(10000) // -sigh- sometimes the menu can add/remove so fast that the text doesn't update until a hover. somehow this fixes that. + if validWindows.isEmpty { - noneAvailable.isHidden = false - sleep(2) // WTF? + DispatchQueue.main.async { self.noneAvailable.isHidden = false } return // nothing to add if no windows exist, so why bother } @@ -107,7 +101,9 @@ extension AppDelegate: NSMenuDelegate { win.attributedTitle = getFancyWindowString(window: window) win.title = String(window.windowID) win.identifier = NSUserInterfaceItemIdentifier("window") - menu.insertItem(win, at: menu.numberOfItems - 3) + DispatchQueue.main.async { [self] in + menu.insertItem(win, at: menu.numberOfItems - 3) + } } func getFancyWindowString(window: SCWindow) -> NSAttributedString { @@ -118,14 +114,21 @@ extension AppDelegate: NSMenuDelegate { return str } + func header(_ title: String, size: CGFloat = 10) -> NSMenuItem { + let headerItem: NSMenuItem + if #available(macOS 14.0, *) { + headerItem = NSMenuItem.sectionHeader(title: title.uppercased()) + } else { + headerItem = NSMenuItem(title: title, action: nil, keyEquivalent: "") + headerItem.attributedTitle = NSAttributedString(string: title.uppercased(), attributes: [.font: NSFont.systemFont(ofSize: size, weight: .heavy)]) + } + return headerItem + } + func menuWillOpen(_ menu: NSMenu) { if !isRecording { updateAvailableContent(buildMenu: false) - updateTimer?.invalidate() - updateTimer = Timer.scheduledTimer(withTimeInterval: 0.1, repeats: false) { _ in - self.updateMenu() - } - RunLoop.current.add(updateTimer!, forMode: .common) + self.updateMenu() } } diff --git a/Azayaka/Preferences.swift b/Azayaka/Preferences.swift index 2b33850..80ddd74 100644 --- a/Azayaka/Preferences.swift +++ b/Azayaka/Preferences.swift @@ -8,13 +8,13 @@ import SwiftUI struct Preferences: View { - @AppStorage("audioFormat") private var audioFormat: AudioFormat = .aac - @AppStorage("audioQuality") private var audioQuality: AudioQuality = .high - @AppStorage("frameRate") private var frameRate: Int = 60 - @AppStorage("videoFormat") private var videoFormat: VideoFormat = .mp4 - @AppStorage("encoder") private var encoder: Encoder = .h264 + @AppStorage("audioFormat") private var audioFormat: AudioFormat = .aac + @AppStorage("audioQuality") private var audioQuality: AudioQuality = .high + @AppStorage("frameRate") private var frameRate: Int = 60 + @AppStorage("videoFormat") private var videoFormat: VideoFormat = .mp4 + @AppStorage("encoder") private var encoder: Encoder = .h264 @AppStorage("saveDirectory") private var saveDirectory: String? - @AppStorage("hideSelf") private var hideSelf: Bool = false + @AppStorage("hideSelf") private var hideSelf: Bool = false var body: some View { VStack(alignment: .leading) { diff --git a/Azayaka/Processing.swift b/Azayaka/Processing.swift index 7b09e12..08cf0a7 100644 --- a/Azayaka/Processing.swift +++ b/Azayaka/Processing.swift @@ -10,15 +10,13 @@ import AVFAudio import ScreenCaptureKit // https://developer.apple.com/documentation/screencapturekit/capturing_screen_content_in_macos +// For Sonoma updated to https://developer.apple.com/forums/thread/727709 func createPCMBuffer(for sampleBuffer: CMSampleBuffer) -> AVAudioPCMBuffer? { - var ablPointer: UnsafePointer? - try? sampleBuffer.withAudioBufferList { audioBufferList, blockBuffer in - ablPointer = audioBufferList.unsafePointer + try? sampleBuffer.withAudioBufferList { audioBufferList, _ -> AVAudioPCMBuffer? in + guard let absd = sampleBuffer.formatDescription?.audioStreamBasicDescription else { return nil } + guard let format = AVAudioFormat(standardFormatWithSampleRate: absd.mSampleRate, channels: absd.mChannelsPerFrame) else { return nil } + return AVAudioPCMBuffer(pcmFormat: format, bufferListNoCopy: audioBufferList.unsafePointer) } - guard let audioBufferList = ablPointer, - let absd = sampleBuffer.formatDescription?.audioStreamBasicDescription, - let format = AVAudioFormat(standardFormatWithSampleRate: absd.mSampleRate, channels: absd.mChannelsPerFrame) else { return nil } - return AVAudioPCMBuffer(pcmFormat: format, bufferListNoCopy: audioBufferList) } extension AppDelegate { @@ -35,17 +33,19 @@ extension AppDelegate { filePath = "\(getFilePath()).\(fileEnding)" vW = try? AVAssetWriter.init(outputURL: URL(fileURLWithPath: filePath), fileType: fileType!) + let encoderIsH265 = ud.string(forKey: "encoder") == Encoder.h265.rawValue let fpsMultiplier: Double = Double(ud.integer(forKey: "frameRate"))/8 - let encoderMultiplier: Double = ud.string(forKey: "encoder") == Encoder.h265.rawValue ? 0.5 : 0.9 + let encoderMultiplier: Double = encoderIsH265 ? 0.5 : 0.9 + let targetBitrate = (Double(conf.width) * Double(conf.height) * fpsMultiplier * encoderMultiplier) let videoSettings: [String: Any] = [ - AVVideoCodecKey: ud.string(forKey: "encoder") == Encoder.h264.rawValue ? AVVideoCodecType.h264 : AVVideoCodecType.hevc, + AVVideoCodecKey: encoderIsH265 ? AVVideoCodecType.hevc : AVVideoCodecType.h264, // yes, not ideal if we want more than these encoders in the future, but it's ok for now AVVideoWidthKey: conf.width, AVVideoHeightKey: conf.height, AVVideoCompressionPropertiesKey: [ - AVVideoAverageBitRateKey: (Double(conf.width) * Double(conf.height) * fpsMultiplier * encoderMultiplier), + AVVideoAverageBitRateKey: targetBitrate, AVVideoExpectedSourceFrameRateKey: ud.integer(forKey: "frameRate") - ] + ] as [String : Any] ] vwInput = AVAssetWriterInput(mediaType: AVMediaType.video, outputSettings: videoSettings) awInput = AVAssetWriterInput(mediaType: AVMediaType.audio, outputSettings: audioSettings) diff --git a/Azayaka/Recording.swift b/Azayaka/Recording.swift index 17db467..01a4ada 100644 --- a/Azayaka/Recording.swift +++ b/Azayaka/Recording.swift @@ -95,7 +95,7 @@ extension AppDelegate { switch ud.string(forKey: "audioFormat") { case AudioFormat.aac.rawValue: audioSettings[AVFormatIDKey] = kAudioFormatMPEG4AAC - audioSettings[AVEncoderBitRateKey] = ud.integer(forKey: "audioQuality")*1000 + audioSettings[AVEncoderBitRateKey] = ud.integer(forKey: "audioQuality") * 1000 case AudioFormat.alac.rawValue: audioSettings[AVFormatIDKey] = kAudioFormatAppleLossless audioSettings[AVEncoderBitDepthHintKey] = 16 @@ -103,7 +103,7 @@ extension AppDelegate { audioSettings[AVFormatIDKey] = kAudioFormatFLAC case AudioFormat.opus.rawValue: audioSettings[AVFormatIDKey] = ud.string(forKey: "videoFormat") != VideoFormat.mp4.rawValue ? kAudioFormatOpus : kAudioFormatMPEG4AAC - audioSettings[AVEncoderBitRateKey] = ud.integer(forKey: "audioQuality")*1000 + audioSettings[AVEncoderBitRateKey] = ud.integer(forKey: "audioQuality") * 1000 default: assertionFailure("unknown audio format while setting audio settings: " + (ud.string(forKey: "audioFormat") ?? "[no defaults]")) }