diff --git a/ios/ProtonStream/Player/MpvPlayer.swift b/ios/ProtonStream/Player/MpvPlayer.swift index 8c80c32..3e8d144 100644 --- a/ios/ProtonStream/Player/MpvPlayer.swift +++ b/ios/ProtonStream/Player/MpvPlayer.swift @@ -339,7 +339,12 @@ final class MpvPlayer { paused = value onStateChanged?() case 5: muted = value - case 8: if !loading { buffering = value } + case 8: + // Said, so the lock screen's clock stops through a stall. + if !loading, buffering != value { + buffering = value + onStateChanged?() + } case 13: if !loading { seeking = value } default: break } diff --git a/ios/ProtonStream/Player/PlayerHost.swift b/ios/ProtonStream/Player/PlayerHost.swift index d061b45..b2fda74 100644 --- a/ios/ProtonStream/Player/PlayerHost.swift +++ b/ios/ProtonStream/Player/PlayerHost.swift @@ -75,7 +75,12 @@ final class PlayerHost { private(set) var index = 0 /// Playing, but not on screen: the episode keeps going and the mini /// transport is what says so. - var minimized = false + var minimized = false { + // The mini transport has no picture to keep awake for; back on screen + // and still playing, it has one again. Nothing else recomputes this + // until the next pause, so the screen slept under the video. + didSet { updateIdleTimer() } + } /// The open file's chapters, already resolved into openings and endings. private(set) var chapters = ChapterPlan(entries: [], creditsStart: nil) /// What stopped this episode, until the viewer dismisses it. @@ -479,9 +484,11 @@ final class PlayerHost { func enteredBackground() { save() guard isOpen, let player else { return } - if SettingsStore().backgroundAudio { - player.setVideoEnabled(false) - } else { + // The picture goes either way: with background audio off the lock + // screen's Play can still start the file, and decoding video into a + // layer nobody sees is work iOS ends a process for. + player.setVideoEnabled(false) + if !SettingsStore().backgroundAudio { player.setPaused(true) } publishNowPlaying() @@ -508,12 +515,15 @@ final class PlayerHost { info[MPNowPlayingInfoPropertyMediaType] = MPNowPlayingInfoMediaType.video.rawValue info[MPMediaItemPropertyPlaybackDuration] = player?.duration ?? 0 info[MPNowPlayingInfoPropertyElapsedPlaybackTime] = player?.position ?? 0 - info[MPNowPlayingInfoPropertyPlaybackRate] = (player?.paused ?? true) ? 0.0 : speed + // Zero while it waits on the network too, or the lock screen's clock + // runs on through a stall. + let moving = !(player?.paused ?? true) && !(player?.buffering ?? false) + info[MPNowPlayingInfoPropertyPlaybackRate] = moving ? speed : 0.0 center.nowPlayingInfo = info let commands = MPRemoteCommandCenter.shared() commands.nextTrackCommand.isEnabled = next != nil commands.previousTrackCommand.isEnabled = index > 0 - UIApplication.shared.isIdleTimerDisabled = !(player?.paused ?? true) && !minimized + updateIdleTimer() let artKey = mediaKey(episode) if artworkFor != artKey { @@ -531,6 +541,11 @@ final class PlayerHost { } } + /// Keep the screen on while a picture is playing on it, and only then. + private func updateIdleTimer() { + UIApplication.shared.isIdleTimerDisabled = isOpen && !(player?.paused ?? true) && !minimized + } + private func updateElapsed(_ seconds: Double) { let center = MPNowPlayingInfoCenter.default() guard var info = center.nowPlayingInfo else { return } @@ -565,6 +580,18 @@ final class PlayerHost { MainActor.assumeIsolated { self?.retreat() } return .success } + // Ten seconds either way: what a video's lock-screen transport offers + // in place of a music player's previous and next. + commands.skipForwardCommand.preferredIntervals = [10] + commands.skipForwardCommand.addTarget { [weak self] _ in + MainActor.assumeIsolated { self?.seek(to: (self?.player?.position ?? 0) + 10) } + return .success + } + commands.skipBackwardCommand.preferredIntervals = [10] + commands.skipBackwardCommand.addTarget { [weak self] _ in + MainActor.assumeIsolated { self?.seek(to: max((self?.player?.position ?? 0) - 10, 0)) } + return .success + } commands.stopCommand.addTarget { [weak self] _ in MainActor.assumeIsolated { self?.close() } return .success