yattee/Model/Player/Backends/MPVBackend.swift

713 lines
21 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 Libmpv
2022-02-16 20:23:11 +00:00
import Logging
import MediaPlayer
2022-06-29 22:43:41 +00:00
import Repeat
2022-02-16 20:23:11 +00:00
import SwiftUI
final class MPVBackend: PlayerBackend {
static var timeUpdateInterval = 0.5
static var networkStateUpdateInterval = 0.1
static var refreshRateUpdateInterval = 0.5
2022-06-16 17:44:39 +00:00
2022-02-16 20:23:11 +00:00
private var logger = Logger(label: "mpv-backend")
var model: PlayerModel { .shared }
var controls: PlayerControlsModel { .shared }
var playerTime: PlayerTimeModel { .shared }
var networkState: NetworkStateModel { .shared }
var seek: SeekModel { .shared }
2022-02-16 20:23:11 +00:00
var stream: Stream?
var video: Video?
2022-07-05 17:20:25 +00:00
var captions: Captions? { didSet {
2022-09-28 14:27:01 +00:00
guard let captions else {
if client?.areSubtitlesAdded == true {
client?.removeSubs()
}
2022-07-05 17:20:25 +00:00
return
}
addSubTrack(captions.url)
}}
2022-02-16 20:23:11 +00:00
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-09-28 14:27:01 +00:00
guard let self else {
2022-04-03 14:46:33 +00:00
return
}
self.controls.isLoadingVideo = self.isLoadingVideo
2022-06-24 23:39:29 +00:00
self.setNeedsNetworkStateUpdates(true)
self.model.objectWillChange.send()
2022-02-27 20:31:17 +00:00
}
}}
2022-02-16 20:23:11 +00:00
var hasStarted = false
var isPaused = false
2022-02-16 20:23:11 +00:00
var isPlaying = true { didSet {
2022-06-29 22:43:41 +00:00
networkStateTimer.start()
2022-02-16 20:23:11 +00:00
if isPlaying {
startClientUpdates()
} else {
stopControlsUpdates()
}
updateControlsIsPlaying()
2022-04-03 15:03:56 +00:00
#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-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 isSeeking = false {
didSet {
DispatchQueue.main.async { [weak self] in
2022-09-28 14:27:01 +00:00
guard let self else { return }
self.model.isSeeking = self.isSeeking
}
}
}
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 } }
2022-06-29 22:43:41 +00:00
private var clientTimer: Repeater!
private var networkStateTimer: Repeater!
private var refreshRateTimer: Repeater!
2022-02-16 20:23:11 +00:00
private var onFileLoaded: (() -> Void)?
2023-09-23 13:07:27 +00:00
var controlsUpdates = false
2022-02-16 20:23:11 +00:00
private var timeObserverThrottle = Throttle(interval: 2)
2022-11-10 22:19:34 +00:00
var suggestedPlaybackRates: [Double] {
2022-11-18 22:45:49 +00:00
[0.25, 0.33, 0.5, 0.67, 0.75, 1, 1.25, 1.5, 1.75, 2, 3, 4]
2022-11-10 22:19:34 +00:00
}
func canPlayAtRate(_ rate: Double) -> Bool {
rate > 0 && rate <= 100
}
2022-06-07 21:27:48 +00:00
var tracks: Int {
client?.tracksCount ?? -1
}
2022-07-09 00:21:04 +00:00
var aspectRatio: Double {
client?.aspectRatio ?? VideoPlayerView.defaultAspectRatio
}
2022-06-16 17:44:39 +00:00
var frameDropCount: Int {
client?.frameDropCount ?? 0
}
var outputFps: Double {
client?.outputFps ?? 0
}
2022-11-10 17:11:28 +00:00
var formattedOutputFps: String {
String(format: "%.2ffps", outputFps)
}
var hwDecoder: String {
client?.hwDecoder ?? "unknown"
}
var bufferingState: Double {
client?.bufferingState ?? 0
}
var cacheDuration: Double {
client?.cacheDuration ?? 0
}
2022-11-10 17:11:28 +00:00
var videoFormat: String {
client?.videoFormat ?? "unknown"
}
var videoCodec: String {
client?.videoCodec ?? "unknown"
}
var currentVo: String {
client?.currentVo ?? "unknown"
}
var videoWidth: Double? {
if let width = client?.width, width != "unknown" {
return Double(width)
}
return nil
}
var videoHeight: Double? {
if let height = client?.height, height != "unknown" {
return Double(height)
}
return nil
}
var audioFormat: String {
client?.audioFormat ?? "unknown"
}
var audioCodec: String {
client?.audioCodec ?? "unknown"
}
var currentAo: String {
client?.currentAo ?? "unknown"
}
var audioChannels: String {
client?.audioChannels ?? "unknown"
}
var audioSampleRate: String {
client?.audioSampleRate ?? "unknown"
}
2022-09-01 23:05:31 +00:00
init() {
clientTimer = .init(interval: .seconds(Self.timeUpdateInterval), mode: .infinite) { [weak self] _ in
guard let self, self.model.activeBackend == .mpv else {
return
}
self.getTimeUpdates()
2022-06-29 22:43:41 +00:00
}
2022-06-24 23:32:21 +00:00
2022-06-29 22:43:41 +00:00
networkStateTimer = .init(interval: .seconds(Self.networkStateUpdateInterval), mode: .infinite) { [weak self] _ in
guard let self, self.model.activeBackend == .mpv else {
return
}
self.updateNetworkState()
2022-06-29 22:43:41 +00:00
}
refreshRateTimer = .init(interval: .seconds(Self.refreshRateUpdateInterval), mode: .infinite) { [weak self] _ in
guard let self, self.model.activeBackend == .mpv else { return }
self.checkAndUpdateRefreshRate()
}
}
typealias AreInIncreasingOrder = (Stream, Stream) -> Bool
2022-02-16 20:23:11 +00:00
func canPlay(_ stream: Stream) -> Bool {
stream.resolution != .unknown && stream.format != .av1
2022-02-16 20:23:11 +00:00
}
2022-08-26 20:17:21 +00:00
func playStream(_ stream: Stream, of video: Video, preservingTime: Bool, upgrading: Bool) {
2022-04-03 15:03:56 +00:00
#if !os(macOS)
if model.presentingPlayer {
2023-05-16 16:51:07 +00:00
DispatchQueue.main.async {
UIApplication.shared.isIdleTimerDisabled = true
}
2022-04-03 15:03:56 +00:00
}
#endif
2022-07-05 17:20:25 +00:00
var captions: Captions?
if Defaults[.captionsAutoShow] == true {
2024-05-20 12:40:25 +00:00
let captionsDefaultLanguageCode = Defaults[.captionsDefaultLanguageCode],
captionsFallbackLanguageCode = Defaults[.captionsFallbackLanguageCode]
// Try to get captions with the default language code first
captions = video.captions.first { $0.code == captionsDefaultLanguageCode } ??
video.captions.first { $0.code.contains(captionsDefaultLanguageCode) }
// If there are still no captions, try to get captions with the fallback language code
if captions.isNil && !captionsFallbackLanguageCode.isEmpty {
captions = video.captions.first { $0.code == captionsFallbackLanguageCode } ??
video.captions.first { $0.code.contains(captionsFallbackLanguageCode) }
}
} else {
captions = nil
2022-07-05 17:20:25 +00:00
}
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
2022-07-05 17:20:25 +00:00
self?.captions = captions
2022-02-16 20:23:11 +00:00
}
}
let startPlaying = {
#if !os(macOS)
do {
try AVAudioSession.sharedInstance().setActive(true)
} catch {
self.logger.error("Error setting up audio session: \(error)")
}
#endif
2022-02-16 20:23:11 +00:00
DispatchQueue.main.async { [weak self] in
2022-09-28 14:27:01 +00:00
guard let self else {
2022-02-16 20:23:11 +00:00
return
}
self.startClientUpdates()
if Defaults[.captionsAutoShow] { self.client?.setSubToAuto() } else { self.client?.setSubToNo() }
PlayerModel.shared.captions = self.captions
2022-02-16 20:23:11 +00:00
if !preservingTime,
2022-08-26 20:17:21 +00:00
!upgrading,
2022-02-16 20:23:11 +00:00
let segment = self.model.sponsorBlock.segments.first,
self.model.lastSkipped.isNil
{
self.seek(to: segment.endTime, seekType: .segmentSkip(segment.category)) { finished in
2022-02-16 20:23:11 +00:00
guard finished else {
return
}
self.model.lastSkipped = segment
self.play()
self.model.handleOnPlayStream(stream)
2022-02-16 20:23:11 +00:00
}
} else {
self.play()
self.model.handleOnPlayStream(stream)
2022-02-16 20:23:11 +00:00
}
}
}
let replaceItem: (CMTime?) -> Void = { [weak self] time in
2022-09-28 14:27:01 +00:00
guard let self else {
2022-02-16 20:23:11 +00:00
return
}
self.stop()
DispatchQueue.main.async { [weak self] in
2023-05-07 19:54:48 +00:00
guard let self, let client = self.client else {
return
2022-02-16 20:23:11 +00:00
}
if let url = stream.singleAssetURL {
self.onFileLoaded = {
updateCurrentStream()
startPlaying()
}
2022-11-12 02:05:56 +00:00
if video.isLocal, video.localStreamIsFile {
2022-12-04 12:21:50 +00:00
if url.startAccessingSecurityScopedResource() {
URLBookmarkModel.shared.saveBookmark(url)
}
2022-11-10 17:11:28 +00:00
}
client.loadFile(url, bitrate: stream.bitrate, kind: stream.kind, sub: captions?.url, time: time, forceSeekable: stream.kind == .hls) { [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
client.loadFile(fileToLoad, audio: audioTrack, bitrate: stream.bitrate, kind: stream.kind, sub: captions?.url, time: time, forceSeekable: stream.kind == .hls) { [weak self] _ in
self?.isLoadingVideo = true
self?.pause()
}
2022-02-16 20:23:11 +00:00
}
}
}
if preservingTime {
if model.preservedTime.isNil || upgrading {
2022-02-16 20:23:11 +00:00
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 startRefreshRateUpdates() {
refreshRateTimer.start()
}
func stopRefreshRateUpdates() {
refreshRateTimer.pause()
}
2022-02-16 20:23:11 +00:00
func play() {
startClientUpdates()
startRefreshRateUpdates()
2022-02-16 20:23:11 +00:00
if controls.presentingControls {
2022-03-27 11:42:20 +00:00
startControlsUpdates()
}
2022-05-21 20:58:11 +00:00
setRate(model.currentRate)
// After the video has ended, hitting play restarts the video from the beginning.
2024-08-24 15:52:35 +00:00
if let currentTime, currentTime.seconds.formattedAsPlaybackTime() == model.playerTime.duration.seconds.formattedAsPlaybackTime() &&
currentTime.seconds > 0 && model.playerTime.duration.seconds > 0
{
seek(to: 0, seekType: .loopRestart)
}
2022-02-16 20:23:11 +00:00
client?.play()
isPlaying = true
isPaused = false
// Setting hasStarted to true the first time player started
if !hasStarted {
hasStarted = true
}
2022-02-16 20:23:11 +00:00
}
func pause() {
stopClientUpdates()
stopRefreshRateUpdates()
2022-02-16 20:23:11 +00:00
client?.pause()
isPaused = true
isPlaying = false
2022-02-16 20:23:11 +00:00
}
func togglePlay() {
2023-06-17 12:09:51 +00:00
if isPlaying {
pause()
} else {
play()
}
2022-02-16 20:23:11 +00:00
}
func cancelLoads() {
stop()
}
2022-02-16 20:23:11 +00:00
func stop() {
stopClientUpdates()
stopRefreshRateUpdates()
2022-02-16 20:23:11 +00:00
client?.stop()
isPlaying = false
isPaused = false
hasStarted = false
2022-02-16 20:23:11 +00:00
}
2022-08-29 11:55:23 +00:00
func seek(to time: CMTime, seekType _: SeekType, completionHandler: ((Bool) -> Void)?) {
client?.seek(to: time) { [weak self] _ in
self?.getTimeUpdates()
2022-02-16 20:23:11 +00:00
self?.updateControls()
completionHandler?(true)
}
}
func setRate(_ rate: Double) {
client?.setDoubleAsync("speed", rate)
2022-02-16 20:23:11 +00:00
}
func closeItem() {
pause()
stop()
2022-08-26 20:17:21 +00:00
self.video = nil
self.stream = nil
}
2022-02-16 20:23:11 +00:00
2022-08-18 22:40:46 +00:00
func closePiP() {}
2022-02-16 20:23:11 +00:00
func startControlsUpdates() {
2022-08-14 22:16:37 +00:00
guard model.presentingPlayer, model.controls.presentingControls, !model.controls.presentingOverlays else {
2022-08-14 16:53:03 +00:00
self.logger.info("ignored controls update start")
return
}
2022-02-16 20:23:11 +00:00
self.logger.info("starting controls updates")
controlsUpdates = true
}
func stopControlsUpdates() {
self.logger.info("stopping controls updates")
controlsUpdates = false
}
func startClientUpdates() {
2022-06-29 22:43:41 +00:00
clientTimer.start()
2022-02-16 20:23:11 +00:00
}
2022-04-17 09:32:04 +00:00
private var handleSegmentsThrottle = Throttle(interval: 1)
func getTimeUpdates() {
2022-02-16 20:23:11 +00:00
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 {
2022-09-28 14:27:01 +00:00
if let currentTime {
2022-04-17 09:32:04 +00:00
model.handleSegments(at: currentTime)
}
2022-02-16 20:23:11 +00:00
}
2022-02-16 21:10:57 +00:00
timeObserverThrottle.execute {
2023-06-07 20:25:10 +00:00
self.model.updateWatch(time: self.currentTime)
2022-02-16 20:23:11 +00:00
}
self.model.updateTime(self.currentTime!)
2022-02-16 20:23:11 +00:00
}
private func stopClientUpdates() {
2022-06-29 22:43:41 +00:00
clientTimer.pause()
2022-02-16 20:23:11 +00:00
}
private func updateControlsIsPlaying() {
guard model.activeBackend == .mpv else { return }
2022-02-16 20:23:11 +00:00
DispatchQueue.main.async { [weak self] in
self?.controls.isPlaying = self?.isPlaying ?? false
2022-02-16 20:23:11 +00:00
}
}
private func checkAndUpdateRefreshRate() {
guard let screenRefreshRate = client?.getScreenRefreshRate() else {
logger.warning("Failed to get screen refresh rate.")
return
}
let contentFps = client?.currentContainerFps ?? screenRefreshRate
guard Defaults[.mpvSetRefreshToContentFPS] else {
// If the current refresh rate doesn't match the screen refresh rate, reset it
if client?.currentRefreshRate != screenRefreshRate {
client?.updateRefreshRate(to: screenRefreshRate)
client?.currentRefreshRate = screenRefreshRate
#if !os(macOS)
notifyViewToUpdateDisplayLink(with: screenRefreshRate)
#endif
logger.info("Reset refresh rate to screen's rate: \(screenRefreshRate) Hz")
}
return
}
// Adjust the refresh rate to match the content if it differs
if screenRefreshRate != contentFps {
client?.updateRefreshRate(to: contentFps)
client?.currentRefreshRate = contentFps
#if !os(macOS)
notifyViewToUpdateDisplayLink(with: contentFps)
#endif
logger.info("Adjusted screen refresh rate to match content: \(contentFps) Hz")
} else if client?.currentRefreshRate != screenRefreshRate {
// Ensure the refresh rate is set back to the screen's rate if no adjustment is needed
client?.updateRefreshRate(to: screenRefreshRate)
client?.currentRefreshRate = screenRefreshRate
#if !os(macOS)
notifyViewToUpdateDisplayLink(with: screenRefreshRate)
#endif
logger.info("Checked and reset refresh rate to screen's rate: \(screenRefreshRate) Hz")
}
}
#if !os(macOS)
private func notifyViewToUpdateDisplayLink(with refreshRate: Int) {
NotificationCenter.default.post(name: .updateDisplayLinkFrameRate, object: nil, userInfo: ["refreshRate": refreshRate])
}
#endif
2022-02-16 20:23:11 +00:00
func handle(_ event: UnsafePointer<mpv_event>!) {
logger.info(.init(stringLiteral: "RECEIVED event: \(String(cString: mpv_event_name(event.pointee.event_id)))"))
2022-02-16 20:23:11 +00:00
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-16 20:23:11 +00:00
+ "\(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
2023-09-23 14:42:46 +00:00
case MPV_EVENT_PROPERTY_CHANGE:
let dataOpaquePtr = OpaquePointer(event.pointee.data)
if let property = UnsafePointer<mpv_event_property>(dataOpaquePtr)?.pointee {
let propertyName = String(cString: property.name)
handlePropertyChange(propertyName, property)
}
2022-03-27 19:22:13 +00:00
case MPV_EVENT_PLAYBACK_RESTART:
isLoadingVideo = false
isSeeking = false
2022-03-27 19:22:13 +00:00
onFileLoaded?()
startClientUpdates()
onFileLoaded = nil
2022-09-01 16:55:38 +00:00
case MPV_EVENT_VIDEO_RECONFIG:
model.updateAspectRatio()
case MPV_EVENT_SEEK:
isSeeking = true
2022-02-27 20:31:17 +00:00
2022-02-16 20:23:11 +00:00
case MPV_EVENT_END_FILE:
2022-11-10 21:20:44 +00:00
let reason = event!.pointee.data.load(as: mpv_end_file_reason.self)
if reason != MPV_END_FILE_REASON_STOP {
DispatchQueue.main.async { [weak self] in
guard let self else { return }
NavigationModel.shared.presentAlert(title: "Error while opening file")
self.model.closeCurrentItem(finished: true)
self.getTimeUpdates()
self.eofPlaybackModeAction()
}
} else {
DispatchQueue.main.async { [weak self] in self?.handleEndOfFile() }
}
2022-02-16 20:23:11 +00:00
default:
logger.info(.init(stringLiteral: "UNHANDLED event: \(String(cString: mpv_event_name(event.pointee.event_id)))"))
2022-02-16 20:23:11 +00:00
}
}
func handleEndOfFile() {
2022-07-03 21:18:27 +00:00
guard client.eofReached else {
2022-02-16 20:23:11 +00:00
return
}
2022-07-10 22:24:56 +00:00
eofPlaybackModeAction()
2022-02-16 20:23:11 +00:00
}
func setNeedsDrawing(_ needsDrawing: Bool) {
client?.setNeedsDrawing(needsDrawing)
}
2022-03-27 11:42:20 +00:00
func setSize(_ width: Double, _ height: Double) {
client?.setSize(width, height)
2022-03-27 11:42:20 +00:00
}
2022-06-07 21:20:24 +00:00
func addVideoTrack(_ url: URL) {
client?.addVideoTrack(url)
2022-06-07 21:20:24 +00:00
}
2022-07-05 17:20:25 +00:00
func addSubTrack(_ url: URL) {
if client?.areSubtitlesAdded == true {
client?.removeSubs()
}
2022-07-05 17:20:25 +00:00
client?.addSubTrack(url)
}
2022-06-07 21:20:24 +00:00
func setVideoToAuto() {
client?.setVideoToAuto()
2022-06-07 21:20:24 +00:00
}
func setVideoToNo() {
client?.setVideoToNo()
}
func updateNetworkState() {
DispatchQueue.main.async { [weak self] in
2023-02-24 17:19:55 +00:00
guard let self, let client = self.client else { return }
self.networkState.pausedForCache = client.pausedForCache
self.networkState.cacheDuration = client.cacheDuration
self.networkState.bufferingState = client.bufferingState
}
2022-06-24 23:32:21 +00:00
if !networkState.needsUpdates {
2022-06-29 22:43:41 +00:00
networkStateTimer.pause()
}
}
2022-06-24 23:39:29 +00:00
func setNeedsNetworkStateUpdates(_ needsUpdates: Bool) {
if needsUpdates {
2022-06-29 22:43:41 +00:00
networkStateTimer.start()
2022-06-24 23:39:29 +00:00
} else {
2022-06-29 22:43:41 +00:00
networkStateTimer.pause()
2022-06-24 23:39:29 +00:00
}
2022-06-07 21:20:24 +00:00
}
2022-08-20 20:31:03 +00:00
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)
2022-08-20 20:31:03 +00:00
if model.musicMode {
startMusicMode()
} else {
stopMusicMode()
}
}
2023-09-23 14:42:46 +00:00
private func handlePropertyChange(_ name: String, _ property: mpv_event_property) {
switch name {
case "pause":
if let paused = UnsafePointer<Bool>(OpaquePointer(property.data))?.pointee {
if paused {
DispatchQueue.main.async { [weak self] in self?.handleEndOfFile() }
} else {
isLoadingVideo = false
isSeeking = false
}
isPlaying = !paused
networkStateTimer.start()
}
2023-09-23 16:05:13 +00:00
case "core-idle":
if let idle = UnsafePointer<Bool>(OpaquePointer(property.data))?.pointee {
if !idle {
isLoadingVideo = false
isSeeking = false
networkStateTimer.start()
}
}
2023-09-23 14:42:46 +00:00
default:
logger.info("MPV backend received unhandled property: \(name)")
}
}
2022-02-16 20:23:11 +00:00
}