yattee/Model/Player/Backends/PlayerBackend.swift

249 lines
9.7 KiB
Swift
Raw Normal View History

2022-02-16 20:23:11 +00:00
import CoreMedia
import Defaults
import Foundation
import Logging
#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 }
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 hasStarted: Bool { get }
var isPaused: Bool { get }
2022-02-16 20:23:11 +00:00
var isPlaying: Bool { get }
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
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()
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-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)
func cancelLoads()
2022-02-16 20:23:11 +00:00
}
extension PlayerBackend {
var logger: Logger {
return Logger(label: "stream.yattee.player.backend")
}
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)
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) {
let seconds = CMTime.secondsInDefaultTimescale(seconds)
2022-09-01 23:05:31 +00:00
model.seek.registerSeek(at: seconds, type: seekType, restore: currentTime)
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 {
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)
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-11-27 22:49:18 +00:00
#if os(tvOS)
if Defaults[.closeVideoOnEOF] {
if model.activeBackend == .appleAVPlayer {
model.avPlayerBackend.controller?.dismiss(animated: false)
}
2023-11-27 22:49:18 +00:00
model.resetQueue()
model.hide()
}
#else
if Defaults[.closeVideoOnEOF] {
model.resetQueue()
model.hide()
} else if Defaults[.exitFullscreenOnEOF], model.playingFullScreen {
2023-11-27 22:49:18 +00:00
model.exitFullScreen()
}
#endif
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
}
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting, formatOrder: [QualityProfile.Format]) -> Stream? {
logger.info("Starting bestPlayable function")
logger.info("Total streams received: \(streams.count)")
logger.info("Max resolution allowed: \(String(describing: maxResolution.value))")
logger.info("Format order: \(formatOrder)")
// Filter out non-HLS streams and streams with resolution more than maxResolution
let nonHLSStreams = streams.filter {
let isHLS = $0.kind == .hls
// Safely unwrap resolution and maxResolution.value to avoid crashes
let isWithinResolution = ($0.resolution != nil && maxResolution.value != nil) ? $0.resolution! <= maxResolution.value! : false
logger.info("Stream ID: \($0.id) - Kind: \(String(describing: $0.kind)) - Resolution: \(String(describing: $0.resolution)) - Bitrate: \($0.bitrate ?? 0)")
logger.info("Is HLS: \(isHLS), Is within resolution: \(isWithinResolution)")
return !isHLS && isWithinResolution
}
logger.info("Non-HLS streams after filtering: \(nonHLSStreams.count)")
// Find max resolution and bitrate from non-HLS streams
let bestResolutionStream = nonHLSStreams.max { $0.resolution < $1.resolution }
let bestBitrateStream = nonHLSStreams.max { $0.bitrate ?? 0 < $1.bitrate ?? 0 }
logger.info("Best resolution stream: \(String(describing: bestResolutionStream?.id)) with resolution: \(String(describing: bestResolutionStream?.resolution))")
logger.info("Best bitrate stream: \(String(describing: bestBitrateStream?.id)) with bitrate: \(String(describing: bestBitrateStream?.bitrate))")
let bestResolution = bestResolutionStream?.resolution ?? maxResolution.value
let bestBitrate = bestBitrateStream?.bitrate ?? bestResolutionStream?.resolution.bitrate ?? maxResolution.value.bitrate
logger.info("Final best resolution selected: \(String(describing: bestResolution))")
logger.info("Final best bitrate selected: \(bestBitrate)")
let adjustedStreams = streams.map { stream in
if stream.kind == .hls {
logger.info("Adjusting HLS stream ID: \(stream.id)")
stream.resolution = bestResolution
stream.bitrate = bestBitrate
stream.format = .hls
} else if stream.kind == .stream {
logger.info("Adjusting non-HLS stream ID: \(stream.id)")
stream.format = .stream
}
return stream
}
let filteredStreams = adjustedStreams.filter { stream in
// Safely unwrap resolution and maxResolution.value to avoid crashes
let isWithinResolution = (stream.resolution != nil && maxResolution.value != nil) ? stream.resolution! <= maxResolution.value! : false
logger.info("Filtered stream ID: \(stream.id) - Is within max resolution: \(isWithinResolution)")
return isWithinResolution
}
logger.info("Filtered streams count after adjustments: \(filteredStreams.count)")
let bestStream = filteredStreams.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 {
logger.info("Failed to extract lhsFormat or rhsFormat for streams \(lhs.id) and \(rhs.id)")
return false
}
let lhsFormatIndex = formatOrder.firstIndex(of: lhsFormat) ?? Int.max
let rhsFormatIndex = formatOrder.firstIndex(of: rhsFormat) ?? Int.max
logger.info("Comparing formats for streams \(lhs.id) and \(rhs.id) - LHS Format Index: \(lhsFormatIndex), RHS Format Index: \(rhsFormatIndex)")
return lhsFormatIndex > rhsFormatIndex
}
logger.info("Comparing resolutions for streams \(lhs.id) and \(rhs.id) - LHS Resolution: \(String(describing: lhs.resolution)), RHS Resolution: \(String(describing: rhs.resolution))")
return lhs.resolution < rhs.resolution
}
logger.info("Best stream selected: \(String(describing: bestStream?.id)) with resolution: \(String(describing: bestStream?.resolution)) and format: \(String(describing: bestStream?.format))")
return bestStream
}
func updateControls(completionHandler: (() -> Void)? = nil) {
logger.info("updating controls")
guard model.presentingPlayer, !model.controls.presentingOverlays else {
logger.info("ignored controls update")
completionHandler?()
return
}
DispatchQueue.main.async(qos: .userInteractive) {
#if !os(macOS)
guard UIApplication.shared.applicationState != .background else {
logger.info("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
completionHandler?()
}
}
2022-02-16 20:23:11 +00:00
}