mirror of
https://github.com/yattee/yattee.git
synced 2025-08-06 10:44:06 +00:00
Controls layouts, gestures and settings
This commit is contained in:
@@ -145,7 +145,7 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
avPlayer.replaceCurrentItem(with: nil)
|
||||
}
|
||||
|
||||
func seek(to time: CMTime, completionHandler: ((Bool) -> Void)?) {
|
||||
func seek(to time: CMTime, seekType _: PlayerTimeModel.SeekType, completionHandler: ((Bool) -> Void)?) {
|
||||
guard !model.live else { return }
|
||||
|
||||
avPlayer.seek(
|
||||
@@ -156,12 +156,6 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
)
|
||||
}
|
||||
|
||||
func seek(relative time: CMTime, completionHandler: ((Bool) -> Void)? = nil) {
|
||||
if let currentTime = currentTime {
|
||||
seek(to: currentTime + time, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
func setRate(_ rate: Float) {
|
||||
avPlayer.rate = rate
|
||||
}
|
||||
@@ -461,10 +455,11 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
if self.model.activeBackend != .appleAVPlayer {
|
||||
self.startPictureInPictureOnSwitch = true
|
||||
let seconds = self.model.mpvBackend.currentTime?.seconds ?? 0
|
||||
self.seek(to: seconds) { finished in
|
||||
guard finished else { return }
|
||||
self.model.pause()
|
||||
self.model.changeActiveBackend(from: .mpv, to: .appleAVPlayer, changingStream: false)
|
||||
self.seek(to: seconds, seekType: .backendSync) { _ in
|
||||
DispatchQueue.main.async {
|
||||
self.model.pause()
|
||||
self.model.changeActiveBackend(from: .mpv, to: .appleAVPlayer, changingStream: false)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -537,9 +532,7 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
#endif
|
||||
|
||||
if self.controlsUpdates {
|
||||
self.playerTime.duration = self.playerItemDuration ?? .zero
|
||||
self.playerTime.currentTime = self.currentTime ?? .zero
|
||||
self.model.objectWillChange.send()
|
||||
self.updateControls()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -607,8 +600,6 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
}
|
||||
}
|
||||
|
||||
func updateControls() {}
|
||||
|
||||
func startControlsUpdates() {
|
||||
guard model.presentingPlayer, model.controls.presentingControls, !model.controls.presentingOverlays else {
|
||||
logger.info("ignored controls update start")
|
||||
@@ -680,6 +671,7 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
}
|
||||
}
|
||||
|
||||
func getTimeUpdates() {}
|
||||
func setNeedsDrawing(_: Bool) {}
|
||||
func setSize(_: Double, _: Double) {}
|
||||
func setNeedsNetworkStateUpdates(_: Bool) {}
|
||||
|
@@ -8,7 +8,7 @@ import Repeat
|
||||
import SwiftUI
|
||||
|
||||
final class MPVBackend: PlayerBackend {
|
||||
static var controlsUpdateInterval = 0.5
|
||||
static var timeUpdateInterval = 0.5
|
||||
static var networkStateUpdateInterval = 1.0
|
||||
|
||||
private var logger = Logger(label: "mpv-backend")
|
||||
@@ -131,8 +131,8 @@ final class MPVBackend: PlayerBackend {
|
||||
self.playerTime = playerTime
|
||||
self.networkState = networkState
|
||||
|
||||
clientTimer = .init(interval: .seconds(Self.controlsUpdateInterval), mode: .infinite) { [weak self] _ in
|
||||
self?.getClientUpdates()
|
||||
clientTimer = .init(interval: .seconds(Self.timeUpdateInterval), mode: .infinite) { [weak self] _ in
|
||||
self?.getTimeUpdates()
|
||||
}
|
||||
|
||||
networkStateTimer = .init(interval: .seconds(Self.networkStateUpdateInterval), mode: .infinite) { [weak self] _ in
|
||||
@@ -204,7 +204,7 @@ final class MPVBackend: PlayerBackend {
|
||||
let segment = self.model.sponsorBlock.segments.first,
|
||||
self.model.lastSkipped.isNil
|
||||
{
|
||||
self.seek(to: segment.endTime) { finished in
|
||||
self.seek(to: segment.endTime, seekType: .segmentSkip(segment.category)) { finished in
|
||||
guard finished else {
|
||||
return
|
||||
}
|
||||
@@ -299,17 +299,9 @@ final class MPVBackend: PlayerBackend {
|
||||
client?.stop()
|
||||
}
|
||||
|
||||
func seek(to time: CMTime, completionHandler: ((Bool) -> Void)?) {
|
||||
func seek(to time: CMTime, seekType _: PlayerTimeModel.SeekType, completionHandler: ((Bool) -> Void)?) {
|
||||
client?.seek(to: time) { [weak self] _ in
|
||||
self?.getClientUpdates()
|
||||
self?.updateControls()
|
||||
completionHandler?(true)
|
||||
}
|
||||
}
|
||||
|
||||
func seek(relative time: CMTime, completionHandler: ((Bool) -> Void)? = nil) {
|
||||
client?.seek(relative: time) { [weak self] _ in
|
||||
self?.getClientUpdates()
|
||||
self?.getTimeUpdates()
|
||||
self?.updateControls()
|
||||
completionHandler?(true)
|
||||
}
|
||||
@@ -328,31 +320,6 @@ final class MPVBackend: PlayerBackend {
|
||||
|
||||
func closePiP() {}
|
||||
|
||||
func updateControls() {
|
||||
self.logger.info("updating controls")
|
||||
|
||||
guard model.presentingPlayer, !model.controls.presentingOverlays else {
|
||||
self.logger.info("ignored controls update")
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async(qos: .userInteractive) { [weak self] in
|
||||
guard let self = self else {
|
||||
return
|
||||
}
|
||||
|
||||
#if !os(macOS)
|
||||
guard UIApplication.shared.applicationState != .background else {
|
||||
self.logger.info("not performing controls updates in background")
|
||||
return
|
||||
}
|
||||
#endif
|
||||
|
||||
self.playerTime.currentTime = self.currentTime ?? .zero
|
||||
self.playerTime.duration = self.playerItemDuration ?? .zero
|
||||
}
|
||||
}
|
||||
|
||||
func startControlsUpdates() {
|
||||
guard model.presentingPlayer, model.controls.presentingControls, !model.controls.presentingOverlays else {
|
||||
self.logger.info("ignored controls update start")
|
||||
@@ -373,7 +340,7 @@ final class MPVBackend: PlayerBackend {
|
||||
|
||||
private var handleSegmentsThrottle = Throttle(interval: 1)
|
||||
|
||||
private func getClientUpdates() {
|
||||
func getTimeUpdates() {
|
||||
currentTime = client?.currentTime
|
||||
playerItemDuration = client?.duration
|
||||
|
||||
@@ -458,8 +425,7 @@ final class MPVBackend: PlayerBackend {
|
||||
return
|
||||
}
|
||||
|
||||
getClientUpdates()
|
||||
|
||||
getTimeUpdates()
|
||||
eofPlaybackModeAction()
|
||||
}
|
||||
|
||||
|
@@ -1,6 +1,9 @@
|
||||
import CoreMedia
|
||||
import Defaults
|
||||
import Foundation
|
||||
#if !os(macOS)
|
||||
import UIKit
|
||||
#endif
|
||||
|
||||
protocol PlayerBackend {
|
||||
var model: PlayerModel! { get set }
|
||||
@@ -38,9 +41,8 @@ protocol PlayerBackend {
|
||||
|
||||
func stop()
|
||||
|
||||
func seek(to time: CMTime, completionHandler: ((Bool) -> Void)?)
|
||||
func seek(to seconds: Double, completionHandler: ((Bool) -> Void)?)
|
||||
func seek(relative time: CMTime, completionHandler: ((Bool) -> Void)?)
|
||||
func seek(to time: CMTime, seekType: PlayerTimeModel.SeekType, completionHandler: ((Bool) -> Void)?)
|
||||
func seek(to seconds: Double, seekType: PlayerTimeModel.SeekType, completionHandler: ((Bool) -> Void)?)
|
||||
|
||||
func setRate(_ rate: Float)
|
||||
|
||||
@@ -51,7 +53,8 @@ protocol PlayerBackend {
|
||||
func startMusicMode()
|
||||
func stopMusicMode()
|
||||
|
||||
func updateControls()
|
||||
func getTimeUpdates()
|
||||
func updateControls(completionHandler: (() -> Void)?)
|
||||
func startControlsUpdates()
|
||||
func stopControlsUpdates()
|
||||
|
||||
@@ -64,16 +67,23 @@ protocol PlayerBackend {
|
||||
}
|
||||
|
||||
extension PlayerBackend {
|
||||
func seek(to time: CMTime, completionHandler: ((Bool) -> Void)? = nil) {
|
||||
seek(to: time, completionHandler: completionHandler)
|
||||
func seek(to time: CMTime, seekType: PlayerTimeModel.SeekType, completionHandler: ((Bool) -> Void)? = nil) {
|
||||
playerTime.registerSeek(at: time, type: seekType, restore: currentTime)
|
||||
seek(to: time, seekType: seekType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func seek(to seconds: Double, completionHandler: ((Bool) -> Void)? = nil) {
|
||||
seek(to: .secondsInDefaultTimescale(seconds), completionHandler: completionHandler)
|
||||
func seek(to seconds: Double, seekType: PlayerTimeModel.SeekType, completionHandler: ((Bool) -> Void)? = nil) {
|
||||
let seconds = CMTime.secondsInDefaultTimescale(seconds)
|
||||
playerTime.registerSeek(at: seconds, type: seekType, restore: currentTime)
|
||||
seek(to: seconds, seekType: seekType, completionHandler: completionHandler)
|
||||
}
|
||||
|
||||
func seek(relative time: CMTime, completionHandler: ((Bool) -> Void)? = nil) {
|
||||
seek(relative: time, completionHandler: completionHandler)
|
||||
func seek(relative time: CMTime, seekType: PlayerTimeModel.SeekType, completionHandler: ((Bool) -> Void)? = nil) {
|
||||
if let currentTime = currentTime, let duration = playerItemDuration {
|
||||
let seekTime = min(max(0, currentTime.seconds + time.seconds), duration.seconds)
|
||||
playerTime.registerSeek(at: .secondsInDefaultTimescale(seekTime), type: seekType, restore: currentTime)
|
||||
seek(to: seekTime, seekType: seekType, completionHandler: completionHandler)
|
||||
}
|
||||
}
|
||||
|
||||
func eofPlaybackModeAction() {
|
||||
@@ -92,7 +102,7 @@ extension PlayerBackend {
|
||||
model.advanceToNextItem()
|
||||
}
|
||||
case .loopOne:
|
||||
model.backend.seek(to: .zero) { _ in
|
||||
model.backend.seek(to: .zero, seekType: .loopRestart) { _ in
|
||||
self.model.play()
|
||||
}
|
||||
case .related:
|
||||
@@ -101,4 +111,27 @@ extension PlayerBackend {
|
||||
model.advanceToItem(item)
|
||||
}
|
||||
}
|
||||
|
||||
func updateControls(completionHandler: (() -> Void)? = nil) {
|
||||
print("updating controls")
|
||||
|
||||
guard model.presentingPlayer, !model.controls.presentingOverlays else {
|
||||
print("ignored controls update")
|
||||
completionHandler?()
|
||||
return
|
||||
}
|
||||
|
||||
DispatchQueue.main.async(qos: .userInteractive) {
|
||||
#if !os(macOS)
|
||||
guard UIApplication.shared.applicationState != .background else {
|
||||
print("not performing controls updates in background")
|
||||
completionHandler?()
|
||||
return
|
||||
}
|
||||
#endif
|
||||
self.playerTime.currentTime = self.currentTime ?? .zero
|
||||
self.playerTime.duration = self.playerItemDuration ?? .zero
|
||||
completionHandler?()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -13,7 +13,7 @@ final class PlayerControlsModel: ObservableObject {
|
||||
var timer: Timer?
|
||||
|
||||
#if os(tvOS)
|
||||
var reporter = PassthroughSubject<String, Never>()
|
||||
private(set) var reporter = PassthroughSubject<String, Never>()
|
||||
#endif
|
||||
|
||||
var player: PlayerModel!
|
||||
|
@@ -98,7 +98,7 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
@Published var queue = [PlayerQueueItem]() { didSet { handleQueueChange() } }
|
||||
@Published var currentItem: PlayerQueueItem! { didSet { handleCurrentItemChange() } }
|
||||
@Published var videoBeingOpened: Video?
|
||||
@Published var videoBeingOpened: Video? { didSet { playerTime.reset() } }
|
||||
@Published var historyVideos = [Video]()
|
||||
|
||||
@Published var preservedTime: CMTime?
|
||||
@@ -505,7 +505,16 @@ final class PlayerModel: ObservableObject {
|
||||
self.backend.setNeedsDrawing(self.presentingPlayer)
|
||||
}
|
||||
|
||||
controls.hide()
|
||||
#if os(tvOS)
|
||||
if presentingPlayer {
|
||||
controls.show()
|
||||
Delay.by(1) { [weak self] in
|
||||
self?.controls.hide()
|
||||
}
|
||||
}
|
||||
#else
|
||||
controls.hide()
|
||||
#endif
|
||||
|
||||
#if !os(macOS)
|
||||
UIApplication.shared.isIdleTimerDisabled = presentingPlayer
|
||||
@@ -531,6 +540,8 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
logger.info("changing backend from \(from.rawValue) to \(to.rawValue)")
|
||||
|
||||
let wasPlaying = isPlaying
|
||||
|
||||
if to == .mpv {
|
||||
closePiP()
|
||||
}
|
||||
@@ -543,18 +554,22 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
self.backend.didChangeTo()
|
||||
|
||||
fromBackend.pause()
|
||||
if wasPlaying {
|
||||
fromBackend.pause()
|
||||
}
|
||||
|
||||
guard var stream = stream, changingStream else {
|
||||
return
|
||||
}
|
||||
|
||||
if let stream = toBackend.stream, toBackend.video == fromBackend.video {
|
||||
toBackend.seek(to: fromBackend.currentTime?.seconds ?? .zero) { finished in
|
||||
toBackend.seek(to: fromBackend.currentTime?.seconds ?? .zero, seekType: .backendSync) { finished in
|
||||
guard finished else {
|
||||
return
|
||||
}
|
||||
toBackend.play()
|
||||
if wasPlaying {
|
||||
toBackend.play()
|
||||
}
|
||||
}
|
||||
|
||||
self.stream = stream
|
||||
@@ -764,17 +779,17 @@ final class PlayerModel: ObservableObject {
|
||||
skipBackwardCommand.preferredIntervals = preferredIntervals
|
||||
|
||||
skipForwardCommand.addTarget { [weak self] _ in
|
||||
self?.backend.seek(relative: .secondsInDefaultTimescale(10))
|
||||
self?.backend.seek(relative: .secondsInDefaultTimescale(10), seekType: .userInteracted)
|
||||
return .success
|
||||
}
|
||||
|
||||
skipBackwardCommand.addTarget { [weak self] _ in
|
||||
self?.backend.seek(relative: .secondsInDefaultTimescale(-10))
|
||||
self?.backend.seek(relative: .secondsInDefaultTimescale(-10), seekType: .userInteracted)
|
||||
return .success
|
||||
}
|
||||
|
||||
previousTrackCommand.addTarget { [weak self] _ in
|
||||
self?.backend.seek(to: .zero)
|
||||
self?.backend.seek(to: .zero, seekType: .userInteracted)
|
||||
return .success
|
||||
}
|
||||
|
||||
@@ -801,7 +816,7 @@ final class PlayerModel: ObservableObject {
|
||||
MPRemoteCommandCenter.shared().changePlaybackPositionCommand.addTarget { [weak self] remoteEvent in
|
||||
guard let event = remoteEvent as? MPChangePlaybackPositionCommandEvent else { return .commandFailed }
|
||||
|
||||
self?.backend.seek(to: event.positionTime)
|
||||
self?.backend.seek(to: event.positionTime, seekType: .userInteracted)
|
||||
|
||||
return .success
|
||||
}
|
||||
|
@@ -49,7 +49,7 @@ extension PlayerModel {
|
||||
return
|
||||
}
|
||||
|
||||
backend.seek(to: segment.endTime)
|
||||
backend.seek(to: segment.endTime, seekType: .segmentSkip(segment.category))
|
||||
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
withAnimation {
|
||||
@@ -79,7 +79,7 @@ extension PlayerModel {
|
||||
}
|
||||
|
||||
restoredSegments.append(segment)
|
||||
backend.seek(to: time)
|
||||
backend.seek(to: time, seekType: .segmentRestore)
|
||||
resetLastSegment()
|
||||
}
|
||||
|
||||
|
@@ -1,13 +1,35 @@
|
||||
import CoreMedia
|
||||
import Foundation
|
||||
import SwiftUI
|
||||
|
||||
final class PlayerTimeModel: ObservableObject {
|
||||
enum SeekType: Equatable {
|
||||
case segmentSkip(String)
|
||||
case segmentRestore
|
||||
case userInteracted
|
||||
case loopRestart
|
||||
case backendSync
|
||||
|
||||
var presentable: Bool {
|
||||
self != .backendSync
|
||||
}
|
||||
}
|
||||
|
||||
static let timePlaceholder = "--:--"
|
||||
|
||||
@Published var currentTime = CMTime.zero
|
||||
@Published var duration = CMTime.zero
|
||||
|
||||
var player: PlayerModel?
|
||||
@Published var lastSeekTime: CMTime?
|
||||
@Published var lastSeekType: SeekType?
|
||||
@Published var restoreSeekTime: CMTime?
|
||||
|
||||
@Published var gestureSeek = 0.0
|
||||
@Published var gestureStart = 0.0
|
||||
|
||||
@Published var seekOSDDismissed = true
|
||||
|
||||
var player: PlayerModel!
|
||||
|
||||
var forceHours: Bool {
|
||||
duration.seconds >= 60 * 60
|
||||
@@ -30,15 +52,73 @@ final class PlayerTimeModel: ObservableObject {
|
||||
}
|
||||
|
||||
var withoutSegmentsPlaybackTime: String {
|
||||
guard let withoutSegmentsDuration = player?.playerItemDurationWithoutSponsorSegments?.seconds else {
|
||||
return Self.timePlaceholder
|
||||
}
|
||||
|
||||
guard let withoutSegmentsDuration = player?.playerItemDurationWithoutSponsorSegments?.seconds else { return Self.timePlaceholder }
|
||||
return withoutSegmentsDuration.formattedAsPlaybackTime(forceHours: forceHours) ?? Self.timePlaceholder
|
||||
}
|
||||
|
||||
var lastSeekPlaybackTime: String {
|
||||
guard let time = lastSeekTime else { return 0.formattedAsPlaybackTime(allowZero: true, forceHours: forceHours) ?? Self.timePlaceholder }
|
||||
return time.seconds.formattedAsPlaybackTime(allowZero: true, forceHours: forceHours) ?? Self.timePlaceholder
|
||||
}
|
||||
|
||||
var restoreSeekPlaybackTime: String {
|
||||
guard let time = restoreSeekTime else { return Self.timePlaceholder }
|
||||
return time.seconds.formattedAsPlaybackTime(allowZero: true, forceHours: forceHours) ?? Self.timePlaceholder
|
||||
}
|
||||
|
||||
var gestureSeekDestinationTime: Double {
|
||||
min(duration.seconds, max(0, gestureStart + gestureSeek))
|
||||
}
|
||||
|
||||
var gestureSeekDestinationPlaybackTime: String {
|
||||
guard gestureSeek != 0 else { return Self.timePlaceholder }
|
||||
return gestureSeekDestinationTime.formattedAsPlaybackTime(allowZero: true, forceHours: forceHours) ?? Self.timePlaceholder
|
||||
}
|
||||
|
||||
func onSeekGestureStart(completionHandler: (() -> Void)? = nil) {
|
||||
player.backend.getTimeUpdates()
|
||||
player.backend.updateControls {
|
||||
self.gestureStart = self.currentTime.seconds
|
||||
completionHandler?()
|
||||
}
|
||||
}
|
||||
|
||||
func onSeekGestureEnd() {
|
||||
player.backend.updateControls()
|
||||
player.backend.seek(to: gestureSeekDestinationTime, seekType: .userInteracted)
|
||||
}
|
||||
|
||||
func registerSeek(at time: CMTime, type: SeekType, restore restoreTime: CMTime? = nil) {
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
withAnimation {
|
||||
self?.lastSeekTime = time
|
||||
self?.lastSeekType = type
|
||||
self?.restoreSeekTime = restoreTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func restoreTime() {
|
||||
guard let time = restoreSeekTime else { return }
|
||||
switch lastSeekType {
|
||||
case .segmentSkip:
|
||||
player.restoreLastSkippedSegment()
|
||||
default:
|
||||
player?.backend.seek(to: time, seekType: .userInteracted)
|
||||
}
|
||||
}
|
||||
|
||||
func resetSeek() {
|
||||
withAnimation {
|
||||
lastSeekTime = nil
|
||||
lastSeekType = nil
|
||||
}
|
||||
}
|
||||
|
||||
func reset() {
|
||||
currentTime = .zero
|
||||
duration = .zero
|
||||
resetSeek()
|
||||
gestureSeek = 0
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user