From 4fe53c0a077efa7d88ecff70ec4feea95b8e6a5b Mon Sep 17 00:00:00 2001 From: hl-valdemar Date: Thu, 22 Oct 2026 14:26:16 +0200 Subject: [PATCH] fix: verify stream queue startup and canonical report paths --- Checks/app-startup.swift | 2 -- Sources/App/TerebiApp.swift | 52 ++++++++++++++++----------------- Sources/CLI/TerebiCLI.swift | 2 +- docs/miruro-playback-handoff.md | 4 ++- 4 files changed, 29 insertions(+), 31 deletions(-) diff --git a/Checks/app-startup.swift b/Checks/app-startup.swift index c8e3e18..977536b 100644 --- a/Checks/app-startup.swift +++ b/Checks/app-startup.swift @@ -1015,8 +1015,6 @@ private enum CLIInstanceCheck { "canChangeQuality": model.canChangeYouTubeQuality, "qualityHeights": model.youtubeHeights, "windows": windows.count, - "allWindows": NSApp.windows.map { ["class": NSStringFromClass(type(of: $0)), - "visible": $0.isVisible, "main": $0.canBecomeMain, "content": $0.contentView != nil] as [String: Any] }, "hasAudio": model.state.has_audio, "hasVideo": model.state.has_video, "error": model.errorText ?? "", diff --git a/Sources/App/TerebiApp.swift b/Sources/App/TerebiApp.swift index 8ecd0f0..1b2ff7c 100644 --- a/Sources/App/TerebiApp.swift +++ b/Sources/App/TerebiApp.swift @@ -387,6 +387,7 @@ final class PlayerModel: ObservableObject { guard canOpen, progressInitializationError == nil else { return } queueCastDevice = nil queue.replaceStreams(streamQueue.entries.map { (url: $0.url, label: $0.label) }) + if fileName == nil { configureStartWindow(showing: false) } showsQueue = queue.items.count > 1 showsYouTubeInput = false openQueueSelection(keepingReceiver: false) @@ -920,7 +921,7 @@ final class PlayerModel: ObservableObject { queueAnimation = nil queueAnimationCompletion = nil let targetWidth: CGFloat = showsQueue ? 260 : 0 - guard let window, !isCompact, !compactWindow.isTransitioning else { + guard let window, fileName != nil, !isCompact, !compactWindow.isTransitioning else { queueWidth = targetWidth queueWindowVideoWidth = nil return @@ -1264,6 +1265,10 @@ final class PlayerModel: ObservableObject { private func poll() { guard let handle else { return } + if window?.isVisible == true, let streamQueue = Self.startupStreamQueue { + Self.startupStreamQueue = nil + openStreamQueue(streamQueue) + } if reportingEnabled && !reportingErrorShown { if player_get_progress_error(handle, &reportingErrorBuffer) { showReportingError(reportingErrorMessage) @@ -3118,7 +3123,7 @@ private struct PlayerContent: View { var body: some View { let titleControlsVisible = model.showsQueue || model.isQueueTransitioning || model.controlsVisible - let startScreen = model.fileName == nil && !model.isCompact && !model.compactWindow.isTransitioning + let startScreen = model.fileName == nil && model.queue.items.isEmpty && !model.isCompact && !model.compactWindow.isTransitioning HStack(spacing: 0) { videoContent if !model.isCompact && !model.compactWindow.isTransitioning && model.queueWidth > 0 { @@ -3520,6 +3525,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate { let events = NSAppleEventManager.shared() events.setEventHandler(self, andSelector: #selector(openInternetURL(_:withReplyEvent:)), forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL)) + // Pre-main model/AppKit initialization suppresses SwiftUI's initial player window. + if let error = PlayerModel.shared.progressInitializationError { + PlayerModel.shared.shutdown() + FileHandle.standardError.write(Data("terebi: \(error)\n".utf8)) + NSApp.setActivationPolicy(.regular) + NSApp.activate() + let alert = NSAlert() + alert.alertStyle = .critical + alert.messageText = "Could not initialize playback progress reporting" + alert.informativeText = error + alert.runModal() + exit(73) + } } @objc private func openInternetURL(_ event: NSAppleEventDescriptor, withReplyEvent reply: NSAppleEventDescriptor) { @@ -3546,9 +3564,6 @@ final class AppDelegate: NSObject, NSApplicationDelegate { if let index = arguments.firstIndex(of: "--source-file"), index + 1 < arguments.count { PlayerModel.shared.openResolvedSource(URL(fileURLWithPath: arguments[index + 1])) } - if let streamQueue = PlayerModel.startupStreamQueue { - PlayerModel.shared.openStreamQueue(streamQueue) - } } // Let SwiftUI create the initial window for a Finder/document launch. @@ -3574,7 +3589,6 @@ struct TerebiApp: App { PlayerContent(model: model) .focusedSceneObject(model) } - .defaultLaunchBehavior(.presented) .defaultSize(width: 640, height: 400) .windowResizability(.contentSize) .windowStyle(.hiddenTitleBar) @@ -3604,19 +3618,16 @@ public func terebiAppMain() { exit(78) } let arguments = Array(CommandLine.arguments.dropFirst()) - if let index = arguments.firstIndex(of: "--queue-file") { + let queueArguments = arguments.filter { $0.hasPrefix("--queue-file=") } + if let argument = queueArguments.first { do { - guard index + 1 < arguments.count, arguments[index + 1].hasPrefix("/"), - arguments.filter({ $0 == "--queue-file" }).count == 1, + let path = String(argument.dropFirst("--queue-file=".count)) + guard path.hasPrefix("/"), queueArguments.count == 1, !arguments.contains("--progress-file"), !arguments.contains("--source-file"), !arguments.contains("--open-media") else { throw CocoaError(.fileReadCorruptFile) } - PlayerModel.startupStreamQueue = try StreamQueue.load(from: URL(fileURLWithPath: arguments[index + 1])) - _ = NSApplication.shared - if let error = PlayerModel.shared.progressInitializationError { - throw NSError(domain: "Terebi.Queue", code: 73, userInfo: [NSLocalizedDescriptionKey: error]) - } + PlayerModel.startupStreamQueue = try StreamQueue.load(from: URL(fileURLWithPath: path)) } catch { FileHandle.standardError.write(Data("terebi: \(error.localizedDescription)\n".utf8)) exit(73) @@ -3633,19 +3644,6 @@ public func terebiAppMain() { exit(73) } PlayerModel.startupProgressFile = arguments[index + 1] - _ = NSApplication.shared - if let error = PlayerModel.shared.progressInitializationError { - PlayerModel.shared.shutdown() - FileHandle.standardError.write(Data("terebi: \(error)\n".utf8)) - NSApp.setActivationPolicy(.regular) - NSApp.activate() - let alert = NSAlert() - alert.alertStyle = .critical - alert.messageText = "Could not initialize playback progress reporting" - alert.informativeText = error - alert.runModal() - exit(73) - } } TerebiApp.main() } diff --git a/Sources/CLI/TerebiCLI.swift b/Sources/CLI/TerebiCLI.swift index 7d8fa16..a0147b0 100644 --- a/Sources/CLI/TerebiCLI.swift +++ b/Sources/CLI/TerebiCLI.swift @@ -74,7 +74,7 @@ enum TerebiCLI { configuration.arguments += ["--progress-file", progressFile.path] } if let queueFile { - configuration.arguments += ["--queue-file", queueFile.path] + configuration.arguments += ["--queue-file=" + queueFile.path] } if let sourceFile { configuration.arguments += ["--source-file", sourceFile.path] diff --git a/docs/miruro-playback-handoff.md b/docs/miruro-playback-handoff.md index 80bb3f3..6005acc 100644 --- a/docs/miruro-playback-handoff.md +++ b/docs/miruro-playback-handoff.md @@ -49,7 +49,9 @@ The supplied order is preserved (local-file batches still use natural filename o Each report is immutably bound to its stable identity in the native decoder. Switching entries drains/closes the previous writer before beginning the next; revisits append only after verifying the original private report inode, owner, permissions and link count. An unrelated or replaced file is never reopened as an owned session, and an unregistered identity is never written to the previously selected report. Only the active item owns a running writer/open report descriptor. A reporting failure is shown separately without rerouting facts or changing acknowledged playback/completion semantics. Single-stream `--progress-file` remains unchanged. -Runnable queue checks: `make check-cli check-queue check-reporting check-resolved`. The existing `check-resolved` local HTTP/HLS/VTT harness also drives a real native `PlayerModel` through a mixed YouTube/Miruro queue with injected fixture resolution, stale-result cancellation, manual navigation, natural-end advancement, fresh revisits/resume and isolated v1/v2 reports. Run `TEREBI_CHECK_STREAM_QUEUE_ONLY=1 make check-startup` for focused actual CLI→LaunchServices→GUI queue delivery, native decode, manual previous/next, natural-end advancement and per-item report revisits using the existing bundled-extractor/local-media fixture. These checks require no browser or live streaming service. Full `make check-startup` includes this queue scenario too. These additions have not yet been executed as part of this change. +Runnable queue checks: `make check-cli check-queue check-reporting check-resolved`. The existing `check-resolved` local HTTP/HLS/VTT harness also drives a real native `PlayerModel` through a mixed YouTube/Miruro queue with injected fixture resolution, stale-result cancellation, manual navigation, natural-end advancement, fresh revisits/resume and isolated v1/v2 reports. Run `TEREBI_CHECK_STREAM_QUEUE_ONLY=1 make check-startup` for focused actual CLI→LaunchServices→GUI queue delivery, native decode, manual previous/next, natural-end advancement and per-item report revisits using the existing bundled-extractor/local-media fixture. These checks require no browser or live streaming service. Full `make check-startup` includes this queue scenario too. + +Current local verification: `make check-cli check-queue check-reporting check-resolved all` passed. The packaged queue GUI smoke did not complete because native state snapshots stalled; no complete GUI pass is claimed. Temporary tracing was removed, and further UI testing is deferred to the user at their request. Tubular's handoff document records its terminal/capture and Rust checks separately. ### Optional native metadata