fix(iOS): show recorder errors as full-screen overlay instead of fleeting toast

- Added recorderErrorMessage @Published var for persistent error display
- Recording errors now shown as overlay with error details + Dismiss button
- presentShareSheet now logs errors and sets recorderErrorMessage on failure
- Errors like 'No window scene' or 'No view controller' visible on screen
This commit is contained in:
yumoqing 2026-07-17 21:28:15 +08:00
parent 04fb3acecc
commit f73bc4532e
2 changed files with 42 additions and 8 deletions

View File

@ -440,6 +440,30 @@ struct PlayerContentView: View {
// 播放状态指示(loading/buffering/error) // 播放状态指示(loading/buffering/error)
PlayerStatusOverlay(bridge: bridge) PlayerStatusOverlay(bridge: bridge)
// 录制错误覆盖层(显示在界面上不会一闪而过)
#if os(iOS)
if let errMsg = bridge.recorderErrorMessage {
VStack(spacing: 16) {
Image(systemName: "exclamationmark.triangle.fill")
.font(.system(size: 36))
.foregroundColor(.orange)
Text("Recording Error")
.font(.headline).foregroundColor(.white)
Text(errMsg)
.font(.system(size: 14))
.foregroundColor(.white.opacity(0.8))
.multilineTextAlignment(.center)
.padding(.horizontal, 40)
Button("Dismiss") {
bridge.recorderErrorMessage = nil
}
.buttonStyle(.borderedProminent)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.black.opacity(0.75))
}
#endif
// 左上角应用图标 — iOS 锁屏控制 / macOS 切换控制栏 // 左上角应用图标 — iOS 锁屏控制 / macOS 切换控制栏
MiniPlayerIcon() MiniPlayerIcon()
.frame(width: 32, height: 32) .frame(width: 32, height: 32)

View File

@ -124,6 +124,7 @@ final class PlayerBridge: ObservableObject {
let hlsRecorder = HLSRecorder() let hlsRecorder = HLSRecorder()
let pipController = PiPController() let pipController = PiPController()
@Published var pendingExportURL: URL? @Published var pendingExportURL: URL?
@Published var recorderErrorMessage: String? // 录制错误直接显示在界面上
#endif #endif
// 诊断日志 // 诊断日志
private var diagLogURL: URL? private var diagLogURL: URL?
@ -178,12 +179,14 @@ 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: %@", url.lastPathComponent) NSLog("[MiniPlayer] onRecordingSaved: %@", url.lastPathComponent)
self?.recorderErrorMessage = nil
self?.showToast("Saved: \(url.lastPathComponent)") self?.showToast("Saved: \(url.lastPathComponent)")
// 直接用 UIKit present,不依赖 SwiftUI sheet binding
self?.presentShareSheet(for: url) self?.presentShareSheet(for: url)
} }
hlsRecorder.onError = { [weak self] msg in hlsRecorder.onError = { [weak self] msg in
NSLog("[MiniPlayer] onError: %@", msg)
self?.recorderErrorMessage = msg
self?.showToast(msg) self?.showToast(msg)
} }
#endif #endif
@ -192,22 +195,29 @@ final class PlayerBridge: ObservableObject {
#if os(iOS) #if os(iOS)
/// 直接通过 UIKit present UIActivityViewController,绕过 SwiftUI sheet 可能的多 sheet 冲突 /// 直接通过 UIKit present UIActivityViewController,绕过 SwiftUI sheet 可能的多 sheet 冲突
func presentShareSheet(for url: URL) { func presentShareSheet(for url: URL) {
NSLog("[MiniPlayer] presentShareSheet: %@", url.lastPathComponent)
guard let windowScene = UIApplication.shared.connectedScenes guard let windowScene = UIApplication.shared.connectedScenes
.compactMap({ $0 as? UIWindowScene }) .compactMap({ $0 as? UIWindowScene })
.first(where: { $0.activationState == .foregroundActive }) .first(where: { $0.activationState == .foregroundActive })
?? UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }).first ?? UIApplication.shared.connectedScenes.compactMap({ $0 as? UIWindowScene }).first
else { return } else {
recorderErrorMessage = "No window scene available"
return
}
var topVC = windowScene.keyWindow?.rootViewController var topVC = windowScene.keyWindow?.rootViewController
?? windowScene.windows.first(where: { !$0.isHidden && $0.bounds.size != .zero })?.rootViewController ?? windowScene.windows.first(where: { !$0.isHidden && $0.bounds.size != .zero })?.rootViewController
while let presented = topVC?.presentedViewController { topVC = presented } while let presented = topVC?.presentedViewController { topVC = presented }
let activityVC = UIActivityViewController(activityItems: [url], applicationActivities: nil) guard let presenter = topVC else {
activityVC.completionWithItemsHandler = { _, _, _, _ in recorderErrorMessage = "No view controller to present share sheet"
// 录制完成后可以删掉临时文件?不,用户可能需要再次分享 return
} }
topVC?.present(activityVC, animated: true)
NSLog("[MiniPlayer] presentShareSheet: presented for %@", url.lastPathComponent) let activityVC = UIActivityViewController(activityItems: [url], applicationActivities: nil)
activityVC.completionWithItemsHandler = { _, _, _, _ in }
presenter.present(activityVC, animated: true)
NSLog("[MiniPlayer] presentShareSheet: presented successfully")
} }
/// 配置 AVAudioSession,支持后台音频和 PiP /// 配置 AVAudioSession,支持后台音频和 PiP