1
0
mirror of https://github.com/yattee/yattee.git synced 2024-12-13 13:50:32 +05:30
yattee/Model/Player/Backends/MPVBackend.swift

547 lines
15 KiB
Swift
Raw Normal View History

2022-02-17 01:53:11 +05:30
import AVFAudio
import CoreMedia
import Defaults
2022-02-17 01:53:11 +05:30
import Foundation
import Logging
import MediaPlayer
2022-06-30 04:13:41 +05:30
import Repeat
2022-02-17 01:53:11 +05:30
import SwiftUI
final class MPVBackend: PlayerBackend {
static var controlsUpdateInterval = 0.5
2022-08-25 22:39:55 +05:30
static var networkStateUpdateInterval = 1.0
2022-06-16 23:14:39 +05:30
2022-02-17 01:53:11 +05:30
private var logger = Logger(label: "mpv-backend")
var model: PlayerModel!
var controls: PlayerControlsModel!
var playerTime: PlayerTimeModel!
var networkState: NetworkStateModel!
2022-02-17 01:53:11 +05:30
var stream: Stream?
var video: Video?
2022-07-05 22:50:25 +05:30
var captions: Captions? { didSet {
guard let captions = captions else {
client.removeSubs()
return
}
addSubTrack(captions.url)
}}
2022-02-17 01:53:11 +05:30
var currentTime: CMTime?
var loadedVideo = false
2022-02-28 02:01:17 +05:30
var isLoadingVideo = true { didSet {
DispatchQueue.main.async { [weak self] in
2022-04-03 20:16:33 +05:30
guard let self = self else {
return
}
self.controls?.isLoadingVideo = self.isLoadingVideo
2022-06-25 05:09:29 +05:30
self.setNeedsNetworkStateUpdates(true)
self.model?.objectWillChange.send()
2022-02-28 02:01:17 +05:30
}
}}
2022-02-17 01:53:11 +05:30
var isPlaying = true { didSet {
2022-06-30 04:13:41 +05:30
networkStateTimer.start()
2022-02-17 01:53:11 +05:30
if isPlaying {
model.updateAspectRatio()
2022-02-17 01:53:11 +05:30
startClientUpdates()
} else {
stopControlsUpdates()
}
updateControlsIsPlaying()
2022-04-03 20:33:56 +05:30
#if os(macOS)
if isPlaying {
ScreenSaverManager.shared.disable(reason: "Yattee is playing video")
} else {
ScreenSaverManager.shared.enable()
}
MPNowPlayingInfoCenter.default().playbackState = isPlaying ? .playing : .paused
#else
2022-05-21 02:53:14 +05:30
DispatchQueue.main.async {
UIApplication.shared.isIdleTimerDisabled = self.model.presentingPlayer && self.isPlaying
}
2022-04-03 20:33:56 +05:30
#endif
2022-02-17 01:53:11 +05:30
}}
var isSeeking = false {
didSet {
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
self.model.isSeeking = self.isSeeking
}
}
}
2022-02-17 01:53:11 +05:30
var playerItemDuration: CMTime?
2022-02-28 02:01:17 +05:30
#if !os(macOS)
var controller: MPVViewController!
#endif
2022-02-17 01:53:11 +05:30
var client: MPVClient! { didSet { client.backend = self } }
2022-06-30 04:13:41 +05:30
private var clientTimer: Repeater!
private var networkStateTimer: Repeater!
2022-02-17 01:53:11 +05:30
private var onFileLoaded: (() -> Void)?
2022-08-24 02:59:50 +05:30
internal var controlsUpdates = false
2022-02-17 01:53:11 +05:30
private var timeObserverThrottle = Throttle(interval: 2)
2022-06-08 02:57:48 +05:30
var tracks: Int {
client?.tracksCount ?? -1
}
2022-07-09 05:51:04 +05:30
var aspectRatio: Double {
client?.aspectRatio ?? VideoPlayerView.defaultAspectRatio
}
2022-06-16 23:14:39 +05:30
var frameDropCount: Int {
client?.frameDropCount ?? 0
}
var outputFps: Double {
client?.outputFps ?? 0
}
var hwDecoder: String {
client?.hwDecoder ?? "unknown"
}
var bufferingState: Double {
client?.bufferingState ?? 0
}
var cacheDuration: Double {
client?.cacheDuration ?? 0
}
init(
model: PlayerModel,
controls: PlayerControlsModel? = nil,
playerTime: PlayerTimeModel? = nil,
networkState: NetworkStateModel? = nil
) {
2022-02-17 01:53:11 +05:30
self.model = model
self.controls = controls
self.playerTime = playerTime
self.networkState = networkState
2022-02-17 01:53:11 +05:30
2022-06-30 04:13:41 +05:30
clientTimer = .init(interval: .seconds(Self.controlsUpdateInterval), mode: .infinite) { [weak self] _ in
self?.getClientUpdates()
}
2022-06-25 05:02:21 +05:30
2022-06-30 04:13:41 +05:30
networkStateTimer = .init(interval: .seconds(Self.networkStateUpdateInterval), mode: .infinite) { [weak self] _ in
self?.updateNetworkState()
}
2022-02-17 01:53:11 +05:30
}
typealias AreInIncreasingOrder = (Stream, Stream) -> Bool
2022-03-28 00:29:22 +05:30
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting) -> Stream? {
streams
2022-05-22 01:08:26 +05:30
.filter { $0.kind != .hls && $0.resolution <= maxResolution.value }
.max { lhs, rhs in
let predicates: [AreInIncreasingOrder] = [
2022-05-22 01:08:26 +05:30
{ $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-17 01:53:11 +05:30
streams.first { $0.kind == .hls } ??
streams.first
}
func canPlay(_ stream: Stream) -> Bool {
stream.resolution != .unknown && stream.format != .av1
2022-02-17 01:53:11 +05:30
}
2022-08-27 01:47:21 +05:30
func playStream(_ stream: Stream, of video: Video, preservingTime: Bool, upgrading: Bool) {
2022-04-03 20:33:56 +05:30
#if !os(macOS)
if model.presentingPlayer {
UIApplication.shared.isIdleTimerDisabled = true
}
#endif
2022-07-05 22:50:25 +05:30
var captions: Captions?
if let captionsLanguageCode = Defaults[.captionsLanguageCode] {
captions = video.captions.first { $0.code == captionsLanguageCode } ??
video.captions.first { $0.code.contains(captionsLanguageCode) }
}
2022-02-17 01:53:11 +05:30
let updateCurrentStream = {
DispatchQueue.main.async { [weak self] in
self?.stream = stream
self?.video = video
self?.model.stream = stream
2022-07-05 22:50:25 +05:30
self?.captions = captions
2022-02-17 01:53:11 +05:30
}
}
let startPlaying = {
DispatchQueue.main.async { [weak self] in
guard let self = self else {
return
}
self.startClientUpdates()
if !preservingTime,
2022-08-27 01:47:21 +05:30
!upgrading,
2022-02-17 01:53:11 +05:30
let segment = self.model.sponsorBlock.segments.first,
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-17 01:53:11 +05:30
}
if let url = stream.singleAssetURL {
self.onFileLoaded = {
updateCurrentStream()
startPlaying()
}
2022-08-21 02:55:06 +05:30
self.client.loadFile(url, sub: captions?.url, time: time, forceSeekable: stream.kind == .hls) { [weak self] _ in
self?.isLoadingVideo = true
}
} else {
2022-06-08 02:50:24 +05:30
self.onFileLoaded = {
updateCurrentStream()
startPlaying()
2022-02-17 01:53:11 +05:30
}
2022-06-08 02:50:24 +05:30
let fileToLoad = self.model.musicMode ? stream.audioAsset.url : stream.videoAsset.url
let audioTrack = self.model.musicMode ? nil : stream.audioAsset.url
2022-08-21 02:55:06 +05:30
self.client?.loadFile(fileToLoad, audio: audioTrack, sub: captions?.url, time: time, forceSeekable: stream.kind == .hls) { [weak self] _ in
self?.isLoadingVideo = true
self?.pause()
}
2022-02-17 01:53:11 +05:30
}
}
}
if preservingTime {
if model.preservedTime.isNil {
model.saveTime {
replaceItem(self.model.preservedTime)
}
} else {
replaceItem(self.model.preservedTime)
}
} else {
replaceItem(nil)
}
2022-03-28 00:54:32 +05:30
startClientUpdates()
2022-02-17 01:53:11 +05:30
}
func play() {
isPlaying = true
startClientUpdates()
if controls?.presentingControls ?? false {
2022-03-27 17:12:20 +05:30
startControlsUpdates()
}
2022-05-22 02:28:11 +05:30
setRate(model.currentRate)
2022-02-17 01:53:11 +05:30
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
2022-02-17 01:53:11 +05:30
self?.getClientUpdates()
self?.updateControls()
completionHandler?(true)
}
}
func seek(relative time: CMTime, completionHandler: ((Bool) -> Void)? = nil) {
client?.seek(relative: time) { [weak self] _ in
2022-02-17 01:53:11 +05:30
self?.getClientUpdates()
self?.updateControls()
completionHandler?(true)
}
}
2022-04-17 02:20:37 +05:30
func setRate(_ rate: Float) {
2022-05-21 02:50:18 +05:30
client?.setDoubleAsync("speed", Double(rate))
2022-02-17 01:53:11 +05:30
}
func closeItem() {
client?.pause()
client?.stop()
2022-08-27 01:47:21 +05:30
self.video = nil
self.stream = nil
}
2022-02-17 01:53:11 +05:30
2022-08-19 04:10:46 +05:30
func closePiP() {}
2022-02-17 01:53:11 +05:30
func updateControls() {
2022-08-14 22:23:03 +05:30
self.logger.info("updating controls")
guard model.presentingPlayer, !model.controls.presentingOverlays else {
self.logger.info("ignored controls update")
2022-06-16 23:14:39 +05:30
return
}
2022-08-14 22:23:03 +05:30
2022-08-24 02:59:50 +05:30
DispatchQueue.main.async(qos: .userInteractive) { [weak self] in
2022-06-16 05:33:15 +05:30
guard let self = self else {
return
}
2022-08-21 01:58:31 +05:30
#if !os(macOS)
guard UIApplication.shared.applicationState != .background else {
self.logger.info("not performing controls updates in background")
return
}
#endif
self.playerTime.currentTime = self.currentTime ?? .zero
self.playerTime.duration = self.playerItemDuration ?? .zero
2022-02-17 01:53:11 +05:30
}
}
func startControlsUpdates() {
2022-08-15 03:46:37 +05:30
guard model.presentingPlayer, model.controls.presentingControls, !model.controls.presentingOverlays else {
2022-08-14 22:23:03 +05:30
self.logger.info("ignored controls update start")
return
}
2022-02-17 01:53:11 +05:30
self.logger.info("starting controls updates")
controlsUpdates = true
}
func stopControlsUpdates() {
self.logger.info("stopping controls updates")
controlsUpdates = false
}
func startClientUpdates() {
2022-06-30 04:13:41 +05:30
clientTimer.start()
2022-02-17 01:53:11 +05:30
}
2022-04-17 15:02:04 +05:30
private var handleSegmentsThrottle = Throttle(interval: 1)
2022-02-17 01:53:11 +05:30
private func getClientUpdates() {
currentTime = client?.currentTime
playerItemDuration = client?.duration
if controlsUpdates {
updateControls()
}
2022-02-17 02:40:57 +05:30
model.updateNowPlayingInfo()
2022-04-17 15:02:04 +05:30
handleSegmentsThrottle.execute {
if let currentTime = currentTime {
model.handleSegments(at: currentTime)
}
2022-02-17 01:53:11 +05:30
}
2022-02-17 02:40:57 +05:30
timeObserverThrottle.execute {
2022-02-17 01:53:11 +05:30
self.model.updateWatch()
}
}
private func stopClientUpdates() {
2022-06-30 04:13:41 +05:30
clientTimer.pause()
2022-02-17 01:53:11 +05:30
}
private func updateControlsIsPlaying() {
DispatchQueue.main.async { [weak self] in
2022-03-29 21:01:27 +05:30
self?.controls?.isPlaying = self?.isPlaying ?? false
2022-02-17 01:53:11 +05:30
}
}
func handle(_ event: UnsafePointer<mpv_event>!) {
logger.info(.init(stringLiteral: "RECEIVED event: \(String(cString: mpv_event_name(event.pointee.event_id)))"))
2022-02-17 01:53:11 +05:30
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: "\(String(cString: (logmsg!.pointee.prefix)!)), "
2022-02-17 01:53:11 +05:30
+ "\(String(cString: (logmsg!.pointee.level)!)), "
+ "\(String(cString: (logmsg!.pointee.text)!))"))
case MPV_EVENT_FILE_LOADED:
onFileLoaded?()
2022-03-20 04:35:09 +05:30
startClientUpdates()
2022-02-17 01:53:11 +05:30
onFileLoaded = nil
2022-03-28 00:52:13 +05:30
case MPV_EVENT_PLAYBACK_RESTART:
isLoadingVideo = false
isSeeking = false
2022-03-28 00:52:13 +05:30
onFileLoaded?()
startClientUpdates()
onFileLoaded = nil
case MPV_EVENT_PAUSE:
2022-07-04 02:48:27 +05:30
DispatchQueue.main.async { [weak self] in self?.handleEndOfFile() }
isPlaying = false
2022-06-30 04:13:41 +05:30
networkStateTimer.start()
2022-02-28 02:01:17 +05:30
case MPV_EVENT_UNPAUSE:
isPlaying = true
2022-02-28 02:01:17 +05:30
isLoadingVideo = false
isSeeking = false
2022-06-30 04:13:41 +05:30
networkStateTimer.start()
case MPV_EVENT_SEEK:
isSeeking = true
2022-02-28 02:01:17 +05:30
2022-02-17 01:53:11 +05:30
case MPV_EVENT_END_FILE:
2022-07-04 02:48:27 +05:30
DispatchQueue.main.async { [weak self] in self?.handleEndOfFile() }
2022-02-17 01:53:11 +05:30
default:
logger.info(.init(stringLiteral: "UNHANDLED event: \(String(cString: mpv_event_name(event.pointee.event_id)))"))
2022-02-17 01:53:11 +05:30
}
}
func handleEndOfFile() {
2022-07-04 02:48:27 +05:30
guard client.eofReached else {
2022-02-17 01:53:11 +05:30
return
}
getClientUpdates()
2022-07-11 03:54:56 +05:30
eofPlaybackModeAction()
2022-02-17 01:53:11 +05:30
}
func setNeedsDrawing(_ needsDrawing: Bool) {
client?.setNeedsDrawing(needsDrawing)
}
2022-03-27 17:12:20 +05:30
func setSize(_ width: Double, _ height: Double) {
client?.setSize(width, height)
2022-03-27 17:12:20 +05:30
}
2022-06-08 02:50:24 +05:30
func addVideoTrack(_ url: URL) {
client?.addVideoTrack(url)
2022-06-08 02:50:24 +05:30
}
2022-07-05 22:50:25 +05:30
func addSubTrack(_ url: URL) {
client?.removeSubs()
client?.addSubTrack(url)
}
2022-06-08 02:50:24 +05:30
func setVideoToAuto() {
client?.setVideoToAuto()
2022-06-08 02:50:24 +05:30
}
func setVideoToNo() {
client?.setVideoToNo()
}
func updateNetworkState() {
guard let client = client, let networkState = networkState else {
return
}
DispatchQueue.main.async {
networkState.pausedForCache = client.pausedForCache
networkState.cacheDuration = client.cacheDuration
networkState.bufferingState = client.bufferingState
}
2022-06-25 05:02:21 +05:30
if !networkState.needsUpdates {
2022-06-30 04:13:41 +05:30
networkStateTimer.pause()
}
}
2022-06-25 05:09:29 +05:30
func setNeedsNetworkStateUpdates(_ needsUpdates: Bool) {
if needsUpdates {
2022-06-30 04:13:41 +05:30
networkStateTimer.start()
2022-06-25 05:09:29 +05:30
} else {
2022-06-30 04:13:41 +05:30
networkStateTimer.pause()
2022-06-25 05:09:29 +05:30
}
2022-06-08 02:50:24 +05:30
}
2022-08-21 02:01:03 +05:30
func startMusicMode() {
setVideoToNo()
}
func stopMusicMode() {
addVideoTrackFromStream()
setVideoToAuto()
controls.resetTimer()
}
func addVideoTrackFromStream() {
if let videoTrackURL = model.stream?.videoAsset?.url,
tracks < 2
{
logger.info("adding video track")
addVideoTrack(videoTrackURL)
}
setVideoToAuto()
}
func didChangeTo() {
setNeedsDrawing(model.presentingPlayer)
if model.musicMode {
startMusicMode()
} else {
stopMusicMode()
}
}
2022-02-17 01:53:11 +05:30
}