mirror of
https://github.com/yattee/yattee.git
synced 2025-12-13 03:28:14 +00:00
Compare commits
69 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d7a101ce0 | ||
|
|
b2114174b4 | ||
|
|
e9f502a486 | ||
|
|
6978e9437c | ||
|
|
2026201a5f | ||
|
|
633af02577 | ||
|
|
1fd62f04aa | ||
|
|
e749307a0e | ||
|
|
d76ec881be | ||
|
|
72a39a2c75 | ||
|
|
8a84db5a2d | ||
|
|
663c37e3d2 | ||
|
|
ea2b329df2 | ||
|
|
bd79f56800 | ||
|
|
9a650b4ac0 | ||
|
|
13382270d5 | ||
|
|
24626c2299 | ||
|
|
18ac577c7f | ||
|
|
617af2cd20 | ||
|
|
1b778318dc | ||
|
|
c9ce574c7a | ||
|
|
9a1f0d7aaa | ||
|
|
1cb695848c | ||
|
|
740a2f85ac | ||
|
|
1a22ac71be | ||
|
|
f0d581d512 | ||
|
|
049a42f2e8 | ||
|
|
cea2684a29 | ||
|
|
772e5016c4 | ||
|
|
ed3d9a7d7c | ||
|
|
bde9aade11 | ||
|
|
a194738bb6 | ||
|
|
45567254f2 | ||
|
|
a3139ad059 | ||
|
|
598f17479f | ||
|
|
e888abfba9 | ||
|
|
e1e53b2d36 | ||
|
|
9510d91d61 | ||
|
|
59da0e71b6 | ||
|
|
6bdfb7368c | ||
|
|
7b26fdf400 | ||
|
|
67b41e36d5 | ||
|
|
c9c60349df | ||
|
|
6a70663f06 | ||
|
|
3d556d836f | ||
|
|
8feeb33a55 | ||
|
|
497c3bfc12 | ||
|
|
b51eadc7a9 | ||
|
|
7d0c1180c4 | ||
|
|
0c1fb02d50 | ||
|
|
6f358fab56 | ||
|
|
7631e2a8ed | ||
|
|
3a5f3fdfde | ||
|
|
e3633bdaf7 | ||
|
|
e912d910bc | ||
|
|
5ccb0f90d5 | ||
|
|
278bc343c2 | ||
|
|
5dc197664d | ||
|
|
192550ba7a | ||
|
|
3369e23e74 | ||
|
|
4381511c91 | ||
|
|
af99df9b8a | ||
|
|
21f21cc944 | ||
|
|
e1d8bb8125 | ||
|
|
d948ea6887 | ||
|
|
66eb8051bf | ||
|
|
95d3170d31 | ||
|
|
74b6adb247 | ||
|
|
a45522f710 |
40
CHANGELOG.md
40
CHANGELOG.md
@@ -1,15 +1,13 @@
|
||||
## Build 189
|
||||
* Improved thumbnail handling by @stonerl in https://github.com/yattee/yattee/pull/740
|
||||
* iOS: make timestamps in comments touchable by @stonerl in https://github.com/yattee/yattee/pull/741
|
||||
* Improvements to opening channels from Videos by @stonerl in https://github.com/yattee/yattee/pull/742
|
||||
* Allow hiding comments by @stonerl in https://github.com/yattee/yattee/pull/744
|
||||
* Add option to exit fullscreen on end by @stonerl in https://github.com/yattee/yattee/pull/570
|
||||
* Only updateWatch status while video is playing by @stonerl in https://github.com/yattee/yattee/pull/745
|
||||
* Xcode 16 - update recommended settings by @stonerl in https://github.com/yattee/yattee/pull/737
|
||||
* Translations update from Hosted Weblate by @weblate in https://github.com/yattee/yattee/pull/724
|
||||
* Updated dependencies
|
||||
* Fixed crashes
|
||||
* Other minor changes and improvements
|
||||
## Build 193
|
||||
* Invidious: propper HTTP basic auth support by @stonerl in https://github.com/yattee/yattee/pull/762
|
||||
* Apply correct orientation by @stonerl in https://github.com/yattee/yattee/pull/770
|
||||
* Circular Invidious logo by @stonerl in https://github.com/yattee/yattee/pull/769
|
||||
* Video Thumbnails: retry 3 times fetching from URL by @stonerl in https://github.com/yattee/yattee/pull/768
|
||||
* Make thumbnail fill the view in music mode by @stonerl in https://github.com/yattee/yattee/pull/766
|
||||
* Changes to defaults by @stonerl in https://github.com/yattee/yattee/pull/767
|
||||
* Fixed fullscreen handling for backgrounding by @stonerl in https://github.com/yattee/yattee/pull/772
|
||||
* Update now playing info when using system controls – Partial fix for 503 by @stonerl in https://github.com/yattee/yattee/pull/765
|
||||
* Fix crash on HLS live playback by @stonerl in https://github.com/yattee/yattee/pull/775
|
||||
|
||||
## Previous builds
|
||||
* Add skip, play/pause, and fullscreen shortcuts to macOS player (by @rickykresslein)
|
||||
@@ -28,6 +26,24 @@
|
||||
* Add import export of missing settings
|
||||
* macOS: Fix settings windows layout
|
||||
* Fix seek OSD layout on tvOS, revert OSD position
|
||||
* Fix mpv crashing on macOS by @stonerl in https://github.com/yattee/yattee/pull/754
|
||||
* Refreshed icons for iOS and macOS by @stonerl in https://github.com/yattee/yattee/pull/752
|
||||
* Add new MPVKit repo by @stonerl in https://github.com/yattee/yattee/pull/753
|
||||
* Add Chinese (Simplified) - zh-Hans to LanguageCodes by @stonerl in https://github.com/yattee/yattee/pull/757
|
||||
* Color changes to VideoActions by @stonerl in https://github.com/yattee/yattee/pull/759
|
||||
* Hide VideoActions Bar when no buttons is visible by @stonerl in https://github.com/yattee/yattee/pull/760
|
||||
* Improved stream resolution handling by @stonerl in https://github.com/yattee/yattee/pull/747
|
||||
* Fix some potential crashes by @stonerl in https://github.com/yattee/yattee/pull/748
|
||||
* Fix regression and improve curentChapter handling by @stonerl in https://github.com/yattee/yattee/pull/749
|
||||
* Refined chapter font scaling by @stonerl in https://github.com/yattee/yattee/pull/750
|
||||
* Improved thumbnail handling by @stonerl in https://github.com/yattee/yattee/pull/740
|
||||
* iOS: make timestamps in comments touchable by @stonerl in https://github.com/yattee/yattee/pull/741
|
||||
* Improvements to opening channels from Videos by @stonerl in https://github.com/yattee/yattee/pull/742
|
||||
* Allow hiding comments by @stonerl in https://github.com/yattee/yattee/pull/744
|
||||
* Add option to exit fullscreen on end by @stonerl in https://github.com/yattee/yattee/pull/570
|
||||
* Only updateWatch status while video is playing by @stonerl in https://github.com/yattee/yattee/pull/745
|
||||
* Xcode 16 - update recommended settings by @stonerl in https://github.com/yattee/yattee/pull/737
|
||||
* Translations update from Hosted Weblate by @weblate in https://github.com/yattee/yattee/pull/724
|
||||
* tvOS: Allow account picker by long pressing channels button in subscriptions view by @patelhiren in https://github.com/yattee/yattee/pull/704
|
||||
* tvOS: Refined Subscriptions View by @patelhiren in https://github.com/yattee/yattee/pull/697
|
||||
* More responsive UI when Favorites are used. by @stonerl in https://github.com/yattee/yattee/pull/695
|
||||
|
||||
@@ -10,8 +10,8 @@ GEM
|
||||
artifactory (3.0.17)
|
||||
atomos (0.1.3)
|
||||
aws-eventstream (1.3.0)
|
||||
aws-partitions (1.968.0)
|
||||
aws-sdk-core (3.201.5)
|
||||
aws-partitions (1.970.0)
|
||||
aws-sdk-core (3.202.2)
|
||||
aws-eventstream (~> 1, >= 1.3.0)
|
||||
aws-partitions (~> 1, >= 1.651.0)
|
||||
aws-sigv4 (~> 1.9)
|
||||
|
||||
@@ -10,11 +10,28 @@ struct AccountsBridge: Defaults.Bridge {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse the urlString to check for embedded username and password
|
||||
var sanitizedUrlString = value.urlString
|
||||
if var urlComponents = URLComponents(string: value.urlString) {
|
||||
if let user = urlComponents.user, let password = urlComponents.password {
|
||||
// Sanitize the embedded username and password
|
||||
let sanitizedUser = user.addingPercentEncoding(withAllowedCharacters: .urlUserAllowed) ?? user
|
||||
let sanitizedPassword = password.addingPercentEncoding(withAllowedCharacters: .urlPasswordAllowed) ?? password
|
||||
|
||||
// Update the URL components with sanitized credentials
|
||||
urlComponents.user = sanitizedUser
|
||||
urlComponents.password = sanitizedPassword
|
||||
|
||||
// Reconstruct the sanitized URL
|
||||
sanitizedUrlString = urlComponents.string ?? value.urlString
|
||||
}
|
||||
}
|
||||
|
||||
return [
|
||||
"id": value.id,
|
||||
"instanceID": value.instanceID ?? "",
|
||||
"name": value.name,
|
||||
"apiURL": value.urlString,
|
||||
"apiURL": sanitizedUrlString,
|
||||
"username": value.username,
|
||||
"password": value.password ?? ""
|
||||
]
|
||||
|
||||
@@ -247,27 +247,27 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
}
|
||||
|
||||
func feed(_ page: Int?) -> Resource? {
|
||||
resource(baseURL: account.url, path: "\(Self.basePath)/auth/feed")
|
||||
resourceWithAuthCheck(baseURL: account.url, path: "\(Self.basePath)/auth/feed")
|
||||
.withParam("page", String(page ?? 1))
|
||||
}
|
||||
|
||||
var feed: Resource? {
|
||||
resource(baseURL: account.url, path: basePathAppending("auth/feed"))
|
||||
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/feed"))
|
||||
}
|
||||
|
||||
var subscriptions: Resource? {
|
||||
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||
}
|
||||
|
||||
func subscribe(_ channelID: String, onCompletion: @escaping () -> Void = {}) {
|
||||
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||
.child(channelID)
|
||||
.request(.post)
|
||||
.onCompletion { _ in onCompletion() }
|
||||
}
|
||||
|
||||
func unsubscribe(_ channelID: String, onCompletion: @escaping () -> Void) {
|
||||
resource(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/subscriptions"))
|
||||
.child(channelID)
|
||||
.request(.delete)
|
||||
.onCompletion { _ in onCompletion() }
|
||||
@@ -308,11 +308,11 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
return nil
|
||||
}
|
||||
|
||||
return resource(baseURL: account.url, path: basePathAppending("auth/playlists"))
|
||||
return resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/playlists"))
|
||||
}
|
||||
|
||||
func playlist(_ id: String) -> Resource? {
|
||||
resource(baseURL: account.url, path: basePathAppending("auth/playlists/\(id)"))
|
||||
resourceWithAuthCheck(baseURL: account.url, path: basePathAppending("auth/playlists/\(id)"))
|
||||
}
|
||||
|
||||
func playlistVideos(_ id: String) -> Resource? {
|
||||
@@ -445,6 +445,9 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
|
||||
urlComponents.scheme = instanceURLComponents.scheme
|
||||
urlComponents.host = instanceURLComponents.host
|
||||
urlComponents.user = instanceURLComponents.user
|
||||
urlComponents.password = instanceURLComponents.password
|
||||
urlComponents.port = instanceURLComponents.port
|
||||
|
||||
guard let url = urlComponents.url else {
|
||||
return nil
|
||||
@@ -553,6 +556,30 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
)
|
||||
}
|
||||
|
||||
// Determines if the request requires Basic Auth credentials to be removed
|
||||
private func needsBasicAuthRemoval(for path: String) -> Bool {
|
||||
return path.hasPrefix("\(Self.basePath)/auth/")
|
||||
}
|
||||
|
||||
// Creates a resource URL with consideration for removing Basic Auth credentials
|
||||
private func createResourceURL(baseURL: URL, path: String) -> URL {
|
||||
var resourceURL = baseURL
|
||||
|
||||
// Remove Basic Auth credentials if required
|
||||
if needsBasicAuthRemoval(for: path), var urlComponents = URLComponents(url: baseURL, resolvingAgainstBaseURL: false) {
|
||||
urlComponents.user = nil
|
||||
urlComponents.password = nil
|
||||
resourceURL = urlComponents.url ?? baseURL
|
||||
}
|
||||
|
||||
return resourceURL.appendingPathComponent(path)
|
||||
}
|
||||
|
||||
func resourceWithAuthCheck(baseURL: URL, path: String) -> Resource {
|
||||
let sanitizedURL = createResourceURL(baseURL: baseURL, path: path)
|
||||
return super.resource(absoluteURL: sanitizedURL)
|
||||
}
|
||||
|
||||
private func extractThumbnails(from details: JSON) -> [Thumbnail] {
|
||||
details["videoThumbnails"].arrayValue.compactMap { json in
|
||||
guard let url = json["url"].url,
|
||||
@@ -563,13 +590,20 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
|
||||
return nil
|
||||
}
|
||||
|
||||
// some of instances are not configured properly and return thumbnails links
|
||||
// with incorrect scheme
|
||||
// Some instances are not configured properly and return thumbnail links
|
||||
// with an incorrect scheme or a missing port.
|
||||
components.scheme = accountUrlComponents.scheme
|
||||
components.port = accountUrlComponents.port
|
||||
|
||||
// If basic HTTP authentication is used,
|
||||
// the username and password need to be prepended to the URL.
|
||||
components.user = accountUrlComponents.user
|
||||
components.password = accountUrlComponents.password
|
||||
|
||||
guard let thumbnailUrl = components.url else {
|
||||
return nil
|
||||
}
|
||||
print("Final thumbnail URL: \(thumbnailUrl)")
|
||||
|
||||
return Thumbnail(url: thumbnailUrl, quality: .init(rawValue: quality)!)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ import Foundation
|
||||
|
||||
final class MenuModel: ObservableObject {
|
||||
static let shared = MenuModel()
|
||||
private var cancellables = [AnyCancellable]()
|
||||
private var cancellables = Set<AnyCancellable>()
|
||||
|
||||
init() {
|
||||
registerChildModel(AccountsModel.shared)
|
||||
@@ -12,10 +12,16 @@ final class MenuModel: ObservableObject {
|
||||
}
|
||||
|
||||
func registerChildModel<T: ObservableObject>(_ model: T?) {
|
||||
guard !model.isNil else {
|
||||
guard let model else {
|
||||
return
|
||||
}
|
||||
|
||||
cancellables.append(model!.objectWillChange.sink { [weak self] _ in self?.objectWillChange.send() })
|
||||
model.objectWillChange
|
||||
.receive(on: DispatchQueue.main) // Ensure the update occurs on the main thread
|
||||
.debounce(for: .milliseconds(10), scheduler: DispatchQueue.main) // Debounce to avoid immediate feedback loops
|
||||
.sink { [weak self] _ in
|
||||
self?.objectWillChange.send()
|
||||
}
|
||||
.store(in: &cancellables)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +102,7 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
|
||||
private var frequentTimeObserver: Any?
|
||||
private var infrequentTimeObserver: Any?
|
||||
private var playerTimeControlStatusObserver: Any?
|
||||
private var playerTimeControlStatusObserver: NSKeyValueObservation?
|
||||
|
||||
private var statusObservation: NSKeyValueObservation?
|
||||
|
||||
@@ -119,10 +119,30 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
#if os(iOS)
|
||||
controller.player = avPlayer
|
||||
#endif
|
||||
logger.info("AVPlayerBackend initialized.")
|
||||
}
|
||||
|
||||
deinit {
|
||||
// Invalidate any observers to avoid memory leaks
|
||||
statusObservation?.invalidate()
|
||||
playerTimeControlStatusObserver?.invalidate()
|
||||
|
||||
// Remove any time observers added to AVPlayer
|
||||
if let frequentObserver = frequentTimeObserver {
|
||||
avPlayer.removeTimeObserver(frequentObserver)
|
||||
}
|
||||
if let infrequentObserver = infrequentTimeObserver {
|
||||
avPlayer.removeTimeObserver(infrequentObserver)
|
||||
}
|
||||
|
||||
// Remove notification observers
|
||||
removeItemDidPlayToEndTimeObserver()
|
||||
|
||||
logger.info("AVPlayerBackend deinitialized.")
|
||||
}
|
||||
|
||||
func canPlay(_ stream: Stream) -> Bool {
|
||||
stream.kind == .hls || stream.kind == .stream || (stream.kind == .adaptive && stream.format == .mp4)
|
||||
stream.kind == .hls || stream.kind == .stream
|
||||
}
|
||||
|
||||
func playStream(
|
||||
@@ -779,7 +799,7 @@ final class AVPlayerBackend: PlayerBackend {
|
||||
opened = true
|
||||
controller.startPictureInPicture()
|
||||
} else {
|
||||
print("PiP not possible, waited \(delay) seconds")
|
||||
self.logger.info("PiP not possible, waited \(delay) seconds")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ import AVFAudio
|
||||
import CoreMedia
|
||||
import Defaults
|
||||
import Foundation
|
||||
import Libmpv
|
||||
import Logging
|
||||
import MediaPlayer
|
||||
import MPVKit
|
||||
import Repeat
|
||||
import SwiftUI
|
||||
|
||||
@@ -464,6 +464,8 @@ final class MPVBackend: PlayerBackend {
|
||||
timeObserverThrottle.execute {
|
||||
self.model.updateWatch(time: self.currentTime)
|
||||
}
|
||||
|
||||
self.model.updateTime(self.currentTime!)
|
||||
}
|
||||
|
||||
private func stopClientUpdates() {
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import CoreMedia
|
||||
import Defaults
|
||||
import Foundation
|
||||
import Libmpv
|
||||
import Logging
|
||||
import MPVKit
|
||||
#if !os(macOS)
|
||||
import Siesta
|
||||
import UIKit
|
||||
@@ -99,6 +99,11 @@ final class MPVClient: ObservableObject {
|
||||
checkError(mpv_set_option_string(mpv, "demuxer-lavf-analyzeduration", "1"))
|
||||
checkError(mpv_set_option_string(mpv, "demuxer-lavf-probe-info", Defaults[.mpvDemuxerLavfProbeInfo]))
|
||||
|
||||
// Disable ytdl, since it causes crashes on macOS.
|
||||
#if os(macOS)
|
||||
checkError(mpv_set_option_string(mpv, "ytdl", "no"))
|
||||
#endif
|
||||
|
||||
checkError(mpv_initialize(mpv))
|
||||
|
||||
let api = UnsafeMutableRawPointer(mutating: (MPV_RENDER_API_TYPE_OPENGL as NSString).utf8String)
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import CoreMedia
|
||||
import Defaults
|
||||
import Foundation
|
||||
import Logging
|
||||
#if !os(macOS)
|
||||
import UIKit
|
||||
#endif
|
||||
@@ -75,6 +76,10 @@ protocol PlayerBackend {
|
||||
}
|
||||
|
||||
extension PlayerBackend {
|
||||
var logger: Logger {
|
||||
return Logger(label: "stream.yattee.player.backend")
|
||||
}
|
||||
|
||||
func seek(to time: CMTime, seekType: SeekType, completionHandler: ((Bool) -> Void)? = nil) {
|
||||
model.seek.registerSeek(at: time, type: seekType, restore: currentTime)
|
||||
seek(to: time, seekType: seekType, completionHandler: completionHandler)
|
||||
@@ -140,55 +145,89 @@ extension PlayerBackend {
|
||||
}
|
||||
|
||||
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting, formatOrder: [QualityProfile.Format]) -> Stream? {
|
||||
// filter out non-HLS streams and streams with resolution more than maxResolution
|
||||
let nonHLSStreams = streams.filter {
|
||||
$0.kind != .hls && $0.resolution <= maxResolution.value
|
||||
}
|
||||
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)")
|
||||
|
||||
// find max resolution and bitrate from non-HLS streams
|
||||
// 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
|
||||
|
||||
return streams.map { stream in
|
||||
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
|
||||
}
|
||||
.filter { stream in
|
||||
stream.resolution <= maxResolution.value
|
||||
|
||||
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
|
||||
}
|
||||
.max { lhs, rhs in
|
||||
|
||||
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 {
|
||||
print("Failed to extract lhsFormat or rhsFormat")
|
||||
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) {
|
||||
print("updating controls")
|
||||
logger.info("updating controls")
|
||||
|
||||
guard model.presentingPlayer, !model.controls.presentingOverlays else {
|
||||
print("ignored controls update")
|
||||
logger.info("ignored controls update")
|
||||
completionHandler?()
|
||||
return
|
||||
}
|
||||
@@ -196,7 +235,7 @@ extension PlayerBackend {
|
||||
DispatchQueue.main.async(qos: .userInteractive) {
|
||||
#if !os(macOS)
|
||||
guard UIApplication.shared.applicationState != .background else {
|
||||
print("not performing controls updates in background")
|
||||
logger.info("not performing controls updates in background")
|
||||
completionHandler?()
|
||||
return
|
||||
}
|
||||
|
||||
@@ -56,6 +56,7 @@ final class PlayerModel: ObservableObject {
|
||||
@Published var presentingPlayer = false { didSet { handlePresentationChange() } }
|
||||
@Published var activeBackend = PlayerBackendType.mpv
|
||||
@Published var forceBackendOnPlay: PlayerBackendType?
|
||||
@Published var wasFullscreen = false
|
||||
|
||||
var avPlayerBackend = AVPlayerBackend()
|
||||
var mpvBackend = MPVBackend()
|
||||
@@ -684,7 +685,7 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
|
||||
// First, we need to create an array with supported formats.
|
||||
let formatOrderPiP: [QualityProfile.Format] = [.hls, .stream, .mp4]
|
||||
let formatOrderPiP: [QualityProfile.Format] = [.stream, .hls]
|
||||
|
||||
guard let video = currentVideo else { return }
|
||||
guard let stream = avPlayerBackend.bestPlayable(availableStreams, maxResolution: .hd720p30, formatOrder: formatOrderPiP) else { return }
|
||||
@@ -880,26 +881,29 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
|
||||
func updateRemoteCommandCenter() {
|
||||
let skipForwardCommand = MPRemoteCommandCenter.shared().skipForwardCommand
|
||||
let skipBackwardCommand = MPRemoteCommandCenter.shared().skipBackwardCommand
|
||||
let previousTrackCommand = MPRemoteCommandCenter.shared().previousTrackCommand
|
||||
let nextTrackCommand = MPRemoteCommandCenter.shared().nextTrackCommand
|
||||
let commandCenter = MPRemoteCommandCenter.shared()
|
||||
let skipForwardCommand = commandCenter.skipForwardCommand
|
||||
let skipBackwardCommand = commandCenter.skipBackwardCommand
|
||||
let previousTrackCommand = commandCenter.previousTrackCommand
|
||||
let nextTrackCommand = commandCenter.nextTrackCommand
|
||||
|
||||
if !remoteCommandCenterConfigured {
|
||||
remoteCommandCenterConfigured = true
|
||||
|
||||
#if !os(macOS)
|
||||
try? AVAudioSession.sharedInstance().setCategory(
|
||||
.playback,
|
||||
mode: .moviePlayback
|
||||
)
|
||||
|
||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||
#endif
|
||||
|
||||
let interval = TimeInterval(systemControlsSeekDuration) ?? 10
|
||||
let preferredIntervals = [NSNumber(value: interval)]
|
||||
|
||||
// Remove existing targets to avoid duplicates
|
||||
skipForwardCommand.removeTarget(nil)
|
||||
skipBackwardCommand.removeTarget(nil)
|
||||
previousTrackCommand.removeTarget(nil)
|
||||
nextTrackCommand.removeTarget(nil)
|
||||
commandCenter.playCommand.removeTarget(nil)
|
||||
commandCenter.pauseCommand.removeTarget(nil)
|
||||
commandCenter.togglePlayPauseCommand.removeTarget(nil)
|
||||
commandCenter.changePlaybackPositionCommand.removeTarget(nil)
|
||||
|
||||
// Re-add targets for handling commands
|
||||
skipForwardCommand.preferredIntervals = preferredIntervals
|
||||
skipBackwardCommand.preferredIntervals = preferredIntervals
|
||||
|
||||
@@ -923,22 +927,22 @@ final class PlayerModel: ObservableObject {
|
||||
return .success
|
||||
}
|
||||
|
||||
MPRemoteCommandCenter.shared().playCommand.addTarget { [weak self] _ in
|
||||
commandCenter.playCommand.addTarget { [weak self] _ in
|
||||
self?.play()
|
||||
return .success
|
||||
}
|
||||
|
||||
MPRemoteCommandCenter.shared().pauseCommand.addTarget { [weak self] _ in
|
||||
commandCenter.pauseCommand.addTarget { [weak self] _ in
|
||||
self?.pause()
|
||||
return .success
|
||||
}
|
||||
|
||||
MPRemoteCommandCenter.shared().togglePlayPauseCommand.addTarget { [weak self] _ in
|
||||
commandCenter.togglePlayPauseCommand.addTarget { [weak self] _ in
|
||||
self?.togglePlay()
|
||||
return .success
|
||||
}
|
||||
|
||||
MPRemoteCommandCenter.shared().changePlaybackPositionCommand.addTarget { [weak self] remoteEvent in
|
||||
commandCenter.changePlaybackPositionCommand.addTarget { [weak self] remoteEvent in
|
||||
guard let event = remoteEvent as? MPChangePlaybackPositionCommandEvent else { return .commandFailed }
|
||||
|
||||
self?.backend.seek(to: event.positionTime, seekType: .userInteracted)
|
||||
@@ -979,6 +983,17 @@ final class PlayerModel: ObservableObject {
|
||||
avPlayerBackend.bindPlayerToLayer()
|
||||
}
|
||||
|
||||
#if os(iOS)
|
||||
if wasFullscreen {
|
||||
wasFullscreen = false
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
Delay.by(0.3) {
|
||||
self?.enterFullScreen()
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
guard closePiPAndOpenPlayerOnEnteringForeground, playingInPictureInPicture else {
|
||||
return
|
||||
}
|
||||
@@ -993,6 +1008,15 @@ final class PlayerModel: ObservableObject {
|
||||
} else if !playingInPictureInPicture {
|
||||
avPlayerBackend.removePlayerFromLayer()
|
||||
}
|
||||
#if os(iOS)
|
||||
guard playingFullScreen else { return }
|
||||
wasFullscreen = playingFullScreen
|
||||
DispatchQueue.main.async { [weak self] in
|
||||
Delay.by(0.3) {
|
||||
self?.exitFullScreen(showControls: false)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -1017,18 +1041,22 @@ final class PlayerModel: ObservableObject {
|
||||
guard activeBackend == .mpv else { return }
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
if activeBackend == .appleAVPlayer, avPlayerUsesSystemControls {
|
||||
return
|
||||
}
|
||||
#endif
|
||||
|
||||
guard let video = currentItem?.video else {
|
||||
MPNowPlayingInfoCenter.default().nowPlayingInfo = .none
|
||||
return
|
||||
}
|
||||
|
||||
let currentTime = (backend.currentTime?.seconds.isFinite ?? false) ? backend.currentTime!.seconds : 0
|
||||
|
||||
// Determine the media type based on musicMode
|
||||
let mediaType: NSNumber
|
||||
if musicMode {
|
||||
mediaType = MPMediaType.anyAudio.rawValue as NSNumber
|
||||
} else {
|
||||
mediaType = MPMediaType.anyVideo.rawValue as NSNumber
|
||||
}
|
||||
|
||||
// Prepare the Now Playing info dictionary
|
||||
var nowPlayingInfo: [String: AnyObject] = [
|
||||
MPMediaItemPropertyTitle: video.displayTitle as AnyObject,
|
||||
MPMediaItemPropertyArtist: video.displayAuthor as AnyObject,
|
||||
@@ -1036,7 +1064,7 @@ final class PlayerModel: ObservableObject {
|
||||
MPNowPlayingInfoPropertyElapsedPlaybackTime: currentTime as AnyObject,
|
||||
MPNowPlayingInfoPropertyPlaybackQueueCount: queue.count as AnyObject,
|
||||
MPNowPlayingInfoPropertyPlaybackQueueIndex: 1 as AnyObject,
|
||||
MPMediaItemPropertyMediaType: MPMediaType.anyVideo.rawValue as AnyObject
|
||||
MPMediaItemPropertyMediaType: mediaType
|
||||
]
|
||||
|
||||
if !currentArtwork.isNil {
|
||||
@@ -1057,7 +1085,7 @@ final class PlayerModel: ObservableObject {
|
||||
|
||||
func updateCurrentArtwork() {
|
||||
guard let video = currentVideo,
|
||||
let thumbnailURL = video.thumbnailURL(quality: .medium)
|
||||
let thumbnailURL = video.thumbnailURL(quality: Constants.isIPhone ? .medium : .maxres)
|
||||
else {
|
||||
return
|
||||
}
|
||||
@@ -1240,7 +1268,7 @@ final class PlayerModel: ObservableObject {
|
||||
}
|
||||
|
||||
private func destroyKeyPressMonitor() {
|
||||
if let keyPressMonitor = keyPressMonitor {
|
||||
if let keyPressMonitor {
|
||||
NSEvent.removeMonitor(keyPressMonitor)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -127,6 +127,7 @@ extension PlayerModel {
|
||||
var streamByQualityProfile: Stream? {
|
||||
let profile = qualityProfile ?? .defaultProfile
|
||||
|
||||
// First attempt: Filter by both `canPlay` and `isPreferred`
|
||||
if let streamPreferredForProfile = backend.bestPlayable(
|
||||
availableStreams.filter { backend.canPlay($0) && profile.isPreferred($0) },
|
||||
maxResolution: profile.resolution, formatOrder: profile.formats
|
||||
@@ -134,7 +135,24 @@ extension PlayerModel {
|
||||
return streamPreferredForProfile
|
||||
}
|
||||
|
||||
return backend.bestPlayable(availableStreams.filter { backend.canPlay($0) }, maxResolution: profile.resolution, formatOrder: profile.formats)
|
||||
// Fallback: Filter by `canPlay` only
|
||||
let fallbackStream = backend.bestPlayable(
|
||||
availableStreams.filter { backend.canPlay($0) },
|
||||
maxResolution: profile.resolution, formatOrder: profile.formats
|
||||
)
|
||||
|
||||
// If no stream is found, trigger the error handler
|
||||
guard let finalStream = fallbackStream else {
|
||||
let error = RequestError(
|
||||
userMessage: "No supported streams available.",
|
||||
cause: NSError(domain: "stream.yatte.app", code: -1, userInfo: [NSLocalizedDescriptionKey: "No supported streams available"])
|
||||
)
|
||||
videoLoadFailureHandler(error, video: currentVideo)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Return the found stream
|
||||
return finalStream
|
||||
}
|
||||
|
||||
func advanceToNextItem() {
|
||||
|
||||
@@ -6,12 +6,12 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
|
||||
static var defaultProfile = Self(id: "default", backend: .mpv, resolution: .hd720p60, formats: [.stream], order: Array(Format.allCases.indices))
|
||||
|
||||
enum Format: String, CaseIterable, Identifiable, Defaults.Serializable {
|
||||
case hls
|
||||
case stream
|
||||
case avc1
|
||||
case stream
|
||||
case webm
|
||||
case mp4
|
||||
case av1
|
||||
case webm
|
||||
case hls
|
||||
|
||||
var id: String {
|
||||
rawValue
|
||||
@@ -30,18 +30,18 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
|
||||
|
||||
var streamFormat: Stream.Format? {
|
||||
switch self {
|
||||
case .hls:
|
||||
return nil
|
||||
case .stream:
|
||||
return nil
|
||||
case .avc1:
|
||||
return .avc1
|
||||
case .stream:
|
||||
return nil
|
||||
case .webm:
|
||||
return .webm
|
||||
case .mp4:
|
||||
return .mp4
|
||||
case .av1:
|
||||
return .av1
|
||||
case .webm:
|
||||
return .webm
|
||||
case .hls:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -59,14 +59,16 @@ struct QualityProfile: Hashable, Identifiable, Defaults.Serializable {
|
||||
}
|
||||
|
||||
var formatsDescription: String {
|
||||
if formats.count == Format.allCases.count {
|
||||
switch formats.count {
|
||||
case Format.allCases.count:
|
||||
return "Any format".localized()
|
||||
}
|
||||
if formats.count <= 3 {
|
||||
case 0:
|
||||
return "No format selected".localized()
|
||||
case 1 ... 3:
|
||||
return formats.map(\.description).joined(separator: ", ")
|
||||
default:
|
||||
return String(format: "%@ formats".localized(), String(formats.count))
|
||||
}
|
||||
|
||||
return String(format: "%@ formats".localized(), String(formats.count))
|
||||
}
|
||||
|
||||
func isPreferred(_ stream: Stream) -> Bool {
|
||||
|
||||
@@ -5,26 +5,153 @@ import Foundation
|
||||
// swiftlint:disable:next final_class
|
||||
class Stream: Equatable, Hashable, Identifiable {
|
||||
enum Resolution: String, CaseIterable, Comparable, Defaults.Serializable {
|
||||
// Some 16:19 and 16:10 resolutions are also used in 2:1 videos
|
||||
|
||||
// 8K UHD (16:9) Resolutions
|
||||
case hd4320p60
|
||||
case hd4320p50
|
||||
case hd4320p48
|
||||
case hd4320p30
|
||||
case hd4320p25
|
||||
case hd4320p24
|
||||
|
||||
// 5K (16:9) Resolutions
|
||||
case hd2560p60
|
||||
case hd2560p50
|
||||
case hd2560p48
|
||||
case hd2560p30
|
||||
case hd2560p25
|
||||
case hd2560p24
|
||||
|
||||
// 2:1 Aspect Ratio (Univisium) Resolutions
|
||||
case hd2880p60
|
||||
case hd2880p50
|
||||
case hd2880p48
|
||||
case hd2880p30
|
||||
case hd2880p25
|
||||
case hd2880p24
|
||||
|
||||
// 16:10 Resolutions
|
||||
case hd2400p60
|
||||
case hd2400p50
|
||||
case hd2400p48
|
||||
case hd2400p30
|
||||
case hd2400p25
|
||||
case hd2400p24
|
||||
|
||||
// 16:9 Resolutions
|
||||
case hd2160p60
|
||||
case hd2160p50
|
||||
case hd2160p48
|
||||
case hd2160p30
|
||||
case hd2160p25
|
||||
case hd2160p24
|
||||
|
||||
// 16:10 Resolutions
|
||||
case hd1600p60
|
||||
case hd1600p50
|
||||
case hd1600p48
|
||||
case hd1600p30
|
||||
case hd1600p25
|
||||
case hd1600p24
|
||||
|
||||
// 16:9 Resolutions
|
||||
case hd1440p60
|
||||
case hd1440p50
|
||||
case hd1440p48
|
||||
case hd1440p30
|
||||
case hd1440p25
|
||||
case hd1440p24
|
||||
|
||||
// 16:10 Resolutions
|
||||
case hd1280p60
|
||||
case hd1280p50
|
||||
case hd1280p48
|
||||
case hd1280p30
|
||||
case hd1280p25
|
||||
case hd1280p24
|
||||
|
||||
// 16:10 Resolutions
|
||||
case hd1200p60
|
||||
case hd1200p50
|
||||
case hd1200p48
|
||||
case hd1200p30
|
||||
case hd1200p25
|
||||
case hd1200p24
|
||||
|
||||
// 16:9 Resolutions
|
||||
case hd1080p60
|
||||
case hd1080p50
|
||||
case hd1080p48
|
||||
case hd1080p30
|
||||
case hd1080p25
|
||||
case hd1080p24
|
||||
|
||||
// 16:10 Resolutions
|
||||
case hd1050p60
|
||||
case hd1050p50
|
||||
case hd1050p48
|
||||
case hd1050p30
|
||||
case hd1050p25
|
||||
case hd1050p24
|
||||
|
||||
// 16:9 Resolutions
|
||||
case hd960p60
|
||||
case hd960p50
|
||||
case hd960p48
|
||||
case hd960p30
|
||||
case hd960p25
|
||||
case hd960p24
|
||||
|
||||
// 16:10 Resolutions
|
||||
case hd900p60
|
||||
case hd900p50
|
||||
case hd900p48
|
||||
case hd900p30
|
||||
case hd900p25
|
||||
case hd900p24
|
||||
|
||||
// 16:10 Resolutions
|
||||
case hd800p60
|
||||
case hd800p50
|
||||
case hd800p48
|
||||
case hd800p30
|
||||
case hd800p25
|
||||
case hd800p24
|
||||
|
||||
// 16:9 Resolutions
|
||||
case hd720p60
|
||||
case hd720p50
|
||||
case hd720p48
|
||||
case hd720p30
|
||||
case hd720p25
|
||||
case hd720p24
|
||||
|
||||
// Standard Definition (SD) Resolutions
|
||||
case sd854p30
|
||||
case sd854p25
|
||||
case sd768p30
|
||||
case sd768p25
|
||||
case sd640p30
|
||||
case sd640p25
|
||||
case sd480p30
|
||||
case sd480p25
|
||||
|
||||
case sd428p30
|
||||
case sd428p25
|
||||
case sd360p30
|
||||
case sd360p25
|
||||
case sd320p30
|
||||
case sd320p25
|
||||
case sd240p30
|
||||
case sd240p25
|
||||
case sd214p30
|
||||
case sd214p25
|
||||
case sd144p30
|
||||
case sd144p25
|
||||
case sd128p30
|
||||
case sd128p25
|
||||
|
||||
case unknown
|
||||
|
||||
var name: String {
|
||||
@@ -59,22 +186,94 @@ class Stream: Equatable, Hashable, Identifiable {
|
||||
|
||||
var bitrate: Int {
|
||||
switch self {
|
||||
case .hd2160p60, .hd2160p50, .hd2160p48, .hd2160p30:
|
||||
// 8K UHD (16:9) Resolutions
|
||||
case .hd4320p60, .hd4320p50, .hd4320p48, .hd4320p30, .hd4320p25, .hd4320p24:
|
||||
return 85_000_000 // 85 Mbit/s
|
||||
|
||||
// 5K (16:9) Resolutions
|
||||
case .hd2880p60, .hd2880p50, .hd2880p48, .hd2880p30, .hd2880p25, .hd2880p24:
|
||||
return 45_000_000 // 45 Mbit/s
|
||||
|
||||
// 2:1 Aspect Ratio (Univisium) Resolutions
|
||||
case .hd2560p60, .hd2560p50, .hd2560p48, .hd2560p30, .hd2560p25, .hd2560p24:
|
||||
return 30_000_000 // 30 Mbit/s
|
||||
|
||||
// 16:10 Resolutions
|
||||
case .hd2400p60, .hd2400p50, .hd2400p48, .hd2400p30, .hd2400p25, .hd2400p24:
|
||||
return 35_000_000 // 35 Mbit/s
|
||||
|
||||
// 4K UHD (16:9) Resolutions
|
||||
case .hd2160p60, .hd2160p50, .hd2160p48, .hd2160p30, .hd2160p25, .hd2160p24:
|
||||
return 56_000_000 // 56 Mbit/s
|
||||
case .hd1440p60, .hd1440p50, .hd1440p48, .hd1440p30:
|
||||
|
||||
// 16:10 Resolutions
|
||||
case .hd1600p60, .hd1600p50, .hd1600p48, .hd1600p30, .hd1600p25, .hd1600p24:
|
||||
return 20_000_000 // 20 Mbit/s
|
||||
|
||||
// 1440p (16:9) Resolutions
|
||||
case .hd1440p60, .hd1440p50, .hd1440p48, .hd1440p30, .hd1440p25, .hd1440p24:
|
||||
return 24_000_000 // 24 Mbit/s
|
||||
case .hd1080p60, .hd1080p50, .hd1080p48, .hd1080p30:
|
||||
|
||||
// 1280p (16:10) Resolutions
|
||||
case .hd1280p60, .hd1280p50, .hd1280p48, .hd1280p30, .hd1280p25, .hd1280p24:
|
||||
return 15_000_000 // 15 Mbit/s
|
||||
|
||||
// 1200p (16:10) Resolutions
|
||||
case .hd1200p60, .hd1200p50, .hd1200p48, .hd1200p30, .hd1200p25, .hd1200p24:
|
||||
return 18_000_000 // 18 Mbit/s
|
||||
|
||||
// 1080p (16:9) Resolutions
|
||||
case .hd1080p60, .hd1080p50, .hd1080p48, .hd1080p30, .hd1080p25, .hd1080p24:
|
||||
return 12_000_000 // 12 Mbit/s
|
||||
case .hd720p60, .hd720p50, .hd720p48, .hd720p30:
|
||||
|
||||
// 1050p (16:10) Resolutions
|
||||
case .hd1050p60, .hd1050p50, .hd1050p48, .hd1050p30, .hd1050p25, .hd1050p24:
|
||||
return 10_000_000 // 10 Mbit/s
|
||||
|
||||
// 960p Resolutions
|
||||
case .hd960p60, .hd960p50, .hd960p48, .hd960p30, .hd960p25, .hd960p24:
|
||||
return 8_000_000 // 8 Mbit/s
|
||||
|
||||
// 900p (16:10) Resolutions
|
||||
case .hd900p60, .hd900p50, .hd900p48, .hd900p30, .hd900p25, .hd900p24:
|
||||
return 7_000_000 // 7 Mbit/s
|
||||
|
||||
// 800p (16:10) Resolutions
|
||||
case .hd800p60, .hd800p50, .hd800p48, .hd800p30, .hd800p25, .hd800p24:
|
||||
return 6_000_000 // 6 Mbit/s
|
||||
|
||||
// 720p (16:9) Resolutions
|
||||
case .hd720p60, .hd720p50, .hd720p48, .hd720p30, .hd720p25, .hd720p24:
|
||||
return 9_500_000 // 9.5 Mbit/s
|
||||
case .sd480p30:
|
||||
|
||||
// Standard Definition (SD) Resolutions
|
||||
case .sd854p30, .sd854p25, .sd768p30, .sd768p25, .sd640p30, .sd640p25:
|
||||
return 4_000_000 // 4 Mbit/s
|
||||
case .sd360p30:
|
||||
|
||||
case .sd480p30, .sd480p25:
|
||||
return 2_500_000 // 2.5 Mbit/s
|
||||
|
||||
case .sd428p30, .sd428p25:
|
||||
return 2_000_000 // 2 Mbit/s
|
||||
|
||||
case .sd360p30, .sd360p25:
|
||||
return 1_500_000 // 1.5 Mbit/s
|
||||
case .sd240p30:
|
||||
|
||||
case .sd320p30, .sd320p25:
|
||||
return 1_200_000 // 1.2 Mbit/s
|
||||
|
||||
case .sd240p30, .sd240p25:
|
||||
return 1_000_000 // 1 Mbit/s
|
||||
case .sd144p30:
|
||||
|
||||
case .sd214p30, .sd214p25:
|
||||
return 800_000 // 0.8 Mbit/s
|
||||
|
||||
case .sd144p30, .sd144p25:
|
||||
return 600_000 // 0.6 Mbit/s
|
||||
|
||||
case .sd128p30, .sd128p25:
|
||||
return 400_000 // 0.4 Mbit/s
|
||||
|
||||
case .unknown:
|
||||
return 0
|
||||
}
|
||||
|
||||
@@ -5,10 +5,27 @@ final class ThumbnailsModel: ObservableObject {
|
||||
static var shared = ThumbnailsModel()
|
||||
|
||||
@Published var unloadable = Set<URL>()
|
||||
private var retryCounts = [URL: Int]()
|
||||
private let maxRetries = 3
|
||||
private let retryDelay: TimeInterval = 1.0
|
||||
|
||||
func insertUnloadable(_ url: URL) {
|
||||
DispatchQueue.main.async {
|
||||
self.unloadable.insert(url)
|
||||
let retries = (retryCounts[url] ?? 0) + 1
|
||||
|
||||
if retries >= maxRetries {
|
||||
DispatchQueue.main.async {
|
||||
self.unloadable.insert(url)
|
||||
self.retryCounts.removeValue(forKey: url)
|
||||
}
|
||||
} else {
|
||||
DispatchQueue.main.async {
|
||||
self.retryCounts[url] = retries
|
||||
}
|
||||
DispatchQueue.global().asyncAfter(deadline: .now() + retryDelay) {
|
||||
DispatchQueue.main.async {
|
||||
self.retryCounts[url] = retries
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,2 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<svg width="512pt" height="512pt" version="1.0" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg"><g><rect x="-.0072516" y=".00056299" width="512.01" height="512.02" fill="#575757" stroke-width=".063019"/><path d="m247.17 455.95c-19.792-0.78921-38.719-4.2564-57.154-10.47-60.968-20.55-108.68-68.579-127-127.86-7.8955-25.538-10.062-53.943-6.2586-82.067 3.7105-27.439 13.603-53.515 29.342-77.344 12.069-18.273 29.138-36.277 47.228-49.816 36.891-27.61 85.944-42.49 132.38-40.157 25.88 1.3001 49.939 6.765 73.106 16.606 8.1948 3.481 20.024 9.6845 27.696 14.525 14.15 8.9272 22.367 15.498 34.482 27.573 13.254 13.211 22.128 24.276 30.398 37.906 7.2081 11.879 14.099 27.15 18.229 40.397 1.5996 5.1305 4.442 16.456 5.6852 22.653 2.3908 11.917 2.6998 15.722 2.7049 33.312 6e-3 18.515-0.46256 24.413-2.9166 36.758-9.3274 46.92-35.58 88.167-74.872 117.64-22.814 17.112-50.027 29.535-78.547 35.858-16.714 3.7059-35.421 5.2453-54.498 4.4846zm-35.1-78.786c-5.3e-4 -0.52647-0.0741-2.0564-0.16311-3.3999l-0.16178-2.4427-4.7018-0.26271c-4.0477-0.22614-4.7968-0.33363-5.3847-0.77253-2.0235-1.5108-1.4679-6.0695 2.2494-18.457 0.8637-2.8781 3.3371-11.321 5.4966-18.762 2.1594-7.4409 5.2002-17.836 6.7573-23.101 1.5571-5.2648 4.1948-14.282 5.8615-20.038 1.6667-5.7562 3.6145-12.4 4.3284-14.764 0.71391-2.3641 3.2583-11.037 5.6542-19.272 4.9475-17.007 8.1626-27.723 8.9438-29.811 0.51852-1.3858 0.54785-1.4139 0.99761-0.95317 0.25486 0.26106 3.8462 7.3667 7.9807 15.79 4.1345 8.4236 13.089 26.573 19.898 40.331 17.188 34.73 37.849 76.578 43.261 87.622l4.5356 9.257 11.359-0.0895c6.2475-0.0492 11.615-0.19623 11.929-0.32672 0.5614-0.23385 0.54167-0.2959-1.3723-4.3176-1.068-2.2442-8.1436-16.601-15.724-31.904-48.687-98.293-61.22-123.86-67.889-138.48-4.7022-10.309-6.9031-14.807-7.7139-15.762-0.82931-0.97742-1.6319-1.0638-2.3704-0.25525-1.1993 1.313-4.1046 10.063-9.3869 28.27-2.0569 7.0899-6.5372 22.425-9.9562 34.077-6.6396 22.629-8.5182 29.037-14.33 48.883-2.0354 6.9495-4.7977 16.369-6.1385 20.931-1.3408 4.5628-4.033 13.81-5.9826 20.549-4.304 14.877-6.136 20.889-7.3886 24.25-2.1371 5.7334-2.5723 6.3292-4.9216 6.7384-0.88855 0.15472-2.4102 0.28196-3.3815 0.28275-2.1993 3e-3 -3.5494 0.36339-4.0558 1.0863-0.42176 0.60215-0.56421 4.8802-0.18251 5.4812 0.20573 0.32388 2.4672 0.37414 23.34 0.51873l8.6151 0.0597-7e-4 -0.95723zm36.751-205.59c4.3282-0.92335 8.4607-4.943 9.4374-9.1796 0.36569-1.5862 0.32543-4.9758-0.077-6.4799-0.85108-3.1813-3.2688-6.291-6.039-7.7675-3.8111-2.0313-9.456-2.0295-13.272 5e-3 -5.9828 3.1888-8.1556 11.089-4.7878 17.408 2.6995 5.0648 8.3611 7.3754 14.738 6.015z" fill="#f0f0f0" stroke-width=".025526"/></g><g transform="matrix(.069892 0 0 -.069892 44.236 474.48)"><path d="m2787 4669c-124-65-123-255 3-319 86-44 196-16 247 62 58 87 26 211-67 258-51 26-132 26-183-1z" fill="#00b6f0" stroke="#00b6f0" stroke-width="4.25"/><path d="m2882 4108c-12-16-63-166-102-303-30-104-101-350-165-565-20-69-58-199-85-290-26-91-64-221-85-290-20-69-58-199-85-290-26-91-64-221-85-290-20-69-57-195-81-280-59-207-93-299-115-310-10-6-35-10-56-10-73 0-84-8-81-54l3-41 228-3 228-2-3 47-3 48-73 3c-66 3-74 5-84 27-13 28 0 104 37 225 13 41 47 156 75 255s66 230 85 290c18 61 56 191 85 290 28 99 66 230 85 290 18 61 56 191 85 290 85 297 123 419 131 429 5 5 17-11 28-35 10-24 192-393 403-819s447-902 523-1058l139-282h168c92 0 168 4 168 8s-75 158-166 342c-588 1183-969 1958-1033 2100-29 63-69 151-89 195-44 95-58 110-80 83z" fill="#575757"/></g></svg>
|
||||
<!-- Generated by Pixelmator Pro 3.6.7 -->
|
||||
<svg width="512" height="512" viewBox="0 0 512 512" xmlns="http://www.w3.org/2000/svg">
|
||||
<g id="Group">
|
||||
<path id="Path" fill="#f0f0f0" stroke="none" d="M 244.186371 511.752167 C 219.045975 510.71109 195.004303 506.137482 171.587616 497.941071 C 94.144188 470.833344 33.538929 407.477814 10.268302 329.279663 C 0.239193 295.592224 -2.512759 258.122925 2.318441 221.024231 C 7.031626 184.829193 19.597385 150.432068 39.58955 118.998993 C 54.919968 94.894897 76.601517 71.145599 99.579987 53.286163 C 146.440094 16.865601 208.748688 -2.762817 267.733124 0.314728 C 300.60672 2.029694 331.167175 9.238464 360.594604 22.219849 C 371.003937 26.811676 386.029724 34.994751 395.774933 41.379883 C 413.748718 53.155853 424.186218 61.823517 439.575043 77.75174 C 456.410675 95.178497 467.682678 109.774475 478.1875 127.753906 C 487.343475 143.423645 496.096527 163.56778 501.34256 181.042023 C 503.374359 187.809723 506.984924 202.749298 508.564056 210.923828 C 511.600952 226.643677 511.993439 231.662842 511.999939 254.866028 C 512.007507 279.289337 511.412323 287.069458 508.295135 303.353882 C 496.447205 365.24649 463.100311 419.655823 413.19043 458.533966 C 384.211426 481.106567 349.644592 497.493866 313.417664 505.834595 C 292.186981 510.723083 268.424774 512.753723 244.192581 511.750305 Z M 199.601273 407.824738 C 199.600616 407.13028 199.507141 405.112122 199.394073 403.339905 L 199.188583 400.117706 L 193.216202 399.771149 C 188.074692 399.472839 187.123169 399.331085 186.376404 398.752106 C 183.806091 396.759216 184.51181 390.745789 189.233658 374.405304 C 190.33078 370.608765 193.472549 359.471619 196.215607 349.656189 C 198.958557 339.840759 202.82106 326.12854 204.798935 319.183411 C 206.776825 312.238525 210.127289 300.343872 212.2444 292.751038 C 214.361496 285.15802 216.835648 276.394104 217.742447 273.275696 C 218.649307 270.157227 221.881256 258.716736 224.924591 247.853851 C 231.209076 225.419739 235.292999 211.284149 236.285294 208.529846 C 236.943924 206.701843 236.981201 206.664764 237.55249 207.272522 C 237.876221 207.616882 242.438049 216.990021 247.689819 228.101257 C 252.941574 239.212921 264.315857 263.153992 272.964874 281.302307 C 294.797607 327.11499 321.04184 382.317078 327.916321 396.885345 L 333.677551 409.096344 L 348.10614 408.978271 C 356.041901 408.913391 362.859833 408.719421 363.258698 408.547302 C 363.971802 408.238831 363.946777 408.156982 361.515564 402.851898 C 360.158997 399.891571 351.171295 380.953369 341.54248 360.767029 C 279.69873 231.107727 263.778931 197.38205 255.30777 178.09668 C 249.3349 164.497955 246.53923 158.564606 245.509338 157.30484 C 244.455933 156.015533 243.436447 155.901581 242.498398 156.96814 C 240.974991 158.700165 237.284607 170.24234 230.574875 194.259399 C 227.962112 203.611725 222.271103 223.840454 217.928177 239.210693 C 209.49437 269.060883 207.108093 277.513733 199.725769 303.692749 C 197.14035 312.859924 193.631577 325.285278 191.928467 331.303101 C 190.225357 337.321899 186.805634 349.519958 184.329178 358.409424 C 178.862122 378.033875 176.535034 385.964355 174.94397 390.397858 C 172.229355 397.960846 171.676529 398.746796 168.692398 399.28656 C 167.563736 399.490662 165.63089 399.658478 164.39711 399.659515 C 161.603485 399.663513 159.888535 400.138885 159.245316 401.092468 C 158.709564 401.88678 158.528641 407.530029 159.013474 408.322784 C 159.274811 408.750031 162.147385 408.816345 188.66066 409.00708 L 199.603806 409.085815 L 199.602936 407.82312 Z M 246.283508 136.628906 C 251.781326 135.410889 257.030548 130.108551 258.271179 124.519989 C 258.735718 122.427612 258.68457 117.95636 258.17337 115.97229 C 257.092316 111.775818 254.02124 107.673767 250.502441 105.726105 C 245.661484 103.0466 238.49118 103.04895 233.643967 105.732697 C 226.044434 109.939087 223.284454 120.360321 227.562363 128.69577 C 230.991348 135.376801 238.182877 138.424713 246.28302 136.630219 Z"/>
|
||||
<path id="Circle" fill="#575757" stroke="none" d="M 256 0 C 114.61525 0 0 114.615257 0 256 C 0 397.384735 114.61525 512 256 512 C 397.384735 512 512 397.384735 512 256 C 512 114.615257 397.384735 0 256 0 Z M 256 4 C 395.175446 4 508 116.824524 508 256 C 508 395.175446 395.175446 508 256 508 C 116.824524 508 4 395.175446 4 256 C 4 116.824524 116.824524 4 256 4 Z"/>
|
||||
</g>
|
||||
<g id="g1">
|
||||
<path id="path1" fill="#00b6f0" stroke="#00b6f0" stroke-width="0.297331" d="M 234.067764 106.178009 C 223.288239 112.003052 223.375183 129.030151 234.328568 134.765594 C 241.804688 138.70871 251.367157 136.199432 255.800674 129.209381 C 260.842682 121.41275 258.060883 110.300354 249.976257 106.088379 C 245.54274 103.758362 238.501282 103.758362 234.067764 106.178009 Z"/>
|
||||
<path id="path2" fill="#575757" stroke="none" d="M 242.34436 157.257843 C 241.282883 158.735199 236.77153 172.585571 233.321655 185.235535 C 230.667953 194.83847 224.387421 217.55304 218.72612 237.405212 C 216.956955 243.776398 213.595551 255.779999 211.207184 264.182556 C 208.907288 272.585114 205.545883 284.588745 203.688263 290.9599 C 201.919098 297.331055 198.557724 309.334686 196.169357 317.737244 C 193.869431 326.139801 190.508026 338.143433 188.650406 344.514587 C 186.881271 350.885742 183.608307 362.52005 181.485321 370.368591 C 176.266296 389.482056 173.258743 397.976929 171.312653 398.992645 C 170.428085 399.546631 168.216629 399.915985 166.359024 399.915985 C 159.901581 399.915985 158.928543 400.654663 159.193924 404.9021 L 159.459305 408.687897 L 179.627701 408.964874 L 199.796112 409.149567 L 199.530731 404.809784 L 199.265381 400.377686 L 192.807953 400.100647 C 186.969711 399.823669 186.262039 399.638977 185.377472 397.607605 C 184.227524 395.022217 185.377472 388.0047 188.650406 376.832092 C 189.800354 373.046326 192.807953 362.427704 195.28476 353.286499 C 197.761581 344.145264 201.122986 332.049255 202.803696 326.509125 C 204.395935 320.876648 207.757339 308.872986 210.322601 299.731781 C 212.799438 290.590576 216.160843 278.494598 217.841522 272.954437 C 219.433777 267.32196 222.795181 255.318329 225.360458 246.177094 C 232.879379 218.753387 236.240784 207.488464 236.948441 206.565094 C 237.390732 206.103394 238.45224 207.58078 239.425278 209.796844 C 240.309845 212.012909 256.40918 246.084747 275.073822 285.419769 C 293.738434 324.754761 314.614532 368.706543 321.337311 383.110901 L 333.632965 409.149567 L 348.493896 409.149567 C 356.632019 409.149567 363.354828 408.780212 363.354828 408.410889 C 363.354828 408.041534 356.72049 393.821838 348.670807 376.832092 C 296.657532 267.598999 262.955078 196.038818 257.293793 182.927185 C 254.728485 177.110016 251.19017 168.984467 249.421021 164.921692 C 245.52887 156.149841 244.290451 154.764771 242.34436 157.257843 Z"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
||||
|
Before Width: | Height: | Size: 3.4 KiB After Width: | Height: | Size: 6.6 KiB |
@@ -39,6 +39,30 @@ enum Constants {
|
||||
#endif
|
||||
}
|
||||
|
||||
static var isTvOS: Bool {
|
||||
#if os(tvOS)
|
||||
true
|
||||
#else
|
||||
false
|
||||
#endif
|
||||
}
|
||||
|
||||
static var isMacOS: Bool {
|
||||
#if os(macOS)
|
||||
true
|
||||
#else
|
||||
false
|
||||
#endif
|
||||
}
|
||||
|
||||
static var isIOS: Bool {
|
||||
#if os(iOS)
|
||||
true
|
||||
#else
|
||||
false
|
||||
#endif
|
||||
}
|
||||
|
||||
static var progressViewScale: Double {
|
||||
#if os(macOS)
|
||||
0.4
|
||||
|
||||
@@ -20,14 +20,14 @@ extension Defaults.Keys {
|
||||
static let showOpenActionsToolbarItem = Key<Bool>("showOpenActionsToolbarItem", default: false)
|
||||
#if os(iOS)
|
||||
static let showDocuments = Key<Bool>("showDocuments", default: false)
|
||||
static let lockPortraitWhenBrowsing = Key<Bool>("lockPortraitWhenBrowsing", default: UIDevice.current.userInterfaceIdiom == .phone)
|
||||
static let lockPortraitWhenBrowsing = Key<Bool>("lockPortraitWhenBrowsing", default: Constants.isIPhone)
|
||||
#endif
|
||||
|
||||
#if !os(tvOS)
|
||||
#if os(macOS)
|
||||
static let accountPickerDisplaysUsernameDefault = true
|
||||
#else
|
||||
static let accountPickerDisplaysUsernameDefault = UIDevice.current.userInterfaceIdiom == .pad
|
||||
static let accountPickerDisplaysUsernameDefault = Constants.isIPad
|
||||
#endif
|
||||
static let accountPickerDisplaysUsername = Key<Bool>("accountPickerDisplaysUsername", default: accountPickerDisplaysUsernameDefault)
|
||||
#endif
|
||||
@@ -41,9 +41,9 @@ extension Defaults.Keys {
|
||||
static let showChannelAvatarInVideosListing = Key<Bool>("showChannelAvatarInVideosListing", default: true)
|
||||
|
||||
static let playerButtonSingleTapGesture = Key<PlayerTapGestureAction>("playerButtonSingleTapGesture", default: .togglePlayer)
|
||||
static let playerButtonDoubleTapGesture = Key<PlayerTapGestureAction>("playerButtonDoubleTapGesture", default: .nothing)
|
||||
static let playerButtonShowsControlButtonsWhenMinimized = Key<Bool>("playerButtonShowsControlButtonsWhenMinimized", default: false)
|
||||
static let playerButtonIsExpanded = Key<Bool>("playerButtonIsExpanded", default: false)
|
||||
static let playerButtonDoubleTapGesture = Key<PlayerTapGestureAction>("playerButtonDoubleTapGesture", default: .togglePlayerVisibility)
|
||||
static let playerButtonShowsControlButtonsWhenMinimized = Key<Bool>("playerButtonShowsControlButtonsWhenMinimized", default: true)
|
||||
static let playerButtonIsExpanded = Key<Bool>("playerButtonIsExpanded", default: true)
|
||||
static let playerBarMaxWidth = Key<String>("playerBarMaxWidth", default: "600")
|
||||
static let channelOnThumbnail = Key<Bool>("channelOnThumbnail", default: false)
|
||||
static let timeOnThumbnail = Key<Bool>("timeOnThumbnail", default: true)
|
||||
@@ -64,7 +64,7 @@ extension Defaults.Keys {
|
||||
static let closeVideoOnEOF = Key<Bool>("closeVideoOnEOF", default: false)
|
||||
|
||||
#if !os(macOS)
|
||||
static let pauseOnEnteringBackground = Key<Bool>("pauseOnEnteringBackground", default: true)
|
||||
static let pauseOnEnteringBackground = Key<Bool>("pauseOnEnteringBackground", default: false)
|
||||
#endif
|
||||
|
||||
#if os(iOS)
|
||||
@@ -79,7 +79,7 @@ extension Defaults.Keys {
|
||||
|
||||
static let showChapters = Key<Bool>("showChapters", default: true)
|
||||
static let showChapterThumbnails = Key<Bool>("showChapterThumbnails", default: true)
|
||||
static let showChapterThumbnailsOnlyWhenDifferent = Key<Bool>("showChapterThumbnailsOnlyWhenDifferent", default: true)
|
||||
static let showChapterThumbnailsOnlyWhenDifferent = Key<Bool>("showChapterThumbnailsOnlyWhenDifferent", default: false)
|
||||
static let expandChapters = Key<Bool>("expandChapters", default: true)
|
||||
static let showRelated = Key<Bool>("showRelated", default: true)
|
||||
static let showInspector = Key<ShowInspectorSetting>("showInspector", default: .onlyLocal)
|
||||
@@ -94,10 +94,10 @@ extension Defaults.Keys {
|
||||
|
||||
#if os(iOS)
|
||||
static let honorSystemOrientationLock = Key<Bool>("honorSystemOrientationLock", default: true)
|
||||
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: UIDevice.current.userInterfaceIdiom == .phone)
|
||||
static let enterFullscreenInLandscape = Key<Bool>("enterFullscreenInLandscape", default: Constants.isIPhone)
|
||||
static let rotateToLandscapeOnEnterFullScreen = Key<FullScreenRotationSetting>(
|
||||
"rotateToLandscapeOnEnterFullScreen",
|
||||
default: UIDevice.current.userInterfaceIdiom == .phone ? .landscapeRight : .disabled
|
||||
default: Constants.isIPhone ? .landscapeRight : .disabled
|
||||
)
|
||||
#endif
|
||||
|
||||
@@ -116,14 +116,14 @@ extension Defaults.Keys {
|
||||
|
||||
// MARK: GROUP - Controls
|
||||
|
||||
static let avPlayerUsesSystemControls = Key<Bool>("avPlayerUsesSystemControls", default: true)
|
||||
static let avPlayerUsesSystemControls = Key<Bool>("avPlayerUsesSystemControls", default: Constants.isTvOS)
|
||||
static let horizontalPlayerGestureEnabled = Key<Bool>("horizontalPlayerGestureEnabled", default: true)
|
||||
static let seekGestureSensitivity = Key<Double>("seekGestureSensitivity", default: 30.0)
|
||||
static let seekGestureSpeed = Key<Double>("seekGestureSpeed", default: 0.5)
|
||||
|
||||
#if os(iOS)
|
||||
static let playerControlsLayoutDefault = UIDevice.current.userInterfaceIdiom == .pad ? PlayerControlsLayout.medium : .small
|
||||
static let fullScreenPlayerControlsLayoutDefault = UIDevice.current.userInterfaceIdiom == .pad ? PlayerControlsLayout.medium : .small
|
||||
static let playerControlsLayoutDefault = Constants.isIPad ? PlayerControlsLayout.medium : .small
|
||||
static let fullScreenPlayerControlsLayoutDefault = Constants.isIPad ? PlayerControlsLayout.medium : .small
|
||||
#elseif os(tvOS)
|
||||
static let playerControlsLayoutDefault = PlayerControlsLayout.tvRegular
|
||||
static let fullScreenPlayerControlsLayoutDefault = PlayerControlsLayout.tvRegular
|
||||
@@ -175,61 +175,152 @@ extension Defaults.Keys {
|
||||
|
||||
// MARK: GROUP - Quality
|
||||
|
||||
static let hd2160pMPVProfile = QualityProfile(id: "hd2160pMPVProfile", backend: .mpv, resolution: .hd2160p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd1080pMPVProfile = QualityProfile(id: "hd1080pMPVProfile", backend: .mpv, resolution: .hd1080p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd720pMPVProfile = QualityProfile(id: "hd720pMPVProfile", backend: .mpv, resolution: .hd720p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd720pAVPlayerProfile = QualityProfile(id: "hd720pAVPlayerProfile", backend: .appleAVPlayer, resolution: .hd720p30, formats: [.hls, .stream], order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let sd360pAVPlayerProfile = QualityProfile(id: "sd360pAVPlayerProfile", backend: .appleAVPlayer, resolution: .sd360p30, formats: [.hls, .stream], order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd2160p60MPVProfile = QualityProfile(id: "hd2160p60MPVProfile", backend: .mpv, resolution: .hd2160p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd1080p60MPVProfile = QualityProfile(id: "hd1080p60MPVProfile", backend: .mpv, resolution: .hd1080p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd1080pMPVProfile = QualityProfile(id: "hd1080pMPVProfile", backend: .mpv, resolution: .hd1080p30, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd720p60MPVProfile = QualityProfile(id: "hd720p60MPVProfile", backend: .mpv, resolution: .hd720p60, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd720pMPVProfile = QualityProfile(id: "hd720pMPVProfile", backend: .mpv, resolution: .hd720p30, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let sd360pMPVProfile = QualityProfile(id: "sd360pMPVProfile", backend: .mpv, resolution: .sd360p30, formats: QualityProfile.Format.allCases, order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let hd720pAVPlayerProfile = QualityProfile(id: "hd720pAVPlayerProfile", backend: .appleAVPlayer, resolution: .hd720p30, formats: [.stream, .hls], order: Array(QualityProfile.Format.allCases.indices))
|
||||
static let sd360pAVPlayerProfile = QualityProfile(id: "sd360pAVPlayerProfile", backend: .appleAVPlayer, resolution: .sd360p30, formats: [.stream, .hls], order: Array(QualityProfile.Format.allCases.indices))
|
||||
|
||||
#if os(iOS)
|
||||
static let qualityProfilesDefault = UIDevice.current.userInterfaceIdiom == .pad ? [
|
||||
hd2160pMPVProfile,
|
||||
hd1080pMPVProfile,
|
||||
hd720pMPVProfile,
|
||||
hd720pAVPlayerProfile,
|
||||
sd360pAVPlayerProfile
|
||||
] : [
|
||||
hd1080pMPVProfile,
|
||||
hd720pMPVProfile,
|
||||
hd720pAVPlayerProfile,
|
||||
sd360pAVPlayerProfile
|
||||
]
|
||||
enum QualityProfiles {
|
||||
// iPad-specific settings
|
||||
enum iPad {
|
||||
static let qualityProfilesDefault = [
|
||||
hd1080p60MPVProfile,
|
||||
hd1080pMPVProfile,
|
||||
hd720p60MPVProfile,
|
||||
hd720pMPVProfile
|
||||
]
|
||||
|
||||
static let batteryCellularProfileDefault = hd720pMPVProfile.id
|
||||
static let batteryNonCellularProfileDefault = hd720p60MPVProfile.id
|
||||
static let chargingCellularProfileDefault = hd1080pMPVProfile.id
|
||||
static let chargingNonCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
}
|
||||
|
||||
// iPhone-specific settings
|
||||
enum iPhone {
|
||||
static let qualityProfilesDefault = [
|
||||
hd1080p60MPVProfile,
|
||||
hd1080pMPVProfile,
|
||||
hd720p60MPVProfile,
|
||||
hd720pMPVProfile,
|
||||
sd360pMPVProfile
|
||||
]
|
||||
|
||||
static let batteryCellularProfileDefault = sd360pMPVProfile.id
|
||||
static let batteryNonCellularProfileDefault = hd720p60MPVProfile.id
|
||||
static let chargingCellularProfileDefault = hd720pMPVProfile.id
|
||||
static let chargingNonCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
}
|
||||
|
||||
// Access the correct profile based on device type
|
||||
static var currentProfile: (qualityProfilesDefault: [QualityProfile], batteryCellularProfileDefault: String, batteryNonCellularProfileDefault: String, chargingCellularProfileDefault: String, chargingNonCellularProfileDefault: String) {
|
||||
if Constants.isIPad {
|
||||
return (
|
||||
qualityProfilesDefault: iPad.qualityProfilesDefault,
|
||||
batteryCellularProfileDefault: iPad.batteryCellularProfileDefault,
|
||||
batteryNonCellularProfileDefault: iPad.batteryNonCellularProfileDefault,
|
||||
chargingCellularProfileDefault: iPad.chargingCellularProfileDefault,
|
||||
chargingNonCellularProfileDefault: iPad.chargingNonCellularProfileDefault
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
qualityProfilesDefault: iPhone.qualityProfilesDefault,
|
||||
batteryCellularProfileDefault: iPhone.batteryCellularProfileDefault,
|
||||
batteryNonCellularProfileDefault: iPhone.batteryNonCellularProfileDefault,
|
||||
chargingCellularProfileDefault: iPhone.chargingCellularProfileDefault,
|
||||
chargingNonCellularProfileDefault: iPhone.chargingNonCellularProfileDefault
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
static let batteryCellularProfileDefault = hd720pAVPlayerProfile.id
|
||||
static let batteryNonCellularProfileDefault = hd720pAVPlayerProfile.id
|
||||
static let chargingCellularProfileDefault = hd720pAVPlayerProfile.id
|
||||
static let chargingNonCellularProfileDefault = hd1080pMPVProfile.id
|
||||
#elseif os(tvOS)
|
||||
static let qualityProfilesDefault = [
|
||||
hd2160pMPVProfile,
|
||||
hd1080pMPVProfile,
|
||||
hd720pMPVProfile,
|
||||
hd720pAVPlayerProfile
|
||||
]
|
||||
static let batteryCellularProfileDefault = hd1080pMPVProfile.id
|
||||
static let batteryNonCellularProfileDefault = hd1080pMPVProfile.id
|
||||
static let chargingCellularProfileDefault = hd1080pMPVProfile.id
|
||||
static let chargingNonCellularProfileDefault = hd1080pMPVProfile.id
|
||||
enum QualityProfiles {
|
||||
// tvOS-specific settings
|
||||
enum tvOS {
|
||||
static let qualityProfilesDefault = [
|
||||
hd2160p60MPVProfile,
|
||||
hd1080p60MPVProfile,
|
||||
hd720p60MPVProfile,
|
||||
hd720pAVPlayerProfile
|
||||
]
|
||||
|
||||
static let batteryCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
static let batteryNonCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
static let chargingCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
static let chargingNonCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
}
|
||||
|
||||
// Access the correct profile based on device type
|
||||
static var currentProfile: (qualityProfilesDefault: [QualityProfile], batteryCellularProfileDefault: String, batteryNonCellularProfileDefault: String, chargingCellularProfileDefault: String, chargingNonCellularProfileDefault: String) {
|
||||
(
|
||||
qualityProfilesDefault: tvOS.qualityProfilesDefault,
|
||||
batteryCellularProfileDefault: tvOS.batteryCellularProfileDefault,
|
||||
batteryNonCellularProfileDefault: tvOS.batteryNonCellularProfileDefault,
|
||||
chargingCellularProfileDefault: tvOS.chargingCellularProfileDefault,
|
||||
chargingNonCellularProfileDefault: tvOS.chargingNonCellularProfileDefault
|
||||
)
|
||||
}
|
||||
}
|
||||
#else
|
||||
static let qualityProfilesDefault = [
|
||||
hd2160pMPVProfile,
|
||||
hd1080pMPVProfile,
|
||||
hd720pMPVProfile,
|
||||
hd720pAVPlayerProfile
|
||||
]
|
||||
static let batteryCellularProfileDefault = hd1080pMPVProfile.id
|
||||
static let batteryNonCellularProfileDefault = hd1080pMPVProfile.id
|
||||
static let chargingCellularProfileDefault = hd1080pMPVProfile.id
|
||||
static let chargingNonCellularProfileDefault = hd1080pMPVProfile.id
|
||||
enum QualityProfiles {
|
||||
// macOS-specific settings
|
||||
enum macOS {
|
||||
static let qualityProfilesDefault = [
|
||||
hd2160p60MPVProfile,
|
||||
hd1080p60MPVProfile,
|
||||
hd1080pMPVProfile,
|
||||
hd720p60MPVProfile
|
||||
]
|
||||
|
||||
static let batteryCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
static let batteryNonCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
static let chargingCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
static let chargingNonCellularProfileDefault = hd1080p60MPVProfile.id
|
||||
}
|
||||
|
||||
// Access the correct profile for other platforms
|
||||
static var currentProfile: (qualityProfilesDefault: [QualityProfile], batteryCellularProfileDefault: String, batteryNonCellularProfileDefault: String, chargingCellularProfileDefault: String, chargingNonCellularProfileDefault: String) {
|
||||
(
|
||||
qualityProfilesDefault: macOS.qualityProfilesDefault,
|
||||
batteryCellularProfileDefault: macOS.batteryCellularProfileDefault,
|
||||
batteryNonCellularProfileDefault: macOS.batteryNonCellularProfileDefault,
|
||||
chargingCellularProfileDefault: macOS.chargingCellularProfileDefault,
|
||||
chargingNonCellularProfileDefault: macOS.chargingNonCellularProfileDefault
|
||||
)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
static let batteryCellularProfile = Key<QualityProfile.ID>("batteryCellularProfile", default: batteryCellularProfileDefault)
|
||||
static let batteryNonCellularProfile = Key<QualityProfile.ID>("batteryNonCellularProfile", default: batteryNonCellularProfileDefault)
|
||||
static let chargingCellularProfile = Key<QualityProfile.ID>("chargingCellularProfile", default: chargingCellularProfileDefault)
|
||||
static let chargingNonCellularProfile = Key<QualityProfile.ID>("chargingNonCellularProfile", default: chargingNonCellularProfileDefault)
|
||||
static let forceAVPlayerForLiveStreams = Key<Bool>("forceAVPlayerForLiveStreams", default: true)
|
||||
|
||||
static let qualityProfiles = Key<[QualityProfile]>("qualityProfiles", default: qualityProfilesDefault)
|
||||
static let batteryCellularProfile = Key<QualityProfile.ID>(
|
||||
"batteryCellularProfile",
|
||||
default: QualityProfiles.currentProfile.batteryCellularProfileDefault
|
||||
)
|
||||
static let batteryNonCellularProfile = Key<QualityProfile.ID>(
|
||||
"batteryNonCellularProfile",
|
||||
default: QualityProfiles.currentProfile.batteryNonCellularProfileDefault
|
||||
)
|
||||
static let chargingCellularProfile = Key<QualityProfile.ID>(
|
||||
"chargingCellularProfile",
|
||||
default: QualityProfiles.currentProfile.chargingCellularProfileDefault
|
||||
)
|
||||
static let chargingNonCellularProfile = Key<QualityProfile.ID>(
|
||||
"chargingNonCellularProfile",
|
||||
default: QualityProfiles.currentProfile.chargingNonCellularProfileDefault
|
||||
)
|
||||
static let forceAVPlayerForLiveStreams = Key<Bool>(
|
||||
"forceAVPlayerForLiveStreams",
|
||||
default: true
|
||||
)
|
||||
static let qualityProfiles = Key<[QualityProfile]>(
|
||||
"qualityProfiles",
|
||||
default: QualityProfiles.currentProfile.qualityProfilesDefault
|
||||
)
|
||||
|
||||
// MARK: GROUP - History
|
||||
|
||||
|
||||
@@ -47,6 +47,7 @@ enum LanguageCodes: String, CaseIterable {
|
||||
case Vietnamese = "vi"
|
||||
case Xhosa = "xh"
|
||||
case Chinese = "zh"
|
||||
case Chinese_Hans = "zh-Hans"
|
||||
case Zulu = "zu"
|
||||
|
||||
var description: String {
|
||||
@@ -147,6 +148,8 @@ enum LanguageCodes: String, CaseIterable {
|
||||
return "Xhosa"
|
||||
case .Chinese:
|
||||
return "Chinese"
|
||||
case .Chinese_Hans:
|
||||
return "Chinese (Simplified)"
|
||||
case .Zulu:
|
||||
return "Zulu"
|
||||
}
|
||||
|
||||
@@ -248,31 +248,33 @@ struct PlayerControls: View {
|
||||
return [player.playerSize.height - inset, 500].min()!
|
||||
}
|
||||
|
||||
@ViewBuilder var controlsBackground: some View {
|
||||
ZStack {
|
||||
if player.musicMode,
|
||||
let url = controlsBackgroundURL
|
||||
{
|
||||
ThumbnailView(url: url)
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.transition(.opacity)
|
||||
.animation(.default)
|
||||
} else if player.videoForDisplay == nil {
|
||||
Color.black
|
||||
@ViewBuilder
|
||||
var controlsBackground: some View {
|
||||
GeometryReader { geometry in
|
||||
ZStack {
|
||||
if player.musicMode,
|
||||
let video = player.videoForDisplay
|
||||
{
|
||||
let thumbnail = thumbnails.best(video)
|
||||
if let url = thumbnail.url,
|
||||
let quality = thumbnail.quality
|
||||
{
|
||||
let aspectRatio = (quality == .default || quality == .high) ? Constants.aspectRatio4x3 : Constants.aspectRatio16x9
|
||||
|
||||
ThumbnailView(url: url)
|
||||
.aspectRatio(aspectRatio, contentMode: .fill)
|
||||
.frame(width: geometry.size.width, height: geometry.size.height)
|
||||
.transition(.opacity)
|
||||
.animation(.default)
|
||||
.clipped()
|
||||
}
|
||||
} else if player.videoForDisplay == nil {
|
||||
Color.black
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var controlsBackgroundURL: URL? {
|
||||
if let video = player.videoForDisplay,
|
||||
let url = thumbnails.best(video).url
|
||||
{
|
||||
return url
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
var timeline: some View {
|
||||
TimelineView(context: .player).foregroundColor(.primary)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import GLKit
|
||||
import Libmpv
|
||||
import Logging
|
||||
import MPVKit
|
||||
import OpenGLES
|
||||
|
||||
final class MPVOGLView: GLKView {
|
||||
|
||||
@@ -29,18 +29,14 @@ struct ChaptersView: View {
|
||||
ScrollView(.horizontal) {
|
||||
ScrollViewReader { scrollViewProxy in
|
||||
LazyHStack(spacing: 20) {
|
||||
chapterViews(for: chapters[...], scrollViewProxy: scrollViewProxy)
|
||||
chapterViews(for: chapters[...])
|
||||
}
|
||||
.padding(.horizontal, 15)
|
||||
.onAppear {
|
||||
if let currentChapterIndex = player.currentChapterIndex {
|
||||
scrollViewProxy.scrollTo(currentChapterIndex, anchor: .center)
|
||||
}
|
||||
scrollToCurrentChapter(scrollViewProxy)
|
||||
}
|
||||
.onChange(of: player.currentChapterIndex) { currentChapterIndex in
|
||||
if let index = currentChapterIndex {
|
||||
scrollViewProxy.scrollTo(index, anchor: .center)
|
||||
}
|
||||
.onChange(of: player.currentChapterIndex) { _ in
|
||||
scrollToCurrentChapter(scrollViewProxy)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -53,7 +49,8 @@ struct ChaptersView: View {
|
||||
}
|
||||
}
|
||||
#else
|
||||
Section { chapterViews(for: chapters[...]) }.padding(.horizontal)
|
||||
Section { chapterViews(for: chapters[...]) }
|
||||
.padding(.horizontal)
|
||||
#endif
|
||||
} else {
|
||||
#if os(iOS)
|
||||
@@ -80,7 +77,7 @@ struct ChaptersView: View {
|
||||
}
|
||||
|
||||
#if !os(tvOS)
|
||||
private func chapterViews(for chaptersToShow: ArraySlice<Chapter>, opacity: Double = 1.0, clickable: Bool = true, scrollViewProxy _: ScrollViewProxy? = nil) -> some View {
|
||||
private func chapterViews(for chaptersToShow: ArraySlice<Chapter>, opacity: Double = 1.0, clickable: Bool = true) -> some View {
|
||||
ForEach(Array(chaptersToShow.indices), id: \.self) { index in
|
||||
let chapter = chaptersToShow[index]
|
||||
ChapterView(chapter: chapter, chapterIndex: index, showThumbnail: showThumbnails)
|
||||
@@ -89,6 +86,14 @@ struct ChaptersView: View {
|
||||
.allowsHitTesting(clickable)
|
||||
}
|
||||
}
|
||||
|
||||
private func scrollToCurrentChapter(_ scrollViewProxy: ScrollViewProxy) {
|
||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { // Slight delay to ensure the view is fully rendered
|
||||
if let currentChapterIndex = player.currentChapterIndex {
|
||||
scrollViewProxy.scrollTo(currentChapterIndex, anchor: .center)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
|
||||
@@ -86,6 +86,10 @@ struct VideoActions: View {
|
||||
}
|
||||
}
|
||||
|
||||
func isAnyActionVisible() -> Bool {
|
||||
return Action.allCases.contains { isVisible($0) }
|
||||
}
|
||||
|
||||
func isActionable(_ action: Action) -> Bool {
|
||||
switch action {
|
||||
case .share:
|
||||
@@ -198,10 +202,10 @@ struct VideoActions: View {
|
||||
VStack(spacing: 3) {
|
||||
Image(systemName: systemImage)
|
||||
.frame(width: 20, height: 20)
|
||||
.foregroundColor(active ? Color("AppRedColor") : .accentColor)
|
||||
.foregroundColor(active ? Color("AppRedColor") : .primary)
|
||||
if playerActionsButtonLabelStyle.text {
|
||||
Text(name.localized())
|
||||
.foregroundColor(active ? Color("AppRedColor") : .secondary)
|
||||
.foregroundColor(active ? Color("AppRedColor") : .primary)
|
||||
.font(.caption2)
|
||||
.allowsTightening(true)
|
||||
.lineLimit(1)
|
||||
|
||||
@@ -235,15 +235,22 @@ struct VideoDetails: View {
|
||||
)
|
||||
#endif
|
||||
// swiftlint:enable trailing_closure
|
||||
|
||||
VideoActions(video: player.videoForDisplay)
|
||||
.padding(.vertical, 5)
|
||||
.frame(maxHeight: 50)
|
||||
.frame(maxWidth: .infinity)
|
||||
.borderTop(height: 0.5, color: Color("ControlsBorderColor"))
|
||||
.borderBottom(height: 0.5, color: Color("ControlsBorderColor"))
|
||||
.animation(nil, value: player.currentItem)
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
if VideoActions().isAnyActionVisible() {
|
||||
VideoActions(video: player.videoForDisplay)
|
||||
.padding(.vertical, 5)
|
||||
.frame(maxHeight: 50)
|
||||
.frame(maxWidth: .infinity)
|
||||
.borderTop(height: 0.5, color: Color("ControlsBorderColor"))
|
||||
.borderBottom(height: 0.5, color: Color("ControlsBorderColor"))
|
||||
.animation(nil, value: player.currentItem)
|
||||
.frame(minWidth: 0, maxWidth: .infinity)
|
||||
} else {
|
||||
Rectangle()
|
||||
.fill(Color.clear)
|
||||
.frame(height: 0.5)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color("ControlsBorderColor"))
|
||||
}
|
||||
|
||||
ScrollViewReader { proxy in
|
||||
pageView
|
||||
|
||||
@@ -86,6 +86,7 @@ struct InstanceForm: View {
|
||||
.autocapitalization(.none)
|
||||
.keyboardType(.URL)
|
||||
#endif
|
||||
.disableAutocorrection(true)
|
||||
|
||||
#if os(tvOS)
|
||||
VStack {
|
||||
|
||||
@@ -329,8 +329,8 @@ struct PlayerSettings: View {
|
||||
|
||||
private var rotateToLandscapeOnEnterFullScreenPicker: some View {
|
||||
Picker("Rotate when entering fullscreen on landscape video", selection: $rotateToLandscapeOnEnterFullScreen) {
|
||||
Text("Landscape left").tag(FullScreenRotationSetting.landscapeRight)
|
||||
Text("Landscape right").tag(FullScreenRotationSetting.landscapeLeft)
|
||||
Text("Landscape left").tag(FullScreenRotationSetting.landscapeLeft)
|
||||
Text("Landscape right").tag(FullScreenRotationSetting.landscapeRight)
|
||||
Text("No rotation").tag(FullScreenRotationSetting.disabled)
|
||||
}
|
||||
.modifier(SettingsPickerModifier())
|
||||
@@ -361,9 +361,9 @@ struct PlayerSettings: View {
|
||||
|
||||
private var captionsFontScaleSizePicker: some View {
|
||||
Picker("Size", selection: $captionsFontScaleSize) {
|
||||
Text("Small").tag(String("0.5"))
|
||||
Text("Small").tag(String("0.725"))
|
||||
Text("Medium").tag(String("1.0"))
|
||||
Text("Large").tag(String("2.0"))
|
||||
Text("Large").tag(String("1.5"))
|
||||
}
|
||||
.onChange(of: captionsFontScaleSize) { _ in
|
||||
PlayerModel.shared.mpvBackend.client.setSubFontSize(scaleSize: captionsFontScaleSize)
|
||||
|
||||
@@ -301,7 +301,7 @@ struct QualityProfileForm: View {
|
||||
func isFormatDisabled(_ format: QualityProfile.Format) -> Bool {
|
||||
guard backend == .appleAVPlayer else { return false }
|
||||
|
||||
let avPlayerFormats = [QualityProfile.Format.hls, .stream, .mp4]
|
||||
let avPlayerFormats = [.stream, QualityProfile.Format.hls]
|
||||
|
||||
return !avPlayerFormats.contains(format)
|
||||
}
|
||||
|
||||
@@ -191,7 +191,7 @@ struct YatteeApp: App {
|
||||
|
||||
NavigationModel.shared.tabSelection = section ?? .search
|
||||
|
||||
DispatchQueue.global(qos: .userInitiated).async {
|
||||
DispatchQueue.main.async {
|
||||
playlists.load()
|
||||
}
|
||||
|
||||
|
||||
@@ -619,9 +619,6 @@
|
||||
3797104928D3D10600D5F53C /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 3797104828D3D10600D5F53C /* SDWebImageSwiftUI */; };
|
||||
3797104B28D3D18800D5F53C /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 3797104A28D3D18800D5F53C /* SDWebImageSwiftUI */; };
|
||||
3797104D28D3D19100D5F53C /* SDWebImageSwiftUI in Frameworks */ = {isa = PBXBuildFile; productRef = 3797104C28D3D19100D5F53C /* SDWebImageSwiftUI */; };
|
||||
3797665B2C79FA6900C10DBD /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3797665A2C79FA6900C10DBD /* MPVKit */; };
|
||||
3797665D2C79FA7500C10DBD /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3797665C2C79FA7500C10DBD /* MPVKit */; };
|
||||
3797665F2C79FA7D00C10DBD /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3797665E2C79FA7D00C10DBD /* MPVKit */; };
|
||||
3797757D268922D100DD52A8 /* Siesta in Frameworks */ = {isa = PBXBuildFile; productRef = 3797757C268922D100DD52A8 /* Siesta */; };
|
||||
37977583268922F600DD52A8 /* InvidiousAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37977582268922F600DD52A8 /* InvidiousAPI.swift */; };
|
||||
37977584268922F600DD52A8 /* InvidiousAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37977582268922F600DD52A8 /* InvidiousAPI.swift */; };
|
||||
@@ -1080,6 +1077,9 @@
|
||||
E258F38A2BF61BD2005B8C28 /* URLTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = E258F3892BF61BD2005B8C28 /* URLTester.swift */; };
|
||||
E258F38B2BF61BD2005B8C28 /* URLTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = E258F3892BF61BD2005B8C28 /* URLTester.swift */; };
|
||||
E258F38C2BF61BD2005B8C28 /* URLTester.swift in Sources */ = {isa = PBXBuildFile; fileRef = E258F3892BF61BD2005B8C28 /* URLTester.swift */; };
|
||||
E265D0C22C7D217000D2BB8E /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = E265D0C12C7D217000D2BB8E /* MPVKit */; };
|
||||
E265D0C42C7D218A00D2BB8E /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = E265D0C32C7D218A00D2BB8E /* MPVKit */; };
|
||||
E265D0C62C7D21A300D2BB8E /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = E265D0C52C7D21A300D2BB8E /* MPVKit */; };
|
||||
E27568B92BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */; };
|
||||
E27568BA2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */; };
|
||||
E27568BB2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */ = {isa = PBXBuildFile; fileRef = E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */; };
|
||||
@@ -1599,7 +1599,7 @@
|
||||
3797104928D3D10600D5F53C /* SDWebImageSwiftUI in Frameworks */,
|
||||
37BD07B92698AB2E003EBB87 /* Siesta in Frameworks */,
|
||||
37FB284D2722099E00A57617 /* SDWebImageWebPCoder in Frameworks */,
|
||||
3797665B2C79FA6900C10DBD /* MPVKit in Frameworks */,
|
||||
E265D0C22C7D217000D2BB8E /* MPVKit in Frameworks */,
|
||||
37CF8B8428535E4F00B71E37 /* SDWebImage in Frameworks */,
|
||||
37C7367A2AC33010007630E1 /* SwiftUIIntrospect in Frameworks */,
|
||||
);
|
||||
@@ -1624,7 +1624,7 @@
|
||||
375B8AB728B583BD00397B31 /* KeychainAccess in Frameworks */,
|
||||
3703205E27D2BB12007A0CB8 /* SDWebImageWebPCoder in Frameworks */,
|
||||
37CF8B8628535E5A00B71E37 /* SDWebImage in Frameworks */,
|
||||
3797665D2C79FA7500C10DBD /* MPVKit in Frameworks */,
|
||||
E265D0C42C7D218A00D2BB8E /* MPVKit in Frameworks */,
|
||||
3703205C27D2BAF3007A0CB8 /* SwiftyJSON in Frameworks */,
|
||||
);
|
||||
runOnlyForDeploymentPostprocessing = 0;
|
||||
@@ -1670,7 +1670,7 @@
|
||||
372915E42687E33E00F5A35B /* Defaults in Frameworks */,
|
||||
3772003B27E8EEC800CB2475 /* libbz2.tbd in Frameworks */,
|
||||
37BADCA9269A570B009BE4FB /* Alamofire in Frameworks */,
|
||||
3797665F2C79FA7D00C10DBD /* MPVKit in Frameworks */,
|
||||
E265D0C62C7D21A300D2BB8E /* MPVKit in Frameworks */,
|
||||
37D4B19D2671817900C925CA /* SwiftyJSON in Frameworks */,
|
||||
3797757D268922D100DD52A8 /* Siesta in Frameworks */,
|
||||
);
|
||||
@@ -2585,7 +2585,7 @@
|
||||
371AC0AB294D1A490085989E /* CachedAsyncImage */,
|
||||
379325D429A265A300181CF1 /* Logging */,
|
||||
37C736792AC33010007630E1 /* SwiftUIIntrospect */,
|
||||
3797665A2C79FA6900C10DBD /* MPVKit */,
|
||||
E265D0C12C7D217000D2BB8E /* MPVKit */,
|
||||
);
|
||||
productName = "Yattee (iOS)";
|
||||
productReference = 37D4B0C92671614900C925CA /* Yattee.app */;
|
||||
@@ -2624,7 +2624,7 @@
|
||||
371AC0B1294D1C230085989E /* CachedAsyncImage */,
|
||||
379325D629A265AE00181CF1 /* Logging */,
|
||||
37C736772AC32B28007630E1 /* SwiftUIIntrospect */,
|
||||
3797665C2C79FA7500C10DBD /* MPVKit */,
|
||||
E265D0C32C7D218A00D2BB8E /* MPVKit */,
|
||||
);
|
||||
productName = "Yattee (macOS)";
|
||||
productReference = 37D4B0CF2671614900C925CA /* Yattee.app */;
|
||||
@@ -2702,7 +2702,7 @@
|
||||
377F9F75294403880043F856 /* Cache */,
|
||||
371AC0B3294D1C290085989E /* CachedAsyncImage */,
|
||||
379325D829A265B500181CF1 /* Logging */,
|
||||
3797665E2C79FA7D00C10DBD /* MPVKit */,
|
||||
E265D0C52C7D21A300D2BB8E /* MPVKit */,
|
||||
);
|
||||
productName = Yattee;
|
||||
productReference = 37D4B158267164AE00C925CA /* Yattee.app */;
|
||||
@@ -2822,7 +2822,7 @@
|
||||
374D11E52943C56300CB4350 /* XCRemoteSwiftPackageReference "Cache" */,
|
||||
371AC0AA294D1A490085989E /* XCRemoteSwiftPackageReference "swiftui-cached-async-image" */,
|
||||
379325D329A265A300181CF1 /* XCRemoteSwiftPackageReference "swift-log" */,
|
||||
379766592C79FA6900C10DBD /* XCRemoteSwiftPackageReference "MPVKit" */,
|
||||
E265D0C02C7D217000D2BB8E /* XCRemoteSwiftPackageReference "MPVKit" */,
|
||||
);
|
||||
productRefGroup = 37D4B0CA2671614900C925CA /* Products */;
|
||||
projectDirPath = "";
|
||||
@@ -4103,7 +4103,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "Open in Yattee/Open in Yattee.entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 193;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
|
||||
@@ -4134,7 +4134,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 193;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
INFOPLIST_FILE = "Open in Yattee/Info.plist";
|
||||
@@ -4165,7 +4165,7 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 193;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
@@ -4185,7 +4185,7 @@
|
||||
buildSettings = {
|
||||
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 193;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
|
||||
MACOSX_DEPLOYMENT_TARGET = 11.0;
|
||||
@@ -4348,7 +4348,7 @@
|
||||
CODE_SIGN_ENTITLEMENTS = "iOS/Yattee (iOS).entitlements";
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 193;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = (
|
||||
"DEBUG=1",
|
||||
@@ -4400,7 +4400,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 193;
|
||||
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
|
||||
@@ -4452,7 +4452,7 @@
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 193;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
ENABLE_HARDENED_RUNTIME = YES;
|
||||
@@ -4491,7 +4491,7 @@
|
||||
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 193;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
"DEVELOPMENT_TEAM[sdk=macosx*]" = 78Z5H3M6RJ;
|
||||
ENABLE_APP_SANDBOX = YES;
|
||||
@@ -4525,7 +4525,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 193;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -4548,7 +4548,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 193;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -4573,7 +4573,7 @@
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 193;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -4597,7 +4597,7 @@
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
COMBINE_HIDPI_IMAGES = YES;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 193;
|
||||
DEAD_CODE_STRIPPING = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
@@ -4623,7 +4623,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_IDENTITY = "Apple Development";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 193;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
ENABLE_PREVIEWS = YES;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
@@ -4663,7 +4663,7 @@
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Distribution";
|
||||
CODE_SIGN_STYLE = Manual;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 193;
|
||||
DEVELOPMENT_ASSET_PATHS = "";
|
||||
"DEVELOPMENT_TEAM[sdk=appletvos*]" = 78Z5H3M6RJ;
|
||||
ENABLE_PREVIEWS = YES;
|
||||
@@ -4703,7 +4703,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 193;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -4726,7 +4726,7 @@
|
||||
isa = XCBuildConfiguration;
|
||||
buildSettings = {
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 189;
|
||||
CURRENT_PROJECT_VERSION = 193;
|
||||
GENERATE_INFOPLIST_FILE = YES;
|
||||
LD_RUNPATH_SEARCH_PATHS = (
|
||||
"$(inherited)",
|
||||
@@ -4960,14 +4960,6 @@
|
||||
minimumVersion = 2.1.0;
|
||||
};
|
||||
};
|
||||
379766592C79FA6900C10DBD /* XCRemoteSwiftPackageReference "MPVKit" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/cxfksword/MPVKit";
|
||||
requirement = {
|
||||
kind = upToNextMajorVersion;
|
||||
minimumVersion = 0.38.0;
|
||||
};
|
||||
};
|
||||
3797757B268922D100DD52A8 /* XCRemoteSwiftPackageReference "siesta" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/bustoutsolutions/siesta";
|
||||
@@ -5040,6 +5032,14 @@
|
||||
minimumVersion = 0.3.0;
|
||||
};
|
||||
};
|
||||
E265D0C02C7D217000D2BB8E /* XCRemoteSwiftPackageReference "MPVKit" */ = {
|
||||
isa = XCRemoteSwiftPackageReference;
|
||||
repositoryURL = "https://github.com/mpvkit/MPVKit.git";
|
||||
requirement = {
|
||||
kind = exactVersion;
|
||||
version = "0.38.0-fix";
|
||||
};
|
||||
};
|
||||
/* End XCRemoteSwiftPackageReference section */
|
||||
|
||||
/* Begin XCSwiftPackageProductDependency section */
|
||||
@@ -5228,21 +5228,6 @@
|
||||
package = 3797104728D3D10600D5F53C /* XCRemoteSwiftPackageReference "SDWebImageSwiftUI" */;
|
||||
productName = SDWebImageSwiftUI;
|
||||
};
|
||||
3797665A2C79FA6900C10DBD /* MPVKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 379766592C79FA6900C10DBD /* XCRemoteSwiftPackageReference "MPVKit" */;
|
||||
productName = MPVKit;
|
||||
};
|
||||
3797665C2C79FA7500C10DBD /* MPVKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 379766592C79FA6900C10DBD /* XCRemoteSwiftPackageReference "MPVKit" */;
|
||||
productName = MPVKit;
|
||||
};
|
||||
3797665E2C79FA7D00C10DBD /* MPVKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 379766592C79FA6900C10DBD /* XCRemoteSwiftPackageReference "MPVKit" */;
|
||||
productName = MPVKit;
|
||||
};
|
||||
3797757C268922D100DD52A8 /* Siesta */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = 3797757B268922D100DD52A8 /* XCRemoteSwiftPackageReference "siesta" */;
|
||||
@@ -5328,6 +5313,21 @@
|
||||
package = 37FB285227220D8400A57617 /* XCRemoteSwiftPackageReference "SDWebImagePINPlugin" */;
|
||||
productName = SDWebImagePINPlugin;
|
||||
};
|
||||
E265D0C12C7D217000D2BB8E /* MPVKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = E265D0C02C7D217000D2BB8E /* XCRemoteSwiftPackageReference "MPVKit" */;
|
||||
productName = MPVKit;
|
||||
};
|
||||
E265D0C32C7D218A00D2BB8E /* MPVKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = E265D0C02C7D217000D2BB8E /* XCRemoteSwiftPackageReference "MPVKit" */;
|
||||
productName = MPVKit;
|
||||
};
|
||||
E265D0C52C7D21A300D2BB8E /* MPVKit */ = {
|
||||
isa = XCSwiftPackageProductDependency;
|
||||
package = E265D0C02C7D217000D2BB8E /* XCRemoteSwiftPackageReference "MPVKit" */;
|
||||
productName = MPVKit;
|
||||
};
|
||||
/* End XCSwiftPackageProductDependency section */
|
||||
|
||||
/* Begin XCVersionGroup section */
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"originHash" : "1f99971d9d21cffe56d0033bc5c38e9fcd5ff46ca5f7d19c76f5ba0a268ce4b6",
|
||||
"originHash" : "515d8e68c4a31658288fb3f94789ee539399b042082c08c39f4c03c27fd8860c",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "activelabel.swift",
|
||||
@@ -25,7 +25,7 @@
|
||||
"location" : "https://github.com/hyperoslo/Cache.git",
|
||||
"state" : {
|
||||
"branch" : "master",
|
||||
"revision" : "d2e8f5a53c601b43371fdc90277d7f64b0e89a25"
|
||||
"revision" : "81a0277cbc6b63f4e0cd6f42c4abefa1011bbfa9"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -58,10 +58,10 @@
|
||||
{
|
||||
"identity" : "mpvkit",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/cxfksword/MPVKit",
|
||||
"location" : "https://github.com/mpvkit/MPVKit.git",
|
||||
"state" : {
|
||||
"revision" : "f646e4b625e9c8a2ff22a7e0bb5557306300be5d",
|
||||
"version" : "0.38.0"
|
||||
"revision" : "ee72059235566df8b455bff15e3f83a1c9053e78",
|
||||
"version" : "0.38.0-fix"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -66,6 +66,11 @@
|
||||
value = "Yes"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
<EnvironmentVariable
|
||||
key = "IDELogRedirectionPolicy"
|
||||
value = "oslogToStdio"
|
||||
isEnabled = "YES">
|
||||
</EnvironmentVariable>
|
||||
</EnvironmentVariables>
|
||||
</LaunchAction>
|
||||
<ProfileAction
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import AVFoundation
|
||||
import Foundation
|
||||
import Logging
|
||||
import UIKit
|
||||
|
||||
final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
var orientationLock = UIInterfaceOrientationMask.all
|
||||
|
||||
private var logger = Logger(label: "stream.yattee.app.delegalate")
|
||||
private(set) static var instance: AppDelegate!
|
||||
|
||||
func application(_: UIApplication, supportedInterfaceOrientationsFor _: UIWindow?) -> UIInterfaceOrientationMask {
|
||||
@@ -12,11 +15,22 @@ final class AppDelegate: UIResponder, UIApplicationDelegate {
|
||||
|
||||
func application(_: UIApplication, didFinishLaunchingWithOptions _: [UIApplication.LaunchOptionsKey: Any]? = nil) -> Bool { // swiftlint:disable:this discouraged_optional_collection
|
||||
Self.instance = self
|
||||
#if os(iOS)
|
||||
UIViewController.swizzleHomeIndicatorProperty()
|
||||
|
||||
#if !os(macOS)
|
||||
UIViewController.swizzleHomeIndicatorProperty()
|
||||
OrientationTracker.shared.startDeviceOrientationTracking()
|
||||
|
||||
// Configure the audio session for playback
|
||||
do {
|
||||
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .moviePlayback)
|
||||
} catch {
|
||||
logger.error("Failed to set audio session category: \(error)")
|
||||
}
|
||||
|
||||
// Begin receiving remote control events
|
||||
UIApplication.shared.beginReceivingRemoteControlEvents()
|
||||
#endif
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import Cocoa
|
||||
import MPVKit
|
||||
import Libmpv
|
||||
import OpenGL.GL
|
||||
import OpenGL.GL3
|
||||
|
||||
|
||||
Reference in New Issue
Block a user