import AppKit import MediaPlayer import TerebiConfiguration // Native focus eligibility normally depends on the user's Full Keyboard Access setting. // Force eligibility only; keep the controls' real AppKit responder/key behavior. private final class KeyboardCheckButton: NSButton { override var acceptsFirstResponder: Bool { true } } private final class KeyboardCheckSlider: NSSlider { override var acceptsFirstResponder: Bool { true } } private final class KeyboardCheckLabel: NSTextField { override var acceptsFirstResponder: Bool { true } } @main @MainActor struct SystemPlaybackChecks { static func require(_ condition: @autoclosure () -> Bool, _ message: String) { if !condition() { fatalError("FAIL: " + message) } } static func until(_ message: String, _ condition: () -> Bool) { let deadline = ContinuousClock.now.advanced(by: .seconds(10)) while !condition(), ContinuousClock.now < deadline { RunLoop.main.run(until: Date(timeIntervalSinceNow: 0.01)) } require(condition(), message) } private final class DragCheckWindow: NSWindow { var drags = 0 var fullscreenToggles = 0 override func performDrag(with event: NSEvent) { drags += 1 } override func toggleFullScreen(_ sender: Any?) { fullscreenToggles += 1 } } static func checkWindowSurface(defaults: UserDefaults) { let model = PlayerModel(defaults: defaults) defer { model.shutdown() } let window = DragCheckWindow(contentRect: NSRect(x: -10_000, y: -10_000, width: 640, height: 360), styleMask: [.titled, .resizable, .fullSizeContentView], backing: .buffered, defer: false) window.isReleasedWhenClosed = false defer { window.close() } let surface = PlaybackSurface.DragView(model: model) surface.frame = window.contentView!.bounds window.contentView!.addSubview(surface) model.attachWindow(window) @MainActor func event(_ type: NSEvent.EventType, clicks: Int = 1) -> NSEvent { NSEvent.mouseEvent(with: type, location: NSPoint(x: 100, y: 100), modifierFlags: [], timestamp: 0, windowNumber: window.windowNumber, context: nil, eventNumber: 0, clickCount: clicks, pressure: 0)! } require(surface.hitTest(NSPoint(x: 100, y: 100)) === surface, "video renderer and unused background route mouse input to their owning surface") require(!surface.mouseDownCanMoveWindow, "native background movement cannot turn clicks or resize into drags") for point in [NSPoint(x: 1, y: 100), NSPoint(x: 639, y: 100), NSPoint(x: 100, y: 1), NSPoint(x: 100, y: surface.bounds.maxY - 1)] { require(surface.hitTest(point) == nil, "playback drag surface leaves all native resize edges untouched") } surface.mouseDown(with: event(.leftMouseDown)) require(window.drags == 0, "mouse-down alone never begins window dragging") surface.mouseUp(with: event(.leftMouseUp)) require(window.drags == 0 && window.fullscreenToggles == 0, "single video click never drags or enters fullscreen") surface.mouseDown(with: event(.leftMouseDown)) surface.mouseDragged(with: event(.leftMouseDragged)) surface.mouseUp(with: event(.leftMouseUp)) require(window.drags == 1 && window.fullscreenToggles == 0, "real mouse-drag callback invokes native window dragging once") surface.mouseDown(with: event(.leftMouseDown, clicks: 2)) surface.mouseUp(with: event(.leftMouseUp, clicks: 2)) require(window.drags == 1 && window.fullscreenToggles == 1, "double video click retains fullscreen instead of dragging") } static func checkCompactProgress() { let times: [(Double, Double, String)] = [ (12.9, 45.9, "12 / 45 s"), (59, 59.999, "59 / 59 s"), (0, 60, "00:00 / 01:00 m"), (312, 2528, "05:12 / 42:08 m"), (3599, 3599.999, "59:59 / 59:59 m"), (59, 3600, "00:00 / 01:00 h"), (4980, 7800, "01:23 / 02:10 h"), (-5, 45, "0 / 45 s"), (80, 45, "45 / 45 s"), ] for (position, duration, expected) in times { require(compactProgressText(position: position, duration: duration, format: .time) == expected, "compact time uses total-duration units, truncation and bounded progress: \(expected)") } for (position, expected) in [(59.9, "59.9"), (59.99, "59.9"), (0.0, "0.0"), (99.89, "99.8"), (99.9, "100.0"), (99.99, "100.0"), (100.0, "100.0"), (105.0, "100.0"), (-1.0, "0.0")] { require(compactProgressText(position: position, duration: 100, format: .percentage) == expected, "compact percentage has one decimal and stays bounded: \(expected)") } for duration in [0.0, -1.0, .nan, .infinity] { require(compactProgressText(position: 10, duration: duration, format: .time) == nil && compactProgressText(position: 10, duration: duration, format: .percentage) == nil, "unknown/invalid duration has no compact progress") } require(compactProgressText(position: .nan, duration: 100, format: .time) == nil && compactProgressText(position: .infinity, duration: 100, format: .percentage) == nil, "nonfinite positions never appear as compact progress") require(compactProgressText(position: 59, duration: 100, format: .none) == nil, "disabled compact progress leaves only the title") } static func checkQueue(directory: URL, defaults: UserDefaults) { let model = PlayerModel(defaults: defaults) defer { model.shutdown() } model.setVolume(0) let video = directory.appendingPathComponent("av.mp4") let audio = directory.appendingPathComponent("audio.wav") model.openFiles([video, audio]) until("queue opens its naturally sorted first file") { model.queue.currentURL == audio && model.state.status == PLAYER_PLAYING && model.canSeek && !model.isBusy } let current = model.queue.currentID! require(model.setPlayback(playing: false), "pause queue before editing") until("queue pause acknowledged") { model.state.status == PLAYER_PAUSED && model.playbackIntent == nil } let revision = model.state.revision model.addToQueue([video]) model.moveQueueItems(fromOffsets: IndexSet(integer: 0), toOffset: 2) require(model.queue.currentID == current && model.state.revision == revision && model.state.status == PLAYER_PAUSED, "append and reorder do not reopen current playback") model.removeQueueItem(model.queue.items.last!.id) require(model.queue.currentID == current && model.state.revision == revision, "removing another queue entry leaves native playback untouched") model.removeQueueItem(current) until("removing final current entry stops despite earlier remaining entries") { model.queue.currentID == nil && model.state.status == PLAYER_EMPTY } require(model.queue.items.count == 1, "stopping at queue end preserves earlier entries") model.selectQueueItem(model.queue.items[0].id) until("explicit queue selection opens remaining entry") { model.queue.currentURL == video && model.state.status == PLAYER_PLAYING && model.canSeek && !model.isBusy } model.openFiles([video, audio]) until("replacement queue starts with audio") { model.queue.currentURL == audio && model.state.status == PLAYER_PLAYING && model.canSeek && !model.isBusy } require(model.seekRelative(model.duration - model.state.position - 0.2), "queue seeks near first EOF") until("natural EOF advances to next queue file") { model.queue.currentURL == video && model.state.status == PLAYER_PLAYING && model.canSeek && !model.isBusy } require(model.seekRelative(model.duration - model.state.position - 0.2), "queue seeks near final EOF") until("final queue EOF stops without wrapping") { model.state.status == PLAYER_ENDED } require(model.queue.currentURL == video && !model.queue.canNext, "final EOF retains last selection") model.openFiles([directory.appendingPathComponent("hdr.mkv"), video]) model.selectQueueItem(model.queue.items.first { $0.url.lastPathComponent == "hdr.mkv" }!.id) until("queued playback error is acknowledged") { model.state.status == PLAYER_ERROR && !model.isBusy } require(model.queue.currentURL?.lastPathComponent == "hdr.mkv", "playback failure stops on failed queue entry rather than skipping") } static func checkKeyboardRouting(defaults: UserDefaults) { let bindings = try! KeyBindings(overrides: [ "play": [], "pause": [], "seek_backward_5s": [], "show_buffer_info": ["space", "return", "left", "shift+left", "tab", "shift+tab", "cmd+b", "f12", "ctrl+alt+b"] ]) let model = PlayerModel(defaults: defaults, configuration: PlayerConfiguration(keys: bindings)) defer { model.shutdown() } let window = NSWindow(contentRect: NSRect(x: -10_000, y: -10_000, width: 640, height: 360), styleMask: [.titled, .resizable], backing: .buffered, defer: false) window.isReleasedWhenClosed = false defer { window.close() } func event(_ code: UInt16, _ text: String, _ flags: NSEvent.ModifierFlags = []) -> NSEvent { NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: flags, timestamp: 0, windowNumber: window.windowNumber, context: nil, characters: text, charactersIgnoringModifiers: text, isARepeat: false, keyCode: code)! } let shortcuts = [event(49, " "), event(36, "\r"), event(123, "\u{F702}"), event(123, "\u{F702}", .shift), event(11, "b", .command), event(111, "\u{F70F}", .function)] func focus(_ responder: NSView) { window.makeFirstResponder(nil) window.contentView?.subviews.forEach { $0.removeFromSuperview() } responder.frame = NSRect(x: 0, y: 0, width: 300, height: 200) window.contentView?.addSubview(responder) require(window.makeFirstResponder(responder), "keyboard regression fixture accepts actual window focus") let focusedResponder = (responder as? NSTextField)?.currentEditor() ?? responder require(window.firstResponder === focusedResponder, "keyboard regression fixture owns actual control or field-editor focus") } let lists = [NSTableView(), NSOutlineView()] for list in lists { list.addTableColumn(NSTableColumn(identifier: NSUserInterfaceItemIdentifier("title"))) focus(list) for shortcut in shortcuts { model.setShowsBufferInfo(false) require(model.handleKeyEvent(shortcut, in: window) && model.showsBufferInfo, "focused queue list dispatches configured \(KeyChord.from(shortcut)!) binding") require(window.firstResponder === list, "playback shortcuts do not steal queue focus") } let traversalModifiers: [NSEvent.ModifierFlags] = [[], .shift] for flags in traversalModifiers { model.setShowsBufferInfo(false) require(!model.handleKeyEvent(event(48, "\t", flags), in: window) && !model.showsBufferInfo, "Tab and Shift-Tab remain native traversal even when configured") } } let label = KeyboardCheckLabel(labelWithString: "Queue item") focus(label) model.setShowsBufferInfo(false) require(model.handleKeyEvent(shortcuts[0], in: window) && model.showsBufferInfo, "noneditable nonselectable queue label does not block playback") let controls: [NSControl] = [KeyboardCheckButton(), KeyboardCheckSlider()] for control in controls { focus(control) for shortcut in shortcuts.prefix(4) { model.setShowsBufferInfo(false) require(!model.handleKeyEvent(shortcut, in: window) && !model.showsBufferInfo, "deliberately focused native button/slider retains activation and navigation") require(window.firstResponder === control, "native control keyboard focus is preserved") } require(model.handleKeyEvent(shortcuts[4], in: window) && model.showsBufferInfo, "modified configured bindings still work with native control focus") } let editor = NSTextView() focus(editor) for shortcut in shortcuts { model.setShowsBufferInfo(false) require(!model.handleKeyEvent(shortcut, in: window) && !model.showsBufferInfo, "text editing retains configured keys instead of dispatching playback") } editor.keyDown(with: shortcuts[0]) require(editor.string == " ", "unconsumed Space reaches actual text entry") label.isSelectable = true focus(label) require(!model.handleKeyEvent(shortcuts[0], in: window), "selectable text retains native text interaction") label.isSelectable = false label.isEditable = true require(!model.handleKeyEvent(shortcuts[0], in: window), "editable text field retains native text entry") focus(lists[0]) let voiceOverChord = event(11, "b", [.control, .option]) require(model.handleKeyEvent(voiceOverChord, in: window) != NSWorkspace.shared.isVoiceOverEnabled, "VoiceOver retains its Control-Option chords when enabled") let sheet = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 200, height: 100), styleMask: .titled, backing: .buffered, defer: false) sheet.isReleasedWhenClosed = false window.beginSheet(sheet) require(!model.handleKeyEvent(shortcuts[0], in: window), "attached sheet blocks player shortcuts") window.endSheet(sheet) sheet.close() let modal = NSApp.beginModalSession(for: window) require(!model.handleKeyEvent(shortcuts[0], in: window), "modal session blocks player shortcuts") NSApp.endModalSession(modal) } static func main() { checkCompactProgress() _ = NSApplication.shared NSApp.setActivationPolicy(.prohibited) let suite = "terebi.system-playback-check.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! defer { defaults.removePersistentDomain(forName: suite) } checkKeyboardRouting(defaults: defaults) checkQueue(directory: URL(fileURLWithPath: CommandLine.arguments[1], isDirectory: true), defaults: defaults) checkWindowSurface(defaults: defaults) defaults.removePersistentDomain(forName: suite) let initialModel = PlayerModel(defaults: defaults) require(!initialModel.showsBufferInfo, "buffer readout defaults off before playback") initialModel.setShowsBufferInfo(true) require(initialModel.showsBufferInfo && initialModel.controlsVisible, "buffer readout can be enabled before playback and reveals controls") initialModel.shutdown() let restoredModel = PlayerModel(defaults: defaults) require(!restoredModel.showsBufferInfo, "session opt-in does not survive model recreation") restoredModel.shutdown() let configuredModel = PlayerModel(defaults: defaults, configuration: PlayerConfiguration(showBufferInfo: true, automaticQuality: true, snapToEdges: false)) require(configuredModel.showsBufferInfo && configuredModel.automaticYouTubeQuality && !configuredModel.compactWindow.snappingEnabled, "configuration initializes all three controls") configuredModel.setShowsBufferInfo(false) configuredModel.setAutomaticYouTubeQuality(false) configuredModel.compactWindow.snappingEnabled = true configuredModel.shutdown() let model = PlayerModel(defaults: defaults) defer { model.shutdown() } require(!model.showsBufferInfo && !model.automaticYouTubeQuality && model.compactWindow.snappingEnabled, "session overrides never replace built-in settings") requirePlaybackPower(system: false, display: false, "idle player permits sleep") model.setVolume(0) let directory = URL(fileURLWithPath: CommandLine.arguments[1], isDirectory: true) let center = MPNowPlayingInfoCenter.default() let commands = MPRemoteCommandCenter.shared() let window = NSWindow(contentRect: NSRect(x: -10_000, y: -10_000, width: 640, height: 360), styleMask: [.titled, .closable, .miniaturizable, .resizable], backing: .buffered, defer: false) window.isReleasedWhenClosed = false defer { window.close() } model.attachWindow(window) let videoFrame = window.frame model.showsQueue = true until("queue opening settles") { !model.isQueueTransitioning } require(abs(window.frame.width - videoFrame.width - 260) < 0.01 && abs(window.frame.height - videoFrame.height) < 0.01, "opening queue grows window without shrinking video") model.showsQueue = false until("queue closing settles") { !model.isQueueTransitioning } require(abs(window.frame.width - videoFrame.width) < 0.01 && abs(window.frame.height - videoFrame.height) < 0.01, "closing queue restores video-only window size") model.open(directory.appendingPathComponent("anamorphic.mp4")) require(model.isBusy && model.perform(.enterCompact) && model.isCompact, "compact entry is available before native media opening acknowledges") until("decoded anamorphic metadata replaces provisional compact geometry") { model.state.status == PLAYER_PLAYING && model.isCompact && !model.compactWindow.isTransitioning && abs(window.frame.width / window.frame.height - 8.0 / 3.0) < 0.01 } model.closeFile() require(!model.isCompact && !model.compactWindow.isTransitioning, "closing loading/loaded media restores immediately") until("anamorphic source closes before the ordinary playback checks") { model.state.status == PLAYER_EMPTY } require(!model.canPerform(.enterCompact), "empty playback does not enable compact entry") model.open(directory.appendingPathComponent("av.mp4")) if !NSWorkspace.shared.isVoiceOverEnabled { model.setPointerInsideWindow(false) require(!model.controlsVisible, "pointer exit hides playback chrome while media is loading") model.setPointerInsideWindow(true) } until("open starts authoritative playback") { model.canTogglePlayback && model.state.status == PLAYER_PLAYING } model.showsQueue = true until("queue opens before live resize") { !model.isQueueTransitioning } 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 until("queue closes after live resize") { !model.isQueueTransitioning } require(center.playbackState == .playing, "playing publishes macOS playback ownership") requirePlaybackPower(system: true, display: true, "local video keeps the Mac and display awake") var bufferingState = model.state bufferingState.buffering = true model.applyState(bufferingState) requirePlaybackPower(system: true, display: true, "buffering does not drop video sleep prevention") require(model.setPlayback(playing: false), "explicit pause accepted") until("pause acknowledged") { model.state.status == PLAYER_PAUSED } requirePlaybackPower(system: false, display: false, "pause releases sleep prevention") let pausedPosition = model.state.position if !NSWorkspace.shared.isVoiceOverEnabled { model.setControlsHovered(true) model.setTitleHovered(true) model.setPointerInsideWindow(false) require(!model.controlsVisible, "pointer exit hides paused playback chrome despite stale hover and inactive window") model.setControlsHovered(false) model.setTitleHovered(false) model.updateWindowState() require(!model.controlsVisible, "late hover/focus updates cannot reshow chrome while the pointer is outside") model.revealControls(forKeyboardNavigation: true) model.revealControls() require(model.controlsVisible, "Tab navigation can reveal controls without moving the pointer") model.setPointerInsideWindow(false) require(!model.controlsVisible, "another pointer exit dismisses keyboard-revealed controls") model.setPointerInsideWindow(true) require(model.controlsVisible, "pointer reentry reveals paused playback chrome") } require(model.setPlayback(playing: false), "repeated pause accepted") until("idempotent pause does not restart playback") { model.state.status == PLAYER_PAUSED && center.playbackState == .paused } require(model.state.position == pausedPosition, "repeated pause preserves position") require(center.nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] as? Double == 0, "paused system timeline does not extrapolate") let playbackKey = try! KeyChord("space") let oppositeIntentRevision = model.state.revision require(model.handleKey(playbackKey) && model.effectivePlaying, "play binding records intent before backend acknowledgement") require(model.handleKey(playbackKey) && !model.effectivePlaying, "same binding resolves pause against pending play intent") require(model.handleKey(playbackKey, isRepeat: true) && !model.effectivePlaying, "held paired shortcut is consumed without oscillating") until("rapid opposite intents acknowledge play before the final pause") { model.state.revision != oppositeIntentRevision && model.state.status == PLAYER_PLAYING } let playingRevision = model.state.revision until("rapid opposite intents settle to an acknowledged native pause") { model.state.revision != playingRevision && model.state.status == PLAYER_PAUSED && !model.effectivePlaying && center.playbackState == .paused } require(center.nowPlayingInfo?[MPNowPlayingInfoPropertyPlaybackRate] as? Double == 0, "latest pause intent stops the system timeline after native acknowledgement") let settledPosition = model.state.position require(!model.perform(.pause) && !model.effectivePlaying, "explicit pause cannot resume an already paused player") let seekableState = model.state var nonseekableState = seekableState nonseekableState.can_seek = false model.applyState(nonseekableState) require(commands.playCommand.isEnabled && !commands.nextTrackCommand.isEnabled && !commands.skipBackwardCommand.isEnabled && !model.seekRelative(1), "nonseekable input retains playback but rejects system seeking") model.applyState(seekableState) require(commands.nextTrackCommand.isEnabled, "seekability changes re-enable commands even when playback metadata is unchanged") for _ in 0..<64 { require(model.seekRelative(1.0 / 64), "rapid native skips retain accumulated target") } until("paused seek acknowledged") { model.state.status == PLAYER_PAUSED && !model.state.seek_pending && abs(model.state.position - settledPosition - 1) < 0.05 } require(model.seekRelative(-100), "backward boundary seek accepted") until("backward seek clamps at start") { !model.state.seek_pending && model.state.position == 0 } require(!model.seekRelative(.nan), "nonfinite external seek rejected") require(model.setPlayback(playing: true), "explicit play accepted") until("play resumes advancing timeline") { model.state.status == PLAYER_PLAYING && model.state.position > 0.1 } require(model.setPlayback(playing: true), "repeated play accepted") until("repeated play keeps playing") { model.state.status == PLAYER_PLAYING && model.state.position > 0.2 } if !NSWorkspace.shared.isVoiceOverEnabled { require(model.perform(.enterCompact), "compact playback is available for hidden-keyboard seeking") until("compact entry completes before hiding controls") { !model.compactWindow.isTransitioning } model.hideControls(hideCursor: false) require(!model.controlsVisible, "playing chrome can be hidden before a keyboard seek") require(model.handleKey(try! KeyChord("left")), "keyboard seek is accepted while chrome is hidden") require(!model.controlsVisible, "keyboard seek must not flash playback chrome") until("hidden keyboard seek acknowledges the start boundary") { require(!model.controlsVisible, "seek acknowledgement must preserve hidden chrome") return !model.state.seek_pending && model.state.position < 0.15 } require(model.perform(.exitCompact), "keyboard seek check restores normal mode") until("compact exit completes") { !model.compactWindow.isTransitioning } } // Closing must hide the last polled retained range before the next state poll. var retainedSnapshot = model.state retainedSnapshot.buffered_start = retainedSnapshot.position retainedSnapshot.buffered_end = min(retainedSnapshot.duration, retainedSnapshot.position + 1) model.applyState(retainedSnapshot) require(model.bufferedRange != nil, "loaded selection exposes retained media") let retainedRange = model.bufferedRange model.setShowsBufferInfo(true) require(model.bufferedRange == retainedRange, "enabling buffer readout preserves retained media") model.setShowsBufferInfo(false) require(model.bufferedRange == retainedRange, "disabling buffer readout preserves retained media") model.closeFile() require(model.bufferedRange == nil, "accepted close immediately hides a stale retained-media snapshot") require(center.nowPlayingInfo == nil && center.playbackState == .stopped, "accepted close releases system playback immediately") requirePlaybackPower(system: false, display: false, "close releases sleep prevention immediately") require(!commands.togglePlayPauseCommand.isEnabled && !commands.nextTrackCommand.isEnabled, "closed source cannot receive system playback or seek commands") let localURL = directory.appendingPathComponent("av.mp4") let progress = PlaybackProgress(defaults: defaults) let savedPosition = seekableState.duration / 2 progress.save(savedPosition, for: localURL) var openRevision = model.state.revision model.open(localURL) until("local saved position is restored before native autoplay") { guard model.state.revision != openRevision else { return false } if model.state.status == PLAYER_PLAYING && !model.state.seek_pending { require(model.state.position >= savedPosition, "local autoplay never acknowledges playback before the saved position") return model.state.position < savedPosition + 0.5 } require(progress.position(for: localURL) == savedPosition, "pre-restoration snapshots cannot overwrite saved history") return false } require(model.setPlayback(playing: false), "restored local playback can pause") until("restored local pause acknowledged") { model.state.status == PLAYER_PAUSED && !model.state.seek_pending } let restoredPosition = model.state.position model.closeFile() require(progress.position(for: localURL) == restoredPosition, "close persists acknowledged restored position") openRevision = model.state.revision model.open(localURL) until("close and reopen resumes the persisted native position") { guard model.state.revision != openRevision && model.state.status == PLAYER_PLAYING && !model.state.seek_pending else { return false } require(model.state.position >= restoredPosition, "reopened local autoplay starts at acknowledged persisted progress") return model.state.position < restoredPosition + 0.5 } model.closeFile() let invalidPosition = seekableState.duration + 1 progress.save(invalidPosition, for: localURL) openRevision = model.state.revision model.open(localURL) until("duration-exceeded bookmark is discarded and native playback advances") { model.state.revision != openRevision && model.state.status == PLAYER_PLAYING && !model.state.seek_pending && model.state.position > 0.1 && model.state.position < savedPosition } require((progress.position(for: localURL) ?? 0) < seekableState.duration, "duration-exceeded bookmark is removed from exposed history") model.closeFile() let replacementPosition = progress.position(for: localURL)! require(replacementPosition > 0 && replacementPosition < savedPosition, "close replaces invalid history with acknowledged playback progress") openRevision = model.state.revision model.open(localURL) until("reopening no longer repeats a duration-exceeded restoration") { guard model.state.revision != openRevision && model.state.status == PLAYER_PLAYING && !model.state.seek_pending else { return false } require(model.state.position >= replacementPosition, "replacement bookmark restores before reopened autoplay") return model.state.position < replacementPosition + 0.5 } model.closeFile() model.open(directory.appendingPathComponent("hdr.mkv")) until("decode error acknowledged") { model.state.status == PLAYER_ERROR && !model.isBusy } require(center.nowPlayingInfo == nil && !commands.playCommand.isEnabled, "failed source does not claim playback keys") requirePlaybackPower(system: false, display: false, "playback failure permits sleep") model.open(localURL) until("playback resumes before end-of-media check") { model.state.status == PLAYER_PLAYING && model.canSeek && !model.state.seek_pending } require(model.seekRelative(model.duration - model.state.position - 0.2), "seek near end accepted") until("video reaches its natural end") { model.state.status == PLAYER_ENDED } requirePlaybackPower(system: false, display: false, "end of media permits sleep") model.open(directory.appendingPathComponent("audio.wav")) until("audio-only playback starts") { model.state.status == PLAYER_PLAYING && !model.state.has_video } requirePlaybackPower(system: true, display: false, "audio keeps the Mac awake but permits display sleep") model.shutdown() require(center.nowPlayingInfo == nil && !commands.pauseCommand.isEnabled, "shutdown releases system playback") requirePlaybackPower(system: false, display: false, "shutdown releases active sleep prevention") print("System playback checks passed") } }