Skip to content

Commit

Permalink
fix crash on macOS 13; fix video PTS; fix hide-self
Browse files Browse the repository at this point in the history
  • Loading branch information
lihaoyun6 committed Apr 23, 2024
1 parent 33686a3 commit e6db5dc
Show file tree
Hide file tree
Showing 10 changed files with 162 additions and 73 deletions.
8 changes: 4 additions & 4 deletions QuickRecorder.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand All @@ -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;
Expand Down
12 changes: 7 additions & 5 deletions QuickRecorder/QuickRecorderApp.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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
]
)

Expand Down
102 changes: 72 additions & 30 deletions QuickRecorder/RecordEngine.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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" })
Expand All @@ -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])
Expand All @@ -59,34 +61,61 @@ 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() }
Task { await record(audioOnly: SCContext.streamType == .systemaudio, filter: filter!) }
}

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)
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -227,36 +252,53 @@ 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),
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))
}

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)
}
Expand Down
47 changes: 39 additions & 8 deletions QuickRecorder/SCContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 }) {
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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()
Expand Down Expand Up @@ -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() {
Expand Down Expand Up @@ -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..<timingInfo.count {
timingInfo[i].decodeTimeStamp = CMTimeSubtract(timingInfo[i].decodeTimeStamp, offset)
timingInfo[i].presentationTimeStamp = CMTimeSubtract(timingInfo[i].presentationTimeStamp, offset)
}

var outSampleBuffer: CMSampleBuffer?
CMSampleBufferCreateCopyWithNewTiming(allocator: nil, sampleBuffer: sample, sampleTimingEntryCount: timingInfo.count, sampleTimingArray: &timingInfo, sampleBufferOut: &outSampleBuffer)

return outSampleBuffer
}

}
21 changes: 7 additions & 14 deletions QuickRecorder/ViewModel/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ struct ContentView: View {
Spacer()
}.padding([.top, .bottom], 10)
}
HStack {

if #available(macOS 14.0, *) {
Button(action: {
closeMainWindow()
}, label: {
Expand All @@ -89,18 +90,10 @@ struct ContentView: View {
.fontWeight(.bold)
.foregroundStyle(.secondary)
.onHover{ hovering in xmarkGlowing = hovering }
}).buttonStyle(PlainButtonStyle())
Spacer()
Button(action: {
NSApp.orderFrontStandardAboutPanel(nil)
}, label: {
Image(systemName: "info.circle")
.opacity(infoGlowing ? 1.0 : 0.4)
.fontWeight(.bold)
.foregroundStyle(.secondary)
.onHover{ hovering in infoGlowing = hovering }
}).buttonStyle(PlainButtonStyle())
}.padding([.leading, .trailing, .top], 7)
})
.buttonStyle(PlainButtonStyle())
.padding([.leading, .trailing, .top], 7)
}
}.frame(width: 800)
}

Expand Down Expand Up @@ -133,7 +126,7 @@ struct ContentView: View {
}
}

func closeMainWindow() { for w in NSApplication.shared.windows.filter({ $0.title == "Main Window".local }) { w.close() } }
func closeMainWindow() { for w in NSApplication.shared.windows.filter({ $0.title == "QuickReader".local }) { w.close() } }

func showAreaSelector() {
guard let screen = SCContext.getScreenWithMouse() else { return }
Expand Down
Loading

0 comments on commit e6db5dc

Please sign in to comment.