Compare commits

..

72 Commits

Author SHA1 Message Date
Arkadiusz Fal
2d7a101ce0 Bump build number to 193 2024-08-31 17:08:23 +02:00
Arkadiusz Fal
b2114174b4 Update CHANGELOG 2024-08-31 17:08:09 +02:00
Arkadiusz Fal
e9f502a486 Merge pull request #775 from stonerl/fix-crash-on-hls-live-playback 2024-08-31 14:54:08 +02:00
Toni Förster
6978e9437c fix crash on HLS live playback
fixes #774

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-31 14:22:28 +02:00
Arkadiusz Fal
2026201a5f Merge pull request #765 from stonerl/partial-fix-for-503
Update now playing info when using system controls – Partial fix for 503
2024-08-31 13:21:41 +02:00
Toni Förster
633af02577 don’t activate AVAudioSession on app start
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-31 13:19:54 +02:00
Arkadiusz Fal
1fd62f04aa Update dependencies 2024-08-31 13:08:46 +02:00
Arkadiusz Fal
e749307a0e Merge pull request #772 from stonerl/re-enter-fullscreen-on-background-return
fixed fullscreen handling for backgrounding
2024-08-31 13:08:40 +02:00
Toni Förster
d76ec881be fixed fullscreen handling for backgrounding
Currently when returning from background, the app is fullscreen but the video is in portrait mode.

Now when entering background we leave fullscreen and enter it again the the app is in foreground.

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-31 13:00:50 +02:00
Arkadiusz Fal
72a39a2c75 Merge pull request #767 from stonerl/new-defaults
Changes to defaults
2024-08-31 12:48:13 +02:00
Arkadiusz Fal
8a84db5a2d Merge pull request #766 from stonerl/music-mode-thumbnail
make thumbnail fill the view in music mode
2024-08-31 12:47:52 +02:00
Arkadiusz Fal
663c37e3d2 Merge pull request #768 from stonerl/improved-image-loading
Video Thumbnails: retry 3 times fetching from URL
2024-08-31 12:47:31 +02:00
Arkadiusz Fal
ea2b329df2 Merge pull request #769 from stonerl/new-invidious-logo
circular invidious logo
2024-08-31 12:46:55 +02:00
Arkadiusz Fal
bd79f56800 Merge pull request #770 from stonerl/correct-landscape-setting
apply correct orientation
2024-08-31 12:46:41 +02:00
Arkadiusz Fal
9a650b4ac0 Merge pull request #762 from stonerl/allow-username-and-password-in-url
Invidious: propper HTTP basic auth support
2024-08-31 12:46:33 +02:00
Toni Förster
13382270d5 Revert "fixed some potential crashes"
This reverts commit bde9aade11.
2024-08-31 02:50:56 +02:00
Toni Förster
24626c2299 apply correct orientation
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-30 23:20:47 +02:00
Toni Förster
18ac577c7f increase retry delay to 1 second
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-30 23:14:41 +02:00
Toni Förster
617af2cd20 format ordering adapted
- MP4 contains av1 encoded video which is not hardware accelerated.
- MP4 format removed from AVPlayer

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-30 22:04:31 +02:00
Toni Förster
1b778318dc circular invidious logo
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-30 20:58:42 +02:00
Toni Förster
c9ce574c7a VideoThumbnails: retry 3 times fetching from URL
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-30 20:01:25 +02:00
Toni Förster
9a1f0d7aaa Changes to defaults
- Don’t use System Controls with AVPlayer by default
- Changed default order of the formats
- no default AVPlayer profiles except for tvOS
- new constants isIOS, isTvOS, isMacOS
- ArtWork for nowPlaying is .medium on iPhone .maxres on all other platforms
- changes to the player bar defaults

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-30 16:03:35 +02:00
Toni Förster
1cb695848c make thumbnail fill the view in music mode
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-30 03:43:00 +02:00
Toni Förster
740a2f85ac updateNowPlayingInfo also with System controls
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-29 15:10:04 +02:00
Toni Förster
1a22ac71be move AVAudioSession configuration to AppDelegate
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-29 15:09:16 +02:00
Toni Förster
f0d581d512 remove autocorrect from location url TextField
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-28 18:05:40 +02:00
Toni Förster
049a42f2e8 allow basic auth with auth endpoint
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-28 17:40:16 +02:00
Toni Förster
cea2684a29 sanitise user and password in url
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-28 16:21:49 +02:00
Toni Förster
772e5016c4 make sure no log entries are lost
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-28 15:43:15 +02:00
Toni Förster
ed3d9a7d7c invidious support for basic auth urls
This adds user, password and port to the proxy and thumbnail url, if they exist.

fixes #614 & #731

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-28 15:42:35 +02:00
Toni Förster
bde9aade11 fixed some potential crashes
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-28 15:39:03 +02:00
Arkadiusz Fal
a194738bb6 Bump build number to 192 2024-08-28 13:40:12 +02:00
Arkadiusz Fal
45567254f2 Update CHANGELOG 2024-08-28 13:40:00 +02:00
Arkadiusz Fal
a3139ad059 Fix swiftformat offense 2024-08-28 13:34:36 +02:00
Arkadiusz Fal
598f17479f Merge pull request #760 from stonerl/hide-video-actions
Hide VideoActions Bar when no buttons is visible
2024-08-28 13:33:43 +02:00
Arkadiusz Fal
e888abfba9 Merge pull request #761 from stonerl/revert-new-appicons
Revert new AppIcons
2024-08-28 13:33:32 +02:00
Toni Förster
e1e53b2d36 Revert new AppIcons
This reverts commit 59da0e71b6.

Revert "Merge pull request #758 from stonerl/new-app-icons-second-try"

This reverts commit 7b26fdf400, reversing
changes made to 67b41e36d5.

Revert "Merge pull request #756 from stonerl/new-app-icons-second-try"

This reverts commit b51eadc7a9, reversing
changes made to 0c1fb02d50.
2024-08-28 12:42:57 +02:00
Toni Förster
9510d91d61 Hide VideoActions Bar when no buttons is visible
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-28 12:32:59 +02:00
Arkadiusz Fal
59da0e71b6 Fix bundle identifier 2024-08-27 22:46:07 +02:00
Arkadiusz Fal
6bdfb7368c Merge pull request #759 from stonerl/videoactions-color
Color changes to VideoActions
2024-08-27 22:26:56 +02:00
Arkadiusz Fal
7b26fdf400 Merge pull request #758 from stonerl/new-app-icons-second-try
change AppStore icon
2024-08-27 22:26:31 +02:00
Arkadiusz Fal
67b41e36d5 Merge pull request #757 from stonerl/zh-hans
Add Chinese (Simplified) - zh-Hans to LanguageCodes
2024-08-27 22:26:17 +02:00
Toni Förster
c9c60349df Color changes to VideoActions
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-27 22:02:00 +02:00
Toni Förster
6a70663f06 add more iOS icons
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-27 17:59:59 +02:00
Toni Förster
3d556d836f change the App Store Icon
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-27 16:26:56 +02:00
Toni Förster
8feeb33a55 distinguish between iOS and macOS AppIcons
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-27 15:45:54 +02:00
Toni Förster
497c3bfc12 Add Chinese (Simplified) - zh-Hans to LanguageCodes
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-27 15:33:06 +02:00
Arkadiusz Fal
b51eadc7a9 Merge pull request #756 from stonerl/new-app-icons-second-try
refreshed icons for iOS and macOS
2024-08-27 14:11:20 +02:00
Toni Förster
7d0c1180c4 refreshed icons for iOS and macOS
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-27 13:31:13 +02:00
Arkadiusz Fal
0c1fb02d50 Merge pull request #755 from yattee/revert-752-new-app-icons
Revert "refreshed icons for iOS and macOS"
2024-08-27 08:21:46 +02:00
Arkadiusz Fal
6f358fab56 Revert "refreshed icons for iOS and macOS" 2024-08-27 08:19:45 +02:00
Arkadiusz Fal
7631e2a8ed Bump build number to 191 2024-08-27 08:00:24 +02:00
Arkadiusz Fal
3a5f3fdfde Update CHANGELOG 2024-08-27 08:00:17 +02:00
Arkadiusz Fal
e3633bdaf7 Merge pull request #752 from stonerl/new-app-icons
refreshed icons for iOS and macOS
2024-08-27 07:56:31 +02:00
Arkadiusz Fal
e912d910bc Merge pull request #754 from stonerl/fix-mpv-crash-on-macos
fix mpv crashing on macOS
2024-08-27 07:56:03 +02:00
Arkadiusz Fal
5ccb0f90d5 Merge pull request #753 from stonerl/use-new-mpvkit-repo
add new MPVKit repo
2024-08-27 07:55:55 +02:00
Toni Förster
278bc343c2 fix mpv crashing on macOS
fixes #712

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-27 04:04:23 +02:00
Toni Förster
5dc197664d add new MPVKit repo
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-26 22:59:24 +02:00
Toni Förster
192550ba7a refreshed icons for iOS and macOS
- added support for adaptive/tinted Icons on iOS.
- closes #700
- new macOS icon set, courtesy of @Kongolabongo (Discourse)

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-26 15:55:36 +02:00
Arkadiusz Fal
3369e23e74 Bump build number to 190 2024-08-26 08:20:57 +02:00
Arkadiusz Fal
4381511c91 Update CHANGELOG 2024-08-26 08:20:51 +02:00
Arkadiusz Fal
af99df9b8a Merge pull request #750 from stonerl/refined-font-sclaing-for-captions
refined chapter font scaling
2024-08-26 08:16:55 +02:00
Arkadiusz Fal
21f21cc944 Merge pull request #749 from stonerl/fix-chapter-regression
fix regression and improve curentChapter handling
2024-08-26 08:16:45 +02:00
Arkadiusz Fal
e1d8bb8125 Merge pull request #748 from stonerl/fix-potential-crashes
fix some potential crashes
2024-08-26 08:16:28 +02:00
Arkadiusz Fal
d948ea6887 Merge pull request #747 from stonerl/fix-endless-loading-of-streams
Improved stream resolution handling
2024-08-26 08:16:19 +02:00
Toni Förster
66eb8051bf refined chapter font scaling
adapted the scaling of chapter fonts after some user feedback

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-26 01:27:09 +02:00
Toni Förster
95d3170d31 fix regression and improve curentChapter handling
with #745 I left som testing changes in the PR that resulted in currentChapter index not being updated. This is fixed now.

Also, the ScrollViewReader waiter 0.5s before jumping to the current Chapter. So it is always drawn correctly.

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-26 00:46:35 +02:00
Toni Förster
74b6adb247 fix some potential crashes
Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-25 23:41:06 +02:00
Toni Förster
a45522f710 Improved stream resolution handling
Invidious now reports the actual resolution and doesn’t hardcode them anymore.

See: https://github.com/iv-org/invidious/pull/4586

- Extended the list of possible resolutions in the StreamModel
- trigger videoLoadFailureHandler if no streams are available
- more logging for backend.bestPlayable

Signed-off-by: Toni Förster <toni.foerster@gmail.com>
2024-08-25 17:23:04 +02:00
Arkadiusz Fal
0b01adf6eb Bump build number to 189 2024-08-24 17:55:30 +02:00
Arkadiusz Fal
444f6bcc03 Update CHANGELOG 2024-08-24 17:55:22 +02:00
Arkadiusz Fal
3f871bce2c Fix possible crashes 2024-08-24 17:52:35 +02:00
34 changed files with 839 additions and 477 deletions

View File

@@ -1,14 +1,13 @@
## Build 188
* 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
* 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)
@@ -27,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

View File

@@ -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)

View File

@@ -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 ?? ""
]

View File

@@ -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)!)
}

View File

@@ -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)
}
}

View File

@@ -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")
}
}
}

View File

@@ -2,9 +2,9 @@ import AVFAudio
import CoreMedia
import Defaults
import Foundation
import Libmpv
import Logging
import MediaPlayer
import MPVKit
import Repeat
import SwiftUI
@@ -360,8 +360,8 @@ final class MPVBackend: PlayerBackend {
setRate(model.currentRate)
// After the video has ended, hitting play restarts the video from the beginning.
if currentTime?.seconds.formattedAsPlaybackTime() == model.playerTime.duration.seconds.formattedAsPlaybackTime() &&
currentTime!.seconds > 0 && model.playerTime.duration.seconds > 0
if let currentTime, currentTime.seconds.formattedAsPlaybackTime() == model.playerTime.duration.seconds.formattedAsPlaybackTime() &&
currentTime.seconds > 0 && model.playerTime.duration.seconds > 0
{
seek(to: 0, seekType: .loopRestart)
}
@@ -464,6 +464,8 @@ final class MPVBackend: PlayerBackend {
timeObserverThrottle.execute {
self.model.updateWatch(time: self.currentTime)
}
self.model.updateTime(self.currentTime!)
}
private func stopClientUpdates() {

View File

@@ -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)

View File

@@ -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
}

View File

@@ -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)
}
}

View File

@@ -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() {

View File

@@ -69,7 +69,10 @@ final class PlaylistsModel: ObservableObject {
.onSuccess { resource in
self.error = nil
if let playlists: [Playlist] = resource.typedContent() {
self.playlists = playlists
DispatchQueue.main.async { [weak self] in
guard let self else { return }
self.playlists = playlists
}
PlaylistsCacheModel.shared.storePlaylist(account: account, playlists: playlists)
onSuccess()
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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
}
}
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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)
}

View File

@@ -1,6 +1,6 @@
import GLKit
import Libmpv
import Logging
import MPVKit
import OpenGLES
final class MPVOGLView: GLKView {

View File

@@ -1,211 +0,0 @@
import AVKit
import Defaults
import SwiftUI
final class PlayerViewController: UIViewController {
var playerLoaded = false
var commentsModel: CommentsModel!
var navigationModel: NavigationModel!
var playerModel: PlayerModel!
var subscriptionsModel: SubscriptionsModel!
var playerView = AVPlayerViewController()
let persistenceController = PersistenceController.shared
#if !os(tvOS)
var aspectRatio: Double? {
let ratio = Double(playerView.videoBounds.width) / Double(playerView.videoBounds.height)
guard ratio.isFinite else {
return VideoPlayerView.defaultAspectRatio // swiftlint:disable:this implicit_return
}
return [ratio, 1.0].max()!
}
#endif
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
loadPlayer()
#if os(tvOS)
if !playerView.isBeingPresented, !playerView.isBeingDismissed {
present(playerView, animated: false)
}
#endif
}
#if os(tvOS)
override func viewDidDisappear(_ animated: Bool) {
super.viewDidDisappear(animated)
if !playerModel.presentingPlayer, !Defaults[.pauseOnHidingPlayer], !playerModel.isPlaying {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) { [weak self] in
self?.playerModel.play()
}
}
}
#endif
func loadPlayer() {
guard !playerLoaded else {
return
}
playerModel.controller = self
playerView.player = playerModel.player
playerView.allowsPictureInPicturePlayback = true
#if os(iOS)
if #available(iOS 14.2, *) {
playerView.canStartPictureInPictureAutomaticallyFromInline = true
}
#endif
playerView.delegate = self
#if os(tvOS)
var infoViewControllers = [UIHostingController<AnyView>]()
if CommentsModel.enabled {
infoViewControllers.append(infoViewController([.comments], title: "Comments"))
}
var queueSections = [NowPlayingView.ViewSection.playingNext]
if Defaults[.showHistoryInPlayer] {
queueSections.append(.playedPreviously)
}
infoViewControllers.append(contentsOf: [
infoViewController([.related], title: "Related"),
infoViewController(queueSections, title: "Queue")
])
playerView.customInfoViewControllers = infoViewControllers
#else
embedViewController()
#endif
}
#if os(tvOS)
func infoViewController(
_ sections: [NowPlayingView.ViewSection],
title: String
) -> UIHostingController<AnyView> {
let controller = UIHostingController(
rootView:
AnyV/Users/arek/Developer/Yattee/Shared/Player/PlayerViewController.swift.iew(
NowPlayingView(sections: sections, inInfoViewController: true)
.frame(maxHeight: 600)
.environmentObject(commentsModel)
.environmentObject(playerModel)
.environmentObject(subscriptionsModel)
.environment(\.managedObjectContext, persistenceController.container.viewContext)
)
)
controller.title = title
return controller
}
#else
func embedViewController() {
playerView.view.frame = view.bounds
addChild(playerView)
view.addSubview(playerView.view)
playerView.didMove(toParent: self)
}
#endif
}
extension PlayerViewController: AVPlayerViewControllerDelegate {
func playerViewControllerShouldDismiss(_: AVPlayerViewController) -> Bool {
true
}
func playerViewControllerShouldAutomaticallyDismissAtPictureInPictureStart(_: AVPlayerViewController) -> Bool {
true
}
func playerViewControllerWillBeginDismissalTransition(_: AVPlayerViewController) {
if Defaults[.pauseOnHidingPlayer] {
playerModel.pause()
}
dismiss(animated: false)
}
func playerViewControllerDidEndDismissalTransition(_: AVPlayerViewController) {}
func playerViewController(
_: AVPlayerViewController,
willBeginFullScreenPresentationWithAnimationCoordinator context: UIViewControllerTransitionCoordinator
) {
playerModel.playingFullscreen = true
#if os(iOS)
if !context.isCancelled, Defaults[.lockLandscapeWhenEnteringFullscreen] {
Orientation.lockOrientation(.landscape, andRotateTo: UIDevice.current.orientation.isLandscape ? nil : .landscapeRight)
}
#endif
}
func playerViewController(
_: AVPlayerViewController,
willEndFullScreenPresentationWithAnimationCoordinator coordinator: UIViewControllerTransitionCoordinator
) {
let wasPlaying = playerModel.isPlaying
coordinator.animate(alongsideTransition: nil) { context in
#if os(iOS)
if wasPlaying {
self.playerModel.play()
}
#endif
if !context.isCancelled {
#if os(iOS)
self.playerModel.lockedOrientation = nil
if Defaults[.enterFullscreenInLandscape] {
Orientation.lockOrientation(.portrait, andRotateTo: .portrait)
}
self.playerModel.playingFullscreen = false
if wasPlaying {
self.playerModel.play()
}
#endif
}
}
}
func playerViewController(
_: AVPlayerViewController,
restoreUserInterfaceForPictureInPictureStopWithCompletionHandler completionHandler: @escaping (Bool) -> Void
) {
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
if self.navigationModel.presentingChannel {
self.playerModel.playerNavigationLinkActive = true
} else {
self.playerModel.show()
}
#if os(tvOS)
if self.playerModel.playingInPictureInPicture {
self.present(self.playerView, animated: false) {
completionHandler(true)
}
}
#else
completionHandler(true)
#endif
}
}
func playerViewControllerWillStartPictureInPicture(_: AVPlayerViewController) {
playerModel.playingInPictureInPicture = true
playerModel.playerNavigationLinkActive = false
}
func playerViewControllerWillStopPictureInPicture(_: AVPlayerViewController) {
playerModel.playingInPictureInPicture = false
}
}

View File

@@ -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
}

View File

@@ -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)

View File

@@ -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

View File

@@ -86,6 +86,7 @@ struct InstanceForm: View {
.autocapitalization(.none)
.keyboardType(.URL)
#endif
.disableAutocorrection(true)
#if os(tvOS)
VStack {

View File

@@ -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)

View File

@@ -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)
}

View File

@@ -191,7 +191,7 @@ struct YatteeApp: App {
NavigationModel.shared.tabSelection = section ?? .search
DispatchQueue.global(qos: .userInitiated).async {
DispatchQueue.main.async {
playlists.load()
}

View File

@@ -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 = 188;
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 = 188;
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 = 188;
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 = 188;
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 = 188;
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 = 188;
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 = 188;
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 = 188;
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 = 188;
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 = 188;
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 = 188;
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 = 188;
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 = 188;
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 = 188;
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 = 188;
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 = 188;
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 */

View File

@@ -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"
}
},
{

View File

@@ -66,6 +66,11 @@
value = "Yes"
isEnabled = "YES">
</EnvironmentVariable>
<EnvironmentVariable
key = "IDELogRedirectionPolicy"
value = "oslogToStdio"
isEnabled = "YES">
</EnvironmentVariable>
</EnvironmentVariables>
</LaunchAction>
<ProfileAction

View File

@@ -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
}

View File

@@ -1,5 +1,5 @@
import Cocoa
import MPVKit
import Libmpv
import OpenGL.GL
import OpenGL.GL3