fix: block app termination while recording is finalizing

- RecorderState.isFinalizing flag set during stopRecording → finishWriting
- applicationShouldTerminate returns .terminateLater if finalizing
- Shows alert '正在保存录制文件...' to inform user
- After save dialog completes, calls NSApp.reply(toApplicationShouldTerminate: true)
- All failure paths also check and resolve pendingTerminate
- Fixes: 9-min recording lost because user closed app before async finishWriting completed
This commit is contained in:
yumoqing 2026-06-25 21:27:02 +08:00
parent eb3e98d567
commit d4a39dd7c3
2 changed files with 47 additions and 1 deletions

View File

@ -28,6 +28,22 @@ class AppDel: NSObject, NSApplicationDelegate {
} }
} }
func applicationShouldTerminate(_ sender: NSApplication) -> NSApplication.TerminateReply {
if RecorderState.isFinalizing {
NSLog("[MiniPlayer] ⏳ Termination blocked: recording is finalizing")
RecorderState.pendingTerminate = true
//
let alert = NSAlert()
alert.messageText = "正在保存录制文件..."
alert.informativeText = "请等待录制保存完成后再关闭应用。"
alert.alertStyle = .informational
alert.addButton(withTitle: "确定")
alert.runModal()
return .terminateLater
}
return .terminateNow
}
func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool {
return true return true
} }

View File

@ -162,6 +162,12 @@ private func tapProcessCallback(
// MARK: - PlayerRecorder // MARK: - PlayerRecorder
/// finalize退
enum RecorderState {
static nonisolated(unsafe) var isFinalizing: Bool = false
static nonisolated(unsafe) var pendingTerminate: Bool = false
}
/// AVPlayerItemVideoOutput + MTAudioProcessingTap /// AVPlayerItemVideoOutput + MTAudioProcessingTap
/// ///
@MainActor @MainActor
@ -287,6 +293,7 @@ final class PlayerRecorder: NSObject {
guard isRecording else { return } guard isRecording else { return }
isRecording = false isRecording = false
isRunning = false isRunning = false
RecorderState.isFinalizing = true // 退 finishWriting
captureTimer?.invalidate() captureTimer?.invalidate()
captureTimer = nil captureTimer = nil
stopDurationTimer() stopDurationTimer()
@ -314,8 +321,14 @@ final class PlayerRecorder: NSObject {
writer?.finishWriting { [weak self] in writer?.finishWriting { [weak self] in
NSLog("[Recorder] finishWriting callback invoked") NSLog("[Recorder] finishWriting callback invoked")
DispatchQueue.main.async { DispatchQueue.main.async {
RecorderState.isFinalizing = false // 退
guard let self = self, let w = self.writer else { guard let self = self, let w = self.writer else {
NSLog("[Recorder] ⚠️ self or writer is nil in finishWriting callback") NSLog("[Recorder] ⚠️ self or writer is nil in finishWriting callback")
if RecorderState.pendingTerminate {
RecorderState.pendingTerminate = false
NSApp.reply(toApplicationShouldTerminate: true)
}
return return
} }
let vFrames = self.captureFrameCount let vFrames = self.captureFrameCount
@ -333,6 +346,7 @@ final class PlayerRecorder: NSObject {
} else { } else {
NSLog("[Recorder] ⚠️ Temp file missing: %@", tempURL.path) NSLog("[Recorder] ⚠️ Temp file missing: %@", tempURL.path)
self.onError?("Recording file missing") self.onError?("Recording file missing")
self.checkPendingTerminate()
} }
} else { } else {
NSLog("[Recorder] ✗ finishWriting failed: status=%d, error=%@", NSLog("[Recorder] ✗ finishWriting failed: status=%d, error=%@",
@ -341,6 +355,7 @@ final class PlayerRecorder: NSObject {
if let url = self.tempURL { if let url = self.tempURL {
try? FileManager.default.removeItem(at: url) try? FileManager.default.removeItem(at: url)
} }
self.checkPendingTerminate()
} }
} }
} }
@ -632,7 +647,13 @@ final class PlayerRecorder: NSObject {
panel.begin { [weak self] response in panel.begin { [weak self] response in
Task { @MainActor in Task { @MainActor in
guard let self = self else { return } guard let self = self else {
if RecorderState.pendingTerminate {
RecorderState.pendingTerminate = false
NSApp.reply(toApplicationShouldTerminate: true)
}
return
}
if response == .OK, let finalURL = panel.url { if response == .OK, let finalURL = panel.url {
do { do {
@ -653,9 +674,18 @@ final class PlayerRecorder: NSObject {
NSLog("[Recorder] User cancelled save, deleting temp file") NSLog("[Recorder] User cancelled save, deleting temp file")
try? FileManager.default.removeItem(at: tempURL) try? FileManager.default.removeItem(at: tempURL)
} }
self.checkPendingTerminate()
} }
} }
} }
private func checkPendingTerminate() {
if RecorderState.pendingTerminate {
RecorderState.pendingTerminate = false
NSLog("[MiniPlayer] Pending terminate triggered, replying to terminate")
NSApp.reply(toApplicationShouldTerminate: true)
}
}
} }
#endif #endif