Compare commits

..

38 Commits

Author SHA1 Message Date
Arkadiusz Fal
d6cfadab9a Bump build number to 185 2024-05-23 12:11:58 +02:00
Arkadiusz Fal
5b917ef91d Update CHANGELOG 2024-05-23 11:55:31 +02:00
Arkadiusz Fal
34cb7860b3 Fix build issues 2024-05-23 11:55:31 +02:00
Arkadiusz Fal
934bd65752 Fix swiftformat offenses 2024-05-23 11:44:58 +02:00
Arkadiusz Fal
e53985534e Update packages 2024-05-23 11:44:26 +02:00
Arkadiusz Fal
03e4c6d4e6 Merge pull request #689 from stonerl/more-mpv-settings
MPV: speed up playback start
2024-05-23 11:41:56 +02:00
Arkadiusz Fal
335e99cb7b Merge pull request #680 from stonerl/add-user-agent-to-header
Add User-Agent to request
2024-05-23 11:37:53 +02:00
Arkadiusz Fal
ae9aa6fac7 Merge branch 'main' into add-user-agent-to-header 2024-05-23 11:37:46 +02:00
Arkadiusz Fal
2f4fb9fc67 Merge pull request #684 from stonerl/better-caption-handling
Improved Captions handling
2024-05-23 11:37:03 +02:00
Arkadiusz Fal
f6bea6e045 Merge pull request #685 from stonerl/chapter-images-for-invidious
Invidious: add images to chapters
2024-05-23 11:35:51 +02:00
Arkadiusz Fal
fa712d8177 Merge pull request #688 from patelhiren/main
Fix thumbnails failing to load on tvOS
2024-05-23 11:34:19 +02:00
Arkadiusz Fal
03d24fbc42 Merge pull request #682 from stonerl/faster-chapter-extraction
faster chapter extraction
2024-05-23 11:34:07 +02:00
Arkadiusz Fal
4fd3a37705 Merge pull request #681 from stonerl/speed-up-sorting
speed up sorting for Stream
2024-05-23 11:33:51 +02:00
Arkadiusz Fal
a66857b1fb Merge pull request #683 from weblate/weblate-yattee-localizable-strings
Translations update from Hosted Weblate
2024-05-23 11:33:14 +02:00
Toni Förster
e44c7f84c8 MPV: speed up playback start
We initialize MPV with more options so it does not have to guess. This dramatically speeds up playback start.
2024-05-23 10:53:45 +02:00
Hiren Patel
6b5ecbdd8b - Fix thumbnails failing to load on tvOS
Thumbnails fail to load on tvOS when using SDImageAWebPCoder. Use SDImageWebPCoder on tvOS.
2024-05-21 22:58:14 -04:00
Toni Förster
15ce82a686 added en-GB to LanguageCodes 2024-05-20 21:53:48 +02:00
Toni Förster
7e3e393c65 Invidious: add images to chapters
Invidious, by design, has no images attached to chapters, in contrast to Piped.

Since the majority of videos with chapters don't have chapter-specific images and only use the videos' thumbnail, there is no difference here when compared to Piped's native thumbnail support.
2024-05-20 20:11:41 +02:00
Toni Förster
108b4de483 allow user to choose captions color 2024-05-20 17:50:47 +02:00
Toni Förster
7c9810ddf0 tvOS does not support WebKit 2024-05-20 16:03:13 +02:00
Toni Förster
96df7fdec5 let the user select caption size 2024-05-20 15:47:35 +02:00
Toni Förster
4fa5a15ad4 fallback language for captions 2024-05-20 14:42:24 +02:00
Toni Förster
c9125644ed improvements to captions on tvOS 2024-05-20 14:20:08 +02:00
Toni Förster
4db02b2638 Improved Captions handling
New options for captions in `Settings-Player`:

- Always show captions
- Default language

User can now select whether they want to show captions automatically when the video starts, and select the language.

Captions selector now shows proper name -> `English (en)` instead of only `en`
2024-05-20 03:50:51 +02:00
Mohammed Al Otaibi
9c5f066e55 Translated using Weblate (Arabic)
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/ar/
2024-05-19 23:01:50 +02:00
joaooliva
c7908d08ae Translated using Weblate (Portuguese (Brazil))
Currently translated at 100.0% (562 of 562 strings)

Translation: Yattee/Localizable.strings
Translate-URL: https://hosted.weblate.org/projects/yattee/localizable-strings/pt_BR/
2024-05-19 23:01:49 +02:00
Toni Förster
c9fb41c8e8 faster chapter extraction
The extraction of chapters is now faster since it is run in parallel for each pattern. Also a new pattern hast been added: "(start) title"
2024-05-19 17:43:35 +02:00
Toni Förster
2e9cceafa5 Add User-Agent to request
We generate a User-Agent for the platform we are running on and add it to the URLTester requests and also to MPV and AVPlayer
2024-05-19 13:46:14 +02:00
Toni Förster
fa09b2021c speed up sorting for Stream
This should help to start playback a bit faster.
2024-05-19 12:39:47 +02:00
Arkadiusz Fal
90777d91f6 Bump build number to 184 2024-05-19 11:52:38 +02:00
Arkadiusz Fal
6959778775 Update CHANGELOG 2024-05-19 11:52:38 +02:00
Arkadiusz Fal
0f43efef6f Fix build issue 2024-05-19 11:52:37 +02:00
Arkadiusz Fal
959fb0d1fc Merge pull request #676 from stonerl/fix-pip-broken-with-mpv
fix PiP Mode Not Working Using MPV
2024-05-19 09:51:38 +02:00
Arkadiusz Fal
81be57904b Merge pull request #679 from stonerl/mpv-settings
Advanced Settings: cache-pause-initial
2024-05-19 09:51:26 +02:00
Arkadiusz Fal
a42345896d Merge pull request #677 from stonerl/quality-reorderdering-iOS16
changed description for Format reordering
2024-05-19 09:51:10 +02:00
Toni Förster
43fc9e20c0 Advanced Settings: cache-pause-initial
`cache-pause-initial` status can now be selected by the user.

Also, on macOS and iOS, a link next to the option leads the user to the info on the mpv website.
2024-05-19 03:50:33 +02:00
Toni Förster
1a1bd1ba5b fix PiP Mode Not Working Using MPV
fixes #674

I accidentally broke PiP when using MPV. While testing this, I noticed that PiP sometimes does not start, so I tried to make MPV to PiP a bit more robust.
2024-05-19 00:58:52 +02:00
Toni Förster
99aca8e23c changed description for Format reordering
reordering Formats only works on iOS 16 and newer
2024-05-19 00:46:01 +02:00
25 changed files with 765 additions and 126 deletions

View File

@@ -1,8 +1,12 @@
## Build 183
* Conditional proxying by @stonerl in https://github.com/yattee/yattee/pull/662
* HomeView: Changes to Favourites and History Widget by @stonerl in https://github.com/yattee/yattee/pull/672
* Snappy UI - Offloading non UI task to background threads by @stonerl in https://github.com/yattee/yattee/pull/671
* Translations update from Hosted Weblate by @weblate in https://github.com/yattee/yattee/pull/673
## Build 185
* Fix thumbnails failing to load on tvOS by @patelhiren in https://github.com/yattee/yattee/pull/688
* speed up sorting for Stream by @stonerl in https://github.com/yattee/yattee/pull/681
* faster chapter extraction by @stonerl in https://github.com/yattee/yattee/pull/682
* Invidious: add images to chapters by @stonerl in https://github.com/yattee/yattee/pull/685
* Improved Captions handling by @stonerl in https://github.com/yattee/yattee/pull/684
* Add User-Agent to request by @stonerl in https://github.com/yattee/yattee/pull/680
* MPV: speed up playback start by @stonerl in https://github.com/yattee/yattee/pull/689
* Translations update from Hosted Weblate by @weblate in https://github.com/yattee/yattee/pull/683
## Previous builds
* Add skip, play/pause, and fullscreen shortcuts to macOS player (by @rickykresslein)
@@ -36,6 +40,12 @@
* HLS: set target bitrate / AVPlayer: higher resolution by @stonerl in https://github.com/yattee/yattee/pull/667
* Fix #619: Remove ports from shared YouTube links by @0x000C in https://github.com/yattee/yattee/pull/627
* XCode enable IDEPreferLogStreaming by @stonerl in https://github.com/yattee/yattee/pull/638
* Conditional proxying by @stonerl in https://github.com/yattee/yattee/pull/662
* HomeView: Changes to Favourites and History Widget by @stonerl in https://github.com/yattee/yattee/pull/672
* Snappy UI - Offloading non UI task to background threads by @stonerl in https://github.com/yattee/yattee/pull/671
* Fix PiP Mode Not Working Using MPV by @stonerl in https://github.com/yattee/yattee/pull/676
* Advanced Settings: cache-pause-initial by @stonerl in https://github.com/yattee/yattee/pull/679
* Changed description for Format reordering by @stonerl in https://github.com/yattee/yattee/pull/677
* Add Chinese (Traditional) localization (by @rexcsk)
* Localization fixes
* Updated localizations

View File

@@ -57,13 +57,13 @@ GEM
artifactory (3.0.17)
atomos (0.1.3)
aws-eventstream (1.3.0)
aws-partitions (1.931.0)
aws-partitions (1.933.0)
aws-sdk-core (3.196.1)
aws-eventstream (~> 1, >= 1.3.0)
aws-partitions (~> 1, >= 1.651.0)
aws-sigv4 (~> 1.8)
jmespath (~> 1, >= 1.6.1)
aws-sdk-kms (1.81.0)
aws-sdk-kms (1.82.0)
aws-sdk-core (~> 3, >= 3.193.0)
aws-sigv4 (~> 1.1)
aws-sdk-s3 (1.151.0)

View File

@@ -502,7 +502,7 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
keywords: json["keywords"].arrayValue.compactMap { $0.string },
streams: extractStreams(from: json),
related: extractRelated(from: json),
chapters: extractChapters(from: description),
chapters: createChapters(from: description, thumbnails: json),
captions: extractCaptions(from: json)
)
}
@@ -575,6 +575,22 @@ final class InvidiousAPI: Service, ObservableObject, VideosAPI {
}
}
private func createChapters(from description: String, thumbnails: JSON) -> [Chapter] {
var chapters = extractChapters(from: description)
if !chapters.isEmpty {
let thumbnailsData = extractThumbnails(from: thumbnails)
let thumbnailURL = thumbnailsData.first { $0.quality == .medium }?.url
for chapter in chapters.indices {
if let url = thumbnailURL {
chapters[chapter].image = url
}
}
}
return chapters
}
private static var contentItemsKeys = ["items", "videos", "latestVideos", "playlists", "relatedChannels"]
private func extractChannelPage(from json: JSON, forceNotLast: Bool = false) -> ChannelPage {

View File

@@ -152,58 +152,94 @@ extension VideosAPI {
/*
The following chapter patterns are covered:
start - end - title / start - end: Title / start - end title
start - title / start: title / start title / [start] - title / [start]: title / [start] title
index. title - start / index. title start
title: (start)
1) "start - end - title" / "start - end: Title" / "start - end title"
2) "start - title" / "start: title" / "start title" / "[start] - title" / "[start]: title" / "[start] title"
3) "index. title - start" / "index. title start"
4) "title: (start)"
5) "(start) title"
The order is important!
These represent:
- "start" and "end" are timestamps, defining the start and end of the individual chapter
- "title" is the name of the chapter
- "index" is the chapter's position in a list
The order of these patterns is important as it determines the priority. The patterns listed first have a higher priority.
In the case of multiple matches, the pattern with the highest priority will be chosen - lower number means higher priority.
*/
let patterns = [
"(?<=\\n|^)\\s*(?:►\\s*)?\\[?(?<start>(?:[0-9]+:){1,2}[0-9]+)\\]?(?:\\s*-\\s*)?(?<end>(?:[0-9]+:){1,2}[0-9]+)?(?:\\s*-\\s*|\\s*[:]\\s*)?(?<title>.*)(?=\\n|$)",
"(?<=\\n|^)\\s*(?:►\\s*)?\\[?(?<start>(?:[0-9]+:){1,2}[0-9]+)\\]?\\s*[-:]?\\s*(?<title>.+)(?=\\n|$)",
"(?<=\\n|^)(?<index>[0-9]+\\.\\s)(?<title>.+?)(?:\\s*-\\s*)?(?<start>(?:[0-9]+:){1,2}[0-9]+)(?=\\n|$)",
"(?<=\\n|^)(?<title>.+?):\\s*\\((?<start>(?:[0-9]+:){1,2}[0-9]+)\\)(?=\\n|$)"
"(?<=\\n|^)(?<title>.+?):\\s*\\((?<start>(?:[0-9]+:){1,2}[0-9]+)\\)(?=\\n|$)",
"(?<=^|\\n)\\((?<start>(?:[0-9]+:){1,2}[0-9]+)\\)\\s*(?<title>.+?)(?=\\n|$)"
]
for pattern in patterns {
guard let chaptersRegularExpression = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) else { continue }
let chapterLines = chaptersRegularExpression.matches(in: description, range: NSRange(description.startIndex..., in: description))
let extractChaptersGroup = DispatchGroup()
var capturedChapters: [Int: [Chapter]] = [:]
let lock = NSLock()
if !chapterLines.isEmpty {
return chapterLines.compactMap { line in
let titleRange = line.range(withName: "title")
let startRange = line.range(withName: "start")
guard let titleSubstringRange = Range(titleRange, in: description),
let startSubstringRange = Range(startRange, in: description)
else {
return nil
}
let titleCapture = String(description[titleSubstringRange]).trimmingCharacters(in: .whitespaces)
let startCapture = String(description[startSubstringRange])
let startComponents = startCapture.components(separatedBy: ":")
guard startComponents.count <= 3 else { return nil }
for (index, pattern) in patterns.enumerated() {
extractChaptersGroup.enter()
DispatchQueue.global().async {
if let chaptersRegularExpression = try? NSRegularExpression(pattern: pattern, options: .caseInsensitive) {
let chapterLines = chaptersRegularExpression.matches(in: description, range: NSRange(description.startIndex..., in: description))
let extractedChapters = chapterLines.compactMap { line -> Chapter? in
let titleRange = line.range(withName: "title")
let startRange = line.range(withName: "start")
var hours: Double?
var minutes: Double?
var seconds: Double?
guard let titleSubstringRange = Range(titleRange, in: description),
let startSubstringRange = Range(startRange, in: description)
else {
return nil
}
if startComponents.count == 3 {
hours = Double(startComponents[0])
minutes = Double(startComponents[1])
seconds = Double(startComponents[2])
} else if startComponents.count == 2 {
minutes = Double(startComponents[0])
seconds = Double(startComponents[1])
let titleCapture = String(description[titleSubstringRange]).trimmingCharacters(in: .whitespaces)
let startCapture = String(description[startSubstringRange])
let startComponents = startCapture.components(separatedBy: ":")
guard startComponents.count <= 3 else { return nil }
var hours: Double?
var minutes: Double?
var seconds: Double?
if startComponents.count == 3 {
hours = Double(startComponents[0])
minutes = Double(startComponents[1])
seconds = Double(startComponents[2])
} else if startComponents.count == 2 {
minutes = Double(startComponents[0])
seconds = Double(startComponents[1])
}
guard var startSeconds = seconds else { return nil }
startSeconds += (minutes ?? 0) * 60
startSeconds += (hours ?? 0) * 60 * 60
return Chapter(title: titleCapture, start: startSeconds)
}
guard var startSeconds = seconds else { return nil }
startSeconds += (minutes ?? 0) * 60
startSeconds += (hours ?? 0) * 60 * 60
return .init(title: titleCapture, start: startSeconds)
if !extractedChapters.isEmpty {
lock.lock()
capturedChapters[index] = extractedChapters
lock.unlock()
}
}
extractChaptersGroup.leave()
}
}
extractChaptersGroup.wait()
// Now we sort the keys of the capturedChapters dictionary.
// These keys correspond to the priority of each pattern.
let sortedKeys = Array(capturedChapters.keys).sorted(by: <)
// Return first non-empty result in the order of patterns
for key in sortedKeys {
if let chapters = capturedChapters[key], !chapters.isEmpty {
return chapters
}
}
return []

View File

@@ -232,7 +232,10 @@ final class AVPlayerBackend: PlayerBackend {
upgrading: Bool = false
) {
asset?.cancelLoading()
asset = AVURLAsset(url: url)
asset = AVURLAsset(
url: url,
options: ["AVURLAssetHTTPHeaderFieldsKey": ["User-Agent": "\(UserAgentManager.shared.userAgent)"]]
)
asset?.loadValuesAsynchronously(forKeys: Self.assetKeysToLoad) { [weak self] in
var error: NSError?
switch self?.asset?.statusOfValue(forKey: "duration", error: &error) {

View File

@@ -217,9 +217,22 @@ final class MPVBackend: PlayerBackend {
#endif
var captions: Captions?
if let captionsLanguageCode = Defaults[.captionsLanguageCode] {
captions = video.captions.first { $0.code == captionsLanguageCode } ??
video.captions.first { $0.code.contains(captionsLanguageCode) }
if Defaults[.captionsAutoShow] == true {
let captionsDefaultLanguageCode = Defaults[.captionsDefaultLanguageCode],
captionsFallbackLanguageCode = Defaults[.captionsFallbackLanguageCode]
// Try to get captions with the default language code first
captions = video.captions.first { $0.code == captionsDefaultLanguageCode } ??
video.captions.first { $0.code.contains(captionsDefaultLanguageCode) }
// If there are still no captions, try to get captions with the fallback language code
if captions.isNil && !captionsFallbackLanguageCode.isEmpty {
captions = video.captions.first { $0.code == captionsFallbackLanguageCode } ??
video.captions.first { $0.code.contains(captionsFallbackLanguageCode) }
}
} else {
captions = nil
}
let updateCurrentStream = {
@@ -254,9 +267,8 @@ final class MPVBackend: PlayerBackend {
self.startClientUpdates()
// Captions should only be displayed when selected by the user,
// not when the video starts. So, we remove them.
self.client?.removeSubs()
if Defaults[.captionsAutoShow] { self.client?.setSubToAuto() } else { self.client?.setSubToNo() }
PlayerModel.shared.captions = self.captions
if !preservingTime,
!upgrading,

View File

@@ -60,14 +60,43 @@ final class MPVClient: ObservableObject {
checkError(mpv_set_option_string(mpv, "input-media-keys", "yes"))
#endif
checkError(mpv_set_option_string(mpv, "cache-pause-initial", "yes"))
// CACHING //
checkError(mpv_set_option_string(mpv, "cache-pause-initial", Defaults[.mpvCachePauseInital] ? "yes" : "no"))
checkError(mpv_set_option_string(mpv, "cache-secs", Defaults[.mpvCacheSecs]))
checkError(mpv_set_option_string(mpv, "cache-pause-wait", Defaults[.mpvCachePauseWait]))
// PLAYBACK //
checkError(mpv_set_option_string(mpv, "keep-open", "yes"))
checkError(mpv_set_option_string(mpv, "hwdec", machine == "x86_64" ? "no" : "auto-safe"))
checkError(mpv_set_option_string(mpv, "vo", "libmpv"))
checkError(mpv_set_option_string(mpv, "demuxer-lavf-analyzeduration", "1"))
checkError(mpv_set_option_string(mpv, "deinterlace", Defaults[.mpvDeinterlace] ? "yes" : "no"))
checkError(mpv_set_option_string(mpv, "sub-scale", Defaults[.captionsFontScaleSize]))
checkError(mpv_set_option_string(mpv, "sub-color", Defaults[.captionsFontColor]))
checkError(mpv_set_option_string(mpv, "user-agent", UserAgentManager.shared.userAgent))
// GPU //
checkError(mpv_set_option_string(mpv, "hwdec", Defaults[.mpvHWdec]))
checkError(mpv_set_option_string(mpv, "vo", "libmpv"))
// We set set everything to OpenGL so MPV doesn't have to probe for other APIs.
checkError(mpv_set_option_string(mpv, "gpu-api", "opengl"))
checkError(mpv_set_option_string(mpv, "opengl-swapinterval", "0"))
#if !os(macOS)
checkError(mpv_set_option_string(mpv, "opengl-es", "yes"))
#endif
// We set this to ordered since we use OpenGL and Apple's implementation is ancient.
checkError(mpv_set_option_string(mpv, "dither", "ordered"))
// DEMUXER //
// We request to test for lavf first and skip probing other demuxer.
checkError(mpv_set_option_string(mpv, "demuxer", "lavf"))
checkError(mpv_set_option_string(mpv, "audio-demuxer", "lavf"))
checkError(mpv_set_option_string(mpv, "sub-demuxer", "lavf"))
checkError(mpv_set_option_string(mpv, "demuxer-lavf-analyzeduration", "1"))
checkError(mpv_set_option_string(mpv, "demuxer-lavf-probe-info", Defaults[.mpvDemuxerLavfProbeInfo]))
checkError(mpv_initialize(mpv))
@@ -405,6 +434,22 @@ final class MPVClient: ObservableObject {
setString("video", "no")
}
func setSubToAuto() {
setString("sub", "auto")
}
func setSubToNo() {
setString("sub", "no")
}
func setSubFontSize(scaleSize: String) {
setString("sub-scale", scaleSize)
}
func setSubFontColor(color: String) {
setString("sub-color", color)
}
var tracksCount: Int {
Int(getString("track-list/count") ?? "-1") ?? -1
}

View File

@@ -133,23 +133,22 @@ extension PlayerBackend {
}
func bestPlayable(_ streams: [Stream], maxResolution: ResolutionSetting, formatOrder: [QualityProfile.Format]) -> Stream? {
// filter out non HLS streams
let nonHLSStreams = streams.filter { $0.kind != .hls }
// filter out non-HLS streams and streams with resolution more than maxResolution
let nonHLSStreams = streams.filter {
$0.kind != .hls && $0.resolution <= maxResolution.value
}
// find max resolution from non HLS streams
let bestResolution = nonHLSStreams
.filter { $0.resolution <= maxResolution.value }
.max { $0.resolution < $1.resolution }
// 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 }
// finde max bitrate from non HLS streams
let bestBitrate = nonHLSStreams
.filter { $0.resolution <= maxResolution.value }
.max { $0.bitrate ?? 0 < $1.bitrate ?? 0 }
let bestResolution = bestResolutionStream?.resolution ?? maxResolution.value
let bestBitrate = bestBitrateStream?.bitrate ?? bestResolutionStream?.resolution.bitrate ?? maxResolution.value.bitrate
return streams.map { stream in
if stream.kind == .hls {
stream.resolution = bestResolution?.resolution ?? maxResolution.value
stream.bitrate = bestBitrate?.bitrate ?? (bestResolution?.resolution.bitrate ?? maxResolution.value.bitrate)
stream.resolution = bestResolution
stream.bitrate = bestBitrate
stream.format = .hls
} else if stream.kind == .stream {
stream.format = .stream

View File

@@ -683,10 +683,11 @@ final class PlayerModel: ObservableObject {
return
}
guard let video = currentVideo else { return }
guard let stream = backend.bestPlayable(availableStreams, maxResolution: .hd720p30, formatOrder: qualityProfile!.formats) else { return }
// First, we need to create an array with supported formats.
let formatOrderPiP: [QualityProfile.Format] = [.hls, .stream, .mp4]
exitFullScreen()
guard let video = currentVideo else { return }
guard let stream = avPlayerBackend.bestPlayable(availableStreams, maxResolution: .hd720p30, formatOrder: formatOrderPiP) else { return }
if avPlayerBackend.video == video {
if activeBackend != .appleAVPlayer {
@@ -698,7 +699,19 @@ final class PlayerModel: ObservableObject {
playStream(stream, of: video, preservingTime: true, upgrading: true, withBackend: avPlayerBackend)
}
controls.objectWillChange.send()
var retryCount = 0
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
if let pipController = self?.pipController, pipController.isPictureInPictureActive, self?.avPlayerBackend.isPlaying == true {
self?.exitFullScreen()
self?.controls.objectWillChange.send()
timer.invalidate()
} else if retryCount < 3, self?.activeBackend == .appleAVPlayer, self?.avPlayerBackend.startPictureInPictureOnSwitch == false {
// If PiP didn't start, try starting it again up to 3 times,
self?.avPlayerBackend.startPictureInPictureOnSwitch = true
self?.avPlayerBackend.tryStartingPictureInPicture()
retryCount += 1
}
}
}
var transitioningToPiP: Bool {
@@ -726,12 +739,19 @@ final class PlayerModel: ObservableObject {
show()
#endif
backend.closePiP()
if previousActiveBackend == .mpv {
saveTime {
self.changeActiveBackend(from: self.activeBackend, to: .mpv, isInClosePip: true)
self.controls.resetTimer()
_ = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] timer in
if self?.activeBackend == .mpv, self?.mpvBackend.isPlaying == true {
self?.backend.closePiP()
self?.controls.resetTimer()
timer.invalidate()
}
}
}
} else {
backend.closePiP()
}
}

View File

@@ -196,11 +196,13 @@ extension PlayerModel {
}
}
func streamsSorter(_ lhs: Stream, _ rhs: Stream) -> Bool {
if lhs.resolution.isNil || rhs.resolution.isNil {
func streamsSorter(lhs: Stream, rhs: Stream) -> Bool {
// Use optional chaining to simplify nil handling
guard let lhsRes = lhs.resolution?.height, let rhsRes = rhs.resolution?.height else {
return lhs.kind < rhs.kind
}
return lhs.kind == rhs.kind ? (lhs.resolution.height > rhs.resolution.height) : (lhs.kind < rhs.kind)
// Compare either kind or resolution based on conditions
return lhs.kind == rhs.kind ? (lhsRes > rhsRes) : (lhs.kind < rhs.kind)
}
}

View File

@@ -103,9 +103,8 @@ enum Constants {
#elseif os(iOS)
if isIPad {
return .sidebar
} else {
return .tab
}
return .tab
#else
return .tab
#endif

View File

@@ -170,7 +170,7 @@ extension Defaults.Keys {
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: .hd720p60, formats: [.hls, .stream], 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))
#if os(iOS)
@@ -266,7 +266,10 @@ extension Defaults.Keys {
static let mpvEnableLogging = Key<Bool>("mpvEnableLogging", default: false)
static let mpvCacheSecs = Key<String>("mpvCacheSecs", default: "120")
static let mpvCachePauseWait = Key<String>("mpvCachePauseWait", default: "3")
static let mpvCachePauseInital = Key<Bool>("mpvCachePauseInitial", default: false)
static let mpvDeinterlace = Key<Bool>("mpvDeinterlace", default: false)
static let mpvHWdec = Key<String>("hwdec", default: "auto-safe")
static let mpvDemuxerLavfProbeInfo = Key<String>("mpvDemuxerLavfProbeInfo", default: "no")
static let showCacheStatus = Key<Bool>("showCacheStatus", default: false)
static let feedCacheSize = Key<String>("feedCacheSize", default: "50")
@@ -300,7 +303,12 @@ extension Defaults.Keys {
static let lastPlayed = Key<PlayerQueueItem?>("lastPlayed")
static let activeBackend = Key<PlayerBackendType>("activeBackend", default: .mpv)
static let captionsAutoShow = Key<Bool>("captionsAutoShow", default: false)
static let captionsLanguageCode = Key<String?>("captionsLanguageCode")
static let captionsDefaultLanguageCode = Key<String>("captionsDefaultLanguageCode", default: LanguageCodes.English.rawValue)
static let captionsFallbackLanguageCode = Key<String>("captionsDefaultFallbackCode", default: LanguageCodes.English.rawValue)
static let captionsFontScaleSize = Key<String>("captionsFontScale", default: "1.0")
static let captionsFontColor = Key<String>("captionsFontColor", default: "#FFFFFF")
static let lastUsedPlaylistID = Key<Playlist.ID?>("lastPlaylistID")
static let lastAccountIsPublic = Key<Bool>("lastAccountIsPublic", default: false)

109
Shared/LanguageCodes.swift Normal file
View File

@@ -0,0 +1,109 @@
enum LanguageCodes: String, CaseIterable {
case Afrikaans = "af"
case Arabic = "ar"
case Azerbaijani = "az"
case Bengali = "bn"
case Catalan = "ca"
case Czech = "cs"
case Welsh = "cy"
case Danish = "da"
case German = "de"
case Greek = "el"
case English = "en"
case English_GB = "en-GB"
case Spanish = "es"
case Persian = "fa"
case Finnish = "fi"
case Filipino = "fil"
case French = "fr"
case Irish = "ga"
case Hebrew = "he"
case Hindi = "hi"
case Hungarian = "hu"
case Indonesian = "id"
case Italian = "it"
case Japanese = "ja"
case Javanese = "jv"
case Korean = "ko"
case Lithuanian = "lt"
case Malay = "ms"
case Maltese = "mt"
case Dutch = "nl"
case Norwegian = "no"
case Polish = "pl"
case Portuguese = "pt"
case Romanian = "ro"
case Russian = "ru"
case Slovak = "sk"
case Slovene = "sl"
case Swedish = "sv"
case Swahili = "sw"
case Thai = "th"
case Tagalog = "tl"
case Turkish = "tr"
case Ukrainian = "uk"
case Urdu = "ur"
case Uzbek = "uz"
case Vietnamese = "vi"
case Xhosa = "xh"
case Chinese = "zh"
case Zulu = "zu"
var description: String {
switch self {
case .Afrikaans: return "Afrikaans"
case .Arabic: return "Arabic"
case .Azerbaijani: return "Azerbaijani"
case .Bengali: return "Bengali"
case .Catalan: return "Catalan"
case .Czech: return "Czech"
case .Welsh: return "Welsh"
case .Danish: return "Danish"
case .German: return "German"
case .Greek: return "Greek"
case .English: return "English"
case .English_GB: return "English (United Kingdom)"
case .Spanish: return "Spanish"
case .Persian: return "Persian"
case .Finnish: return "Finnish"
case .Filipino: return "Filipino"
case .French: return "French"
case .Irish: return "Irish"
case .Hebrew: return "Hebrew"
case .Hindi: return "Hindi"
case .Hungarian: return "Hungarian"
case .Indonesian: return "Indonesian"
case .Italian: return "Italian"
case .Japanese: return "Japanese"
case .Javanese: return "Javanese"
case .Korean: return "Korean"
case .Lithuanian: return "Lithuanian"
case .Malay: return "Malay"
case .Maltese: return "Maltese"
case .Dutch: return "Dutch"
case .Norwegian: return "Norwegian"
case .Polish: return "Polish"
case .Portuguese: return "Portuguese"
case .Romanian: return "Romanian"
case .Russian: return "Russian"
case .Slovak: return "Slovak"
case .Slovene: return "Slovene"
case .Swedish: return "Swedish"
case .Swahili: return "Swahili"
case .Thai: return "Thai"
case .Tagalog: return "Tagalog"
case .Turkish: return "Turkish"
case .Ukrainian: return "Ukrainian"
case .Urdu: return "Urdu"
case .Uzbek: return "Uzbek"
case .Vietnamese: return "Vietnamese"
case .Xhosa: return "Xhosa"
case .Chinese: return "Chinese"
case .Zulu: return "Zulu"
}
}
static func languageName(for code: String) -> String {
return LanguageCodes(rawValue: code)?.description ?? "Unknown"
}
}

View File

@@ -333,9 +333,19 @@ struct ControlsOverlay: View {
} label: {
HStack(spacing: 4) {
Image(systemName: "text.bubble")
if let captions = captionsBinding.wrappedValue {
Text(captions.code)
.foregroundColor(.primary)
if let captions = captionsBinding.wrappedValue,
let language = LanguageCodes(rawValue: captions.code)
{
Text("\(language.description.capitalized) (\(language.rawValue))")
.foregroundColor(.accentColor)
} else {
if captionsBinding.wrappedValue == nil {
Text("Not available")
} else {
Text("Disabled")
.foregroundColor(.accentColor)
}
}
}
.frame(width: 240)
@@ -351,8 +361,18 @@ struct ControlsOverlay: View {
ControlsOverlayButton(focusedField: $focusedField, field: .captions) {
HStack(spacing: 8) {
Image(systemName: "text.bubble")
if let captions = captionsBinding.wrappedValue {
Text(captions.code)
if let captions = captionsBinding.wrappedValue,
let language = LanguageCodes(rawValue: captions.code)
{
Text("\(language.description.capitalized) (\(language.rawValue))")
.foregroundColor(.accentColor)
} else {
if captionsBinding.wrappedValue == nil {
Text("Not available")
} else {
Text("Disabled")
.foregroundColor(.accentColor)
}
}
}
.frame(maxWidth: 320)

View File

@@ -1,3 +1,4 @@
import Combine
import Defaults
import SwiftUI
@@ -383,23 +384,35 @@ struct PlaybackSettings: View {
}
@ViewBuilder private var captionsButton: some View {
let videoCaptions = player.currentVideo?.captions
#if os(macOS)
captionsPicker
.labelsHidden()
.frame(maxWidth: 300)
#elseif os(iOS)
Menu {
captionsPicker
if videoCaptions?.isEmpty == false {
captionsPicker
}
} label: {
HStack(spacing: 4) {
Image(systemName: "text.bubble")
if let captions = player.captions {
Text(captions.code)
if let captions = player.captions,
let language = LanguageCodes(rawValue: captions.code)
{
Text("\(language.description.capitalized) (\(language.rawValue))")
.foregroundColor(.accentColor)
} else {
if videoCaptions?.isEmpty == true {
Text("Not available")
} else {
Text("Disabled")
}
}
}
.frame(alignment: .trailing)
.frame(height: 40)
.disabled(videoCaptions?.isEmpty == true)
}
.transaction { t in t.animation = .none }
.buttonStyle(.plain)

View File

@@ -65,7 +65,7 @@ import SwiftUI
}
static var thumbnailHeight: Double {
thumbnailWidth / 1.7777
thumbnailWidth / (16 / 9)
}
}

View File

@@ -5,8 +5,11 @@ struct AdvancedSettings: View {
@Default(.showMPVPlaybackStats) private var showMPVPlaybackStats
@Default(.mpvCacheSecs) private var mpvCacheSecs
@Default(.mpvCachePauseWait) private var mpvCachePauseWait
@Default(.mpvCachePauseInital) private var mpvCachePauseInital
@Default(.mpvDeinterlace) private var mpvDeinterlace
@Default(.mpvEnableLogging) private var mpvEnableLogging
@Default(.mpvHWdec) private var mpvHWdec
@Default(.mpvDemuxerLavfProbeInfo) private var mpvDemuxerLavfProbeInfo
@Default(.showCacheStatus) private var showCacheStatus
@Default(.feedCacheSize) private var feedCacheSize
@Default(.showPlayNowInBackendContextMenu) private var showPlayNowInBackendContextMenu
@@ -68,9 +71,39 @@ struct AdvancedSettings: View {
mpvEnableLoggingToggle
#endif
Toggle(isOn: $mpvCachePauseInital) {
HStack {
Text("cache-pause-initial")
#if !os(tvOS)
Image(systemName: "link")
.accessibilityAddTraits([.isButton, .isLink])
.font(.footnote)
#if os(iOS)
.onTapGesture {
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-cache-pause-initial")!)
}
#elseif os(macOS)
.onHover(perform: onHover(_:))
#endif
#endif
}
}
HStack {
Text("cache-secs")
.frame(minWidth: 140, alignment: .leading)
#if !os(tvOS)
Image(systemName: "link")
.accessibilityAddTraits([.isButton, .isLink])
.font(.footnote)
#if os(iOS)
.onTapGesture {
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-cache-secs")!)
}
#elseif os(macOS)
.onHover(perform: onHover(_:))
#endif
#endif
TextField("cache-secs", text: $mpvCacheSecs)
#if !os(macOS)
.keyboardType(.numberPad)
@@ -79,8 +112,22 @@ struct AdvancedSettings: View {
.multilineTextAlignment(.trailing)
HStack {
Text("cache-pause-wait")
.frame(minWidth: 140, alignment: .leading)
Group {
Text("cache-pause-wait")
#if !os(tvOS)
Image(systemName: "link")
.accessibilityAddTraits([.isButton, .isLink])
.font(.footnote)
#if os(iOS)
.onTapGesture {
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-cache-pause-wait")!)
}
#elseif os(macOS)
.onHover(perform: onHover(_:))
#endif
#endif
}.frame(minWidth: 140, alignment: .leading)
TextField("cache-pause-wait", text: $mpvCachePauseWait)
#if !os(macOS)
.keyboardType(.numberPad)
@@ -88,7 +135,79 @@ struct AdvancedSettings: View {
}
.multilineTextAlignment(.trailing)
Toggle("deinterlace", isOn: $mpvDeinterlace)
Toggle(isOn: $mpvDeinterlace) {
HStack {
Text("deinterlace")
#if !os(tvOS)
Image(systemName: "link")
.accessibilityAddTraits([.isButton, .isLink])
.font(.footnote)
#if os(iOS)
.onTapGesture {
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-deinterlace")!)
}
#elseif os(macOS)
.onHover(perform: onHover(_:))
#endif
#endif
}
}
HStack {
Text("hwdec")
#if !os(tvOS)
Image(systemName: "link")
.accessibilityAddTraits([.isButton, .isLink])
.font(.footnote)
#if os(iOS)
.onTapGesture {
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-hwdec")!)
}
#elseif os(macOS)
.onHover(perform: onHover(_:))
#endif
#endif
Picker("", selection: $mpvHWdec) {
ForEach(["auto", "auto-safe", "auto-copy"], id: \.self) {
Text($0)
}
}
#if !os(tvOS)
.pickerStyle(MenuPickerStyle())
#endif
}
if mpvEnableLogging {
logButton
}
HStack {
Text("demuxer-lavf-probe-info")
#if !os(tvOS)
Image(systemName: "link")
.accessibilityAddTraits([.isButton, .isLink])
.font(.footnote)
#if os(iOS)
.onTapGesture {
UIApplication.shared.open(URL(string: "https://mpv.io/manual/stable/#options-demuxer-lavf-probe-info")!)
}
#elseif os(macOS)
.onHover(perform: onHover(_:))
#endif
#endif
Picker("", selection: $mpvDemuxerLavfProbeInfo) {
ForEach(["yes", "no", "auto", "nostreams"], id: \.self) {
Text($0)
}
}
#if !os(tvOS)
.pickerStyle(MenuPickerStyle())
#endif
}
if mpvEnableLogging {
logButton
@@ -103,20 +222,19 @@ struct AdvancedSettings: View {
}
@ViewBuilder var mpvFooter: some View {
let url = "https://mpv.io/manual/master"
let url = "https://mpv.io/manual/stable/"
VStack(alignment: .leading) {
Text("Restart the app to apply the settings above.")
.padding(.bottom, 1)
VStack(alignment: .leading, spacing: 2) {
#if os(tvOS)
Text("More info can be found in MPV Documentation:")
Text("More info can be found in MPV reference manual:")
Text(url)
#else
Text("More info can be found in:")
Link("MPV Documentation", destination: URL(string: url)!)
#if os(macOS)
.onHover(perform: onHover(_:))
#endif
Text("Further information can be found in the ")
+ Text("MPV reference manual").underline().bold()
+ Text(" by clicking on the link icon next to the option.")
#endif
}
}

View File

@@ -30,12 +30,19 @@ struct PlayerSettings: View {
@Default(.enableReturnYouTubeDislike) private var enableReturnYouTubeDislike
@Default(.showRelated) private var showRelated
@Default(.showInspector) private var showInspector
@Default(.showChapters) private var showChapters
@Default(.showChapterThumbnails) private var showThumbnails
@Default(.showChapterThumbnailsOnlyWhenDifferent) private var showThumbnailsOnlyWhenDifferent
@Default(.expandChapters) private var expandChapters
@Default(.showRelated) private var showRelated
@Default(.captionsAutoShow) private var captionsAutoShow
@Default(.captionsDefaultLanguageCode) private var captionsDefaultLanguageCode
@Default(.captionsFallbackLanguageCode) private var captionsFallbackLanguageCode
@Default(.captionsFontScaleSize) private var captionsFontScaleSize
@Default(.captionsFontColor) private var captionsFontColor
@ObservedObject private var accounts = AccountsModel.shared
@@ -45,6 +52,11 @@ struct PlayerSettings: View {
}
#endif
#if os(tvOS)
@State private var isShowingDefaultLanguagePicker = false
@State private var isShowingFallbackLanguagePicker = false
#endif
var body: some View {
Group {
#if os(macOS)
@@ -93,7 +105,54 @@ struct PlayerSettings: View {
inspectorVisibilityPicker
#endif
}
#endif
Section(header: SettingsHeader(text: "Captions".localized())) {
#if os(tvOS)
Text("Size").font(.subheadline)
#endif
captionsFontScaleSizePicker
#if os(tvOS)
Text("Color").font(.subheadline)
#endif
captionsFontColorPicker
showCaptionsAutoShowToggle
#if !os(tvOS)
captionDefaultLanguagePicker
captionFallbackLanguagePicker
#else
Button(action: { isShowingDefaultLanguagePicker = true }) {
HStack {
Text("Default language")
Spacer()
Text("\(LanguageCodes(rawValue: captionsDefaultLanguageCode)!.description.capitalized) (\(captionsDefaultLanguageCode))").foregroundColor(.secondary)
}
}
.frame(maxWidth: .infinity).sheet(isPresented: $isShowingDefaultLanguagePicker) {
defaultLanguagePickerTVOS(
selectedLanguage: $captionsDefaultLanguageCode,
isShowing: $isShowingDefaultLanguagePicker
)
}
Button(action: { isShowingFallbackLanguagePicker = true }) {
HStack {
Text("Fallback language")
Spacer()
Text("\(LanguageCodes(rawValue: captionsFallbackLanguageCode)!.description.capitalized) (\(captionsFallbackLanguageCode))").foregroundColor(.secondary)
}
}
.frame(maxWidth: .infinity).sheet(isPresented: $isShowingDefaultLanguagePicker) {
fallbackLanguagePickerTVOS(
selectedLanguage: $captionsFallbackLanguageCode,
isShowing: $isShowingFallbackLanguagePicker
)
}
#endif
}
#if !os(tvOS)
Section(header: SettingsHeader(text: "Chapters".localized())) {
showChaptersToggle
showThumbnailsToggle
@@ -279,6 +338,103 @@ struct PlayerSettings: View {
}
#endif
private var showCaptionsAutoShowToggle: some View {
Toggle("Always show captions", isOn: $captionsAutoShow)
}
private var captionsFontScaleSizePicker: some View {
Picker("Size", selection: $captionsFontScaleSize) {
Text("Small").tag(String("0.5"))
Text("Medium").tag(String("1.0"))
Text("Large").tag(String("2.0"))
}
.onChange(of: captionsFontScaleSize) { _ in
PlayerModel.shared.mpvBackend.client.setSubFontSize(scaleSize: captionsFontScaleSize)
}
#if os(macOS)
.labelsHidden()
#endif
}
private var captionsFontColorPicker: some View {
Picker("Color", selection: $captionsFontColor) {
Text("White").tag(String("#FFFFFF"))
Text("Yellow").tag(String("#FFFF00"))
Text("Red").tag(String("#FF0000"))
Text("Orange").tag(String("#FFA500"))
Text("Green").tag(String("#008000"))
Text("Blue").tag(String("#0000FF"))
}
.onChange(of: captionsFontColor) { _ in
PlayerModel.shared.mpvBackend.client.setSubFontColor(color: captionsFontColor)
}
#if os(macOS)
.labelsHidden()
#endif
}
#if !os(tvOS)
private var captionDefaultLanguagePicker: some View {
Picker("Default language", selection: $captionsDefaultLanguageCode) {
ForEach(LanguageCodes.allCases, id: \.self) { language in
Text("\(language.description.capitalized) (\(language.rawValue))").tag(language.rawValue)
}
}
#if os(macOS)
.labelsHidden()
#endif
}
private var captionFallbackLanguagePicker: some View {
Picker("Fallback language", selection: $captionsFallbackLanguageCode) {
ForEach(LanguageCodes.allCases, id: \.self) { language in
Text("\(language.description.capitalized) (\(language.rawValue))").tag(language.rawValue)
}
}
#if os(macOS)
.labelsHidden()
#endif
}
#else
struct defaultLanguagePickerTVOS: View {
@Binding var selectedLanguage: String
@Binding var isShowing: Bool
var body: some View {
NavigationView {
List(LanguageCodes.allCases, id: \.self) { language in
Button(action: {
selectedLanguage = language.rawValue
isShowing = false
}) {
Text("\(language.description.capitalized) (\(language.rawValue))")
}
}
.navigationTitle("Select Default Language")
}
}
}
struct fallbackLanguagePickerTVOS: View {
@Binding var selectedLanguage: String
@Binding var isShowing: Bool
var body: some View {
NavigationView {
List(LanguageCodes.allCases, id: \.self) { language in
Button(action: {
selectedLanguage = language.rawValue
isShowing = false
}) {
Text("\(language.description.capitalized) (\(language.rawValue))")
}
}
.navigationTitle("Select Fallback Language")
}
}
}
#endif
#if !os(tvOS)
private var inspectorVisibilityPicker: some View {
Picker("Inspector", selection: $showInspector) {

View File

@@ -136,9 +136,20 @@ struct QualityProfileForm: View {
var formatsFooter: some View {
VStack(alignment: .leading) {
Text("Formats can be reordered and will be selected in this order.")
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
if #available(iOS 16.0, *) {
Text("Formats can be reordered and will be selected in this order.")
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
} else if #available(iOS 14.0, *) {
Text("Formats will be selected in the order they are listed.")
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
} else {
Text("Formats will be selected in the order they are listed.")
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
}
Text("**Note:** HLS is an adaptive format where specific resolution settings don't apply.")
.foregroundColor(.secondary)
.fixedSize(horizontal: false, vertical: true)
@@ -304,7 +315,7 @@ struct QualityProfileForm: View {
func isResolutionDisabled(_ resolution: ResolutionSetting) -> Bool {
guard backend == .appleAVPlayer else { return false }
return resolution.value > .hd1080p60
return resolution.value > .hd720p30
}
func initializeForm() {

View File

@@ -20,6 +20,7 @@ enum URLTester {
var request = URLRequest(url: url)
request.httpMethod = "HEAD"
request.setValue("bytes=\(range)", forHTTPHeaderField: "Range")
request.setValue(UserAgentManager.shared.userAgent, forHTTPHeaderField: "User-Agent")
var dataTask: URLSessionDataTask?
dataTask = URLSession.shared.dataTask(with: request) { _, response, _ in

View File

@@ -0,0 +1,37 @@
import Logging
#if !os(tvOS)
import WebKit
#endif
final class UserAgentManager {
static let shared = UserAgentManager()
private(set) var userAgent: String
#if !os(tvOS)
private var webView: WKWebView?
#endif
private init() {
/*
In case an error occurs while retrieving the actual User-Agent, and on tvOS,
we set a default User-Agent value that represents a commonly used User-Agent.
*/
userAgent = "Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Trident/5.0)"
#if !os(tvOS)
webView = WKWebView()
webView?.evaluateJavaScript("navigator.userAgent") { [weak self] result, _ in
if let userAgent = result as? String {
DispatchQueue.main.async {
self?.userAgent = userAgent
Logger(label: "stream.yattee.userAgentManager").info("User-Agent: \(userAgent)")
}
} else {
Logger(label: "stream.yattee.userAgentManager").warning("Failed to update User-Agent.")
}
}
#else
Logger(label: "stream.yattee.userAgentManager.tvOS").info("User-Agent: \(userAgent)")
#endif
}
}

View File

@@ -154,7 +154,12 @@ struct YatteeApp: App {
#if DEBUG
SiestaLog.Category.enabled = .common
#endif
SDImageCodersManager.shared.addCoder(SDImageAWebPCoder.shared)
#if os(tvOS)
SDImageCodersManager.shared.addCoder(SDImageWebPCoder.shared)
#else
SDImageCodersManager.shared.addCoder(SDImageAWebPCoder.shared)
#endif
SDWebImageManager.defaultImageCache = PINCache(name: "stream.yattee.app")
if !Defaults[.lastAccountIsPublic] {
@@ -206,6 +211,9 @@ struct YatteeApp: App {
}
#endif
// Initialize UserAgentManager
_ = UserAgentManager.shared
DispatchQueue.global(qos: .userInitiated).async {
URLBookmarkModel.shared.refreshAll()
}

View File

@@ -387,7 +387,7 @@
"Backend" = "الواجهة الخلفية";
"Badge" = "الشارة";
"Close PiP when starting playing other video" = "غلق الفيديو المصغر عند بدء تشغيل فيديو آخر";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "تذكيرات صريحة لإبداء الإعجاب بها أو الإشتراك فيها أو التفاعل معها على أي منصة (منصات) مدفوعة أو مجانية (مثل النقر فوق مقطع الفيديو).\n";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "تذكيرات صريحة لإبداء الإعجاب بها أو الإشتراك فيها أو التفاعل معها على أي منصة (منصات) مدفوعة أو مجانية (مثل النقر فوق مقطع الفيديو).";
"Filter" = " عامل التصفية";
"Frontend URL" = "عنوان URL للواجهة الأمامية";
"Fullscreen size" = "حجم ملء الشاشة";
@@ -628,4 +628,4 @@
"Password required to import" = "كلمة المرور مطلوبة للإستيراد";
"Password saved in import file" = "كلمة المرور محفوظة في ملف الإستيراد";
"Export in progress..." = "جارِ التصدير...";
"In progress..." = "في تَقَدم…";
"In progress..." = "في طور الأجراء…";

View File

@@ -406,7 +406,7 @@
"Country" = "País";
"Clear All" = "Limpar Tudo";
"Clear All Recents" = "Limpar Todos os Recentes";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Lembretes explícitos de dar like, se inscrever ou interagir com eles em qualquer plataforma, paga ou grátis (p.ex. clicar em um vídeo).\n";
"Explicit reminders to like, subscribe or interact with them on any paid or free platform(s) (e.g. click on a video)." = "Lembretes explícitos de dar like, se inscrever ou interagir com eles em qualquer plataforma, paga ou grátis (p.ex. clicar em um vídeo).";
"Duration" = "Duração";
"Edit Quality Profile" = "Editar Perfil de Qualidade";
"Discussions take place in Discord and Matrix. It's a good spot for general questions." = "Discussões acontecem no Discord e no Matrix. É um bom lugar para perguntas gerais.";

View File

@@ -1070,12 +1070,18 @@
37FFC440272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; };
37FFC441272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; };
37FFC442272734C3009FFD26 /* Throttle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 37FFC43F272734C3009FFD26 /* Throttle.swift */; };
E24DC6582BFA124100BF6187 /* UserAgentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24DC6572BFA124100BF6187 /* UserAgentManager.swift */; };
E24DC6592BFA124100BF6187 /* UserAgentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24DC6572BFA124100BF6187 /* UserAgentManager.swift */; };
E24DC65A2BFA124100BF6187 /* UserAgentManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = E24DC6572BFA124100BF6187 /* UserAgentManager.swift */; };
E25028B02BF790F5002CB9FC /* HTTPStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */; };
E25028B12BF790F5002CB9FC /* HTTPStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */; };
E25028B22BF790F5002CB9FC /* HTTPStatus.swift in Sources */ = {isa = PBXBuildFile; fileRef = E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */; };
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 */; };
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 */; };
FA97174C2A494700001FF53D /* MPVKit in Frameworks */ = {isa = PBXBuildFile; productRef = FA97174B2A494700001FF53D /* MPVKit */; };
/* End PBXBuildFile section */
@@ -1545,8 +1551,10 @@
3DA101AD287C30F50027D920 /* DEVELOPMENT_TEAM.template.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DEVELOPMENT_TEAM.template.xcconfig; sourceTree = "<group>"; };
3DA101AE287C30F50027D920 /* DEVELOPMENT_TEAM.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DEVELOPMENT_TEAM.xcconfig; sourceTree = "<group>"; };
3DA101AF287C30F50027D920 /* Shared.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = Shared.xcconfig; sourceTree = "<group>"; };
E24DC6572BFA124100BF6187 /* UserAgentManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = UserAgentManager.swift; sourceTree = "<group>"; };
E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HTTPStatus.swift; sourceTree = "<group>"; };
E258F3892BF61BD2005B8C28 /* URLTester.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = URLTester.swift; sourceTree = "<group>"; };
E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LanguageCodes.swift; sourceTree = "<group>"; };
/* End PBXFileReference section */
/* Begin PBXFrameworksBuildPhase section */
@@ -2295,6 +2303,7 @@
37D2E0D328B67EFC00F64D52 /* Delay.swift */,
3761ABFC26F0F8DE00AA496F /* EnvironmentValues.swift */,
E25028AF2BF790F5002CB9FC /* HTTPStatus.swift */,
E27568B82BFAAC2000BDF0AF /* LanguageCodes.swift */,
375B537828DF6CBB004C1D19 /* Localizable.strings */,
3729037D2739E47400EA99F6 /* MenuCommands.swift */,
37B7958F2771DAE0001CF27B /* OpenURLHandler.swift */,
@@ -2303,6 +2312,7 @@
37FFC43F272734C3009FFD26 /* Throttle.swift */,
378FFBC328660172009E3FBE /* URLParser.swift */,
E258F3892BF61BD2005B8C28 /* URLTester.swift */,
E24DC6572BFA124100BF6187 /* UserAgentManager.swift */,
37D4B0C22671614700C925CA /* YatteeApp.swift */,
37D4B0C42671614800C925CA /* Assets.xcassets */,
37BD07C42698ADEE003EBB87 /* Yattee.entitlements */,
@@ -3149,6 +3159,7 @@
377FC7DC267A081800A6BBAF /* PopularView.swift in Sources */,
3752069D285E910600CA655F /* ChapterView.swift in Sources */,
375EC96A289F232600751258 /* QualityProfilesModel.swift in Sources */,
E24DC6582BFA124100BF6187 /* UserAgentManager.swift in Sources */,
E258F38A2BF61BD2005B8C28 /* URLTester.swift in Sources */,
3751B4B227836902000B7DF4 /* SearchPage.swift in Sources */,
37CC3F4C270CFE1700608308 /* PlayerQueueView.swift in Sources */,
@@ -3225,6 +3236,7 @@
37C3A24D272360470087A57A /* ChannelPlaylist+Fixtures.swift in Sources */,
37484C2D26FC844700287258 /* InstanceSettings.swift in Sources */,
37DD9DA32785BBC900539416 /* NoCommentsView.swift in Sources */,
E27568B92BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */,
377A20A92693C9A2002842B8 /* TypedContentAccessors.swift in Sources */,
37484C3126FCB8F900287258 /* AccountValidator.swift in Sources */,
377ABC48286E5887009C986F /* Sequence+Unique.swift in Sources */,
@@ -3405,6 +3417,7 @@
371F2F1B269B43D300E4A7AB /* NavigationModel.swift in Sources */,
3756C2AB2861151C00E4B059 /* NetworkStateModel.swift in Sources */,
375EC95A289EEB8200751258 /* QualityProfileForm.swift in Sources */,
E27568BA2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */,
37001564271B1F250049C794 /* AccountsModel.swift in Sources */,
378FFBC528660172009E3FBE /* URLParser.swift in Sources */,
3761ABFE26F0F8DE00AA496F /* EnvironmentValues.swift in Sources */,
@@ -3413,6 +3426,7 @@
3743CA4F270EFE3400E4D32B /* PlayerQueueRow.swift in Sources */,
374C053C2724614F009BDDBE /* PlayerTVMenu.swift in Sources */,
377FC7DD267A081A00A6BBAF /* PopularView.swift in Sources */,
E24DC6592BFA124100BF6187 /* UserAgentManager.swift in Sources */,
374924DB2921050B0017D862 /* LocationsSettings.swift in Sources */,
371AC0A0294D13AA0085989E /* UnwatchedFeedCountModel.swift in Sources */,
379E7C342A20FE3900AF8118 /* FocusableSearchTextField.swift in Sources */,
@@ -3878,6 +3892,7 @@
37BA221329526A19000DAD1F /* ControlsGradientView.swift in Sources */,
374C0541272472C0009BDDBE /* PlayerSponsorBlock.swift in Sources */,
37130A61277657300033018A /* PersistenceController.swift in Sources */,
E24DC65A2BFA124100BF6187 /* UserAgentManager.swift in Sources */,
37E70929271CDDAE00D34DDE /* OpenSettingsButton.swift in Sources */,
3717407F2949D40800FDDBC7 /* ChannelLinkView.swift in Sources */,
E25028B22BF790F5002CB9FC /* HTTPStatus.swift in Sources */,
@@ -4000,6 +4015,7 @@
37FB28432721B22200A57617 /* ContentItem.swift in Sources */,
37A7D6EF2B67E3BF009CB1ED /* BrowsingSettingsGroupImporter.swift in Sources */,
37D2E0D228B67DBC00F64D52 /* AnimationCompletionObserverModifier.swift in Sources */,
E27568BB2BFAAC2000BDF0AF /* LanguageCodes.swift in Sources */,
37A7D6EB2B67E334009CB1ED /* BrowsingSettingsGroupExporter.swift in Sources */,
37AAF2A226741C97007FC770 /* FeedView.swift in Sources */,
37484C1B26FC837400287258 /* PlayerSettings.swift in Sources */,
@@ -4087,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 = 183;
CURRENT_PROJECT_VERSION = 185;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "Open in Yattee";
@@ -4118,7 +4134,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 185;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
GENERATE_INFOPLIST_FILE = YES;
INFOPLIST_FILE = "Open in Yattee/Info.plist";
@@ -4149,7 +4165,7 @@
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 185;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 11.0;
@@ -4169,7 +4185,7 @@
buildSettings = {
CLANG_CXX_LANGUAGE_STANDARD = "gnu++17";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 185;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 14.0;
MACOSX_DEPLOYMENT_TARGET = 11.0;
@@ -4333,7 +4349,7 @@
CODE_SIGN_ENTITLEMENTS = "iOS/Yattee (iOS).entitlements";
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 185;
ENABLE_PREVIEWS = YES;
GCC_PREPROCESSOR_DEFINITIONS = (
"DEBUG=1",
@@ -4386,7 +4402,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 185;
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 78Z5H3M6RJ;
ENABLE_PREVIEWS = YES;
GCC_PREPROCESSOR_DEFINITIONS = "GLES_SILENCE_DEPRECATION=1";
@@ -4438,7 +4454,7 @@
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 185;
DEAD_CODE_STRIPPING = YES;
ENABLE_APP_SANDBOX = YES;
ENABLE_HARDENED_RUNTIME = YES;
@@ -4477,7 +4493,7 @@
"CODE_SIGN_IDENTITY[sdk=macosx*]" = "3rd Party Mac Developer Application";
CODE_SIGN_STYLE = Manual;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 185;
DEAD_CODE_STRIPPING = YES;
"DEVELOPMENT_TEAM[sdk=macosx*]" = 78Z5H3M6RJ;
ENABLE_APP_SANDBOX = YES;
@@ -4512,7 +4528,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 185;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4536,7 +4552,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 185;
GENERATE_INFOPLIST_FILE = YES;
IPHONEOS_DEPLOYMENT_TARGET = 15.0;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4562,7 +4578,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 185;
DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4587,7 +4603,7 @@
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 185;
DEAD_CODE_STRIPPING = YES;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
@@ -4613,7 +4629,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_IDENTITY = "Apple Development";
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 185;
DEVELOPMENT_ASSET_PATHS = "";
ENABLE_PREVIEWS = YES;
GENERATE_INFOPLIST_FILE = YES;
@@ -4653,7 +4669,7 @@
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
"CODE_SIGN_IDENTITY[sdk=appletvos*]" = "iPhone Distribution";
CODE_SIGN_STYLE = Manual;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 185;
DEVELOPMENT_ASSET_PATHS = "";
"DEVELOPMENT_TEAM[sdk=appletvos*]" = 78Z5H3M6RJ;
ENABLE_PREVIEWS = YES;
@@ -4694,7 +4710,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 185;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
@@ -4718,7 +4734,7 @@
buildSettings = {
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = YES;
CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 183;
CURRENT_PROJECT_VERSION = 185;
GENERATE_INFOPLIST_FILE = YES;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",