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?
|
2024-08-31 11:00:50 +00:00
|
|
|
|
@Published var wasFullscreen = false
|
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?
|
2023-05-20 14:04:58 +00:00
|
|
|
|
@Default(.rotateToLandscapeOnEnterFullScreen) private var rotateToLandscapeOnEnterFullScreen
|
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-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() {
|
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
|
|
|
|
|
)
|
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 {
|
|
|
|
|
NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
|
|
|
|
|
}
|
|
|
|
|
#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() {
|
2022-08-31 19:24:46 +00:00
|
|
|
|
backend.setNeedsDrawing(presentingPlayer)
|
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)
|
|
|
|
|
if Defaults[.lockPortraitWhenBrowsing] {
|
|
|
|
|
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
|
|
|
|
|
} else {
|
|
|
|
|
Orientation.lockOrientation(.allButUpsideDown)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
OrientationModel.shared.stopOrientationUpdates()
|
|
|
|
|
#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) {
|
2022-08-07 11:15:27 +00:00
|
|
|
|
pause()
|
2022-12-20 22:22:40 +00:00
|
|
|
|
videoBeingOpened = nil
|
2023-05-26 21:20:45 +00:00
|
|
|
|
advancing = false
|
2023-05-26 20:49:38 +00:00
|
|
|
|
forceBackendOnPlay = nil
|
2022-08-07 11:15:27 +00:00
|
|
|
|
|
2022-12-18 12:11:06 +00:00
|
|
|
|
closing = true
|
|
|
|
|
controls.presentingControls = false
|
2022-02-16 20:23:11 +00:00
|
|
|
|
|
2023-06-07 20:25:10 +00:00
|
|
|
|
self.prepareCurrentItemForHistory(finished: finished)
|
|
|
|
|
|
2022-12-18 12:11:06 +00:00
|
|
|
|
self.hide()
|
2022-08-07 11:15:27 +00:00
|
|
|
|
|
2022-12-18 12:11:06 +00:00
|
|
|
|
Delay.by(0.8) { [weak self] in
|
|
|
|
|
guard let self else { return }
|
|
|
|
|
self.closePiP()
|
2022-08-07 11:15:27 +00:00
|
|
|
|
|
2022-12-18 12:11:06 +00:00
|
|
|
|
withAnimation {
|
|
|
|
|
self.currentItem = nil
|
|
|
|
|
}
|
|
|
|
|
self.updateNowPlayingInfo()
|
|
|
|
|
|
|
|
|
|
self.backend.closeItem()
|
|
|
|
|
self.aspectRatio = VideoPlayerView.defaultAspectRatio
|
|
|
|
|
self.resetAutoplay()
|
|
|
|
|
self.closing = false
|
|
|
|
|
self.playingFullScreen = false
|
|
|
|
|
}
|
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() {
|
|
|
|
|
toggleFullscreen(playingFullScreen, showControls: false)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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 {
|
|
|
|
|
lockedOrientation.isNil ? "lock.rotation.open" : "lock.rotation"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func lockOrientationAction() {
|
|
|
|
|
if lockedOrientation.isNil {
|
|
|
|
|
let orientationMask = OrientationTracker.shared.currentInterfaceOrientationMask
|
|
|
|
|
lockedOrientation = orientationMask
|
|
|
|
|
let orientation = OrientationTracker.shared.currentInterfaceOrientation
|
|
|
|
|
Orientation.lockOrientation(orientationMask, andRotateTo: .landscapeLeft)
|
|
|
|
|
// iOS 16 workaround
|
|
|
|
|
Orientation.lockOrientation(orientationMask, andRotateTo: orientation)
|
|
|
|
|
} else {
|
|
|
|
|
lockedOrientation = nil
|
|
|
|
|
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: OrientationTracker.shared.currentInterfaceOrientation)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#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() {
|
2022-06-30 09:46:20 +00:00
|
|
|
|
setNeedsDrawing(presentingPlayer)
|
2024-04-25 14:49:02 +00:00
|
|
|
|
|
2024-09-03 12:58:18 +00:00
|
|
|
|
if !musicMode, activeBackend == .mpv {
|
|
|
|
|
mpvBackend.addVideoTrackFromStream()
|
|
|
|
|
mpvBackend.setVideoToAuto()
|
|
|
|
|
mpvBackend.controls.resetTimer()
|
|
|
|
|
} else if !musicMode, activeBackend == .appleAVPlayer {
|
2024-04-25 14:49:02 +00:00
|
|
|
|
avPlayerBackend.bindPlayerToLayer()
|
|
|
|
|
}
|
2022-06-05 17:12:00 +00:00
|
|
|
|
|
2024-08-31 11:00:50 +00:00
|
|
|
|
#if os(iOS)
|
|
|
|
|
if wasFullscreen {
|
|
|
|
|
wasFullscreen = false
|
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
|
|
|
Delay.by(0.3) {
|
|
|
|
|
self?.enterFullScreen()
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#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() {
|
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
|
|
|
|
}
|
2024-08-31 11:00:50 +00:00
|
|
|
|
#if os(iOS)
|
|
|
|
|
guard playingFullScreen else { return }
|
|
|
|
|
wasFullscreen = playingFullScreen
|
|
|
|
|
DispatchQueue.main.async { [weak self] in
|
|
|
|
|
Delay.by(0.3) {
|
|
|
|
|
self?.exitFullScreen(showControls: false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
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
|
|
|
|
|
2022-08-07 11:48:50 +00:00
|
|
|
|
func toggleFullscreen(_ isFullScreen: Bool, showControls: Bool = true) {
|
|
|
|
|
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 {
|
|
|
|
|
avPlayerBackend.controller.enterFullScreen(animated: true)
|
2023-05-21 17:11:11 +00:00
|
|
|
|
return
|
2023-05-20 20:49:10 +00:00
|
|
|
|
}
|
2023-05-20 14:04:58 +00:00
|
|
|
|
guard rotateToLandscapeOnEnterFullScreen.isRotating else { return }
|
|
|
|
|
if currentVideoIsLandscape {
|
2023-05-20 20:49:10 +00:00
|
|
|
|
let delay = activeBackend == .appleAVPlayer && avPlayerUsesSystemControls ? 0.8 : 0
|
2023-05-20 14:04:58 +00:00
|
|
|
|
// not sure why but first rotation call is ignore so doing rotate to same orientation first
|
2023-05-20 20:49:10 +00:00
|
|
|
|
Delay.by(delay) {
|
|
|
|
|
let orientation = OrientationTracker.shared.currentDeviceOrientation.isLandscape ? OrientationTracker.shared.currentInterfaceOrientation : self.rotateToLandscapeOnEnterFullScreen.interaceOrientation
|
|
|
|
|
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: OrientationTracker.shared.currentInterfaceOrientation)
|
|
|
|
|
Orientation.lockOrientation(.allButUpsideDown, 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
|
|
|
|
}
|
2023-05-23 15:29:10 +00:00
|
|
|
|
let rotationOrientation = Constants.isIPhone ? UIInterfaceOrientation.portrait : nil
|
2022-08-07 11:48:50 +00:00
|
|
|
|
Orientation.lockOrientation(.allButUpsideDown, andRotateTo: rotationOrientation)
|
2022-04-03 12:23:42 +00:00
|
|
|
|
}
|
2023-05-20 14:04:58 +00:00
|
|
|
|
|
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)
|
|
|
|
|
@objc func handleAudioSessionInterruption(_ notification: Notification) {
|
|
|
|
|
logger.info("Audio session interruption received.")
|
|
|
|
|
logger.info("Notification received: \(notification)")
|
|
|
|
|
|
|
|
|
|
guard let info = notification.userInfo,
|
|
|
|
|
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt,
|
|
|
|
|
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)")
|
|
|
|
|
|
|
|
|
|
switch type {
|
|
|
|
|
case .began:
|
|
|
|
|
logger.info("Audio session interrupted.")
|
|
|
|
|
// We need to call pause() to set all variables correctly, and play()
|
|
|
|
|
// directly afterwards, because the .began interrupt is sent after audio
|
|
|
|
|
// ducking ended and playback would pause. Audio ducking usually happens
|
|
|
|
|
// when using headphones.
|
|
|
|
|
pause()
|
|
|
|
|
play()
|
|
|
|
|
case .ended:
|
|
|
|
|
logger.info("Audio session interruption ended.")
|
|
|
|
|
// We need to call pause() to set all variables correctly.
|
|
|
|
|
// Otherwise, playback does not resume when the interruption ends.
|
|
|
|
|
pause()
|
|
|
|
|
play()
|
|
|
|
|
default:
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
#endif
|
|
|
|
|
|
2024-02-17 09:40:27 +00:00
|
|
|
|
#if os(macOS)
|
|
|
|
|
private func assignKeyPressMonitor() {
|
|
|
|
|
keyPressMonitor = NSEvent.addLocalMonitorForEvents(matching: .keyDown) { keyEvent -> NSEvent? in
|
|
|
|
|
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
|
|
|
|
}
|