2022-02-16 20:23:11 +00:00
|
|
|
import AVFAudio
|
|
|
|
import CoreMedia
|
2022-05-28 21:41:23 +00:00
|
|
|
import Defaults
|
2022-02-16 20:23:11 +00:00
|
|
|
import Foundation
|
|
|
|
import Logging
|
2022-08-20 20:09:26 +00:00
|
|
|
import MediaPlayer
|
2023-09-23 14:42:46 +00:00
|
|
|
import MPVKit
|
2022-06-29 22:43:41 +00:00
|
|
|
import Repeat
|
2022-02-16 20:23:11 +00:00
|
|
|
import SwiftUI
|
|
|
|
|
|
|
|
final class MPVBackend: PlayerBackend {
|
2022-08-28 17:18:49 +00:00
|
|
|
static var timeUpdateInterval = 0.5
|
2024-05-17 14:16:48 +00:00
|
|
|
static var networkStateUpdateInterval = 0.1
|
2022-06-16 17:44:39 +00:00
|
|
|
|
2022-02-16 20:23:11 +00:00
|
|
|
private var logger = Logger(label: "mpv-backend")
|
|
|
|
|
2022-11-24 20:36:05 +00:00
|
|
|
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 {
|
2023-02-24 17:19:55 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
|
2022-11-24 20:36:05 +00:00
|
|
|
self.controls.isLoadingVideo = self.isLoadingVideo
|
2022-06-24 23:39:29 +00:00
|
|
|
self.setNeedsNetworkStateUpdates(true)
|
2022-11-24 20:36:05 +00:00
|
|
|
self.model.objectWillChange.send()
|
2022-02-27 20:31:17 +00:00
|
|
|
}
|
|
|
|
}}
|
2022-02-16 20:23:11 +00:00
|
|
|
|
Conditional proxying
I added a new feature. When instances are not proxied, Yattee first checks the URL to make sure it is not a restricted video. Usually, music videos and sports content can only be played back by the same IP address that requested the URL in the first place. That is why some videos do not play when the proxy is disabled.
This approach has multiple advantages. First and foremost, It reduced the load on Invidious/Piped instances, since users can now directly access the videos without going through the instance, which might be severely bandwidth limited. Secondly, users don't need to manually turn on the proxy when they want to watch IP address bound content, since Yattee automatically proxies such content.
Furthermore, adding the proxy option allows mitigating some severe playback issues with invidious instances. Invidious by default returns proxied URLs for videos, and due to some bug in the Invidious proxy, scrubbing or continuing playback at a random timestamp can lead to severe wait times for the users.
This should fix numerous playback issues: #666, #626, #590, #585, #498, #457, #400
2024-05-09 18:07:55 +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-06-18 12:39:49 +00:00
|
|
|
|
2022-02-16 20:23:11 +00:00
|
|
|
if isPlaying {
|
|
|
|
startClientUpdates()
|
|
|
|
} else {
|
|
|
|
stopControlsUpdates()
|
|
|
|
}
|
|
|
|
|
|
|
|
updateControlsIsPlaying()
|
2022-04-03 15:03:56 +00:00
|
|
|
|
2022-08-13 14:17:10 +00:00
|
|
|
#if os(macOS)
|
|
|
|
if isPlaying {
|
|
|
|
ScreenSaverManager.shared.disable(reason: "Yattee is playing video")
|
|
|
|
} else {
|
|
|
|
ScreenSaverManager.shared.enable()
|
|
|
|
}
|
2022-08-20 20:09:26 +00:00
|
|
|
|
|
|
|
MPNowPlayingInfoCenter.default().playbackState = isPlaying ? .playing : .paused
|
2022-08-13 14:17:10 +00:00
|
|
|
#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
|
|
|
}}
|
2022-06-18 12:39:49 +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 }
|
2022-06-18 12:39:49 +00:00
|
|
|
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!
|
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
|
|
|
|
}
|
|
|
|
|
2022-06-17 10:27:01 +00:00
|
|
|
var outputFps: Double {
|
|
|
|
client?.outputFps ?? 0
|
|
|
|
}
|
|
|
|
|
2022-11-10 17:11:28 +00:00
|
|
|
var formattedOutputFps: String {
|
|
|
|
String(format: "%.2ffps", outputFps)
|
|
|
|
}
|
|
|
|
|
2022-06-17 10:27:01 +00:00
|
|
|
var hwDecoder: String {
|
|
|
|
client?.hwDecoder ?? "unknown"
|
|
|
|
}
|
|
|
|
|
2022-06-17 11:43:11 +00:00
|
|
|
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() {
|
2023-12-04 23:07:36 +00:00
|
|
|
// swiftlint:disable shorthand_optional_binding
|
2022-08-28 17:18:49 +00:00
|
|
|
clientTimer = .init(interval: .seconds(Self.timeUpdateInterval), mode: .infinite) { [weak self] _ in
|
2023-12-04 13:47:26 +00:00
|
|
|
guard let self = 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
|
2023-12-04 13:47:26 +00:00
|
|
|
guard let self = self, self.model.activeBackend == .mpv else {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
self.updateNetworkState()
|
2022-06-29 22:43:41 +00:00
|
|
|
}
|
2023-12-04 23:07:36 +00:00
|
|
|
// swiftlint:enable shorthand_optional_binding
|
2023-12-03 23:07:39 +00:00
|
|
|
}
|
|
|
|
|
2022-05-21 19:30:01 +00:00
|
|
|
typealias AreInIncreasingOrder = (Stream, Stream) -> Bool
|
|
|
|
|
2022-02-16 20:23:11 +00:00
|
|
|
func canPlay(_ stream: Stream) -> Bool {
|
2022-05-21 19:30:01 +00:00
|
|
|
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?
|
2024-05-20 00:49:32 +00:00
|
|
|
|
|
|
|
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) }
|
2024-05-20 00:49:32 +00:00
|
|
|
}
|
|
|
|
} 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 = {
|
2022-09-23 18:36:10 +00:00
|
|
|
#if !os(macOS)
|
2024-04-24 12:43:51 +00:00
|
|
|
do {
|
|
|
|
try AVAudioSession.sharedInstance().setActive(true)
|
|
|
|
|
|
|
|
NotificationCenter.default.addObserver(
|
|
|
|
self,
|
|
|
|
selector: #selector(self.handleAudioSessionInterruption(_:)),
|
|
|
|
name: AVAudioSession.interruptionNotification,
|
|
|
|
object: nil
|
|
|
|
)
|
|
|
|
} catch {
|
|
|
|
self.logger.error("Error setting up audio session: \(error)")
|
|
|
|
}
|
2022-09-23 18:36:10 +00:00
|
|
|
#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()
|
|
|
|
|
2024-05-20 00:49:32 +00:00
|
|
|
if Defaults[.captionsAutoShow] { self.client?.setSubToAuto() } else { self.client?.setSubToNo() }
|
|
|
|
PlayerModel.shared.captions = self.captions
|
2024-04-20 23:01:55 +00:00
|
|
|
|
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
|
|
|
|
{
|
2022-08-28 17:18:49 +00:00
|
|
|
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()
|
2023-06-07 19:39:03 +00:00
|
|
|
self.model.handleOnPlayStream(stream)
|
2022-02-16 20:23:11 +00:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
self.play()
|
2023-06-07 19:39:03 +00:00
|
|
|
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()
|
|
|
|
|
2022-05-30 15:36:26 +00:00
|
|
|
DispatchQueue.main.async { [weak self] in
|
2023-05-07 19:54:48 +00:00
|
|
|
guard let self, let client = self.client else {
|
2022-05-30 15:36:26 +00:00
|
|
|
return
|
2022-02-16 20:23:11 +00:00
|
|
|
}
|
|
|
|
|
2022-05-30 15:36:26 +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
|
|
|
}
|
|
|
|
|
2024-05-13 05:54:24 +00:00
|
|
|
client.loadFile(url, bitrate: stream.bitrate, kind: stream.kind, sub: captions?.url, time: time, forceSeekable: stream.kind == .hls) { [weak self] _ in
|
2022-05-30 15:36:26 +00:00
|
|
|
self?.isLoadingVideo = true
|
|
|
|
}
|
|
|
|
} else {
|
2022-06-07 21:20:24 +00:00
|
|
|
self.onFileLoaded = {
|
2022-05-30 15:36:26 +00:00
|
|
|
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
|
|
|
|
|
2024-05-13 05:54:24 +00:00
|
|
|
client.loadFile(fileToLoad, audio: audioTrack, bitrate: stream.bitrate, kind: stream.kind, sub: captions?.url, time: time, forceSeekable: stream.kind == .hls) { [weak self] _ in
|
2022-05-30 15:36:26 +00:00
|
|
|
self?.isLoadingVideo = true
|
|
|
|
self?.pause()
|
|
|
|
}
|
2022-02-16 20:23:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if preservingTime {
|
2024-05-01 15:01:54 +00:00
|
|
|
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 play() {
|
|
|
|
startClientUpdates()
|
|
|
|
|
2022-11-24 20:36:05 +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)
|
|
|
|
|
2024-04-25 17:43:28 +00:00
|
|
|
// After the video has ended, hitting play restarts the video from the beginning.
|
|
|
|
if 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()
|
Conditional proxying
I added a new feature. When instances are not proxied, Yattee first checks the URL to make sure it is not a restricted video. Usually, music videos and sports content can only be played back by the same IP address that requested the URL in the first place. That is why some videos do not play when the proxy is disabled.
This approach has multiple advantages. First and foremost, It reduced the load on Invidious/Piped instances, since users can now directly access the videos without going through the instance, which might be severely bandwidth limited. Secondly, users don't need to manually turn on the proxy when they want to watch IP address bound content, since Yattee automatically proxies such content.
Furthermore, adding the proxy option allows mitigating some severe playback issues with invidious instances. Invidious by default returns proxied URLs for videos, and due to some bug in the Invidious proxy, scrubbing or continuing playback at a random timestamp can lead to severe wait times for the users.
This should fix numerous playback issues: #666, #626, #590, #585, #498, #457, #400
2024-05-09 18:07:55 +00:00
|
|
|
|
|
|
|
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()
|
|
|
|
|
|
|
|
client?.pause()
|
Conditional proxying
I added a new feature. When instances are not proxied, Yattee first checks the URL to make sure it is not a restricted video. Usually, music videos and sports content can only be played back by the same IP address that requested the URL in the first place. That is why some videos do not play when the proxy is disabled.
This approach has multiple advantages. First and foremost, It reduced the load on Invidious/Piped instances, since users can now directly access the videos without going through the instance, which might be severely bandwidth limited. Secondly, users don't need to manually turn on the proxy when they want to watch IP address bound content, since Yattee automatically proxies such content.
Furthermore, adding the proxy option allows mitigating some severe playback issues with invidious instances. Invidious by default returns proxied URLs for videos, and due to some bug in the Invidious proxy, scrubbing or continuing playback at a random timestamp can lead to severe wait times for the users.
This should fix numerous playback issues: #666, #626, #590, #585, #498, #457, #400
2024-05-09 18:07:55 +00:00
|
|
|
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
|
|
|
}
|
|
|
|
|
2022-11-13 12:28:25 +00:00
|
|
|
func cancelLoads() {
|
|
|
|
stop()
|
|
|
|
}
|
|
|
|
|
2022-02-16 20:23:11 +00:00
|
|
|
func stop() {
|
|
|
|
client?.stop()
|
Conditional proxying
I added a new feature. When instances are not proxied, Yattee first checks the URL to make sure it is not a restricted video. Usually, music videos and sports content can only be played back by the same IP address that requested the URL in the first place. That is why some videos do not play when the proxy is disabled.
This approach has multiple advantages. First and foremost, It reduced the load on Invidious/Piped instances, since users can now directly access the videos without going through the instance, which might be severely bandwidth limited. Secondly, users don't need to manually turn on the proxy when they want to watch IP address bound content, since Yattee automatically proxies such content.
Furthermore, adding the proxy option allows mitigating some severe playback issues with invidious instances. Invidious by default returns proxied URLs for videos, and due to some bug in the Invidious proxy, scrubbing or continuing playback at a random timestamp can lead to severe wait times for the users.
This should fix numerous playback issues: #666, #626, #590, #585, #498, #457, #400
2024-05-09 18:07:55 +00:00
|
|
|
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)?) {
|
2022-06-18 12:39:49 +00:00
|
|
|
client?.seek(to: time) { [weak self] _ in
|
2022-08-28 17:18:49 +00:00
|
|
|
self?.getTimeUpdates()
|
2022-02-16 20:23:11 +00:00
|
|
|
self?.updateControls()
|
|
|
|
completionHandler?(true)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-11-10 22:00:17 +00:00
|
|
|
func setRate(_ rate: Double) {
|
|
|
|
client?.setDoubleAsync("speed", rate)
|
2022-02-16 20:23:11 +00:00
|
|
|
}
|
|
|
|
|
2022-05-27 22:59:35 +00:00
|
|
|
func closeItem() {
|
Conditional proxying
I added a new feature. When instances are not proxied, Yattee first checks the URL to make sure it is not a restricted video. Usually, music videos and sports content can only be played back by the same IP address that requested the URL in the first place. That is why some videos do not play when the proxy is disabled.
This approach has multiple advantages. First and foremost, It reduced the load on Invidious/Piped instances, since users can now directly access the videos without going through the instance, which might be severely bandwidth limited. Secondly, users don't need to manually turn on the proxy when they want to watch IP address bound content, since Yattee automatically proxies such content.
Furthermore, adding the proxy option allows mitigating some severe playback issues with invidious instances. Invidious by default returns proxied URLs for videos, and due to some bug in the Invidious proxy, scrubbing or continuing playback at a random timestamp can lead to severe wait times for the users.
This should fix numerous playback issues: #666, #626, #590, #585, #498, #457, #400
2024-05-09 18:07:55 +00:00
|
|
|
pause()
|
|
|
|
stop()
|
2022-08-26 20:17:21 +00:00
|
|
|
self.video = nil
|
|
|
|
self.stream = nil
|
2022-05-27 22:59:35 +00:00
|
|
|
}
|
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)
|
|
|
|
|
2022-08-28 17:18:49 +00:00
|
|
|
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
|
|
|
}
|
2023-12-03 23:07:39 +00:00
|
|
|
|
2023-12-04 23:07:36 +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() {
|
2022-11-24 20:36:05 +00:00
|
|
|
guard model.activeBackend == .mpv else { return }
|
2022-02-16 20:23:11 +00:00
|
|
|
DispatchQueue.main.async { [weak self] in
|
2022-11-24 20:36:05 +00:00
|
|
|
self?.controls.isPlaying = self?.isPlaying ?? false
|
2022-02-16 20:23:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
func handle(_ event: UnsafePointer<mpv_event>!) {
|
2022-06-25 13:14:16 +00:00
|
|
|
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))
|
2022-06-17 10:27:01 +00:00
|
|
|
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
|
2022-06-18 12:39:49 +00:00
|
|
|
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()
|
|
|
|
|
2022-06-18 12:39:49 +00:00
|
|
|
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:
|
2022-06-17 10:27:01 +00:00
|
|
|
logger.info(.init(stringLiteral: "UNHANDLED event: \(String(cString: mpv_event_name(event.pointee.event_id)))"))
|
2022-02-16 20:23:11 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-06-25 13:14:16 +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) {
|
2022-06-18 12:39:49 +00:00
|
|
|
client?.setSize(width, height)
|
2022-03-27 11:42:20 +00:00
|
|
|
}
|
2022-06-07 21:20:24 +00:00
|
|
|
|
|
|
|
func addVideoTrack(_ url: URL) {
|
2022-06-18 12:39:49 +00:00
|
|
|
client?.addVideoTrack(url)
|
2022-06-07 21:20:24 +00:00
|
|
|
}
|
|
|
|
|
2022-07-05 17:20:25 +00:00
|
|
|
func addSubTrack(_ url: URL) {
|
|
|
|
client?.removeSubs()
|
|
|
|
client?.addSubTrack(url)
|
|
|
|
}
|
|
|
|
|
2022-06-07 21:20:24 +00:00
|
|
|
func setVideoToAuto() {
|
2022-06-18 12:39:49 +00:00
|
|
|
client?.setVideoToAuto()
|
2022-06-07 21:20:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func setVideoToNo() {
|
2022-06-18 12:39:49 +00:00
|
|
|
client?.setVideoToNo()
|
|
|
|
}
|
|
|
|
|
|
|
|
func updateNetworkState() {
|
2022-11-24 20:36:05 +00:00
|
|
|
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-18 12:39:49 +00:00
|
|
|
}
|
|
|
|
|
2022-06-24 23:32:21 +00:00
|
|
|
if !networkState.needsUpdates {
|
2022-06-29 22:43:41 +00:00
|
|
|
networkStateTimer.pause()
|
2022-06-18 12:39:49 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
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() {
|
2022-08-31 19:20:12 +00:00
|
|
|
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)")
|
|
|
|
}
|
|
|
|
}
|
2024-04-24 12:43:51 +00:00
|
|
|
|
2024-05-16 16:53:47 +00:00
|
|
|
#if !os(macOS)
|
|
|
|
@objc func handleAudioSessionInterruption(_ notification: Notification) {
|
|
|
|
logger.info("Audio session interruption received.")
|
2024-04-24 12:43:51 +00:00
|
|
|
|
2024-05-16 16:53:47 +00:00
|
|
|
guard let info = notification.userInfo,
|
|
|
|
let typeValue = info[AVAudioSessionInterruptionTypeKey] as? UInt
|
|
|
|
else {
|
|
|
|
logger.info("AVAudioSessionInterruptionTypeKey is missing or not a UInt in userInfo.")
|
|
|
|
return
|
|
|
|
}
|
2024-04-24 12:43:51 +00:00
|
|
|
|
2024-05-16 16:53:47 +00:00
|
|
|
let type = AVAudioSession.InterruptionType(rawValue: typeValue)
|
2024-04-24 12:43:51 +00:00
|
|
|
|
2024-05-16 16:53:47 +00:00
|
|
|
logger.info("Interruption type received: \(String(describing: type))")
|
2024-04-24 12:43:51 +00:00
|
|
|
|
2024-05-16 16:53:47 +00:00
|
|
|
switch type {
|
|
|
|
case .began:
|
|
|
|
pause()
|
|
|
|
logger.info("Audio session interrupted.")
|
|
|
|
default:
|
|
|
|
break
|
|
|
|
}
|
2024-04-24 12:43:51 +00:00
|
|
|
}
|
|
|
|
|
2024-05-16 16:53:47 +00:00
|
|
|
deinit {
|
|
|
|
NotificationCenter.default.removeObserver(self, name: AVAudioSession.interruptionNotification, object: nil)
|
|
|
|
}
|
|
|
|
#endif
|
2022-02-16 20:23:11 +00:00
|
|
|
}
|