fix: track hover across the entire player to prevent overlay feedback flicker

This commit is contained in:
2026-10-20 22:00:31 +02:00
parent 885dea74f2
commit 7036109152
4 changed files with 117 additions and 7 deletions
+87
View File
@@ -1,4 +1,5 @@
import AppKit
import Combine
import Darwin
import MediaPlayer
import ObjectiveC
@@ -6,6 +7,10 @@ import ObjectiveC
@_cdecl("terebi_app_main")
@MainActor
public func checkAppStartup() {
if let index = CommandLine.arguments.firstIndex(of: "--check-overlay-hover") {
OverlayHoverCheck.run(result: URL(fileURLWithPath: CommandLine.arguments[index + 1]))
return
}
if let index = CommandLine.arguments.firstIndex(of: "--check-window-drag") {
WindowDragCheck.run(result: URL(fileURLWithPath: CommandLine.arguments[index + 1]))
return
@@ -25,6 +30,88 @@ public func checkAppStartup() {
AppStartupCheck.run()
}
@MainActor
private enum OverlayHoverCheck {
static var timer: Timer?
static var observation: AnyCancellable?
static var resultURL: URL!
static var step = 0
static var sample = 0
static var hidden = 0
static var savedPointer = NSPoint.zero
static let names = ["video", "title", "playback controls", "top-right controls",
"queue controls", "window exit", "window reentry"]
// Infrastructure watchdog only; verdicts use native hover events and visibility changes.
static let deadline = ContinuousClock.now.advanced(by: .seconds(20))
static func run(result: URL) {
resultURL = result
_ = NSApplication.shared
savedPointer = NSEvent.mouseLocation
timer = Timer.scheduledTimer(withTimeInterval: 0.05, repeats: true) { _ in
MainActor.assumeIsolated { advance() }
}
terebiAppMain()
}
static func finish(_ error: String = "") {
timer?.invalidate()
observation = nil
CGWarpMouseCursorPosition(CGPoint(x: savedPointer.x, y: NSScreen.screens[0].frame.maxY - savedPointer.y))
let report: [String: Any] = ["passed": error.isEmpty, "error": error, "steps": step]
try! JSONSerialization.data(withJSONObject: report).write(to: resultURL, options: .atomic)
PlayerModel.shared.shutdown()
NSApp.terminate(nil)
}
static func advance() {
guard ContinuousClock.now < deadline else { finish("Overlay hover timed out"); return }
let model = PlayerModel.shared
guard model.canTogglePlayback, model.state.has_video,
let window = NSApp.windows.first(where: { $0.canBecomeMain && !($0 is NSPanel) }),
let content = window.contentView else { return }
if observation == nil {
model.setVolume(0)
model.setPlayback(playing: false)
window.makeKeyAndOrderFront(nil)
window.makeMain()
NSApp.activate(ignoringOtherApps: true)
observation = model.$controlsVisible.removeDuplicates().dropFirst().sink { visible in
if !visible { hidden += 1 }
}
}
if step == names.count { finish(); return }
if step == 4 && !model.showsQueue { model.showsQueue = true; return }
guard !model.isQueueTransitioning else { return }
if sample == 0 {
let w = content.bounds.width
let h = content.bounds.height
let points = [NSPoint(x: w / 2, y: h / 2), NSPoint(x: 150, y: h - 16),
NSPoint(x: w / 2, y: 64), NSPoint(x: w - 46, y: h - 16),
NSPoint(x: w - 46, y: h - 16), NSPoint(x: -20, y: h / 2),
NSPoint(x: w - 46, y: h - 16)]
hidden = 0
let screen = window.convertPoint(toScreen: points[step])
CGWarpMouseCursorPosition(CGPoint(x: screen.x, y: NSScreen.screens[0].frame.maxY - screen.y))
let event = NSEvent.mouseEvent(with: .mouseMoved, location: points[step], modifierFlags: [],
timestamp: 0, windowNumber: window.windowNumber, context: nil,
eventNumber: step, clickCount: 0, pressure: 0)!
NSApp.postEvent(NSEvent(cgEvent: event.cgEvent!)!, atStart: false)
}
// Let native tracking and SwiftUI updates settle; no auto-hide timer is under test.
sample += 1
if sample == 20 {
let outside = step == 5
guard model.controlsVisible != outside, outside || hidden == 0 else {
finish("\(names[step]): visible=\(model.controlsVisible), hidden transitions=\(hidden)")
return
}
sample = 0
step += 1
}
}
}
@MainActor
private enum WindowDragCheck {
static var timer: Timer?