diff --git a/QuickRecorder.xcodeproj/project.pbxproj b/QuickRecorder.xcodeproj/project.pbxproj index d4b2e09..daba654 100644 --- a/QuickRecorder.xcodeproj/project.pbxproj +++ b/QuickRecorder.xcodeproj/project.pbxproj @@ -347,7 +347,7 @@ CODE_SIGN_ENTITLEMENTS = QuickRecorder/QuickRecorder.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 103; + CURRENT_PROJECT_VERSION = 104; DEVELOPMENT_ASSET_PATHS = "\"QuickRecorder/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; @@ -361,7 +361,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.0.3; + MARKETING_VERSION = 1.0.4; PRODUCT_BUNDLE_IDENTIFIER = com.lihaoyun6.QuickRecorder; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -377,7 +377,7 @@ CODE_SIGN_ENTITLEMENTS = QuickRecorder/QuickRecorder.entitlements; CODE_SIGN_STYLE = Automatic; COMBINE_HIDPI_IMAGES = YES; - CURRENT_PROJECT_VERSION = 103; + CURRENT_PROJECT_VERSION = 104; DEVELOPMENT_ASSET_PATHS = "\"QuickRecorder/Preview Content\""; DEVELOPMENT_TEAM = ""; ENABLE_HARDENED_RUNTIME = YES; @@ -391,7 +391,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 13.0; - MARKETING_VERSION = 1.0.3; + MARKETING_VERSION = 1.0.4; PRODUCT_BUNDLE_IDENTIFIER = com.lihaoyun6.QuickRecorder; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/QuickRecorder/QuickRecorderApp.swift b/QuickRecorder/QuickRecorderApp.swift index cc3f653..8d011cd 100644 --- a/QuickRecorder/QuickRecorderApp.swift +++ b/QuickRecorder/QuickRecorderApp.swift @@ -28,12 +28,13 @@ struct QuickRecorderApp: App { var body: some Scene { WindowGroup { ContentView() - .navigationTitle("Main Window".local) + .navigationTitle("QuickReader".local) .fixedSize() .onAppear { setMainWindow() } } - .windowStyle(HiddenTitleBarWindowStyle()) + //.windowStyle(HiddenTitleBarWindowStyle()) .windowResizability(.contentSize) + .commands { CommandGroup(replacing: .newItem) {} } Settings { SettingsView() @@ -42,7 +43,7 @@ struct QuickRecorderApp: App { } func setMainWindow() { - for w in NSApplication.shared.windows.filter({ $0.title == "Main Window".local }) { + for w in NSApplication.shared.windows.filter({ $0.title == "QuickReader".local }) { w.level = .floating w.styleMask = [.fullSizeContentView] w.isMovableByWindowBackground = true @@ -110,15 +111,16 @@ class AppDelegate: NSObject, NSApplicationDelegate, SCStreamDelegate, SCStreamOu "frameRate": 60, "highRes": 2, "hideSelf": true, + "highlightMouse" : false, "hideDesktopFiles": false, + "includeMenuBar": true, "videoQuality": 1.0, "videoFormat": VideoFormat.mp4.rawValue, "encoder": Encoder.h264.rawValue, "saveDirectory": saveDirectory, "showMouse": true, "recordMic": false, - "recordWinSound": true, - "highlightMouse" : true + "recordWinSound": true ] ) diff --git a/QuickRecorder/RecordEngine.swift b/QuickRecorder/RecordEngine.swift index c49379f..9a3c5b1 100644 --- a/QuickRecorder/RecordEngine.swift +++ b/QuickRecorder/RecordEngine.swift @@ -37,7 +37,8 @@ extension AppDelegate { SCContext.application = SCContext.availableContent!.applications.filter({ applications.contains($0) }) } else { if SCContext.streamType == .application { SCContext.streamType = nil; return } } - let quickrRecorder = SCContext.getSelf() + let qrSelf = SCContext.getSelf() + let qrWindows = SCContext.getSelfWindows() let dockApp = SCContext.availableContent!.applications.first(where: { $0.bundleIdentifier.description == "com.apple.dock" }) let wallpaper = SCContext.availableContent!.windows.filter({ $0.title != "Dock" && $0.owningApplication?.bundleIdentifier == "com.apple.dock" }) let dockWindow = SCContext.availableContent!.windows.first(where: { $0.title == "Dock" && $0.owningApplication?.bundleIdentifier == "com.apple.dock" }) @@ -49,7 +50,8 @@ extension AppDelegate { if includ.count > 1 { if ud.bool(forKey: "highlightMouse") { includ += mouseWindow } if ud.string(forKey: "background") == BackgroundType.wallpaper.rawValue { if dockApp != nil { includ += wallpaper }} - filter = SCContentFilter(display: SCContext.screen!, including: includ) + filter = SCContentFilter(display: SCContext.screen ?? SCContext.getSCDisplayWithMouse()!, including: includ) + if #available(macOS 14.2, *) { filter?.includeMenuBar = ud.bool(forKey: "includeMenuBar") } } else { SCContext.streamType = .window filter = SCContentFilter(desktopIndependentWindow: includ[0]) @@ -59,20 +61,22 @@ extension AppDelegate { if SCContext.streamType == .screen || SCContext.streamType == .screenarea || SCContext.streamType == .systemaudio { let excluded = [SCRunningApplication]() var except = [SCWindow]() - //if ud.bool(forKey: "hideSelf") { if let qrSelf = SCContext.getSelf() { excluded.append(qrSelf) }} + if ud.bool(forKey: "hideSelf") { if let qrWindows = qrWindows { except += qrWindows }} if ud.bool(forKey: "removeWallpaper") { if dockApp != nil { except += wallpaper}} if ud.bool(forKey: "hideDesktopFiles") { except += desktopFiles } - filter = SCContentFilter(display: SCContext.screen ?? SCContext.availableContent!.displays.first!, excludingApplications: excluded, exceptingWindows: except) + filter = SCContentFilter(display: SCContext.screen ?? SCContext.getSCDisplayWithMouse()!, excludingApplications: excluded, exceptingWindows: except) + if #available(macOS 14.2, *) { filter?.includeMenuBar = (SCContext.streamType == .screen && ud.bool(forKey: "includeMenuBar")) } } if SCContext.streamType == .application { var includ = SCContext.application! var except = [SCWindow]() let withFinder = includ.map{ $0.bundleIdentifier }.contains("com.apple.finder") if withFinder && ud.bool(forKey: "hideDesktopFiles") { except += desktopFiles } - if ud.bool(forKey: "highlightMouse") { if let qr = quickrRecorder { includ.append(qr) }} + if ud.bool(forKey: "hideSelf") { if let qrWindows = qrWindows { except += qrWindows }} + if ud.bool(forKey: "highlightMouse") { if let qrSelf = qrSelf { includ.append(qrSelf) }} if ud.string(forKey: "background") == BackgroundType.wallpaper.rawValue { if let dock = dockApp { includ.append(dock); except.append(dockWindow!)}} - filter = SCContentFilter(display: SCContext.screen ?? SCContext.availableContent!.displays.first!, including: includ, exceptingWindows: except) - + filter = SCContentFilter(display: SCContext.screen ?? SCContext.getSCDisplayWithMouse()!, including: includ, exceptingWindows: except) + if #available(macOS 14.2, *) { filter?.includeMenuBar = ud.bool(forKey: "includeMenuBar") } } } if SCContext.streamType == .systemaudio { prepareAudioRecording() } @@ -80,13 +84,38 @@ extension AppDelegate { } func record(audioOnly: Bool, filter: SCContentFilter) async { + SCContext.timeOffset = CMTimeMake(value: 0, timescale: 0) + SCContext.isPaused = false + SCContext.isResume = false + let conf = SCStreamConfiguration() conf.width = 2 conf.height = 2 if !audioOnly { - conf.width = Int(filter.contentRect.width) * (ud.integer(forKey: "highRes") == 2 ? Int(filter.pointPixelScale) : 1) - conf.height = Int(filter.contentRect.height) * (ud.integer(forKey: "highRes") == 2 ? Int(filter.pointPixelScale) : 1) + if #available(macOS 14.0, *) { + conf.width = Int(filter.contentRect.width) * (ud.integer(forKey: "highRes") == 2 ? Int(filter.pointPixelScale) : 1) + conf.height = Int(filter.contentRect.height) * (ud.integer(forKey: "highRes") == 2 ? Int(filter.pointPixelScale) : 1) + } else { + guard let pointPixelScale = (SCContext.screen ?? SCContext.getSCDisplayWithMouse()!).nsScreen?.backingScaleFactor else { return } + if SCContext.streamType == .application || SCContext.streamType == .windows || SCContext.streamType == .screen { + let frame = (SCContext.screen ?? SCContext.getSCDisplayWithMouse()!).frame + conf.width = Int(frame.width) + conf.height = Int(frame.height) + } + if SCContext.streamType == .window { + let frame = SCContext.window![0].frame + conf.width = Int(frame.width) + conf.height = Int(frame.height) + } + if SCContext.streamType == .screenarea { + let frame = SCContext.screenArea! + conf.width = Int(frame.width) + conf.height = Int(frame.height) + } + conf.width = conf.width * (ud.integer(forKey: "highRes") == 2 ? Int(pointPixelScale) : 1) + conf.height = conf.height * (ud.integer(forKey: "highRes") == 2 ? Int(pointPixelScale) : 1) + } if ud.integer(forKey: "highRes") == 0 { conf.width = Int(conf.width/2) conf.height = Int(conf.height/2) @@ -118,11 +147,7 @@ extension AppDelegate { do { try SCContext.stream.addStreamOutput(self, type: .screen, sampleHandlerQueue: .global()) try SCContext.stream.addStreamOutput(self, type: .audio, sampleHandlerQueue: .global()) - if !audioOnly { - initVideo(conf: conf) - } else { - SCContext.startTime = Date.now - } + if !audioOnly { initVideo(conf: conf) } else { SCContext.startTime = Date.now } try await SCContext.stream.startCapture() } catch { assertionFailure("capture failed".local) @@ -227,10 +252,23 @@ extension AppDelegate { func stream(_ stream: SCStream, didOutputSampleBuffer sampleBuffer: CMSampleBuffer, of outputType: SCStreamOutputType) { if SCContext.isPaused { return } guard sampleBuffer.isValid else { return } + var SampleBuffer = sampleBuffer + if SCContext.isResume { + SCContext.isResume = false + var pts = CMSampleBufferGetPresentationTimeStamp(SampleBuffer) + guard let last = SCContext.lastPTS else { return } + if last.flags.contains(CMTimeFlags.valid) { + if SCContext.timeOffset.flags.contains(CMTimeFlags.valid) { pts = CMTimeSubtract(pts, SCContext.timeOffset) } + let off = CMTimeSubtract(pts, last) + print("adding \(CMTimeGetSeconds(off)) to \(CMTimeGetSeconds(SCContext.timeOffset)) (pts \(CMTimeGetSeconds(SCContext.timeOffset)))") + if SCContext.timeOffset.value == 0 { SCContext.timeOffset = off } else { SCContext.timeOffset = CMTimeAdd(SCContext.timeOffset, off) } + } + SCContext.lastPTS?.flags = [] + } switch outputType { case .screen: if (SCContext.screen == nil && SCContext.window == nil && SCContext.application == nil) || SCContext.streamType == .systemaudio { break } - guard let attachmentsArray = CMSampleBufferGetSampleAttachmentsArray(sampleBuffer, createIfNecessary: false) as? [[SCStreamFrameInfo: Any]], + guard let attachmentsArray = CMSampleBufferGetSampleAttachmentsArray(SampleBuffer, createIfNecessary: false) as? [[SCStreamFrameInfo: Any]], let attachments = attachmentsArray.first else { return } guard let statusRawValue = attachments[SCStreamFrameInfo.status] as? Int, let status = SCFrameStatus(rawValue: statusRawValue), @@ -238,25 +276,29 @@ extension AppDelegate { if SCContext.vW != nil && SCContext.vW?.status == .writing, SCContext.startTime == nil { SCContext.startTime = Date.now - SCContext.vW.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(sampleBuffer)) - } - - if SCContext.vwInput.isReadyForMoreMediaData { - SCContext.vwInput.append(sampleBuffer) + SCContext.vW.startSession(atSourceTime: CMSampleBufferGetPresentationTimeStamp(SampleBuffer)) } + if (SCContext.timeOffset.value > 0) { SampleBuffer = SCContext.adjustTime(sample: SampleBuffer, by: SCContext.timeOffset) ?? sampleBuffer } + var pts = CMSampleBufferGetPresentationTimeStamp(SampleBuffer) + let dur = CMSampleBufferGetDuration(SampleBuffer) + if (dur.value > 0) { pts = CMTimeAdd(pts, dur) } + SCContext.lastPTS = pts + if SCContext.vwInput.isReadyForMoreMediaData { SCContext.vwInput.append(SampleBuffer) } + break case .audio: if SCContext.streamType == .systemaudio { // write directly to file if not video recording - guard let samples = sampleBuffer.asPCMBuffer else { return } - do { - try SCContext.audioFile?.write(from: samples) - } - catch { assertionFailure("audio file writing issue".local) } - } else { // otherwise send the audio data to AVAssetWriter - if SCContext.awInput.isReadyForMoreMediaData { - SCContext.awInput.append(sampleBuffer) - } - } + guard let samples = SampleBuffer.asPCMBuffer else { return } + do { try SCContext.audioFile?.write(from: samples) } + catch { assertionFailure("audio file writing issue".local) } + } else { // otherwise send the audio data to AVAssetWriter + if (SCContext.timeOffset.value > 0) { SampleBuffer = SCContext.adjustTime(sample: SampleBuffer, by: SCContext.timeOffset) ?? sampleBuffer } + var pts = CMSampleBufferGetPresentationTimeStamp(SampleBuffer) + let dur = CMSampleBufferGetDuration(SampleBuffer) + if (dur.value > 0) { pts = CMTimeAdd(pts, dur) } + SCContext.lastPTS = pts + if SCContext.awInput.isReadyForMoreMediaData { SCContext.awInput.append(SampleBuffer) } + } @unknown default: assertionFailure("unknown stream type".local) } diff --git a/QuickRecorder/SCContext.swift b/QuickRecorder/SCContext.swift index 878809e..8b9615c 100644 --- a/QuickRecorder/SCContext.swift +++ b/QuickRecorder/SCContext.swift @@ -14,6 +14,10 @@ import UserNotifications class SCContext { static var audioSettings: [String : Any]! static var isPaused = false + static var isResume = false + static var lastPTS: CMTime? + static var lsatPts: CMTime? + static var timeOffset = CMTimeMake(value: 0, timescale: 0) static var screenArea: NSRect? static let audioEngine = AVAudioEngine() static var backgroundColor: CGColor = CGColor.black @@ -51,6 +55,13 @@ class SCContext { return getApps(isOnScreen: false, hideSelf: false).first(where: { Bundle.main.bundleIdentifier == $0.bundleIdentifier }) } + static func getSelfWindows() -> [SCWindow]? { + return SCContext.availableContent!.windows.filter( { + guard let title = $0.title else { return false } + return $0.owningApplication?.bundleIdentifier == Bundle.main.bundleIdentifier && title != "Mouse Pointer".local + }) + } + static func getApps(isOnScreen: Bool = true, hideSelf: Bool = true) -> [SCRunningApplication] { var apps = [SCRunningApplication]() for app in getWindows(isOnScreen: isOnScreen, hideSelf: hideSelf).map({ $0.owningApplication }) { @@ -89,6 +100,12 @@ class SCContext { return icon } + static func getScreenWithMouse() -> NSScreen? { + let mouseLocation = NSEvent.mouseLocation + let screenWithMouse = NSScreen.screens.first(where: { NSMouseInRect(mouseLocation, $0.frame, false) }) + return screenWithMouse + } + static func getSCDisplayWithMouse() -> SCDisplay? { if let displays = availableContent?.displays { for display in displays { @@ -156,13 +173,6 @@ class SCContext { } } - static func getScreenWithMouse() -> NSScreen? { - let mouseLocation = NSEvent.mouseLocation - let screens = NSScreen.screens - let screenWithMouse = (screens.first { NSMouseInRect(mouseLocation, $0.frame, false) }) - return screenWithMouse - } - private static func requestPermissions() { DispatchQueue.main.async { let alert = NSAlert() @@ -205,7 +215,10 @@ class SCContext { static func pauseRecording() { isPaused.toggle() - if !isPaused { startTime = Date.now - SCContext.timePassed } + if !isPaused { + isResume = true + startTime = Date.now.addingTimeInterval(-1) - SCContext.timePassed + } } static func stopRecording() { @@ -255,4 +268,22 @@ class SCContext { if let error = error { print("Notification failed to send:\(error.localizedDescription)") } } } + + static func adjustTime(sample: CMSampleBuffer, by offset: CMTime) -> CMSampleBuffer? { + guard CMSampleBufferGetFormatDescription(sample) != nil else { return nil } + + var timingInfo = [CMSampleTimingInfo](repeating: CMSampleTimingInfo(), count: Int(CMSampleBufferGetNumSamples(sample))) + CMSampleBufferGetSampleTimingInfoArray(sample, entryCount: timingInfo.count, arrayToFill: &timingInfo, entriesNeededOut: nil) + + for i in 0..