fix: verify stream queue startup and canonical report paths

This commit is contained in:
2026-10-22 14:26:16 +02:00
parent 0588d162d6
commit 4fe53c0a07
4 changed files with 29 additions and 31 deletions
-2
View File
@@ -1015,8 +1015,6 @@ private enum CLIInstanceCheck {
"canChangeQuality": model.canChangeYouTubeQuality, "canChangeQuality": model.canChangeYouTubeQuality,
"qualityHeights": model.youtubeHeights, "qualityHeights": model.youtubeHeights,
"windows": windows.count, "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, "hasAudio": model.state.has_audio,
"hasVideo": model.state.has_video, "hasVideo": model.state.has_video,
"error": model.errorText ?? "", "error": model.errorText ?? "",
+25 -27
View File
@@ -387,6 +387,7 @@ final class PlayerModel: ObservableObject {
guard canOpen, progressInitializationError == nil else { return } guard canOpen, progressInitializationError == nil else { return }
queueCastDevice = nil queueCastDevice = nil
queue.replaceStreams(streamQueue.entries.map { (url: $0.url, label: $0.label) }) queue.replaceStreams(streamQueue.entries.map { (url: $0.url, label: $0.label) })
if fileName == nil { configureStartWindow(showing: false) }
showsQueue = queue.items.count > 1 showsQueue = queue.items.count > 1
showsYouTubeInput = false showsYouTubeInput = false
openQueueSelection(keepingReceiver: false) openQueueSelection(keepingReceiver: false)
@@ -920,7 +921,7 @@ final class PlayerModel: ObservableObject {
queueAnimation = nil queueAnimation = nil
queueAnimationCompletion = nil queueAnimationCompletion = nil
let targetWidth: CGFloat = showsQueue ? 260 : 0 let targetWidth: CGFloat = showsQueue ? 260 : 0
guard let window, !isCompact, !compactWindow.isTransitioning else { guard let window, fileName != nil, !isCompact, !compactWindow.isTransitioning else {
queueWidth = targetWidth queueWidth = targetWidth
queueWindowVideoWidth = nil queueWindowVideoWidth = nil
return return
@@ -1264,6 +1265,10 @@ final class PlayerModel: ObservableObject {
private func poll() { private func poll() {
guard let handle else { return } guard let handle else { return }
if window?.isVisible == true, let streamQueue = Self.startupStreamQueue {
Self.startupStreamQueue = nil
openStreamQueue(streamQueue)
}
if reportingEnabled && !reportingErrorShown { if reportingEnabled && !reportingErrorShown {
if player_get_progress_error(handle, &reportingErrorBuffer) { if player_get_progress_error(handle, &reportingErrorBuffer) {
showReportingError(reportingErrorMessage) showReportingError(reportingErrorMessage)
@@ -3118,7 +3123,7 @@ private struct PlayerContent: View {
var body: some View { var body: some View {
let titleControlsVisible = model.showsQueue || model.isQueueTransitioning || model.controlsVisible 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) { HStack(spacing: 0) {
videoContent videoContent
if !model.isCompact && !model.compactWindow.isTransitioning && model.queueWidth > 0 { if !model.isCompact && !model.compactWindow.isTransitioning && model.queueWidth > 0 {
@@ -3520,6 +3525,19 @@ final class AppDelegate: NSObject, NSApplicationDelegate {
let events = NSAppleEventManager.shared() let events = NSAppleEventManager.shared()
events.setEventHandler(self, andSelector: #selector(openInternetURL(_:withReplyEvent:)), events.setEventHandler(self, andSelector: #selector(openInternetURL(_:withReplyEvent:)),
forEventClass: AEEventClass(kInternetEventClass), andEventID: AEEventID(kAEGetURL)) 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) { @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 { if let index = arguments.firstIndex(of: "--source-file"), index + 1 < arguments.count {
PlayerModel.shared.openResolvedSource(URL(fileURLWithPath: arguments[index + 1])) 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. // Let SwiftUI create the initial window for a Finder/document launch.
@@ -3574,7 +3589,6 @@ struct TerebiApp: App {
PlayerContent(model: model) PlayerContent(model: model)
.focusedSceneObject(model) .focusedSceneObject(model)
} }
.defaultLaunchBehavior(.presented)
.defaultSize(width: 640, height: 400) .defaultSize(width: 640, height: 400)
.windowResizability(.contentSize) .windowResizability(.contentSize)
.windowStyle(.hiddenTitleBar) .windowStyle(.hiddenTitleBar)
@@ -3604,19 +3618,16 @@ public func terebiAppMain() {
exit(78) exit(78)
} }
let arguments = Array(CommandLine.arguments.dropFirst()) 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 { do {
guard index + 1 < arguments.count, arguments[index + 1].hasPrefix("/"), let path = String(argument.dropFirst("--queue-file=".count))
arguments.filter({ $0 == "--queue-file" }).count == 1, guard path.hasPrefix("/"), queueArguments.count == 1,
!arguments.contains("--progress-file"), !arguments.contains("--source-file"), !arguments.contains("--progress-file"), !arguments.contains("--source-file"),
!arguments.contains("--open-media") else { !arguments.contains("--open-media") else {
throw CocoaError(.fileReadCorruptFile) throw CocoaError(.fileReadCorruptFile)
} }
PlayerModel.startupStreamQueue = try StreamQueue.load(from: URL(fileURLWithPath: arguments[index + 1])) PlayerModel.startupStreamQueue = try StreamQueue.load(from: URL(fileURLWithPath: path))
_ = NSApplication.shared
if let error = PlayerModel.shared.progressInitializationError {
throw NSError(domain: "Terebi.Queue", code: 73, userInfo: [NSLocalizedDescriptionKey: error])
}
} catch { } catch {
FileHandle.standardError.write(Data("terebi: \(error.localizedDescription)\n".utf8)) FileHandle.standardError.write(Data("terebi: \(error.localizedDescription)\n".utf8))
exit(73) exit(73)
@@ -3633,19 +3644,6 @@ public func terebiAppMain() {
exit(73) exit(73)
} }
PlayerModel.startupProgressFile = arguments[index + 1] 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() TerebiApp.main()
} }
+1 -1
View File
@@ -74,7 +74,7 @@ enum TerebiCLI {
configuration.arguments += ["--progress-file", progressFile.path] configuration.arguments += ["--progress-file", progressFile.path]
} }
if let queueFile { if let queueFile {
configuration.arguments += ["--queue-file", queueFile.path] configuration.arguments += ["--queue-file=" + queueFile.path]
} }
if let sourceFile { if let sourceFile {
configuration.arguments += ["--source-file", sourceFile.path] configuration.arguments += ["--source-file", sourceFile.path]
+3 -1
View File
@@ -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. 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 ### Optional native metadata