fix: save dialog not appearing after recording on macOS/iOS

macOS:
- Replace deprecated NSSavePanel.begin(completionHandler:) with async begin() API
- The old API has been deprecated since macOS 12 and may silently fail on macOS 26
- Added diagnostic logging throughout recording completion flow

iOS:
- Added diagnostic logging to track onRecordingSaved → pendingExportURL → sheet chain
- Logs file existence, size after recording completes

Diagnostic logging added to help verify the save dialog flow:
- macOS: file size check, showSaveDialog entry, NSSavePanel response
- iOS: recordStream output verification, onRecordingSaved call, sheet binding activation
This commit is contained in:
yumoqing 2026-07-17 20:27:49 +08:00
parent 01bf7d6f4a
commit 04990674cc
4 changed files with 36 additions and 19 deletions

View File

@ -36,10 +36,13 @@ final class HLSRecorder: ObservableObject {
recordTask = Task { recordTask = Task {
do { do {
let outputURL = try await recordStream(url: url) let outputURL = try await recordStream(url: url)
NSLog("[MiniPlayer] recordStream completed: %@", outputURL.path) NSLog("[MiniPlayer] recordStream completed: %@, exists: %d", outputURL.path, FileManager.default.fileExists(atPath: outputURL.path))
let attrs = try? FileManager.default.attributesOfItem(atPath: outputURL.path)
NSLog("[MiniPlayer] Output file size: %lld", (attrs?[.size] as? Int64) ?? 0)
await MainActor.run { await MainActor.run {
self.isRecording = false self.isRecording = false
self.timer?.invalidate() self.timer?.invalidate()
NSLog("[MiniPlayer] Calling onRecordingSaved...")
self.onRecordingSaved?(outputURL) self.onRecordingSaved?(outputURL)
} }
} catch is CancellationError { } catch is CancellationError {

View File

@ -585,10 +585,15 @@ struct BricksAppView: View {
} }
} }
.sheet(isPresented: Binding<Bool>( .sheet(isPresented: Binding<Bool>(
get: { bridge.pendingExportURL != nil }, get: {
let has = bridge.pendingExportURL != nil
if has { NSLog("[MiniPlayer] Sheet binding: pendingExportURL=%@", bridge.pendingExportURL?.lastPathComponent ?? "nil") }
return has
},
set: { if !$0 { bridge.pendingExportURL = nil } } set: { if !$0 { bridge.pendingExportURL = nil } }
)) { )) {
if let url = bridge.pendingExportURL { if let url = bridge.pendingExportURL {
NSLog("[MiniPlayer] Presenting ActivityShareSheet for: %@", url.lastPathComponent)
ActivityShareSheet(url: url) { ActivityShareSheet(url: url) {
bridge.pendingExportURL = nil bridge.pendingExportURL = nil
} }

View File

@ -178,8 +178,10 @@ final class PlayerBridge: ObservableObject {
#if os(iOS) #if os(iOS)
hlsRecorder.onRecordingSaved = { [weak self] url in hlsRecorder.onRecordingSaved = { [weak self] url in
NSLog("[MiniPlayer] onRecordingSaved called, setting pendingExportURL: %@", url.lastPathComponent)
self?.showToast("Saved: \(url.lastPathComponent)") self?.showToast("Saved: \(url.lastPathComponent)")
self?.pendingExportURL = url self?.pendingExportURL = url
NSLog("[MiniPlayer] pendingExportURL set to: %@", url.lastPathComponent)
} }
hlsRecorder.onError = { [weak self] msg in hlsRecorder.onError = { [weak self] msg in
self?.showToast(msg) self?.showToast(msg)

View File

@ -62,10 +62,13 @@ final class PlayerRecorder: NSObject {
try await recorder.start(url: url, outputPath: tempURL.path, duration: 0) try await recorder.start(url: url, outputPath: tempURL.path, duration: 0)
// duration=0 stop() // duration=0 stop()
// start stop() // start stop()
NSLog("[Recorder] ✓ Recording completed: %@", tempURL.path) NSLog("[Recorder] ✓ Recording completed: %@, file exists: %d", tempURL.path, FileManager.default.fileExists(atPath: tempURL.path))
let attrs = try? FileManager.default.attributesOfItem(atPath: tempURL.path)
NSLog("[Recorder] Output file size: %lld", (attrs?[.size] as? Int64) ?? 0)
await MainActor.run { await MainActor.run {
RecorderState.isFinalizing = false RecorderState.isFinalizing = false
self?.stopDurationTimer() self?.stopDurationTimer()
NSLog("[Recorder] Calling showSaveDialog...")
self?.showSaveDialog(tempURL: tempURL) self?.showSaveDialog(tempURL: tempURL)
} }
} catch { } catch {
@ -129,6 +132,7 @@ final class PlayerRecorder: NSObject {
// MARK: - // MARK: -
private func showSaveDialog(tempURL: URL) { private func showSaveDialog(tempURL: URL) {
NSLog("[Recorder] showSaveDialog called for: %@", tempURL.lastPathComponent)
let panel = NSSavePanel() let panel = NSSavePanel()
panel.allowedContentTypes = [.mpeg4Movie] panel.allowedContentTypes = [.mpeg4Movie]
// temp_ // temp_
@ -136,25 +140,28 @@ final class PlayerRecorder: NSObject {
panel.nameFieldStringValue = defaultName panel.nameFieldStringValue = defaultName
panel.directoryURL = saveDirectory panel.directoryURL = saveDirectory
panel.begin { [weak self] response in // NSSavePanel.begin(completionHandler:) macOS 12 macOS 26
Task { @MainActor in // 使 async begin() API
if response == .OK, let url = panel.url { Task { @MainActor [weak self] in
do { NSLog("[Recorder] NSSavePanel.begin() starting...")
if FileManager.default.fileExists(atPath: url.path) { let response = await panel.begin()
try FileManager.default.removeItem(at: url) NSLog("[Recorder] NSSavePanel response: %d", response.rawValue)
} if response == .OK, let url = panel.url {
try FileManager.default.moveItem(at: tempURL, to: url) do {
NSLog("[Recorder] ✓ Saved: %@", url.path) if FileManager.default.fileExists(atPath: url.path) {
self?.onRecordingSaved?(url) try FileManager.default.removeItem(at: url)
} catch {
NSLog("[Recorder] ✗ Save failed: %@", error.localizedDescription)
self?.onError?("Save failed: \(error.localizedDescription)")
} }
} else { try FileManager.default.moveItem(at: tempURL, to: url)
try? FileManager.default.removeItem(at: tempURL) NSLog("[Recorder] ✓ Saved: %@", url.path)
self?.onRecordingSaved?(url)
} catch {
NSLog("[Recorder] ✗ Save failed: %@", error.localizedDescription)
self?.onError?("Save failed: \(error.localizedDescription)")
} }
self?.checkPendingTerminate() } else {
try? FileManager.default.removeItem(at: tempURL)
} }
self?.checkPendingTerminate()
} }
} }