fix: preserve video aspect ratio while resizing queue window
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user