2021-10-05 20:20:09 +00:00
|
|
|
|
import AVKit
|
2021-12-26 21:14:46 +00:00
|
|
|
|
import CoreData
|
2022-01-02 19:43:30 +00:00
|
|
|
|
#if os(iOS)
|
|
|
|
|
import CoreMotion
|
|
|
|
|
#endif
|
2021-10-05 20:20:09 +00:00
|
|
|
|
import Defaults
|
2021-06-14 18:05:02 +00:00
|
|
|
|
import Foundation
|
2021-06-15 16:35:21 +00:00
|
|
|
|
import Logging
|
2021-10-28 20:18:23 +00:00
|
|
|
|
import MediaPlayer
|
2021-10-16 22:48:58 +00:00
|
|
|
|
import Siesta
|
2021-10-23 16:49:45 +00:00
|
|
|
|
import SwiftUI
|
2021-10-16 22:48:58 +00:00
|
|
|
|
import SwiftyJSON
|
2022-01-02 19:43:30 +00:00
|
|
|
|
#if !os(macOS)
|
|
|
|
|
import UIKit
|
|
|
|
|
#endif
|
2021-06-14 18:05:02 +00:00
|
|
|
|
|
2021-09-25 08:18:22 +00:00
|
|
|
|
final class PlayerModel: ObservableObject {
|
2022-07-10 22:24:56 +00:00
|
|
|
|
enum PlaybackMode: String, CaseIterable, Defaults.Serializable {
|
|
|
|
|
case queue, shuffle, loopOne, related
|
|
|
|
|
|
|
|
|
|
var systemImage: String {
|
|
|
|
|
switch self {
|
|
|
|
|
case .queue:
|
|
|
|
|
return "list.number"
|
|
|
|
|
case .shuffle:
|
|
|
|
|
return "shuffle"
|
|
|
|
|
case .loopOne:
|
|
|
|
|
return "repeat.1"
|
|
|
|
|
case .related:
|
|
|
|
|
return "infinity"
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-08-25 23:59:42 +00:00
|
|
|
|
|
|
|
|
|
var description: String {
|
|
|
|
|
switch self {
|
|
|
|
|
case .queue:
|
|
|
|
|
return "Queue"
|
|
|
|
|
case .shuffle:
|
2022-12-19 09:48:30 +00:00
|
|
|
|
return "Queue - shuffled"
|
2022-08-25 23:59:42 +00:00
|
|
|
|
case .loopOne:
|
|
|
|
|
return "Loop one"
|
|
|
|
|
case .related:
|
|
|
|
|
return "Autoplay next"
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-07-10 22:24:56 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-11-24 20:36:05 +00:00
|
|
|
|
static var shared = PlayerModel()
|
2022-08-31 22:00:28 +00:00
|
|
|
|
|
2024-08-31 20:42:17 +00:00
|
|
|
|
let logger = Logger(label: "stream.yattee.player.model")
|
2021-06-15 16:35:21 +00:00
|
|
|
|
|
2021-12-29 18:55:41 +00:00
|
|
|
|
var playerItem: AVPlayerItem?
|
2021-06-15 21:21:57 +00:00
|
|
|
|
|
2022-02-16 20:23:11 +00:00
|
|
|
|
var mpvPlayerView = MPVPlayerView()
|
|
|
|
|
|
2022-08-31 19:24:46 +00:00
|
|
|
|
@Published var presentingPlayer = false { didSet { handlePresentationChange() } }
|
2022-02-16 20:23:11 +00:00
|
|
|
|
@Published var activeBackend = PlayerBackendType.mpv
|
2023-05-26 20:49:38 +00:00
|
|
|
|
@Published var forceBackendOnPlay: PlayerBackendType?
|
2022-02-16 20:23:11 +00:00
|
|
|
|
|
2022-09-01 23:05:31 +00:00
|
|
|
|
var avPlayerBackend = AVPlayerBackend()
|
|
|
|
|
var mpvBackend = MPVBackend()
|
2022-08-14 16:53:03 +00:00
|
|
|
|
#if !os(macOS)
|
|
|
|
|
var mpvController = MPVViewController()
|
|
|
|
|
#endif
|
2022-02-16 20:23:11 +00:00
|
|
|
|
|
|
|
|
|
var backends: [PlayerBackend] {
|
|
|
|
|
[avPlayerBackend, mpvBackend]
|
|
|
|
|
}
|
2021-06-18 10:17:01 +00:00
|
|
|
|
|
2022-02-16 20:23:11 +00:00
|
|
|
|
var backend: PlayerBackend! {
|
|
|
|
|
switch activeBackend {
|
|
|
|
|
case .mpv:
|
|
|
|
|
return mpvBackend
|
|
|
|
|
case .appleAVPlayer:
|
|
|
|
|
return avPlayerBackend
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-24 19:32:32 +00:00
|
|
|
|
var previousActiveBackend: PlayerBackendType?
|
|
|
|
|
|
2022-11-24 20:36:05 +00:00
|
|
|
|
lazy var playerBackendView = PlayerBackendView()
|
2022-08-18 22:40:46 +00:00
|
|
|
|
|
2022-03-27 11:42:20 +00:00
|
|
|
|
@Published var playerSize: CGSize = .zero { didSet {
|
2022-08-14 16:59:04 +00:00
|
|
|
|
#if !os(tvOS)
|
2022-12-18 18:39:03 +00:00
|
|
|
|
#if os(macOS)
|
|
|
|
|
guard videoForDisplay != nil else { return }
|
|
|
|
|
#endif
|
2022-08-14 16:59:04 +00:00
|
|
|
|
backend.setSize(playerSize.width, playerSize.height)
|
|
|
|
|
#endif
|
2022-03-27 11:42:20 +00:00
|
|
|
|
}}
|
2022-07-10 01:15:15 +00:00
|
|
|
|
@Published var aspectRatio = VideoPlayerView.defaultAspectRatio
|
2021-10-05 20:20:09 +00:00
|
|
|
|
@Published var stream: Stream?
|
2024-08-18 12:46:51 +00:00
|
|
|
|
@Published var currentRate = 1.0 { didSet { handleCurrentRateChange() } }
|
2021-06-15 16:35:21 +00:00
|
|
|
|
|
2022-08-29 11:55:23 +00:00
|
|
|
|
@Published var qualityProfileSelection: QualityProfile? { didSet { handleQualityProfileChange() } }
|
2022-08-14 17:06:22 +00:00
|
|
|
|
|
2021-12-19 17:17:04 +00:00
|
|
|
|
@Published var availableStreams = [Stream]() { didSet { handleAvailableStreamsChange() } }
|
2021-10-23 16:49:45 +00:00
|
|
|
|
@Published var streamSelection: Stream? { didSet { rebuildTVMenu() } }
|
2021-10-16 22:48:58 +00:00
|
|
|
|
|
2022-12-21 20:16:47 +00:00
|
|
|
|
@Published var captions: Captions? { didSet {
|
|
|
|
|
mpvBackend.captions = captions
|
|
|
|
|
if let code = captions?.code {
|
|
|
|
|
Defaults[.captionsLanguageCode] = code
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
|
2022-06-26 14:46:29 +00:00
|
|
|
|
@Published var queue = [PlayerQueueItem]() { didSet { handleQueueChange() } }
|
2022-01-09 15:05:05 +00:00
|
|
|
|
@Published var currentItem: PlayerQueueItem! { didSet { handleCurrentItemChange() } }
|
2022-08-29 11:55:23 +00:00
|
|
|
|
@Published var videoBeingOpened: Video? { didSet { seek.reset() } }
|
2021-12-26 21:14:46 +00:00
|
|
|
|
@Published var historyVideos = [Video]()
|
2021-12-17 20:01:05 +00:00
|
|
|
|
@Published var preservedTime: CMTime?
|
2021-06-17 22:43:29 +00:00
|
|
|
|
|
2021-10-23 16:49:45 +00:00
|
|
|
|
@Published var sponsorBlock = SponsorBlockAPI()
|
|
|
|
|
@Published var segmentRestorationTime: CMTime?
|
|
|
|
|
@Published var lastSkipped: Segment? { didSet { rebuildTVMenu() } }
|
|
|
|
|
@Published var restoredSegments = [Segment]()
|
|
|
|
|
|
2022-06-07 21:27:48 +00:00
|
|
|
|
@Published var musicMode = false
|
2022-07-11 16:10:51 +00:00
|
|
|
|
@Published var playbackMode = PlaybackMode.queue { didSet { handlePlaybackModeChange() } }
|
2022-07-10 22:24:56 +00:00
|
|
|
|
@Published var autoplayItem: PlayerQueueItem?
|
|
|
|
|
@Published var autoplayItemSource: Video?
|
|
|
|
|
@Published var advancing = false
|
2022-12-18 12:11:06 +00:00
|
|
|
|
@Published var closing = false
|
2022-07-10 22:24:56 +00:00
|
|
|
|
|
2022-03-19 23:04:56 +00:00
|
|
|
|
@Published var returnYouTubeDislike = ReturnYouTubeDislikeAPI()
|
|
|
|
|
|
2022-06-18 12:39:49 +00:00
|
|
|
|
@Published var isSeeking = false { didSet {
|
2022-06-24 23:39:29 +00:00
|
|
|
|
backend.setNeedsNetworkStateUpdates(true)
|
2022-06-18 12:39:49 +00:00
|
|
|
|
}}
|
|
|
|
|
|
2022-07-10 23:26:35 +00:00
|
|
|
|
#if os(iOS)
|
|
|
|
|
@Published var lockedOrientation: UIInterfaceOrientationMask?
|
2024-09-01 10:42:31 +00:00
|
|
|
|
@Published var isOrientationLocked: Bool {
|
|
|
|
|
didSet {
|
|
|
|
|
Defaults[.isOrientationLocked] = isOrientationLocked
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-06 15:05:20 +00:00
|
|
|
|
@Default(.rotateToLandscapeOnEnterFullScreen) var rotateToLandscapeOnEnterFullScreen
|
|
|
|
|
@Default(.lockPortraitWhenBrowsing) var lockPortraitWhenBrowsing
|
|
|
|
|
var fullscreenInitiatedByButton = false
|
2022-07-10 23:26:35 +00:00
|
|
|
|
#endif
|
|
|
|
|
|
2023-12-04 20:58:49 +00:00
|
|
|
|
@Published var currentChapterIndex: Int?
|
2023-11-28 15:45:36 +00:00
|
|
|
|
|
2022-12-09 00:15:19 +00:00
|
|
|
|
var accounts: AccountsModel { .shared }
|
2022-11-24 20:36:05 +00:00
|
|
|
|
var comments: CommentsModel { .shared }
|
2022-09-01 23:05:31 +00:00
|
|
|
|
var controls: PlayerControlsModel { .shared }
|
|
|
|
|
var playerTime: PlayerTimeModel { .shared }
|
|
|
|
|
var networkState: NetworkStateModel { .shared }
|
|
|
|
|
var seek: SeekModel { .shared }
|
2022-11-24 20:36:05 +00:00
|
|
|
|
var navigation: NavigationModel { .shared }
|
2022-06-29 22:44:32 +00:00
|
|
|
|
|
2021-12-26 21:14:46 +00:00
|
|
|
|
var context: NSManagedObjectContext = PersistenceController.shared.container.viewContext
|
2022-06-07 22:15:15 +00:00
|
|
|
|
var backgroundContext = PersistenceController.shared.container.newBackgroundContext()
|
2021-12-26 21:14:46 +00:00
|
|
|
|
|
2022-07-10 17:51:46 +00:00
|
|
|
|
#if os(tvOS)
|
|
|
|
|
static let fullScreenIsDefault = true
|
|
|
|
|
#else
|
|
|
|
|
static let fullScreenIsDefault = false
|
|
|
|
|
#endif
|
|
|
|
|
@Published var playingFullScreen = PlayerModel.fullScreenIsDefault
|
|
|
|
|
|
2022-02-16 20:23:11 +00:00
|
|
|
|
@Published var playingInPictureInPicture = false
|
2022-05-20 21:23:14 +00:00
|
|
|
|
var pipController: AVPictureInPictureController?
|
|
|
|
|
var pipDelegate = PiPDelegate()
|
2023-05-20 20:49:10 +00:00
|
|
|
|
#if !os(macOS)
|
|
|
|
|
var appleAVPlayerViewControllerDelegate = AppleAVPlayerViewControllerDelegate()
|
|
|
|
|
#endif
|
2021-10-28 17:14:55 +00:00
|
|
|
|
|
2021-11-07 16:52:42 +00:00
|
|
|
|
var playerError: Error? { didSet {
|
2022-08-18 22:36:16 +00:00
|
|
|
|
if let error = playerError {
|
2022-10-26 11:11:35 +00:00
|
|
|
|
navigation.presentAlert(title: "Failed loading video".localized(), message: error.localizedDescription)
|
2022-08-18 22:36:16 +00:00
|
|
|
|
}
|
2021-11-07 16:52:42 +00:00
|
|
|
|
}}
|
|
|
|
|
|
2022-12-09 00:15:19 +00:00
|
|
|
|
@Default(.saveHistory) var saveHistory
|
2022-09-11 16:34:33 +00:00
|
|
|
|
@Default(.saveLastPlayed) var saveLastPlayed
|
|
|
|
|
@Default(.lastPlayed) var lastPlayed
|
2022-08-14 17:06:22 +00:00
|
|
|
|
@Default(.qualityProfiles) var qualityProfiles
|
2023-05-20 20:49:10 +00:00
|
|
|
|
@Default(.avPlayerUsesSystemControls) var avPlayerUsesSystemControls
|
2022-08-22 21:14:27 +00:00
|
|
|
|
@Default(.forceAVPlayerForLiveStreams) var forceAVPlayerForLiveStreams
|
2021-12-19 17:17:04 +00:00
|
|
|
|
@Default(.pauseOnHidingPlayer) private var pauseOnHidingPlayer
|
|
|
|
|
@Default(.closePiPOnNavigation) var closePiPOnNavigation
|
|
|
|
|
@Default(.closePiPOnOpeningPlayer) var closePiPOnOpeningPlayer
|
2022-08-08 18:02:46 +00:00
|
|
|
|
@Default(.resetWatchedStatusOnPlaying) var resetWatchedStatusOnPlaying
|
2022-11-10 22:00:17 +00:00
|
|
|
|
@Default(.playerRate) var playerRate
|
2022-12-19 11:08:27 +00:00
|
|
|
|
@Default(.systemControlsSeekDuration) var systemControlsSeekDuration
|
2024-04-20 23:01:55 +00:00
|
|
|
|
|
2024-02-17 09:40:27 +00:00
|
|
|
|
#if os(macOS)
|
|
|
|
|
@Default(.buttonBackwardSeekDuration) private var buttonBackwardSeekDuration
|
|
|
|
|
@Default(.buttonForwardSeekDuration) private var buttonForwardSeekDuration
|
|
|
|
|
#endif
|
2021-12-19 17:17:04 +00:00
|
|
|
|
|
|
|
|
|
#if !os(macOS)
|
|
|
|
|
@Default(.closePiPAndOpenPlayerOnEnteringForeground) var closePiPAndOpenPlayerOnEnteringForeground
|
|
|
|
|
#endif
|
|
|
|
|
|
2022-02-16 21:10:57 +00:00
|
|
|
|
private var currentArtwork: MPMediaItemArtwork?
|
|
|
|
|
|
2022-08-26 08:25:07 +00:00
|
|
|
|
var onPresentPlayer = [() -> Void]()
|
2023-06-07 19:39:03 +00:00
|
|
|
|
var onPlayStream = [(Stream) -> Void]()
|
2023-06-08 10:22:34 +00:00
|
|
|
|
var rateToRestore: Float?
|
2022-07-11 16:10:51 +00:00
|
|
|
|
private var remoteCommandCenterConfigured = false
|
2024-04-20 23:01:55 +00:00
|
|
|
|
|
2024-09-10 09:04:05 +00:00
|
|
|
|
// Used in the PlayerModel extension in PlayerQueue
|
|
|
|
|
var retryAttempts = [String: Int]()
|
|
|
|
|
|
2024-02-17 09:40:27 +00:00
|
|
|
|
#if os(macOS)
|
|
|
|
|
var keyPressMonitor: Any?
|
|
|
|
|
#endif
|
2022-02-16 20:23:11 +00:00
|
|
|
|
|
2022-11-24 20:36:05 +00:00
|
|
|
|
init() {
|
2024-09-01 10:42:31 +00:00
|
|
|
|
#if os(iOS)
|
|
|
|
|
isOrientationLocked = Defaults[.isOrientationLocked]
|
|
|
|
|
|
2024-09-06 13:11:31 +00:00
|
|
|
|
if isOrientationLocked, lockPortraitWhenBrowsing {
|
2024-09-01 10:42:31 +00:00
|
|
|
|
lockedOrientation = UIInterfaceOrientationMask.portrait
|
|
|
|
|
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
|
|
|
|
} else if isOrientationLocked {
|
|
|
|
|
lockOrientationAction()
|
|
|
|
|
}
|
|
|
|
|
#endif
|
2022-08-14 16:53:03 +00:00
|
|
|
|
#if !os(macOS)
|
|
|
|
|
mpvBackend.controller = mpvController
|
|
|
|
|
mpvBackend.client = mpvController.client
|
2024-08-31 20:42:17 +00:00
|
|
|
|
|
|
|
|
|
// Register for audio session interruption notifications
|
|
|
|
|
NotificationCenter.default.addObserver(
|
|
|
|
|
self,
|
|
|
|
|
selector: #selector(handleAudioSessionInterruption(_:)),
|
|
|
|
|
name: AVAudioSession.interruptionNotification,
|
|
|
|
|
object: nil
|
|
|
|
|
)
|
2024-09-11 18:18:56 +00:00
|
|
|
|
|
|
|
|
|
// Register for audio session route change notifications
|
|
|
|
|
NotificationCenter.default.addObserver(
|
|
|
|
|
self,
|
|
|
|
|
selector: #selector(handleRouteChange(_:)),
|
|
|
|
|
name: AVAudioSession.routeChangeNotification,
|
|
|
|
|
object: AVAudioSession.sharedInstance()
|
|
|
|
|
)
|
2022-08-14 16:53:03 +00:00
|
|
|
|
#endif
|
|
|
|
|
|
2022-07-10 22:24:56 +00:00
|
|
|
|
playbackMode = Defaults[.playbackMode]
|
2022-08-18 22:40:46 +00:00
|
|
|
|
|
|
|
|
|
guard pipController.isNil else { return }
|
|
|
|
|
|
2023-05-20 20:49:10 +00:00
|
|
|
|
pipController = .init(playerLayer: avPlayerBackend.playerLayer)
|
2022-08-18 22:40:46 +00:00
|
|
|
|
pipController?.delegate = pipDelegate
|
2023-05-20 20:49:10 +00:00
|
|
|
|
#if os(iOS)
|
|
|
|
|
if #available(iOS 14.2, *) {
|
|
|
|
|
pipController?.canStartPictureInPictureAutomaticallyFromInline = true
|
|
|
|
|
}
|
|
|
|
|
#endif
|
2022-11-10 22:00:17 +00:00
|
|
|
|
currentRate = playerRate
|
2021-10-24 18:01:08 +00:00
|
|
|
|
}
|
2021-10-24 14:01:36 +00:00
|
|
|
|
|
2024-09-04 07:37:38 +00:00
|
|
|
|
#if !os(macOS)
|
|
|
|
|
deinit {
|
2024-09-11 18:18:56 +00:00
|
|
|
|
NotificationCenter.default.removeObserver(
|
|
|
|
|
self, name: AVAudioSession.interruptionNotification, object: nil
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
NotificationCenter.default.removeObserver(
|
|
|
|
|
self,
|
|
|
|
|
name: AVAudioSession.routeChangeNotification,
|
|
|
|
|
object: AVAudioSession.sharedInstance()
|
|
|
|
|
)
|
2024-09-04 07:37:38 +00:00
|
|
|
|
}
|
|
|
|
|
#endif
|
2024-08-31 20:42:17 +00:00
|
|
|
|
|
2021-12-19 17:17:04 +00:00
|
|
|
|
func show() {
|
2022-05-28 21:41:23 +00:00
|
|
|
|
#if os(macOS)
|
|
|
|
|
if presentingPlayer {
|
2022-01-06 15:35:45 +00:00
|
|
|
|
Windows.player.focus()
|
2024-02-17 09:40:27 +00:00
|
|
|
|
assignKeyPressMonitor()
|
2022-05-28 21:41:23 +00:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
2022-08-25 17:09:55 +00:00
|
|
|
|
#if os(iOS)
|
|
|
|
|
Delay.by(0.5) {
|
|
|
|
|
self.navigation.hideKeyboard()
|
2022-07-09 00:21:04 +00:00
|
|
|
|
}
|
2022-08-25 17:09:55 +00:00
|
|
|
|
#endif
|
|
|
|
|
|
2022-08-29 12:07:27 +00:00
|
|
|
|
presentingPlayer = true
|
2022-05-28 21:41:23 +00:00
|
|
|
|
|
2021-12-19 17:17:04 +00:00
|
|
|
|
#if os(macOS)
|
2022-01-06 15:35:45 +00:00
|
|
|
|
Windows.player.open()
|
|
|
|
|
Windows.player.focus()
|
2024-02-17 09:40:27 +00:00
|
|
|
|
assignKeyPressMonitor()
|
2021-12-19 17:17:04 +00:00
|
|
|
|
#endif
|
2021-06-15 21:21:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-08-25 17:09:55 +00:00
|
|
|
|
func hide(animate: Bool = true) {
|
|
|
|
|
if animate {
|
|
|
|
|
withAnimation(.easeOut(duration: 0.2)) {
|
|
|
|
|
presentingPlayer = false
|
|
|
|
|
}
|
|
|
|
|
} else {
|
2022-08-08 18:02:46 +00:00
|
|
|
|
presentingPlayer = false
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-26 15:23:56 +00:00
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
2023-05-29 13:54:23 +00:00
|
|
|
|
Delay.by(0.3) {
|
|
|
|
|
self?.exitFullScreen(showControls: false)
|
|
|
|
|
}
|
2022-06-26 15:23:56 +00:00
|
|
|
|
}
|
2022-03-27 19:17:52 +00:00
|
|
|
|
|
2022-08-26 20:17:21 +00:00
|
|
|
|
#if os(macOS)
|
2024-02-17 09:40:27 +00:00
|
|
|
|
destroyKeyPressMonitor()
|
2022-08-26 20:17:21 +00:00
|
|
|
|
Windows.player.hide()
|
|
|
|
|
#endif
|
2021-12-19 17:17:04 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-11-08 23:14:28 +00:00
|
|
|
|
func togglePlayer() {
|
2021-12-19 17:17:04 +00:00
|
|
|
|
#if os(macOS)
|
|
|
|
|
if !presentingPlayer {
|
2022-01-06 15:35:45 +00:00
|
|
|
|
Windows.player.open()
|
2021-12-19 17:17:04 +00:00
|
|
|
|
}
|
2022-01-06 15:35:45 +00:00
|
|
|
|
Windows.player.focus()
|
2022-08-26 20:17:21 +00:00
|
|
|
|
|
|
|
|
|
if Windows.player.visible,
|
|
|
|
|
closePiPOnOpeningPlayer
|
|
|
|
|
{
|
|
|
|
|
closePiP()
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-19 17:17:04 +00:00
|
|
|
|
#else
|
|
|
|
|
if presentingPlayer {
|
|
|
|
|
hide()
|
|
|
|
|
} else {
|
|
|
|
|
show()
|
|
|
|
|
}
|
|
|
|
|
#endif
|
2021-11-08 23:14:28 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-12-29 18:39:38 +00:00
|
|
|
|
var isLoadingVideo: Bool {
|
|
|
|
|
guard !currentVideo.isNil else {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-16 20:23:11 +00:00
|
|
|
|
return backend.isLoadingVideo
|
2021-12-29 18:39:38 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-10-16 22:48:58 +00:00
|
|
|
|
var isPlaying: Bool {
|
2022-02-16 20:23:11 +00:00
|
|
|
|
backend.isPlaying
|
2021-10-16 22:48:58 +00:00
|
|
|
|
}
|
|
|
|
|
|
Conditional proxying
I added a new feature. When instances are not proxied, Yattee first checks the URL to make sure it is not a restricted video. Usually, music videos and sports content can only be played back by the same IP address that requested the URL in the first place. That is why some videos do not play when the proxy is disabled.
This approach has multiple advantages. First and foremost, It reduced the load on Invidious/Piped instances, since users can now directly access the videos without going through the instance, which might be severely bandwidth limited. Secondly, users don't need to manually turn on the proxy when they want to watch IP address bound content, since Yattee automatically proxies such content.
Furthermore, adding the proxy option allows mitigating some severe playback issues with invidious instances. Invidious by default returns proxied URLs for videos, and due to some bug in the Invidious proxy, scrubbing or continuing playback at a random timestamp can lead to severe wait times for the users.
This should fix numerous playback issues: #666, #626, #590, #585, #498, #457, #400
2024-05-09 18:07:55 +00:00
|
|
|
|
var isPaused: Bool {
|
|
|
|
|
backend.isPaused
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var hasStarted: Bool {
|
|
|
|
|
backend.hasStarted
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-16 20:23:11 +00:00
|
|
|
|
var playerItemDuration: CMTime? {
|
2022-06-18 12:39:49 +00:00
|
|
|
|
guard !currentItem.isNil else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return backend.playerItemDuration
|
2021-10-22 20:49:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-02-16 20:23:11 +00:00
|
|
|
|
var playerItemDurationWithoutSponsorSegments: CMTime? {
|
2022-08-31 19:24:46 +00:00
|
|
|
|
PlayerTimeModel.shared.duration - .secondsInDefaultTimescale(
|
2022-02-16 20:23:11 +00:00
|
|
|
|
sponsorBlock.segments.reduce(0) { $0 + $1.duration }
|
|
|
|
|
)
|
2021-10-22 23:04:03 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-02-16 20:23:11 +00:00
|
|
|
|
var videoDuration: TimeInterval? {
|
2022-07-21 22:44:21 +00:00
|
|
|
|
playerItemDuration?.seconds ?? currentItem?.duration ?? currentVideo?.length
|
2021-10-22 20:49:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-02-16 20:23:11 +00:00
|
|
|
|
var time: CMTime? {
|
|
|
|
|
currentItem?.playbackTime
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var live: Bool {
|
|
|
|
|
currentVideo?.live ?? false
|
2021-10-22 20:49:31 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-07-21 22:44:21 +00:00
|
|
|
|
var playingLive: Bool {
|
|
|
|
|
guard live,
|
2022-09-28 14:27:01 +00:00
|
|
|
|
let videoDuration,
|
2022-07-21 22:44:21 +00:00
|
|
|
|
let time = backend.currentTime?.seconds else { return false }
|
|
|
|
|
|
|
|
|
|
return videoDuration - time < 30
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var liveStreamInAVPlayer: Bool {
|
|
|
|
|
live && activeBackend == .appleAVPlayer
|
|
|
|
|
}
|
|
|
|
|
|
2021-10-05 20:20:09 +00:00
|
|
|
|
func togglePlay() {
|
2022-02-16 20:23:11 +00:00
|
|
|
|
backend.togglePlay()
|
2021-06-15 16:35:21 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-10-05 20:20:09 +00:00
|
|
|
|
func play() {
|
2022-02-16 20:23:11 +00:00
|
|
|
|
backend.play()
|
2021-07-29 22:28:28 +00:00
|
|
|
|
}
|
|
|
|
|
|
2021-10-05 20:20:09 +00:00
|
|
|
|
func pause() {
|
2022-02-16 20:23:11 +00:00
|
|
|
|
backend.pause()
|
2021-09-25 08:18:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-05-29 14:38:37 +00:00
|
|
|
|
func play(_ video: Video, at time: CMTime? = nil, showingPlayer: Bool = true) {
|
2022-05-30 19:00:53 +00:00
|
|
|
|
pause()
|
2022-12-18 12:11:06 +00:00
|
|
|
|
videoBeingOpened = video
|
2022-05-30 19:00:53 +00:00
|
|
|
|
|
2023-04-22 08:56:42 +00:00
|
|
|
|
navigation.presentingChannelSheet = false
|
2022-12-17 23:08:30 +00:00
|
|
|
|
|
2022-08-20 20:31:03 +00:00
|
|
|
|
var changeBackendHandler: (() -> Void)?
|
|
|
|
|
|
2022-08-22 21:14:27 +00:00
|
|
|
|
if let backend = (live && forceAVPlayerForLiveStreams) ? PlayerBackendType.appleAVPlayer :
|
|
|
|
|
(qualityProfile?.backend ?? QualityProfilesModel.shared.automaticProfile?.backend),
|
|
|
|
|
activeBackend != backend,
|
|
|
|
|
backend == .appleAVPlayer || !avPlayerBackend.startPictureInPictureOnPlay
|
2022-08-20 20:31:03 +00:00
|
|
|
|
{
|
|
|
|
|
changeBackendHandler = { [weak self] in
|
2022-09-28 14:27:01 +00:00
|
|
|
|
guard let self else { return }
|
2022-08-20 20:31:03 +00:00
|
|
|
|
self.changeActiveBackend(from: self.activeBackend, to: backend)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-30 19:00:53 +00:00
|
|
|
|
#if os(iOS)
|
2022-06-18 12:39:49 +00:00
|
|
|
|
if !playingInPictureInPicture, showingPlayer {
|
2022-08-26 08:25:07 +00:00
|
|
|
|
onPresentPlayer.append { [weak self] in
|
2022-08-20 20:31:03 +00:00
|
|
|
|
changeBackendHandler?()
|
|
|
|
|
self?.playNow(video, at: time)
|
|
|
|
|
}
|
2022-06-18 12:39:49 +00:00
|
|
|
|
show()
|
2022-05-28 21:41:23 +00:00
|
|
|
|
return
|
|
|
|
|
}
|
2022-06-18 12:39:49 +00:00
|
|
|
|
#endif
|
2022-05-28 21:41:23 +00:00
|
|
|
|
|
2022-08-20 20:31:03 +00:00
|
|
|
|
changeBackendHandler?()
|
2022-06-18 12:39:49 +00:00
|
|
|
|
playNow(video, at: time)
|
2021-12-26 21:14:46 +00:00
|
|
|
|
|
|
|
|
|
guard !playingInPictureInPicture else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-05-29 14:38:37 +00:00
|
|
|
|
if showingPlayer {
|
|
|
|
|
show()
|
|
|
|
|
}
|
2021-10-16 22:48:58 +00:00
|
|
|
|
}
|
2021-07-18 22:32:46 +00:00
|
|
|
|
|
2021-10-24 18:01:08 +00:00
|
|
|
|
func playStream(
|
2021-10-16 22:48:58 +00:00
|
|
|
|
_ stream: Stream,
|
|
|
|
|
of video: Video,
|
2021-12-17 19:55:52 +00:00
|
|
|
|
preservingTime: Bool = false,
|
2022-08-26 20:17:21 +00:00
|
|
|
|
upgrading: Bool = false,
|
|
|
|
|
withBackend: PlayerBackend? = nil
|
2021-10-16 22:48:58 +00:00
|
|
|
|
) {
|
2021-11-07 16:52:42 +00:00
|
|
|
|
playerError = nil
|
2022-11-10 17:11:28 +00:00
|
|
|
|
if !upgrading, !video.isLocal {
|
2021-12-17 19:55:52 +00:00
|
|
|
|
resetSegments()
|
|
|
|
|
|
2023-05-26 21:24:00 +00:00
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
2021-12-29 18:55:41 +00:00
|
|
|
|
self?.sponsorBlock.loadSegments(
|
|
|
|
|
videoID: video.videoID,
|
|
|
|
|
categories: Defaults[.sponsorBlockCategories]
|
2022-04-16 17:50:02 +00:00
|
|
|
|
)
|
2022-03-19 23:04:56 +00:00
|
|
|
|
|
2022-03-20 20:31:19 +00:00
|
|
|
|
guard Defaults[.enableReturnYouTubeDislike] else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-03-19 23:04:56 +00:00
|
|
|
|
self?.returnYouTubeDislike.loadDislikes(videoID: video.videoID) { [weak self] dislikes in
|
|
|
|
|
self?.currentItem?.video?.dislikes = dislikes
|
|
|
|
|
}
|
2021-12-17 19:55:52 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2021-10-23 16:49:45 +00:00
|
|
|
|
|
2022-12-03 23:35:07 +00:00
|
|
|
|
if video.isLocal {
|
|
|
|
|
resetSegments()
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-26 21:24:00 +00:00
|
|
|
|
(withBackend ?? backend).playStream(
|
|
|
|
|
stream,
|
|
|
|
|
of: video,
|
|
|
|
|
preservingTime: preservingTime,
|
|
|
|
|
upgrading: upgrading
|
|
|
|
|
)
|
2022-02-16 21:10:57 +00:00
|
|
|
|
|
2023-05-26 20:49:38 +00:00
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
self.forceBackendOnPlay = nil
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-16 21:10:57 +00:00
|
|
|
|
if !upgrading {
|
2023-05-26 21:24:00 +00:00
|
|
|
|
updateCurrentArtwork()
|
2022-02-16 21:10:57 +00:00
|
|
|
|
}
|
2022-02-16 20:23:11 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func saveTime(completionHandler: @escaping () -> Void = {}) {
|
|
|
|
|
guard let currentTime = backend.currentTime, currentTime.seconds > 0 else {
|
2022-05-29 14:38:37 +00:00
|
|
|
|
completionHandler()
|
2022-02-16 20:23:11 +00:00
|
|
|
|
return
|
2021-07-18 22:32:46 +00:00
|
|
|
|
}
|
2021-10-28 20:18:23 +00:00
|
|
|
|
|
2022-02-16 20:23:11 +00:00
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
|
|
|
self?.preservedTime = currentTime
|
|
|
|
|
completionHandler()
|
2021-12-17 19:55:52 +00:00
|
|
|
|
}
|
2021-07-18 22:32:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-02-16 20:23:11 +00:00
|
|
|
|
func upgradeToStream(_ stream: Stream, force: Bool = false) {
|
2022-05-29 15:50:54 +00:00
|
|
|
|
guard let video = currentVideo else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-16 20:23:11 +00:00
|
|
|
|
if !self.stream.isNil, force || self.stream != stream {
|
2022-05-29 15:50:54 +00:00
|
|
|
|
playStream(stream, of: video, preservingTime: true, upgrading: true)
|
2021-12-26 21:14:46 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-19 16:56:47 +00:00
|
|
|
|
private func handleAvailableStreamsChange() {
|
|
|
|
|
rebuildTVMenu()
|
|
|
|
|
|
|
|
|
|
guard stream.isNil else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-26 20:49:38 +00:00
|
|
|
|
if let backend = forceBackendOnPlay ?? ((live && forceAVPlayerForLiveStreams) ? PlayerBackendType.appleAVPlayer : qualityProfile?.backend),
|
2022-08-22 21:14:27 +00:00
|
|
|
|
backend != activeBackend,
|
|
|
|
|
backend == .appleAVPlayer || !(avPlayerBackend.startPictureInPictureOnPlay || playingInPictureInPicture)
|
2022-08-18 22:40:46 +00:00
|
|
|
|
{
|
2022-08-22 21:14:27 +00:00
|
|
|
|
changeActiveBackend(from: activeBackend, to: backend)
|
2022-08-14 17:06:22 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-12-03 23:35:07 +00:00
|
|
|
|
let localStream = (availableStreams.count == 1 && availableStreams.first!.isLocal) ? availableStreams.first : nil
|
|
|
|
|
|
|
|
|
|
guard let stream = localStream ?? streamByQualityProfile,
|
2022-11-17 21:48:43 +00:00
|
|
|
|
let currentVideo
|
|
|
|
|
else {
|
2021-12-19 16:56:47 +00:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-16 16:51:07 +00:00
|
|
|
|
DispatchQueue.global(qos: .userInitiated).async {
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
self.streamSelection = stream
|
|
|
|
|
}
|
|
|
|
|
self.playStream(
|
|
|
|
|
stream,
|
|
|
|
|
of: currentVideo,
|
|
|
|
|
preservingTime: !self.currentItem.playbackTime.isNil
|
|
|
|
|
)
|
|
|
|
|
}
|
2021-12-19 16:56:47 +00:00
|
|
|
|
}
|
2021-12-19 17:17:04 +00:00
|
|
|
|
|
|
|
|
|
private func handlePresentationChange() {
|
2024-09-07 20:22:09 +00:00
|
|
|
|
#if os(macOS)
|
|
|
|
|
// TODO: Check whether this is needed on macOS
|
2024-09-01 10:42:31 +00:00
|
|
|
|
backend.setNeedsDrawing(presentingPlayer)
|
|
|
|
|
#endif
|
2022-05-28 21:41:23 +00:00
|
|
|
|
|
2023-05-20 20:49:10 +00:00
|
|
|
|
#if os(iOS)
|
|
|
|
|
if presentingPlayer, activeBackend == .appleAVPlayer, avPlayerUsesSystemControls, Constants.isIPhone {
|
|
|
|
|
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
2022-08-28 22:29:29 +00:00
|
|
|
|
controls.hide()
|
2023-05-21 09:54:17 +00:00
|
|
|
|
controls.hideOverlays()
|
2022-02-16 20:23:11 +00:00
|
|
|
|
|
2022-04-03 15:03:56 +00:00
|
|
|
|
#if !os(macOS)
|
|
|
|
|
UIApplication.shared.isIdleTimerDisabled = presentingPlayer
|
|
|
|
|
#endif
|
|
|
|
|
|
2021-12-19 17:17:04 +00:00
|
|
|
|
if presentingPlayer, closePiPOnOpeningPlayer, playingInPictureInPicture {
|
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
|
|
|
self?.closePiP()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !presentingPlayer, pauseOnHidingPlayer, !playingInPictureInPicture {
|
2022-08-18 22:40:46 +00:00
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
2021-12-19 17:17:04 +00:00
|
|
|
|
self?.pause()
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-05-21 09:54:17 +00:00
|
|
|
|
|
|
|
|
|
if !presentingPlayer {
|
|
|
|
|
#if os(iOS)
|
2024-09-06 13:11:31 +00:00
|
|
|
|
if lockPortraitWhenBrowsing {
|
2023-05-21 09:54:17 +00:00
|
|
|
|
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
|
|
|
|
} else {
|
2024-09-06 14:47:03 +00:00
|
|
|
|
Orientation.lockOrientation(.all)
|
2023-05-21 09:54:17 +00:00
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
}
|
2021-11-05 14:58:51 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-04-24 19:32:32 +00:00
|
|
|
|
func changeActiveBackend(from: PlayerBackendType, to: PlayerBackendType, changingStream: Bool = true, isInClosePip: Bool = false) {
|
2022-05-29 14:38:37 +00:00
|
|
|
|
guard activeBackend != to else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-22 21:14:27 +00:00
|
|
|
|
logger.info("changing backend from \(from.rawValue) to \(to.rawValue)")
|
|
|
|
|
|
2022-08-28 17:18:49 +00:00
|
|
|
|
let wasPlaying = isPlaying
|
|
|
|
|
|
2024-04-24 19:32:32 +00:00
|
|
|
|
if to == .mpv && !isInClosePip {
|
2022-08-18 22:40:46 +00:00
|
|
|
|
closePiP()
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-16 20:23:11 +00:00
|
|
|
|
Defaults[.activeBackend] = to
|
|
|
|
|
self.activeBackend = to
|
2021-12-29 18:55:41 +00:00
|
|
|
|
|
2022-08-26 20:17:21 +00:00
|
|
|
|
let fromBackend: PlayerBackend = from == .appleAVPlayer ? avPlayerBackend : mpvBackend
|
|
|
|
|
let toBackend: PlayerBackend = to == .appleAVPlayer ? avPlayerBackend : mpvBackend
|
|
|
|
|
|
2022-11-13 12:28:25 +00:00
|
|
|
|
toBackend.cancelLoads()
|
|
|
|
|
fromBackend.cancelLoads()
|
|
|
|
|
|
2022-11-10 22:19:34 +00:00
|
|
|
|
if !self.backend.canPlayAtRate(currentRate) {
|
|
|
|
|
currentRate = self.backend.suggestedPlaybackRates.last { $0 < currentRate } ?? 1.0
|
|
|
|
|
}
|
2023-06-08 10:22:34 +00:00
|
|
|
|
|
|
|
|
|
self.rateToRestore = Float(currentRate)
|
|
|
|
|
|
2022-08-20 20:31:03 +00:00
|
|
|
|
self.backend.didChangeTo()
|
|
|
|
|
|
2022-08-28 17:18:49 +00:00
|
|
|
|
if wasPlaying {
|
|
|
|
|
fromBackend.pause()
|
|
|
|
|
}
|
2022-08-26 20:17:21 +00:00
|
|
|
|
|
2022-09-28 14:27:01 +00:00
|
|
|
|
guard var stream, changingStream else {
|
2021-07-18 22:32:46 +00:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-16 20:23:11 +00:00
|
|
|
|
if let stream = toBackend.stream, toBackend.video == fromBackend.video {
|
2022-08-28 17:18:49 +00:00
|
|
|
|
toBackend.seek(to: fromBackend.currentTime?.seconds ?? .zero, seekType: .backendSync) { finished in
|
2021-10-22 20:49:31 +00:00
|
|
|
|
guard finished else {
|
|
|
|
|
return
|
|
|
|
|
}
|
2022-08-28 17:18:49 +00:00
|
|
|
|
if wasPlaying {
|
|
|
|
|
toBackend.play()
|
|
|
|
|
}
|
2021-10-05 20:20:09 +00:00
|
|
|
|
}
|
2021-10-16 22:48:58 +00:00
|
|
|
|
|
2022-02-16 20:23:11 +00:00
|
|
|
|
self.stream = stream
|
|
|
|
|
streamSelection = stream
|
2021-07-22 12:43:13 +00:00
|
|
|
|
|
2023-05-20 20:49:10 +00:00
|
|
|
|
self.upgradeToStream(stream, force: true)
|
|
|
|
|
|
2021-10-16 22:48:58 +00:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2023-05-20 20:49:10 +00:00
|
|
|
|
if !backend.canPlay(stream) ||
|
|
|
|
|
(to == .mpv && stream.isHLS) ||
|
|
|
|
|
(to == .appleAVPlayer && !stream.isHLS)
|
|
|
|
|
{
|
2022-08-14 17:06:22 +00:00
|
|
|
|
guard let preferredStream = streamByQualityProfile else {
|
2021-10-24 18:01:08 +00:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-02-16 20:23:11 +00:00
|
|
|
|
stream = preferredStream
|
|
|
|
|
streamSelection = preferredStream
|
2021-12-05 17:15:40 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-02-16 20:23:11 +00:00
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
2022-09-28 14:27:01 +00:00
|
|
|
|
guard let self else {
|
2022-05-27 22:59:35 +00:00
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
self.upgradeToStream(stream, force: true)
|
2021-12-26 21:14:46 +00:00
|
|
|
|
}
|
2021-10-28 20:18:23 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-11-10 22:00:17 +00:00
|
|
|
|
func handleCurrentRateChange() {
|
|
|
|
|
backend.setRate(currentRate)
|
|
|
|
|
playerRate = currentRate
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-14 17:06:22 +00:00
|
|
|
|
func handleQualityProfileChange() {
|
|
|
|
|
guard let profile = qualityProfile else { return }
|
|
|
|
|
|
|
|
|
|
if activeBackend != profile.backend { changeActiveBackend(from: activeBackend, to: profile.backend) }
|
|
|
|
|
guard let profileStream = streamByQualityProfile, stream != profileStream else { return }
|
|
|
|
|
|
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
|
|
|
self?.streamSelection = profileStream
|
|
|
|
|
self?.upgradeToStream(profileStream)
|
|
|
|
|
}
|
2021-10-28 20:18:23 +00:00
|
|
|
|
}
|
2021-11-02 17:24:59 +00:00
|
|
|
|
|
2022-11-10 22:00:17 +00:00
|
|
|
|
func rateLabel(_ rate: Double) -> String {
|
2021-11-02 17:24:59 +00:00
|
|
|
|
let formatter = NumberFormatter()
|
|
|
|
|
formatter.minimumFractionDigits = 0
|
|
|
|
|
formatter.maximumFractionDigits = 2
|
|
|
|
|
|
|
|
|
|
return "\(formatter.string(from: NSNumber(value: rate))!)×"
|
|
|
|
|
}
|
2021-12-02 20:19:10 +00:00
|
|
|
|
|
2022-04-03 12:23:42 +00:00
|
|
|
|
func closeCurrentItem(finished: Bool = false) {
|
2024-09-01 10:42:31 +00:00
|
|
|
|
guard !closing else { return }
|
2022-12-18 12:11:06 +00:00
|
|
|
|
closing = true
|
2023-06-07 20:25:10 +00:00
|
|
|
|
|
2024-09-01 10:42:31 +00:00
|
|
|
|
if playingFullScreen { exitFullScreen() }
|
2022-08-07 11:15:27 +00:00
|
|
|
|
|
2024-09-01 10:42:31 +00:00
|
|
|
|
Delay.by(0.3) { [weak self] in
|
2022-12-18 12:11:06 +00:00
|
|
|
|
guard let self else { return }
|
2024-09-01 10:42:31 +00:00
|
|
|
|
pause()
|
|
|
|
|
videoBeingOpened = nil
|
|
|
|
|
advancing = false
|
|
|
|
|
forceBackendOnPlay = nil
|
2022-08-07 11:15:27 +00:00
|
|
|
|
|
2024-09-01 10:42:31 +00:00
|
|
|
|
controls.presentingControls = false
|
|
|
|
|
|
|
|
|
|
self.prepareCurrentItemForHistory(finished: finished)
|
|
|
|
|
self.hide()
|
2022-12-18 12:11:06 +00:00
|
|
|
|
|
2024-09-01 10:42:31 +00:00
|
|
|
|
Delay.by(0.7) { [weak self] in
|
|
|
|
|
guard let self else { return }
|
|
|
|
|
if playingInPictureInPicture { self.closePiP() }
|
|
|
|
|
|
|
|
|
|
withAnimation {
|
|
|
|
|
self.currentItem = nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
self.updateNowPlayingInfo()
|
|
|
|
|
self.backend.closeItem()
|
|
|
|
|
self.aspectRatio = VideoPlayerView.defaultAspectRatio
|
|
|
|
|
self.resetAutoplay()
|
|
|
|
|
self.closing = false
|
|
|
|
|
}
|
2022-12-18 12:11:06 +00:00
|
|
|
|
}
|
2021-12-02 20:19:10 +00:00
|
|
|
|
}
|
2021-12-19 17:17:04 +00:00
|
|
|
|
|
2022-08-26 20:17:21 +00:00
|
|
|
|
func startPiP() {
|
2024-04-24 19:32:32 +00:00
|
|
|
|
previousActiveBackend = activeBackend
|
2022-08-26 20:17:21 +00:00
|
|
|
|
avPlayerBackend.startPictureInPictureOnPlay = false
|
|
|
|
|
avPlayerBackend.startPictureInPictureOnSwitch = false
|
|
|
|
|
|
2024-09-02 20:40:48 +00:00
|
|
|
|
guard activeBackend != .appleAVPlayer else {
|
2022-08-26 20:17:21 +00:00
|
|
|
|
avPlayerBackend.tryStartingPictureInPicture()
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2024-09-02 20:40:48 +00:00
|
|
|
|
avPlayerBackend.startPictureInPictureOnSwitch = true
|
2022-08-26 20:17:21 +00:00
|
|
|
|
|
2024-09-02 20:40:48 +00:00
|
|
|
|
saveTime {
|
|
|
|
|
self.changeActiveBackend(from: .mpv, to: .appleAVPlayer)
|
|
|
|
|
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
|
|
|
|
|
if let pipController = self?.pipController, pipController.isPictureInPictureActive, self?.avPlayerBackend.isPlaying == true {
|
|
|
|
|
self?.exitFullScreen()
|
|
|
|
|
self?.controls.objectWillChange.send()
|
|
|
|
|
timer.invalidate()
|
|
|
|
|
} else if self?.activeBackend == .appleAVPlayer, self?.avPlayerBackend.startPictureInPictureOnSwitch == false {
|
|
|
|
|
self?.avPlayerBackend.startPictureInPictureOnSwitch = true
|
|
|
|
|
self?.avPlayerBackend.tryStartingPictureInPicture()
|
|
|
|
|
}
|
2024-05-18 22:18:18 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
2022-08-26 20:17:21 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var transitioningToPiP: Bool {
|
|
|
|
|
avPlayerBackend.startPictureInPictureOnPlay || avPlayerBackend.startPictureInPictureOnSwitch
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var pipPossible: Bool {
|
|
|
|
|
guard activeBackend == .appleAVPlayer else { return !transitioningToPiP }
|
|
|
|
|
|
2022-09-28 14:27:01 +00:00
|
|
|
|
guard let pipController else { return false }
|
2022-08-26 20:17:21 +00:00
|
|
|
|
guard !pipController.isPictureInPictureActive else { return true }
|
|
|
|
|
|
|
|
|
|
return pipController.isPictureInPicturePossible && !transitioningToPiP
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-19 17:17:04 +00:00
|
|
|
|
func closePiP() {
|
|
|
|
|
guard playingInPictureInPicture else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-26 20:17:21 +00:00
|
|
|
|
avPlayerBackend.startPictureInPictureOnPlay = false
|
|
|
|
|
avPlayerBackend.startPictureInPictureOnSwitch = false
|
|
|
|
|
|
2021-12-19 17:17:04 +00:00
|
|
|
|
#if os(tvOS)
|
|
|
|
|
show()
|
|
|
|
|
#endif
|
2021-12-19 23:36:12 +00:00
|
|
|
|
|
2024-09-02 20:40:48 +00:00
|
|
|
|
avPlayerBackend.closePiP()
|
|
|
|
|
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
|
|
|
|
|
if self?.activeBackend == .appleAVPlayer, self?.avPlayerBackend.isPlaying == true, self?.playingInPictureInPicture == false {
|
|
|
|
|
timer.invalidate()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard previousActiveBackend == .mpv else { return }
|
|
|
|
|
|
|
|
|
|
saveTime {
|
|
|
|
|
self.changeActiveBackend(from: .appleAVPlayer, to: .mpv, isInClosePip: true)
|
|
|
|
|
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
|
|
|
|
|
if self?.activeBackend == .mpv, self?.mpvBackend.isPlaying == true {
|
|
|
|
|
timer.invalidate()
|
2024-05-18 22:18:18 +00:00
|
|
|
|
}
|
2024-04-24 19:32:32 +00:00
|
|
|
|
}
|
2024-09-02 20:40:48 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// We need to remove the itme from the player, if not it will be displayed when next video goe to PiP.
|
|
|
|
|
Delay.by(1.0) {
|
|
|
|
|
self.avPlayerBackend.closeItem()
|
2024-04-24 19:32:32 +00:00
|
|
|
|
}
|
2022-06-26 14:46:29 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-04-22 14:33:08 +00:00
|
|
|
|
var pipImage: String {
|
|
|
|
|
transitioningToPiP ? "pip.fill" : pipController?.isPictureInPictureActive ?? false ? "pip.exit" : "pip.enter"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var fullscreenImage: String {
|
|
|
|
|
playingFullScreen ? "arrow.down.right.and.arrow.up.left" : "arrow.up.left.and.arrow.down.right"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func toggleFullScreenAction() {
|
2024-09-01 10:42:31 +00:00
|
|
|
|
toggleFullscreen(playingFullScreen, showControls: false, initiatedByButton: true)
|
2023-04-22 14:33:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func togglePiPAction() {
|
2023-06-17 12:09:51 +00:00
|
|
|
|
if pipController?.isPictureInPictureActive ?? false {
|
|
|
|
|
closePiP()
|
|
|
|
|
} else {
|
|
|
|
|
startPiP()
|
|
|
|
|
}
|
2023-04-22 14:33:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#if os(iOS)
|
|
|
|
|
var lockOrientationImage: String {
|
2024-09-01 10:42:31 +00:00
|
|
|
|
isOrientationLocked ? "lock.rotation" : "lock.rotation.open"
|
2023-04-22 14:33:08 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func lockOrientationAction() {
|
2024-09-01 10:42:31 +00:00
|
|
|
|
// This makes toggling orientation lock more robust
|
|
|
|
|
if lockedOrientation.isNil || !isOrientationLocked {
|
|
|
|
|
isOrientationLocked = true
|
2023-04-22 14:33:08 +00:00
|
|
|
|
let orientationMask = OrientationTracker.shared.currentInterfaceOrientationMask
|
|
|
|
|
lockedOrientation = orientationMask
|
|
|
|
|
let orientation = OrientationTracker.shared.currentInterfaceOrientation
|
2024-09-01 10:42:31 +00:00
|
|
|
|
Orientation.lockOrientation(orientationMask, andRotateTo: playingFullScreen ? nil : orientation)
|
2023-04-22 14:33:08 +00:00
|
|
|
|
} else {
|
2024-09-01 10:42:31 +00:00
|
|
|
|
isOrientationLocked = false
|
2023-04-22 14:33:08 +00:00
|
|
|
|
lockedOrientation = nil
|
2024-09-06 14:47:03 +00:00
|
|
|
|
Orientation.lockOrientation(.all)
|
2023-04-22 14:33:08 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
func replayAction() {
|
|
|
|
|
backend.seek(to: 0.0, seekType: .userInteracted)
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-26 14:46:29 +00:00
|
|
|
|
func handleQueueChange() {
|
|
|
|
|
Defaults[.queue] = queue
|
|
|
|
|
|
2022-07-11 16:10:51 +00:00
|
|
|
|
updateRemoteCommandCenter()
|
2022-06-26 14:46:29 +00:00
|
|
|
|
controls.objectWillChange.send()
|
2021-12-19 17:17:04 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-01-09 15:05:05 +00:00
|
|
|
|
func handleCurrentItemChange() {
|
2022-12-19 12:45:41 +00:00
|
|
|
|
if currentItem == nil {
|
|
|
|
|
FeedModel.shared.calculateUnwatchedFeed()
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-20 23:01:55 +00:00
|
|
|
|
// Captions need to be set to nil on item change, to clear the previus values.
|
|
|
|
|
captions = nil
|
|
|
|
|
|
2022-01-06 15:35:45 +00:00
|
|
|
|
#if os(macOS)
|
|
|
|
|
Windows.player.window?.title = windowTitle
|
|
|
|
|
#endif
|
2022-01-09 15:05:05 +00:00
|
|
|
|
|
2022-06-29 22:12:37 +00:00
|
|
|
|
DispatchQueue.main.async(qos: .background) { [weak self] in
|
2022-09-28 14:27:01 +00:00
|
|
|
|
guard let self else { return }
|
2022-09-11 16:34:33 +00:00
|
|
|
|
if self.saveLastPlayed {
|
|
|
|
|
self.lastPlayed = self.currentItem
|
|
|
|
|
}
|
2022-07-10 22:24:56 +00:00
|
|
|
|
|
|
|
|
|
if self.playbackMode == .related,
|
|
|
|
|
let video = self.currentVideo,
|
|
|
|
|
self.autoplayItemSource.isNil || self.autoplayItemSource?.videoID != video.videoID
|
|
|
|
|
{
|
|
|
|
|
self.setRelatedAutoplayItem()
|
|
|
|
|
}
|
2022-06-29 22:12:37 +00:00
|
|
|
|
}
|
2022-01-06 15:35:45 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-07-10 22:24:56 +00:00
|
|
|
|
func handlePlaybackModeChange() {
|
|
|
|
|
Defaults[.playbackMode] = playbackMode
|
|
|
|
|
|
2022-07-11 16:10:51 +00:00
|
|
|
|
updateRemoteCommandCenter()
|
|
|
|
|
|
2022-07-10 22:24:56 +00:00
|
|
|
|
guard playbackMode == .related else {
|
|
|
|
|
autoplayItem = nil
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
setRelatedAutoplayItem()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func setRelatedAutoplayItem() {
|
2022-07-11 17:44:49 +00:00
|
|
|
|
guard let video = currentVideo else { return }
|
|
|
|
|
let related = video.related.filter { $0.videoID != autoplayItem?.video?.videoID }
|
2022-07-10 22:24:56 +00:00
|
|
|
|
|
2022-07-11 17:44:49 +00:00
|
|
|
|
let watchFetchRequest = Watch.fetchRequest()
|
|
|
|
|
watchFetchRequest.predicate = NSPredicate(format: "videoID IN %@", related.map(\.videoID) as [String])
|
2022-07-10 22:24:56 +00:00
|
|
|
|
|
2022-07-11 17:44:49 +00:00
|
|
|
|
let results = try? context.fetch(watchFetchRequest)
|
|
|
|
|
|
|
|
|
|
context.perform { [weak self] in
|
2022-09-28 14:27:01 +00:00
|
|
|
|
guard let self,
|
|
|
|
|
let results else { return }
|
2022-07-11 17:44:49 +00:00
|
|
|
|
let resultsIds = results.map(\.videoID)
|
|
|
|
|
|
|
|
|
|
guard let autoplayVideo = related.filter({ !resultsIds.contains($0.videoID) }).randomElement() else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let item = PlayerQueueItem(autoplayVideo)
|
|
|
|
|
self.autoplayItem = item
|
|
|
|
|
self.autoplayItemSource = video
|
|
|
|
|
|
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 3) { [weak self] in
|
2022-09-28 14:27:01 +00:00
|
|
|
|
guard let self else { return }
|
2023-06-17 12:09:51 +00:00
|
|
|
|
self.playerAPI(item.video)?.loadDetails(item, failureHandler: nil) { newItem in
|
2022-07-11 17:44:49 +00:00
|
|
|
|
guard newItem.videoID == self.autoplayItem?.videoID else { return }
|
|
|
|
|
self.autoplayItem = newItem
|
|
|
|
|
self.updateRemoteCommandCenter()
|
|
|
|
|
self.controls.objectWillChange.send()
|
2023-06-17 12:09:51 +00:00
|
|
|
|
}
|
2022-07-11 17:44:49 +00:00
|
|
|
|
}
|
2022-07-10 22:24:56 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-11 16:10:51 +00:00
|
|
|
|
func updateRemoteCommandCenter() {
|
2024-08-29 13:10:04 +00:00
|
|
|
|
let commandCenter = MPRemoteCommandCenter.shared()
|
|
|
|
|
let skipForwardCommand = commandCenter.skipForwardCommand
|
|
|
|
|
let skipBackwardCommand = commandCenter.skipBackwardCommand
|
|
|
|
|
let previousTrackCommand = commandCenter.previousTrackCommand
|
|
|
|
|
let nextTrackCommand = commandCenter.nextTrackCommand
|
2022-07-11 16:10:51 +00:00
|
|
|
|
|
|
|
|
|
if !remoteCommandCenterConfigured {
|
|
|
|
|
remoteCommandCenterConfigured = true
|
|
|
|
|
|
2022-12-19 11:08:27 +00:00
|
|
|
|
let interval = TimeInterval(systemControlsSeekDuration) ?? 10
|
|
|
|
|
let preferredIntervals = [NSNumber(value: interval)]
|
2022-07-11 16:10:51 +00:00
|
|
|
|
|
2024-08-29 13:10:04 +00:00
|
|
|
|
// Remove existing targets to avoid duplicates
|
|
|
|
|
skipForwardCommand.removeTarget(nil)
|
|
|
|
|
skipBackwardCommand.removeTarget(nil)
|
|
|
|
|
previousTrackCommand.removeTarget(nil)
|
|
|
|
|
nextTrackCommand.removeTarget(nil)
|
|
|
|
|
commandCenter.playCommand.removeTarget(nil)
|
|
|
|
|
commandCenter.pauseCommand.removeTarget(nil)
|
|
|
|
|
commandCenter.togglePlayPauseCommand.removeTarget(nil)
|
|
|
|
|
commandCenter.changePlaybackPositionCommand.removeTarget(nil)
|
|
|
|
|
|
|
|
|
|
// Re-add targets for handling commands
|
2022-07-11 16:10:51 +00:00
|
|
|
|
skipForwardCommand.preferredIntervals = preferredIntervals
|
|
|
|
|
skipBackwardCommand.preferredIntervals = preferredIntervals
|
|
|
|
|
|
|
|
|
|
skipForwardCommand.addTarget { [weak self] _ in
|
2022-12-19 11:08:27 +00:00
|
|
|
|
self?.backend.seek(relative: .secondsInDefaultTimescale(interval), seekType: .userInteracted)
|
2022-07-11 16:10:51 +00:00
|
|
|
|
return .success
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
skipBackwardCommand.addTarget { [weak self] _ in
|
2022-12-19 11:08:27 +00:00
|
|
|
|
self?.backend.seek(relative: .secondsInDefaultTimescale(-interval), seekType: .userInteracted)
|
2022-07-11 16:10:51 +00:00
|
|
|
|
return .success
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
previousTrackCommand.addTarget { [weak self] _ in
|
2022-08-28 17:18:49 +00:00
|
|
|
|
self?.backend.seek(to: .zero, seekType: .userInteracted)
|
2022-07-11 16:10:51 +00:00
|
|
|
|
return .success
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
nextTrackCommand.addTarget { [weak self] _ in
|
|
|
|
|
self?.advanceToNextItem()
|
|
|
|
|
return .success
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-29 13:10:04 +00:00
|
|
|
|
commandCenter.playCommand.addTarget { [weak self] _ in
|
2022-07-11 16:10:51 +00:00
|
|
|
|
self?.play()
|
|
|
|
|
return .success
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-29 13:10:04 +00:00
|
|
|
|
commandCenter.pauseCommand.addTarget { [weak self] _ in
|
2022-07-11 16:10:51 +00:00
|
|
|
|
self?.pause()
|
|
|
|
|
return .success
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-29 13:10:04 +00:00
|
|
|
|
commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
|
2022-07-11 16:10:51 +00:00
|
|
|
|
self?.togglePlay()
|
|
|
|
|
return .success
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-29 13:10:04 +00:00
|
|
|
|
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] remoteEvent in
|
2022-07-11 16:10:51 +00:00
|
|
|
|
guard let event = remoteEvent as? MPChangePlaybackPositionCommandEvent else { return .commandFailed }
|
|
|
|
|
|
2022-08-28 17:18:49 +00:00
|
|
|
|
self?.backend.seek(to: event.positionTime, seekType: .userInteracted)
|
2022-07-11 16:10:51 +00:00
|
|
|
|
|
|
|
|
|
return .success
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch Defaults[.systemControlsCommands] {
|
|
|
|
|
case .seek:
|
|
|
|
|
previousTrackCommand.isEnabled = false
|
|
|
|
|
nextTrackCommand.isEnabled = false
|
|
|
|
|
skipForwardCommand.isEnabled = true
|
|
|
|
|
skipBackwardCommand.isEnabled = true
|
|
|
|
|
|
|
|
|
|
case .restartAndAdvanceToNext:
|
|
|
|
|
skipForwardCommand.isEnabled = false
|
|
|
|
|
skipBackwardCommand.isEnabled = false
|
|
|
|
|
previousTrackCommand.isEnabled = true
|
|
|
|
|
nextTrackCommand.isEnabled = isAdvanceToNextItemAvailable
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-07-10 22:24:56 +00:00
|
|
|
|
func resetAutoplay() {
|
|
|
|
|
autoplayItem = nil
|
|
|
|
|
autoplayItemSource = nil
|
|
|
|
|
}
|
|
|
|
|
|
2021-12-19 17:17:04 +00:00
|
|
|
|
#if os(macOS)
|
|
|
|
|
var windowTitle: String {
|
2022-11-11 18:19:48 +00:00
|
|
|
|
currentVideo.isNil ? "Not Playing" : "\(currentVideo!.displayTitle) - \(currentVideo!.displayAuthor)"
|
2021-12-19 17:17:04 +00:00
|
|
|
|
}
|
|
|
|
|
#else
|
|
|
|
|
func handleEnterForeground() {
|
2024-09-07 20:22:09 +00:00
|
|
|
|
DispatchQueue.global(qos: .userInteractive).async { [weak self] in
|
|
|
|
|
guard let self = self else { return }
|
|
|
|
|
|
|
|
|
|
if !self.musicMode, self.activeBackend == .mpv {
|
|
|
|
|
self.mpvBackend.addVideoTrackFromStream()
|
|
|
|
|
self.mpvBackend.setVideoToAuto()
|
|
|
|
|
self.mpvBackend.controls.resetTimer()
|
|
|
|
|
} else if !self.musicMode, self.activeBackend == .appleAVPlayer {
|
|
|
|
|
self.avPlayerBackend.bindPlayerToLayer()
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-09-01 10:42:31 +00:00
|
|
|
|
#if os(iOS)
|
|
|
|
|
OrientationTracker.shared.startDeviceOrientationTracking()
|
|
|
|
|
#endif
|
|
|
|
|
|
2021-12-19 17:17:04 +00:00
|
|
|
|
guard closePiPAndOpenPlayerOnEnteringForeground, playingInPictureInPicture else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
show()
|
2024-09-02 20:40:48 +00:00
|
|
|
|
// Needs to be delayed a bit, otherwise the PiP windows stays open
|
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
|
|
|
|
|
self?.closePiP()
|
|
|
|
|
}
|
2021-12-19 17:17:04 +00:00
|
|
|
|
}
|
2022-01-02 19:43:30 +00:00
|
|
|
|
|
2022-06-05 17:12:00 +00:00
|
|
|
|
func handleEnterBackground() {
|
2024-09-01 10:42:31 +00:00
|
|
|
|
#if os(iOS)
|
|
|
|
|
OrientationTracker.shared.stopDeviceOrientationTracking()
|
|
|
|
|
#endif
|
|
|
|
|
|
2022-06-30 09:46:20 +00:00
|
|
|
|
if Defaults[.pauseOnEnteringBackground], !playingInPictureInPicture, !musicMode {
|
2022-06-18 12:39:49 +00:00
|
|
|
|
pause()
|
2024-09-03 12:58:18 +00:00
|
|
|
|
} else if !playingInPictureInPicture, activeBackend == .appleAVPlayer {
|
2023-05-20 20:49:10 +00:00
|
|
|
|
avPlayerBackend.removePlayerFromLayer()
|
2024-09-03 12:58:18 +00:00
|
|
|
|
} else if activeBackend == .mpv, !musicMode {
|
|
|
|
|
mpvBackend.setVideoToNo()
|
2022-06-18 12:39:49 +00:00
|
|
|
|
}
|
2022-06-05 17:12:00 +00:00
|
|
|
|
}
|
2022-08-08 18:02:46 +00:00
|
|
|
|
#endif
|
2022-06-05 17:12:00 +00:00
|
|
|
|
|
2022-08-08 18:02:46 +00:00
|
|
|
|
func enterFullScreen(showControls: Bool = true) {
|
|
|
|
|
guard !playingFullScreen else { return }
|
2022-01-02 19:43:30 +00:00
|
|
|
|
|
2022-08-08 18:02:46 +00:00
|
|
|
|
logger.info("entering fullscreen")
|
|
|
|
|
toggleFullscreen(false, showControls: showControls)
|
2024-08-20 20:56:55 +00:00
|
|
|
|
self.playingFullScreen = true
|
2022-08-08 18:02:46 +00:00
|
|
|
|
}
|
2022-01-02 19:43:30 +00:00
|
|
|
|
|
2022-08-08 18:02:46 +00:00
|
|
|
|
func exitFullScreen(showControls: Bool = true) {
|
|
|
|
|
guard playingFullScreen else { return }
|
2022-01-02 19:43:30 +00:00
|
|
|
|
|
2022-08-08 18:02:46 +00:00
|
|
|
|
logger.info("exiting fullscreen")
|
|
|
|
|
toggleFullscreen(true, showControls: showControls)
|
2024-08-20 20:56:55 +00:00
|
|
|
|
self.playingFullScreen = false
|
2022-08-08 18:02:46 +00:00
|
|
|
|
}
|
2022-02-16 21:10:57 +00:00
|
|
|
|
|
|
|
|
|
func updateNowPlayingInfo() {
|
2022-08-28 22:29:29 +00:00
|
|
|
|
#if os(tvOS)
|
|
|
|
|
guard activeBackend == .mpv else { return }
|
|
|
|
|
#endif
|
2023-05-20 20:49:10 +00:00
|
|
|
|
|
2022-08-28 22:29:29 +00:00
|
|
|
|
guard let video = currentItem?.video else {
|
|
|
|
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = .none
|
|
|
|
|
return
|
|
|
|
|
}
|
2022-02-16 21:10:57 +00:00
|
|
|
|
|
2022-08-28 22:29:29 +00:00
|
|
|
|
let currentTime = (backend.currentTime?.seconds.isFinite ?? false) ? backend.currentTime!.seconds : 0
|
2024-08-29 13:10:04 +00:00
|
|
|
|
|
|
|
|
|
// Determine the media type based on musicMode
|
|
|
|
|
let mediaType: NSNumber
|
|
|
|
|
if musicMode {
|
|
|
|
|
mediaType = MPMediaType.anyAudio.rawValue as NSNumber
|
|
|
|
|
} else {
|
|
|
|
|
mediaType = MPMediaType.anyVideo.rawValue as NSNumber
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Prepare the Now Playing info dictionary
|
2022-08-28 22:29:29 +00:00
|
|
|
|
var nowPlayingInfo: [String: AnyObject] = [
|
2022-11-10 17:11:28 +00:00
|
|
|
|
MPMediaItemPropertyTitle: video.displayTitle as AnyObject,
|
|
|
|
|
MPMediaItemPropertyArtist: video.displayAuthor as AnyObject,
|
2022-08-28 22:29:29 +00:00
|
|
|
|
MPNowPlayingInfoPropertyIsLiveStream: live as AnyObject,
|
|
|
|
|
MPNowPlayingInfoPropertyElapsedPlaybackTime: currentTime as AnyObject,
|
|
|
|
|
MPNowPlayingInfoPropertyPlaybackQueueCount: queue.count as AnyObject,
|
|
|
|
|
MPNowPlayingInfoPropertyPlaybackQueueIndex: 1 as AnyObject,
|
2024-08-29 13:10:04 +00:00
|
|
|
|
MPMediaItemPropertyMediaType: mediaType
|
2022-08-28 22:29:29 +00:00
|
|
|
|
]
|
2022-08-20 20:28:31 +00:00
|
|
|
|
|
2022-08-28 22:29:29 +00:00
|
|
|
|
if !currentArtwork.isNil {
|
|
|
|
|
nowPlayingInfo[MPMediaItemPropertyArtwork] = currentArtwork as AnyObject
|
|
|
|
|
}
|
2022-02-16 21:10:57 +00:00
|
|
|
|
|
2022-08-28 22:29:29 +00:00
|
|
|
|
if !video.live {
|
|
|
|
|
let itemDuration = (backend.playerItemDuration ?? .zero).seconds
|
|
|
|
|
let duration = itemDuration.isFinite ? Double(itemDuration) : nil
|
2022-02-16 21:10:57 +00:00
|
|
|
|
|
2022-08-28 22:29:29 +00:00
|
|
|
|
if !duration.isNil {
|
|
|
|
|
nowPlayingInfo[MPMediaItemPropertyPlaybackDuration] = duration as AnyObject
|
2022-02-16 21:10:57 +00:00
|
|
|
|
}
|
2022-08-28 22:29:29 +00:00
|
|
|
|
}
|
2022-02-16 21:10:57 +00:00
|
|
|
|
|
2022-08-28 22:29:29 +00:00
|
|
|
|
MPNowPlayingInfoCenter.default().nowPlayingInfo = nowPlayingInfo
|
2022-02-16 21:10:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func updateCurrentArtwork() {
|
|
|
|
|
guard let video = currentVideo,
|
2024-08-30 14:03:35 +00:00
|
|
|
|
let thumbnailURL = video.thumbnailURL(quality: Constants.isIPhone ? .medium : .maxres)
|
2022-02-16 21:10:57 +00:00
|
|
|
|
else {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2022-06-26 21:28:03 +00:00
|
|
|
|
let task = URLSession.shared.dataTask(with: thumbnailURL) { [weak self] thumbnailData, _, _ in
|
2022-09-28 14:27:01 +00:00
|
|
|
|
guard let thumbnailData else {
|
2022-06-26 21:28:03 +00:00
|
|
|
|
return
|
|
|
|
|
}
|
2022-02-16 21:10:57 +00:00
|
|
|
|
|
2022-06-26 21:28:03 +00:00
|
|
|
|
#if os(macOS)
|
|
|
|
|
guard let image = NSImage(data: thumbnailData) else { return }
|
|
|
|
|
#else
|
|
|
|
|
guard let image = UIImage(data: thumbnailData) else { return }
|
|
|
|
|
#endif
|
|
|
|
|
|
|
|
|
|
self?.currentArtwork = MPMediaItemArtwork(boundsSize: image.size) { _ in image }
|
2022-02-16 21:10:57 +00:00
|
|
|
|
}
|
|
|
|
|
|
2022-06-26 21:28:03 +00:00
|
|
|
|
task.resume()
|
2022-02-16 21:10:57 +00:00
|
|
|
|
}
|
2022-04-03 12:23:42 +00:00
|
|
|
|
|
2024-09-01 10:42:31 +00:00
|
|
|
|
func toggleFullscreen(_ isFullScreen: Bool, showControls: Bool = true, initiatedByButton: Bool = false) {
|
2022-08-07 11:48:50 +00:00
|
|
|
|
controls.presentingControls = showControls && isFullScreen
|
2022-04-03 12:23:42 +00:00
|
|
|
|
|
|
|
|
|
#if os(macOS)
|
2022-08-08 18:02:46 +00:00
|
|
|
|
Windows.player.toggleFullScreen()
|
2022-06-16 17:44:39 +00:00
|
|
|
|
#endif
|
|
|
|
|
|
2023-05-20 14:04:58 +00:00
|
|
|
|
playingFullScreen = !isFullScreen
|
|
|
|
|
|
2022-07-09 00:21:04 +00:00
|
|
|
|
#if os(iOS)
|
2023-05-20 14:04:58 +00:00
|
|
|
|
if playingFullScreen {
|
2023-05-20 20:49:10 +00:00
|
|
|
|
if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls {
|
2024-09-06 15:05:20 +00:00
|
|
|
|
fullscreenInitiatedByButton = initiatedByButton
|
2023-05-20 20:49:10 +00:00
|
|
|
|
avPlayerBackend.controller.enterFullScreen(animated: true)
|
2023-05-21 17:11:11 +00:00
|
|
|
|
return
|
2023-05-20 20:49:10 +00:00
|
|
|
|
}
|
2024-09-01 10:42:31 +00:00
|
|
|
|
let lockOrientation = rotateToLandscapeOnEnterFullScreen.interfaceOrientation
|
2023-05-20 14:04:58 +00:00
|
|
|
|
if currentVideoIsLandscape {
|
2024-09-01 10:42:31 +00:00
|
|
|
|
if initiatedByButton {
|
2024-09-06 13:11:31 +00:00
|
|
|
|
Orientation.lockOrientation(isOrientationLocked
|
|
|
|
|
? (lockOrientation == .landscapeRight ? .landscapeRight : .landscapeLeft)
|
|
|
|
|
: .landscape)
|
2023-05-20 20:49:10 +00:00
|
|
|
|
}
|
2024-09-06 13:11:31 +00:00
|
|
|
|
let orientation = OrientationTracker.shared.currentDeviceOrientation.isLandscape
|
|
|
|
|
? OrientationTracker.shared.currentInterfaceOrientation
|
|
|
|
|
: rotateToLandscapeOnEnterFullScreen.interfaceOrientation
|
|
|
|
|
|
|
|
|
|
Orientation.lockOrientation(
|
|
|
|
|
isOrientationLocked
|
|
|
|
|
? (lockOrientation == .landscapeRight ? .landscapeRight : .landscapeLeft)
|
2024-09-06 14:47:03 +00:00
|
|
|
|
: .all,
|
2024-09-06 13:11:31 +00:00
|
|
|
|
andRotateTo: orientation
|
|
|
|
|
)
|
2023-05-20 14:04:58 +00:00
|
|
|
|
}
|
2022-09-01 23:05:54 +00:00
|
|
|
|
} else {
|
2023-05-20 20:49:10 +00:00
|
|
|
|
if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls {
|
|
|
|
|
avPlayerBackend.controller.exitFullScreen(animated: true)
|
2023-05-21 09:54:17 +00:00
|
|
|
|
avPlayerBackend.controller.dismiss(animated: true)
|
2023-05-21 17:11:11 +00:00
|
|
|
|
return
|
2023-05-20 20:49:10 +00:00
|
|
|
|
}
|
2024-09-06 15:05:20 +00:00
|
|
|
|
if lockPortraitWhenBrowsing {
|
2024-09-01 10:42:31 +00:00
|
|
|
|
lockedOrientation = UIInterfaceOrientationMask.portrait
|
|
|
|
|
}
|
2024-09-06 13:11:31 +00:00
|
|
|
|
let rotationOrientation = lockPortraitWhenBrowsing ? UIInterfaceOrientation.portrait : nil
|
2024-09-06 14:47:03 +00:00
|
|
|
|
Orientation.lockOrientation(lockPortraitWhenBrowsing ? .portrait : .all, andRotateTo: rotationOrientation)
|
2022-04-03 12:23:42 +00:00
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
}
|
2022-05-29 18:26:56 +00:00
|
|
|
|
|
|
|
|
|
func setNeedsDrawing(_ needsDrawing: Bool) {
|
|
|
|
|
backends.forEach { $0.setNeedsDrawing(needsDrawing) }
|
|
|
|
|
}
|
2022-06-07 21:27:48 +00:00
|
|
|
|
|
|
|
|
|
func toggleMusicMode() {
|
|
|
|
|
musicMode.toggle()
|
|
|
|
|
|
|
|
|
|
if musicMode {
|
2022-08-29 15:57:43 +00:00
|
|
|
|
aspectRatio = VideoPlayerView.defaultAspectRatio
|
2022-06-07 21:27:48 +00:00
|
|
|
|
controls.presentingControls = true
|
|
|
|
|
controls.removeTimer()
|
2022-08-20 20:31:03 +00:00
|
|
|
|
|
|
|
|
|
backend.startMusicMode()
|
2022-06-07 21:27:48 +00:00
|
|
|
|
} else {
|
2022-08-20 20:31:03 +00:00
|
|
|
|
backend.stopMusicMode()
|
2022-08-29 15:57:43 +00:00
|
|
|
|
Delay.by(0.25) {
|
|
|
|
|
self.updateAspectRatio()
|
|
|
|
|
}
|
2022-06-07 21:27:48 +00:00
|
|
|
|
|
|
|
|
|
controls.resetTimer()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2022-08-13 14:11:07 +00:00
|
|
|
|
func updateAspectRatio() {
|
2022-08-14 16:59:04 +00:00
|
|
|
|
#if !os(tvOS)
|
|
|
|
|
guard aspectRatio != backend.aspectRatio else { return }
|
2022-08-13 14:11:07 +00:00
|
|
|
|
|
2022-08-14 16:59:04 +00:00
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
2022-09-28 14:27:01 +00:00
|
|
|
|
guard let self else { return }
|
2023-06-07 19:56:56 +00:00
|
|
|
|
withAnimation {
|
|
|
|
|
self.aspectRatio = self.backend.aspectRatio
|
|
|
|
|
}
|
2022-08-14 16:59:04 +00:00
|
|
|
|
}
|
|
|
|
|
#endif
|
2022-08-13 14:11:07 +00:00
|
|
|
|
}
|
2022-11-10 17:11:28 +00:00
|
|
|
|
|
2023-05-20 14:04:58 +00:00
|
|
|
|
var currentVideoIsLandscape: Bool {
|
|
|
|
|
guard currentVideo != nil else { return false }
|
|
|
|
|
|
|
|
|
|
return aspectRatio > 1
|
|
|
|
|
}
|
|
|
|
|
|
2022-11-10 17:11:28 +00:00
|
|
|
|
var formattedSize: String {
|
|
|
|
|
guard let videoWidth = backend?.videoWidth, let videoHeight = backend?.videoHeight else { return "unknown" }
|
|
|
|
|
return "\(String(format: "%.2f", videoWidth))\u{d7}\(String(format: "%.2f", videoHeight))"
|
|
|
|
|
}
|
2023-06-07 19:39:03 +00:00
|
|
|
|
|
|
|
|
|
func handleOnPlayStream(_ stream: Stream) {
|
|
|
|
|
backend.setRate(currentRate)
|
|
|
|
|
|
|
|
|
|
onPlayStream.forEach { $0(stream) }
|
|
|
|
|
onPlayStream.removeAll()
|
|
|
|
|
}
|
2023-12-04 20:58:49 +00:00
|
|
|
|
|
|
|
|
|
func updateTime(_ cmTime: CMTime) {
|
|
|
|
|
let time = CMTimeGetSeconds(cmTime)
|
|
|
|
|
let newChapterIndex = chapterForTime(time)
|
|
|
|
|
if currentChapterIndex != newChapterIndex {
|
|
|
|
|
DispatchQueue.main.async {
|
|
|
|
|
self.currentChapterIndex = newChapterIndex
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private func chapterForTime(_ time: Double) -> Int? {
|
|
|
|
|
guard let chapters = self.videoForDisplay?.chapters else {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (index, chapter) in chapters.enumerated() {
|
|
|
|
|
let nextChapterStartTime = index < (chapters.count - 1) ? chapters[index + 1].start : nil
|
|
|
|
|
|
|
|
|
|
if let nextChapterStart = nextChapterStartTime {
|
|
|
|
|
if time >= chapter.start, time < nextChapterStart {
|
|
|
|
|
return index
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if time >= chapter.start {
|
|
|
|
|
return index
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
2024-04-20 23:01:55 +00:00
|
|
|
|
|
2024-08-31 20:42:17 +00:00
|
|
|
|
#if !os(macOS)
|
2024-09-11 18:18:56 +00:00
|
|
|
|
func setAudioSessionActive(_ setActive: Bool) {
|
|
|
|
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) {
|
|
|
|
|
do {
|
|
|
|
|
try AVAudioSession.sharedInstance().setActive(setActive)
|
|
|
|
|
} catch {
|
|
|
|
|
self.logger.error("Error setting up audio session: \(error)")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-08-31 20:42:17 +00:00
|
|
|
|
@objc func handleAudioSessionInterruption(_ notification: Notification) {
|
|
|
|
|
logger.info("Audio session interruption received.")
|
2024-09-11 18:18:56 +00:00
|
|
|
|
logger.info("Notification object: \(String(describing: notification.object))")
|
|
|
|
|
|
|
|
|
|
guard let info = notification.userInfo else {
|
|
|
|
|
logger.info("userInfo is missing in the notification.")
|
|
|
|
|
return
|
|
|
|
|
}
|
2024-08-31 20:42:17 +00:00
|
|
|
|
|
2024-09-11 18:18:56 +00:00
|
|
|
|
// Extract the interruption type
|
|
|
|
|
guard let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
|
2024-08-31 20:42:17 +00:00
|
|
|
|
let type = AVAudioSession.InterruptionType(rawValue: typeValue)
|
|
|
|
|
else {
|
|
|
|
|
logger.info("AVAudioSessionInterruptionTypeKey is missing or not a UInt in userInfo.")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.info("Interruption type received: \(type)")
|
|
|
|
|
|
2024-09-11 18:18:56 +00:00
|
|
|
|
// Check availability for iOS 14.5 or newer to handle interruption reason
|
2024-09-13 09:48:40 +00:00
|
|
|
|
// Currently only for debugging purpose
|
|
|
|
|
#if os(iOS)
|
|
|
|
|
if #available(iOS 14.5, *) {
|
|
|
|
|
// Extract the interruption reason, if available
|
|
|
|
|
if let reasonValue = info[AVAudioSessionInterruptionReasonKey] as? UInt,
|
|
|
|
|
let reason = AVAudioSession.InterruptionReason(rawValue: reasonValue)
|
|
|
|
|
{
|
|
|
|
|
logger.info("Interruption reason received: \(reason)")
|
|
|
|
|
switch reason {
|
|
|
|
|
case .default:
|
|
|
|
|
logger.info("Interruption reason: Default or unspecified interruption occurred.")
|
|
|
|
|
case .appWasSuspended:
|
|
|
|
|
logger.info("Interruption reason: The app was suspended during the interruption.")
|
|
|
|
|
@unknown default:
|
|
|
|
|
logger.info("Unknown interruption reason received.")
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
logger.info("AVAudioSessionInterruptionReasonKey is missing or not a UInt in userInfo.")
|
2024-09-11 18:18:56 +00:00
|
|
|
|
}
|
|
|
|
|
} else {
|
2024-09-13 09:48:40 +00:00
|
|
|
|
logger.info("Interruption reason handling is not available on this iOS version.")
|
2024-09-11 18:18:56 +00:00
|
|
|
|
}
|
2024-09-13 09:48:40 +00:00
|
|
|
|
#endif
|
2024-09-11 18:18:56 +00:00
|
|
|
|
|
|
|
|
|
// Handle the specific interruption type
|
2024-08-31 20:42:17 +00:00
|
|
|
|
switch type {
|
|
|
|
|
case .began:
|
|
|
|
|
pause()
|
2024-09-11 18:18:56 +00:00
|
|
|
|
logger.info("Audio session interrupted (began).")
|
2024-08-31 20:42:17 +00:00
|
|
|
|
case .ended:
|
2024-09-11 18:18:56 +00:00
|
|
|
|
// Extract any interruption options, if available
|
|
|
|
|
if let optionsValue = info[AVAudioSessionInterruptionOptionKey] as? UInt {
|
|
|
|
|
logger.info("Interruption options received: \(optionsValue)")
|
|
|
|
|
if optionsValue & AVAudioSession.InterruptionOptions.shouldResume.rawValue != 0 {
|
|
|
|
|
play()
|
|
|
|
|
logger.info("Interruption option indicates playback should resume automatically.")
|
|
|
|
|
} else {
|
|
|
|
|
logger.info("Interruption option indicates playback should not resume automatically.")
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
logger.info("AVAudioSessionInterruptionOptionKey is missing or not a UInt in userInfo.")
|
|
|
|
|
}
|
2024-08-31 20:42:17 +00:00
|
|
|
|
logger.info("Audio session interruption ended.")
|
2024-09-11 18:18:56 +00:00
|
|
|
|
// Check if audio was resumed or if there's any indication of ducking
|
|
|
|
|
let currentVolume = AVAudioSession.sharedInstance().outputVolume
|
|
|
|
|
logger.info("Current output volume: \(currentVolume)")
|
|
|
|
|
default:
|
|
|
|
|
logger.info("Unknown interruption type received.")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@objc func handleRouteChange(_ notification: Notification) {
|
|
|
|
|
logger.info("Audio route change received.")
|
|
|
|
|
|
|
|
|
|
guard let info = notification.userInfo else {
|
|
|
|
|
logger.info("userInfo is missing in the notification.")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
guard let reasonValue = info[AVAudioSessionRouteChangeReasonKey] as? UInt,
|
|
|
|
|
let reason = AVAudioSession.RouteChangeReason(rawValue: reasonValue)
|
|
|
|
|
else {
|
|
|
|
|
logger.info("AVAudioSessionRouteChangeReasonKey is missing or not a UInt in userInfo.")
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.info("Route change reason received: \(reason)")
|
|
|
|
|
|
|
|
|
|
let currentCategory = AVAudioSession.sharedInstance().category
|
|
|
|
|
logger.info("Current audio session category before change: \(currentCategory)")
|
|
|
|
|
|
|
|
|
|
switch reason {
|
|
|
|
|
case .categoryChange:
|
|
|
|
|
logger.info("Audio session category changed.")
|
|
|
|
|
let newCategory = AVAudioSession.sharedInstance().category
|
|
|
|
|
logger.info("New audio session category: \(newCategory)")
|
|
|
|
|
case .oldDeviceUnavailable, .newDeviceAvailable:
|
|
|
|
|
logger.info("Audio route change may indicate ducking or device change.")
|
|
|
|
|
let currentRoute = AVAudioSession.sharedInstance().currentRoute
|
|
|
|
|
logger.info("Current audio route: \(currentRoute)")
|
|
|
|
|
|
|
|
|
|
for output in currentRoute.outputs {
|
|
|
|
|
logger.info("Output port type: \(output.portType), UID: \(output.uid)")
|
|
|
|
|
switch output.portType {
|
|
|
|
|
case .headphones, .bluetoothA2DP:
|
|
|
|
|
logger.info("Detected port type \(output.portType). Executing play().")
|
|
|
|
|
play()
|
|
|
|
|
default:
|
|
|
|
|
logger.info("Detected port type \(output.portType). Executing pause().")
|
|
|
|
|
pause()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
case .noSuitableRouteForCategory:
|
|
|
|
|
logger.info("No suitable route for the current category.")
|
2024-08-31 20:42:17 +00:00
|
|
|
|
default:
|
2024-09-11 18:18:56 +00:00
|
|
|
|
logger.info("Unhandled route change reason: \(reason)")
|
2024-08-31 20:42:17 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
2024-02-17 09:40:27 +00:00
|
|
|
|
#if os(macOS)
|
|
|
|
|
private func assignKeyPressMonitor() {
|
2024-09-05 16:08:31 +00:00
|
|
|
|
keyPressMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { [weak self] keyEvent -> NSEvent? in
|
|
|
|
|
// Check if the player window is the key window
|
|
|
|
|
guard let self, let window = Windows.playerWindow, window.isKeyWindow else { return keyEvent }
|
|
|
|
|
|
2024-02-17 09:40:27 +00:00
|
|
|
|
switch keyEvent.keyCode {
|
|
|
|
|
case 124:
|
|
|
|
|
if !self.liveStreamInAVPlayer {
|
|
|
|
|
let interval = TimeInterval(self.buttonForwardSeekDuration) ?? 10
|
|
|
|
|
self.backend.seek(
|
|
|
|
|
relative: .secondsInDefaultTimescale(interval),
|
|
|
|
|
seekType: .userInteracted
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
case 123:
|
|
|
|
|
if !self.liveStreamInAVPlayer {
|
|
|
|
|
let interval = TimeInterval(self.buttonBackwardSeekDuration) ?? 10
|
|
|
|
|
self.backend.seek(
|
|
|
|
|
relative: .secondsInDefaultTimescale(-interval),
|
|
|
|
|
seekType: .userInteracted
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
case 3:
|
|
|
|
|
self.toggleFullscreen(
|
|
|
|
|
self.playingFullScreen,
|
|
|
|
|
showControls: false
|
|
|
|
|
)
|
|
|
|
|
case 49:
|
|
|
|
|
if !self.controls.isLoadingVideo {
|
|
|
|
|
self.backend.togglePlay()
|
|
|
|
|
}
|
2024-04-23 09:39:32 +00:00
|
|
|
|
default:
|
|
|
|
|
return keyEvent
|
2024-02-17 09:40:27 +00:00
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-04-20 23:01:55 +00:00
|
|
|
|
|
2024-02-17 09:40:27 +00:00
|
|
|
|
private func destroyKeyPressMonitor() {
|
2024-08-29 13:10:04 +00:00
|
|
|
|
if let keyPressMonitor {
|
2024-02-17 09:40:27 +00:00
|
|
|
|
NSEvent.removeMonitor(keyPressMonitor)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
2021-06-14 18:05:02 +00:00
|
|
|
|
}
|