yattee/Model/Player/Backends/MPVBackend.swift

409 lines
11 KiB
Swift
Raw Normal View History

2022-02-16 20:23:11 +00:00
import AVFAudio
import CoreMedia
import Defaults
2022-02-16 20:23:11 +00:00
import Foundation
import Logging
import SwiftUI
final class MPVBackend: PlayerBackend {
private var logger = Logger(label: "mpv-backend")
var model: PlayerModel!
var controls: PlayerControlsModel!
var stream: Stream?
var video: Video?
var currentTime: CMTime?
var loadedVideo = false
2022-02-27 20:31:17 +00:00
var isLoadingVideo = true { didSet {
DispatchQueue.main.async { [weak self] in
2022-04-03 14:46:33 +00:00
guard let self = self else {
return
}
self.controls.isLoadingVideo = self.isLoadingVideo
if !self.isLoadingVideo {
DispatchQueue.main.asyncAfter(deadline: .now() + 2) { [weak self] in
self?.handleEOF = true
}
}
2022-02-27 20:31:17 +00:00
}
}}
2022-02-16 20:23:11 +00:00
var isPlaying = true { didSet {
if isPlaying {
startClientUpdates()
} else {
stopControlsUpdates()
}
updateControlsIsPlaying()
2022-04-03 15:03:56 +00:00
#if !os(macOS)
2022-05-20 21:23:14 +00:00
DispatchQueue.main.async {
UIApplication.shared.isIdleTimerDisabled = self.model.presentingPlayer && self.isPlaying
}
2022-04-03 15:03:56 +00:00
#endif
2022-02-16 20:23:11 +00:00
}}
var playerItemDuration: CMTime?
2022-02-27 20:31:17 +00:00
#if !os(macOS)
var controller: MPVViewController!
#endif
2022-02-16 20:23:11 +00:00
var client: MPVClient! { didSet { client.backend = self } }
private var clientTimer: RepeatingTimer!
2022-04-03 14:46:33 +00:00
private var handleEOF = false
2022-02-16 20:23:11 +00:00
private var onFileLoaded: (() -> Void)?
private var controlsUpdates = false
private var timeObserverThrottle = Throttle(interval: 2)
2022-06-07 21:27:48 +00:00
var tracks: Int {
client?.tracksCount ?? -1
}
2022-02-16 20:23:11 +00:00
init(model: PlayerModel, controls: PlayerControlsModel? = nil) {
self.model = model
self.controls = controls
clientTimer = .init(timeInterval: 1)
clientTimer.eventHandler = getClientUpdates
}
typealias AreInIncreasingOrder = (Stream, Stream) -> Bool
2022-03-27 18:59:22 +00:00
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream? {
streams
2022-05-21 19:38:26 +00:00
.filter { $0.kind != .hls && $0.resolution <= maxResolution.value }
.max { lhs, rhs in
let predicates: [AreInIncreasingOrder] = [
2022-05-21 19:38:26 +00:00
{ $0.resolution < $1.resolution },
{ $0.format > $1.format }
]
for predicate in predicates {
if !predicate(lhs, rhs), !predicate(rhs, lhs) {
continue
}
return predicate(lhs, rhs)
}
return false
} ??
2022-02-16 20:23:11 +00:00
streams.first { $0.kind == .hls } ??
streams.first
}
func canPlay(_ stream: Stream) -> Bool {
stream.resolution != .unknown && stream.format != .av1
2022-02-16 20:23:11 +00:00
}
func playStream(_ stream: Stream, of video: Video, preservingTime: Bool, upgrading _: Bool) {
2022-04-03 14:46:33 +00:00
handleEOF = false
2022-04-03 15:03:56 +00:00
#if !os(macOS)
if model.presentingPlayer {
UIApplication.shared.isIdleTimerDisabled = true
}
#endif
2022-02-16 20:23:11 +00:00
let updateCurrentStream = {
DispatchQueue.main.async { [weak self] in
self?.stream = stream
self?.video = video
self?.model.stream = stream
}
}
let startPlaying = {
#if !os(macOS)
try? AVAudioSession.sharedInstance().setActive(true)
#endif
DispatchQueue.main.async { [weak self] in
guard let self = self else {
return
}
self.startClientUpdates()
if !preservingTime,
let segment = self.model.sponsorBlock.segments.first,
2022-06-10 11:26:24 +00:00
segment.end > 4,
2022-02-16 20:23:11 +00:00
self.model.lastSkipped.isNil
{
self.seek(to: segment.endTime) { finished in
guard finished else {
return
}
self.model.lastSkipped = segment
self.play()
}
} else {
self.play()
}
}
}
let replaceItem: (CMTime?) -> Void = { [weak self] time in
guard let self = self else {
return
}
self.stop()
DispatchQueue.main.async { [weak self] in
guard let self = self else {
return
2022-02-16 20:23:11 +00:00
}
if let url = stream.singleAssetURL {
self.onFileLoaded = {
updateCurrentStream()
startPlaying()
}
self.client.loadFile(url, time: time) { [weak self] _ in
self?.isLoadingVideo = true
}
} else {
2022-06-07 21:20:24 +00:00
self.onFileLoaded = {
updateCurrentStream()
startPlaying()
2022-02-16 20:23:11 +00:00
}
2022-06-07 21:20:24 +00:00
let fileToLoad = self.model.musicMode ? stream.audioAsset.url : stream.videoAsset.url
let audioTrack = self.model.musicMode ? nil : stream.audioAsset.url
self.client.loadFile(fileToLoad, audio: audioTrack, time: time) { [weak self] _ in
self?.isLoadingVideo = true
self?.pause()
}
2022-02-16 20:23:11 +00:00
}
}
}
if preservingTime {
if model.preservedTime.isNil {
model.saveTime {
replaceItem(self.model.preservedTime)
}
} else {
replaceItem(self.model.preservedTime)
}
} else {
replaceItem(nil)
}
2022-03-27 19:24:32 +00:00
startClientUpdates()
2022-02-16 20:23:11 +00:00
}
func play() {
isPlaying = true
startClientUpdates()
2022-03-27 11:42:20 +00:00
if controls.presentingControls {
startControlsUpdates()
}
2022-05-21 20:58:11 +00:00
setRate(model.currentRate)
2022-02-16 20:23:11 +00:00
client?.play()
}
func pause() {
isPlaying = false
stopClientUpdates()
client?.pause()
}
func togglePlay() {
isPlaying ? pause() : play()
}
func stop() {
client?.stop()
}
func seek(to time: CMTime, completionHandler: ((Bool) -> Void)?) {
client.seek(to: time) { [weak self] _ in
self?.getClientUpdates()
self?.updateControls()
completionHandler?(true)
}
}
func seek(relative time: CMTime, completionHandler: ((Bool) -> Void)? = nil) {
client.seek(relative: time) { [weak self] _ in
self?.getClientUpdates()
self?.updateControls()
completionHandler?(true)
}
}
2022-04-16 20:50:37 +00:00
func setRate(_ rate: Float) {
2022-05-20 21:20:18 +00:00
client?.setDoubleAsync("speed", Double(rate))
2022-02-16 20:23:11 +00:00
}
func closeItem() {
handleEOF = false
client?.pause()
client?.stop()
}
2022-02-16 20:23:11 +00:00
func enterFullScreen() {
model.toggleFullscreen(controls?.playingFullscreen ?? false)
2022-05-29 12:29:43 +00:00
#if os(iOS)
2022-05-29 13:34:40 +00:00
if Defaults[.lockOrientationInFullScreen] {
Orientation.lockOrientation(.landscape, andRotateTo: UIDevice.current.orientation.isLandscape ? nil : .landscapeRight)
}
2022-05-29 12:29:43 +00:00
#endif
}
2022-02-16 20:23:11 +00:00
func exitFullScreen() {}
func closePiP(wasPlaying _: Bool) {}
func updateControls() {
DispatchQueue.main.async { [weak self] in
self?.logger.info("updating controls")
self?.controls.currentTime = self?.currentTime ?? .zero
self?.controls.duration = self?.playerItemDuration ?? .zero
}
}
func startControlsUpdates() {
self.logger.info("starting controls updates")
controlsUpdates = true
}
func stopControlsUpdates() {
self.logger.info("stopping controls updates")
controlsUpdates = false
}
func startClientUpdates() {
clientTimer.resume()
}
2022-04-17 09:32:04 +00:00
private var handleSegmentsThrottle = Throttle(interval: 1)
2022-02-16 20:23:11 +00:00
private func getClientUpdates() {
self.logger.info("getting client updates")
currentTime = client?.currentTime
playerItemDuration = client?.duration
if controlsUpdates {
updateControls()
}
2022-02-16 21:10:57 +00:00
model.updateNowPlayingInfo()
2022-04-17 09:32:04 +00:00
handleSegmentsThrottle.execute {
if let currentTime = currentTime {
model.handleSegments(at: currentTime)
}
2022-02-16 20:23:11 +00:00
}
2022-02-16 21:10:57 +00:00
timeObserverThrottle.execute {
2022-02-16 20:23:11 +00:00
self.model.updateWatch()
}
}
private func stopClientUpdates() {
clientTimer.suspend()
}
private func updateControlsIsPlaying() {
DispatchQueue.main.async { [weak self] in
2022-03-29 15:31:27 +00:00
self?.controls?.isPlaying = self?.isPlaying ?? false
2022-02-16 20:23:11 +00:00
}
}
func handle(_ event: UnsafePointer<mpv_event>!) {
logger.info("\(String(cString: mpv_event_name(event.pointee.event_id)))")
switch event.pointee.event_id {
case MPV_EVENT_SHUTDOWN:
mpv_destroy(client.mpv)
client.mpv = nil
case MPV_EVENT_LOG_MESSAGE:
let logmsg = UnsafeMutablePointer<mpv_event_log_message>(OpaquePointer(event.pointee.data))
logger.info(.init(stringLiteral: "log: \(String(cString: (logmsg!.pointee.prefix)!)), "
+ "\(String(cString: (logmsg!.pointee.level)!)), "
+ "\(String(cString: (logmsg!.pointee.text)!))"))
case MPV_EVENT_FILE_LOADED:
onFileLoaded?()
2022-03-19 23:05:09 +00:00
startClientUpdates()
2022-02-16 20:23:11 +00:00
onFileLoaded = nil
2022-03-27 19:22:13 +00:00
case MPV_EVENT_PLAYBACK_RESTART:
isLoadingVideo = false
onFileLoaded?()
startClientUpdates()
onFileLoaded = nil
2022-02-27 20:31:17 +00:00
case MPV_EVENT_UNPAUSE:
isLoadingVideo = false
2022-02-16 20:23:11 +00:00
case MPV_EVENT_END_FILE:
2022-02-27 20:25:29 +00:00
DispatchQueue.main.async { [weak self] in
self?.handleEndOfFile(event)
}
2022-02-16 20:23:11 +00:00
default:
logger.info(.init(stringLiteral: "event: \(String(cString: mpv_event_name(event.pointee.event_id)))"))
}
}
func handleEndOfFile(_: UnsafePointer<mpv_event>!) {
2022-04-03 14:46:33 +00:00
guard handleEOF, !isLoadingVideo else {
2022-02-16 20:23:11 +00:00
return
}
model.prepareCurrentItemForHistory(finished: true)
if model.queue.isEmpty {
#if !os(macOS)
try? AVAudioSession.sharedInstance().setActive(false)
#endif
model.resetQueue()
model.hide()
} else {
model.advanceToNextItem()
}
}
func setNeedsDrawing(_ needsDrawing: Bool) {
client?.setNeedsDrawing(needsDrawing)
}
2022-03-27 11:42:20 +00:00
func setSize(_ width: Double, _ height: Double) {
self.client?.setSize(width, height)
}
2022-06-07 21:20:24 +00:00
func addVideoTrack(_ url: URL) {
self.client?.addVideoTrack(url)
}
func setVideoToAuto() {
self.client?.setVideoToAuto()
}
func setVideoToNo() {
self.client?.setVideoToNo()
}
2022-02-16 20:23:11 +00:00
}