2022-02-16 20:23:11 +00:00
|
|
|
import CoreMedia
|
|
|
|
import Defaults
|
|
|
|
import Foundation
|
2022-08-28 17:18:49 +00:00
|
|
|
#if !os(macOS)
|
|
|
|
import UIKit
|
|
|
|
#endif
|
2022-02-16 20:23:11 +00:00
|
|
|
|
|
|
|
protocol PlayerBackend {
|
2022-11-10 22:19:34 +00:00
|
|
|
var suggestedPlaybackRates: [Double] { get }
|
2022-11-24 20:36:05 +00:00
|
|
|
var model: PlayerModel { get }
|
|
|
|
var controls: PlayerControlsModel { get }
|
|
|
|
var playerTime: PlayerTimeModel { get }
|
|
|
|
var networkState: NetworkStateModel { get }
|
2022-02-16 20:23:11 +00:00
|
|
|
|
|
|
|
var stream: Stream? { get set }
|
|
|
|
var video: Video? { get set }
|
|
|
|
var currentTime: CMTime? { get }
|
|
|
|
|
|
|
|
var loadedVideo: Bool { get }
|
|
|
|
var isLoadingVideo: Bool { get }
|
|
|
|
|
|
|
|
var isPlaying: Bool { get }
|
2022-06-18 12:39:49 +00:00
|
|
|
var isSeeking: Bool { get }
|
2022-02-16 20:23:11 +00:00
|
|
|
var playerItemDuration: CMTime? { get }
|
|
|
|
|
2022-07-09 00:21:04 +00:00
|
|
|
var aspectRatio: Double { get }
|
2022-08-23 21:29:50 +00:00
|
|
|
var controlsUpdates: Bool { get }
|
2022-07-09 00:21:04 +00:00
|
|
|
|
2022-11-10 17:11:28 +00:00
|
|
|
var videoWidth: Double? { get }
|
|
|
|
var videoHeight: Double? { get }
|
|
|
|
|
2022-02-16 20:23:11 +00:00
|
|
|
func canPlay(_ stream: Stream) -> Bool
|
2022-11-10 22:19:34 +00:00
|
|
|
func canPlayAtRate(_ rate: Double) -> Bool
|
2022-02-16 20:23:11 +00:00
|
|
|
|
|
|
|
func playStream(
|
|
|
|
_ stream: Stream,
|
|
|
|
of video: Video,
|
|
|
|
preservingTime: Bool,
|
|
|
|
upgrading: Bool
|
|
|
|
)
|
|
|
|
|
|
|
|
func play()
|
|
|
|
func pause()
|
|
|
|
func togglePlay()
|
|
|
|
|
|
|
|
func stop()
|
|
|
|
|
2022-08-29 11:55:23 +00:00
|
|
|
func seek(to time: CMTime, seekType: SeekType, completionHandler: ((Bool) -> Void)?)
|
|
|
|
func seek(to seconds: Double, seekType: SeekType, completionHandler: ((Bool) -> Void)?)
|
2022-02-16 20:23:11 +00:00
|
|
|
|
2022-11-10 22:00:17 +00:00
|
|
|
func setRate(_ rate: Double)
|
2022-02-16 20:23:11 +00:00
|
|
|
|
|
|
|
func closeItem()
|
|
|
|
|
2022-08-18 22:40:46 +00:00
|
|
|
func closePiP()
|
2022-02-16 20:23:11 +00:00
|
|
|
|
2022-08-20 20:31:03 +00:00
|
|
|
func startMusicMode()
|
|
|
|
func stopMusicMode()
|
|
|
|
|
2022-08-28 17:18:49 +00:00
|
|
|
func getTimeUpdates()
|
|
|
|
func updateControls(completionHandler: (() -> Void)?)
|
2022-02-16 20:23:11 +00:00
|
|
|
func startControlsUpdates()
|
|
|
|
func stopControlsUpdates()
|
|
|
|
|
2022-08-20 20:31:03 +00:00
|
|
|
func didChangeTo()
|
|
|
|
|
2022-06-24 23:39:29 +00:00
|
|
|
func setNeedsNetworkStateUpdates(_ needsUpdates: Bool)
|
2022-06-18 12:39:49 +00:00
|
|
|
|
2022-02-16 20:23:11 +00:00
|
|
|
func setNeedsDrawing(_ needsDrawing: Bool)
|
2022-03-27 11:42:20 +00:00
|
|
|
func setSize(_ width: Double, _ height: Double)
|
2022-11-13 12:28:25 +00:00
|
|
|
|
|
|
|
func cancelLoads()
|
2022-02-16 20:23:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
extension PlayerBackend {
|
2022-08-29 11:55:23 +00:00
|
|
|
func seek(to time: CMTime, seekType: SeekType, completionHandler: ((Bool) -> Void)? = nil) {
|
2022-09-01 23:05:31 +00:00
|
|
|
model.seek.registerSeek(at: time, type: seekType, restore: currentTime)
|
2022-08-28 17:18:49 +00:00
|
|
|
seek(to: time, seekType: seekType, completionHandler: completionHandler)
|
2022-02-16 20:23:11 +00:00
|
|
|
}
|
|
|
|
|
2022-08-29 11:55:23 +00:00
|
|
|
func seek(to seconds: Double, seekType: SeekType, completionHandler: ((Bool) -> Void)? = nil) {
|
2022-08-28 17:18:49 +00:00
|
|
|
let seconds = CMTime.secondsInDefaultTimescale(seconds)
|
2022-09-01 23:05:31 +00:00
|
|
|
model.seek.registerSeek(at: seconds, type: seekType, restore: currentTime)
|
2022-08-28 17:18:49 +00:00
|
|
|
seek(to: seconds, seekType: seekType, completionHandler: completionHandler)
|
2022-02-16 20:23:11 +00:00
|
|
|
}
|
|
|
|
|
2022-08-29 11:55:23 +00:00
|
|
|
func seek(relative time: CMTime, seekType: SeekType, completionHandler: ((Bool) -> Void)? = nil) {
|
2022-09-28 14:27:01 +00:00
|
|
|
if let currentTime, let duration = playerItemDuration {
|
2022-08-28 17:18:49 +00:00
|
|
|
let seekTime = min(max(0, currentTime.seconds + time.seconds), duration.seconds)
|
2022-09-01 23:05:31 +00:00
|
|
|
model.seek.registerSeek(at: .secondsInDefaultTimescale(seekTime), type: seekType, restore: currentTime)
|
2022-08-28 17:18:49 +00:00
|
|
|
seek(to: seekTime, seekType: seekType, completionHandler: completionHandler)
|
|
|
|
}
|
2022-02-16 20:23:11 +00:00
|
|
|
}
|
2022-07-10 22:24:56 +00:00
|
|
|
|
|
|
|
func eofPlaybackModeAction() {
|
2022-12-18 18:39:03 +00:00
|
|
|
let loopAction = {
|
|
|
|
model.backend.seek(to: .zero, seekType: .loopRestart) { _ in
|
|
|
|
self.model.play()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
guard model.playbackMode != .loopOne else {
|
|
|
|
loopAction()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
2023-04-22 11:56:25 +00:00
|
|
|
switch model.playbackMode {
|
|
|
|
case .queue, .shuffle:
|
|
|
|
model.prepareCurrentItemForHistory(finished: true)
|
|
|
|
|
|
|
|
if model.queue.isEmpty {
|
2023-05-21 10:33:59 +00:00
|
|
|
if Defaults[.closeVideoOnEOF] {
|
|
|
|
#if os(tvOS)
|
|
|
|
if model.activeBackend == .appleAVPlayer {
|
|
|
|
model.avPlayerBackend.controller?.dismiss(animated: false)
|
|
|
|
}
|
|
|
|
#endif
|
|
|
|
model.resetQueue()
|
|
|
|
model.hide()
|
|
|
|
}
|
2022-12-18 18:39:03 +00:00
|
|
|
} else {
|
2023-04-22 11:56:25 +00:00
|
|
|
model.advanceToNextItem()
|
2022-12-18 18:39:03 +00:00
|
|
|
}
|
2023-04-22 11:56:25 +00:00
|
|
|
case .loopOne:
|
|
|
|
loopAction()
|
|
|
|
case .related:
|
|
|
|
guard let item = model.autoplayItem else { return }
|
|
|
|
model.resetAutoplay()
|
|
|
|
model.advanceToItem(item)
|
2022-12-18 18:39:03 +00:00
|
|
|
}
|
2022-07-10 22:24:56 +00:00
|
|
|
}
|
2022-08-28 17:18:49 +00:00
|
|
|
|
2024-04-26 10:27:25 +00:00
|
|
|
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting, formatOrder: [QualityProfile.Format]) -> Stream? {
|
2024-05-11 12:08:40 +00:00
|
|
|
// filter out non HLS streams
|
|
|
|
let nonHLSStreams = streams.filter { $0.kind != .hls }
|
|
|
|
|
|
|
|
// find max resolution from non HLS streams
|
|
|
|
let bestResolution = nonHLSStreams
|
|
|
|
.filter { $0.resolution <= maxResolution.value }
|
2024-05-13 05:54:24 +00:00
|
|
|
.max { $0.resolution < $1.resolution }
|
|
|
|
|
|
|
|
// finde max bitrate from non HLS streams
|
|
|
|
let bestBitrate = nonHLSStreams
|
|
|
|
.filter { $0.resolution <= maxResolution.value }
|
|
|
|
.max { $0.bitrate ?? 0 < $1.bitrate ?? 0 }
|
2024-05-11 12:08:40 +00:00
|
|
|
|
2024-04-26 10:27:25 +00:00
|
|
|
return streams.map { stream in
|
|
|
|
if stream.kind == .hls {
|
2024-05-13 05:54:24 +00:00
|
|
|
stream.resolution = bestResolution?.resolution ?? maxResolution.value
|
|
|
|
stream.bitrate = bestBitrate?.bitrate ?? (bestResolution?.resolution.bitrate ?? maxResolution.value.bitrate)
|
2024-04-26 10:27:25 +00:00
|
|
|
stream.format = .hls
|
|
|
|
} else if stream.kind == .stream {
|
|
|
|
stream.format = .stream
|
|
|
|
}
|
|
|
|
return stream
|
|
|
|
}
|
|
|
|
.filter { stream in
|
|
|
|
stream.resolution <= maxResolution.value
|
|
|
|
}
|
|
|
|
.max { lhs, rhs in
|
|
|
|
if lhs.resolution == rhs.resolution {
|
|
|
|
guard let lhsFormat = QualityProfile.Format(rawValue: lhs.format.rawValue),
|
|
|
|
let rhsFormat = QualityProfile.Format(rawValue: rhs.format.rawValue)
|
|
|
|
else {
|
|
|
|
print("Failed to extract lhsFormat or rhsFormat")
|
|
|
|
return false
|
|
|
|
}
|
|
|
|
|
|
|
|
let lhsFormatIndex = formatOrder.firstIndex(of: lhsFormat) ?? Int.max
|
|
|
|
let rhsFormatIndex = formatOrder.firstIndex(of: rhsFormat) ?? Int.max
|
|
|
|
|
|
|
|
return lhsFormatIndex > rhsFormatIndex
|
|
|
|
}
|
|
|
|
|
|
|
|
return lhs.resolution < rhs.resolution
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-28 17:18:49 +00:00
|
|
|
func updateControls(completionHandler: (() -> Void)? = nil) {
|
|
|
|
print("updating controls")
|
|
|
|
|
|
|
|
guard model.presentingPlayer, !model.controls.presentingOverlays else {
|
|
|
|
print("ignored controls update")
|
|
|
|
completionHandler?()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
DispatchQueue.main.async(qos: .userInteractive) {
|
|
|
|
#if !os(macOS)
|
|
|
|
guard UIApplication.shared.applicationState != .background else {
|
|
|
|
print("not performing controls updates in background")
|
|
|
|
completionHandler?()
|
|
|
|
return
|
|
|
|
}
|
|
|
|
#endif
|
2022-08-31 19:24:46 +00:00
|
|
|
PlayerTimeModel.shared.currentTime = self.currentTime ?? .zero
|
|
|
|
PlayerTimeModel.shared.duration = self.playerItemDuration ?? .zero
|
2022-08-28 17:18:49 +00:00
|
|
|
completionHandler?()
|
|
|
|
}
|
|
|
|
}
|
2022-02-16 20:23:11 +00:00
|
|
|
}
|