fix: preserve video aspect ratio while resizing queue window

This commit is contained in:
2026-10-18 02:18:38 +02:00
parent dd02383c0b
commit 8cd5db5375
4 changed files with 42 additions and 1 deletions
+17
View File
@@ -326,6 +326,23 @@ struct SystemPlaybackChecks {
model.setPointerInsideWindow(true) model.setPointerInsideWindow(true)
} }
until("open starts authoritative playback") { model.canTogglePlayback && model.state.status == PLAYER_PLAYING } until("open starts authoritative playback") { model.canTogglePlayback && model.state.status == PLAYER_PLAYING }
model.showsQueue = true
for delta in [NSSize(width: 160, height: 0), NSSize(width: 0, height: 90),
NSSize(width: -100, height: 0), NSSize(width: 0, height: -60)] {
let proposal = NSSize(width: window.frame.width + delta.width, height: window.frame.height + delta.height)
guard let constrained = window.delegate?.windowWillResize?(window, to: proposal) else {
fatalError("FAIL: native resize delegate must constrain queue video geometry")
}
var frame = window.frame
frame.size = constrained
window.setFrame(frame, display: true)
let content = window.contentRect(forFrameRect: window.frame)
require(abs((content.width - 260) / content.height - model.state.video_aspect_ratio) < 0.001,
"horizontal and vertical queue resizing preserves video aspect with fixed sidebar width")
require(delta.width == 0 ? abs(frame.height - proposal.height) < 1 : abs(frame.width - proposal.width) < 1,
"aspect-locked queue window follows the dragged axis")
}
model.showsQueue = false
require(center.playbackState == .playing, "playing publishes macOS playback ownership") require(center.playbackState == .playing, "playing publishes macOS playback ownership")
requirePlaybackPower(system: true, display: true, "local video keeps the Mac and display awake") requirePlaybackPower(system: true, display: true, "local video keeps the Mac and display awake")
var bufferingState = model.state var bufferingState = model.state
+3 -1
View File
@@ -59,6 +59,8 @@ Use the titlebar's **Queue** icon or **View → Show Queue** to show the editabl
In a normal window, showing the queue expands the window outward while preserving the video's size; hiding it removes that extra width. The sidebar meets the video directly, without a separate divider gap. The window shifts left when necessary near the screen's right edge. Fullscreen keeps its fixed bounds. In a normal window, showing the queue expands the window outward while preserving the video's size; hiding it removes that extra width. The sidebar meets the video directly, without a separate divider gap. The window shifts left when necessary near the screen's right edge. Fullscreen keeps its fixed bounds.
Resizing with the queue open preserves the video's aspect ratio while keeping the sidebar at a fixed width; closing the queue retains ordinary aspect-locked video resizing.
While the sidebar is open, its persistent top header holds the queue count, queue toggle, pin, Cast and settings controls; these do not fade with the video controls. Closing the sidebar returns those controls to the auto-hiding video titlebar. Space and configured playback shortcuts continue working with queue-list focus; Tab navigation, deliberately focused buttons/sliders and text entry keep their native keyboard behavior. While the sidebar is open, its persistent top header holds the queue count, queue toggle, pin, Cast and settings controls; these do not fade with the video controls. Closing the sidebar returns those controls to the auto-hiding video titlebar. Space and configured playback shortcuts continue working with queue-list focus; Tab navigation, deliberately focused buttons/sliders and text entry keep their native keyboard behavior.
Playback advances automatically and stops at the end, without wrapping. **Previous Queue Item** and **Next Queue Item** are available in the sidebar and Playback menu and are disabled at their respective ends. Reordering or removing another item does not interrupt playback; removing the current item plays its next neighbor, or stops if there is none. A playback error stays on that item rather than skipping it. Playback advances automatically and stops at the end, without wrapping. **Previous Queue Item** and **Next Queue Item** are available in the sidebar and Playback menu and are disabled at their respective ends. Reordering or removing another item does not interrupt playback; removing the current item plays its next neighbor, or stops if there is none. A playback error stays on that item rather than skipping it.
@@ -157,7 +159,7 @@ The full suite requires a graphical macOS session and a working audio device; it
The startup checks verify that playing video remains attached, unhidden and sized to its native drag surface, including with the queue open; playback-state checks alone do not establish renderer visibility. The startup checks verify that playing video remains attached, unhidden and sized to its native drag surface, including with the queue open; playback-state checks alone do not establish renderer visibility.
Queue-open resize checks also require positive native resize increments: clearing AppKit's aspect ratio alone leaves zero increments after aspect-locked video. Physical edge-drag resizing still needs manual verification. Queue-open resize checks require positive native resize increments and exercise horizontal/vertical resize-delegate proposals to preserve video aspect independently of the fixed-width sidebar. Physical edge-drag resizing still needs manual verification.
`make benchmark-playback` measures cold source startup, cached/uncached seeks, and sustained native/Cast playback with repeatable loopback fixtures. It requires the same macOS session/audio device and fixture-generation FFmpeg; results exclude live extraction, WAN/TLS, and physical-receiver latency. `make benchmark-playback` measures cold source startup, cached/uncached seeks, and sustained native/Cast playback with repeatable loopback fixtures. It requires the same macOS session/audio device and fixture-generation FFmpeg; results exclude live extraction, WAN/TLS, and physical-receiver latency.
The [benchmark runner](Checks/playback-benchmark.py) accepts `--compare-native <baseline-binary>` for alternating native-only comparisons; add `--seek-work` to isolate cache-miss seeks when both binaries support that mode. Preserve the baseline executable before rebuilding. The [benchmark runner](Checks/playback-benchmark.py) accepts `--compare-native <baseline-binary>` for alternating native-only comparisons; add `--seek-work` to isolate cache-miss seeks when both binaries support that mode. Preserve the baseline executable before rebuilding.
+18
View File
@@ -4,6 +4,7 @@ import QuartzCore
@MainActor @MainActor
final class CompactWindowController: NSObject { final class CompactWindowController: NSObject {
var onChange: (() -> Void)? var onChange: (() -> Void)?
var normalVideoLayout: (() -> (aspect: Double, sidebarWidth: CGFloat)?)?
private(set) var isCompact = false private(set) var isCompact = false
private(set) var isTransitioning = false private(set) var isTransitioning = false
static let transitionDuration: TimeInterval = 0.25 static let transitionDuration: TimeInterval = 0.25
@@ -211,6 +212,23 @@ final class CompactWindowController: NSObject {
let responds = MainActor.assumeIsolated { original?.responds(to: selector) ?? false } let responds = MainActor.assumeIsolated { original?.responds(to: selector) ?? false }
return responds ? original : nil return responds ? original : nil
} }
func windowWillResize(_ sender: NSWindow, to proposedSize: NSSize) -> NSSize {
let proposed = original?.windowWillResize?(sender, to: proposedSize) ?? proposedSize
guard let owner, !owner.isCompact, !owner.isTransitioning,
!sender.styleMask.contains(.fullScreen),
let layout = owner.normalVideoLayout?(),
layout.aspect.isFinite, layout.aspect > 0 else { return proposed }
// The sidebar stays fixed-width; only the video area is aspect-locked.
let chrome = sender.frame.height - sender.contentRect(forFrameRect: sender.frame).height
let widthDriven = abs(proposed.width - sender.frame.width) >=
abs(proposed.height - sender.frame.height) * layout.aspect
let minimumHeight = max(sender.minSize.height - chrome,
(sender.minSize.width - layout.sidebarWidth) / layout.aspect)
let height = max(minimumHeight, widthDriven
? (proposed.width - layout.sidebarWidth) / layout.aspect
: proposed.height - chrome)
return NSSize(width: height * layout.aspect + layout.sidebarWidth, height: height + chrome)
}
func windowDidFailToEnterFullScreen(_ window: NSWindow) { func windowDidFailToEnterFullScreen(_ window: NSWindow) {
owner?.fullScreenFailed() owner?.fullScreenFailed()
+4
View File
@@ -764,6 +764,10 @@ final class PlayerModel: ObservableObject {
self.window = window self.window = window
window.title = fileName ?? "terebi" window.title = fileName ?? "terebi"
window.contentMinSize = NSSize(width: 400, height: 240) window.contentMinSize = NSSize(width: 400, height: 240)
compactWindow.normalVideoLayout = { [weak self] in
guard let self, self.showsQueue, self.state.has_video else { return nil }
return (self.state.video_aspect_ratio, 260)
}
compactWindow.attach(window) compactWindow.attach(window)
updateWindowState() updateWindowState()
fitVideoWindow() fitVideoWindow()