mirror of
https://github.com/cranci1/Sora.git
synced 2026-03-11 17:45:37 +00:00
Video Quality Prefrences Addition (#180)
This commit is contained in:
parent
2beaaf5575
commit
b56ef52ae3
4 changed files with 189 additions and 4 deletions
|
|
@ -6,6 +6,7 @@
|
|||
//
|
||||
|
||||
import Foundation
|
||||
import Network
|
||||
|
||||
class FetchDelegate: NSObject, URLSessionTaskDelegate {
|
||||
private let allowRedirects: Bool
|
||||
|
|
@ -70,3 +71,56 @@ extension URLSession {
|
|||
return URLSession(configuration: configuration, delegate: delegate, delegateQueue: nil)
|
||||
}
|
||||
}
|
||||
|
||||
enum NetworkType {
|
||||
case wifi
|
||||
case cellular
|
||||
case unknown
|
||||
}
|
||||
|
||||
@available(iOS 14.0, *)
|
||||
class NetworkMonitor: ObservableObject {
|
||||
static let shared = NetworkMonitor()
|
||||
|
||||
private let monitor = NWPathMonitor()
|
||||
private let queue = DispatchQueue(label: "NetworkMonitor")
|
||||
|
||||
@Published var currentNetworkType: NetworkType = .unknown
|
||||
@Published var isConnected: Bool = false
|
||||
|
||||
private init() {
|
||||
startMonitoring()
|
||||
}
|
||||
|
||||
private func startMonitoring() {
|
||||
monitor.pathUpdateHandler = { [weak self] path in
|
||||
DispatchQueue.main.async {
|
||||
self?.isConnected = path.status == .satisfied
|
||||
self?.currentNetworkType = self?.getNetworkType(from: path) ?? .unknown
|
||||
}
|
||||
}
|
||||
monitor.start(queue: queue)
|
||||
}
|
||||
|
||||
private func getNetworkType(from path: NWPath) -> NetworkType {
|
||||
if path.usesInterfaceType(.wifi) {
|
||||
return .wifi
|
||||
} else if path.usesInterfaceType(.cellular) {
|
||||
return .cellular
|
||||
} else {
|
||||
return .unknown
|
||||
}
|
||||
}
|
||||
|
||||
static func getCurrentNetworkType() -> NetworkType {
|
||||
if #available(iOS 14.0, *) {
|
||||
return shared.currentNetworkType
|
||||
} else {
|
||||
return .unknown
|
||||
}
|
||||
}
|
||||
|
||||
deinit {
|
||||
monitor.cancel()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,6 +7,63 @@
|
|||
|
||||
import UIKit
|
||||
|
||||
enum VideoQualityPreference: String, CaseIterable {
|
||||
case best = "Best"
|
||||
case p1080 = "1080p"
|
||||
case p720 = "720p"
|
||||
case p420 = "420p"
|
||||
case p360 = "360p"
|
||||
case worst = "Worst"
|
||||
|
||||
static let wifiDefaultKey = "videoQualityWiFi"
|
||||
static let cellularDefaultKey = "videoQualityCellular"
|
||||
|
||||
static let defaultWiFiPreference: VideoQualityPreference = .best
|
||||
static let defaultCellularPreference: VideoQualityPreference = .p720
|
||||
|
||||
static let qualityPriority: [VideoQualityPreference] = [.best, .p1080, .p720, .p420, .p360, .worst]
|
||||
|
||||
static func findClosestQuality(preferred: VideoQualityPreference, availableQualities: [(String, String)]) -> (String, String)? {
|
||||
for (name, url) in availableQualities {
|
||||
if isQualityMatch(preferred: preferred, qualityName: name) {
|
||||
return (name, url)
|
||||
}
|
||||
}
|
||||
|
||||
let preferredIndex = qualityPriority.firstIndex(of: preferred) ?? qualityPriority.count
|
||||
|
||||
for i in 0..<qualityPriority.count {
|
||||
let candidate = qualityPriority[i]
|
||||
for (name, url) in availableQualities {
|
||||
if isQualityMatch(preferred: candidate, qualityName: name) {
|
||||
return (name, url)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return availableQualities.first
|
||||
}
|
||||
|
||||
private static func isQualityMatch(preferred: VideoQualityPreference, qualityName: String) -> Bool {
|
||||
let lowercaseName = qualityName.lowercased()
|
||||
|
||||
switch preferred {
|
||||
case .best:
|
||||
return lowercaseName.contains("best") || lowercaseName.contains("highest") || lowercaseName.contains("max")
|
||||
case .p1080:
|
||||
return lowercaseName.contains("1080") || lowercaseName.contains("1920")
|
||||
case .p720:
|
||||
return lowercaseName.contains("720") || lowercaseName.contains("1280")
|
||||
case .p420:
|
||||
return lowercaseName.contains("420") || lowercaseName.contains("480")
|
||||
case .p360:
|
||||
return lowercaseName.contains("360") || lowercaseName.contains("640")
|
||||
case .worst:
|
||||
return lowercaseName.contains("worst") || lowercaseName.contains("lowest") || lowercaseName.contains("min")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
extension UserDefaults {
|
||||
func color(forKey key: String) -> UIColor? {
|
||||
guard let colorData = data(forKey: key) else { return nil }
|
||||
|
|
@ -30,4 +87,19 @@ extension UserDefaults {
|
|||
Logger.shared.log("Error archiving color: \(error)", type: "Error")
|
||||
}
|
||||
}
|
||||
|
||||
static func getVideoQualityPreference() -> VideoQualityPreference {
|
||||
let networkType = NetworkMonitor.getCurrentNetworkType()
|
||||
|
||||
switch networkType {
|
||||
case .wifi:
|
||||
let rawValue = UserDefaults.standard.string(forKey: VideoQualityPreference.wifiDefaultKey)
|
||||
return VideoQualityPreference(rawValue: rawValue ?? "") ?? VideoQualityPreference.defaultWiFiPreference
|
||||
case .cellular:
|
||||
let rawValue = UserDefaults.standard.string(forKey: VideoQualityPreference.cellularDefaultKey)
|
||||
return VideoQualityPreference(rawValue: rawValue ?? "") ?? VideoQualityPreference.defaultCellularPreference
|
||||
case .unknown:
|
||||
return .p720
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2211,7 +2211,13 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
|
||||
private func switchToQuality(urlString: String) {
|
||||
guard let url = URL(string: urlString),
|
||||
currentQualityURL?.absoluteString != urlString else { return }
|
||||
currentQualityURL?.absoluteString != urlString else {
|
||||
Logger.shared.log("Quality Selection: Switch cancelled - same quality already selected", type: "General")
|
||||
return
|
||||
}
|
||||
|
||||
let qualityName = qualities.first(where: { $0.1 == urlString })?.0 ?? "Unknown"
|
||||
Logger.shared.log("Quality Selection: Switching to quality: \(qualityName) (\(urlString))", type: "General")
|
||||
|
||||
let currentTime = player.currentTime()
|
||||
let wasPlaying = player.rate > 0
|
||||
|
|
@ -2270,7 +2276,10 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
qualityButton.menu = qualitySelectionMenu()
|
||||
|
||||
if let selectedQuality = qualities.first(where: { $0.1 == urlString })?.0 {
|
||||
Logger.shared.log("Quality Selection: Successfully switched to: \(selectedQuality)", type: "General")
|
||||
DropManager.shared.showDrop(title: "Quality: \(selectedQuality)", subtitle: "", duration: 0.5, icon: UIImage(systemName: "eye"))
|
||||
} else {
|
||||
Logger.shared.log("Quality Selection: Switch completed but quality name not found in list", type: "General")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -2320,11 +2329,34 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
baseM3U8URL = url
|
||||
currentQualityURL = url
|
||||
|
||||
let networkType = NetworkMonitor.getCurrentNetworkType()
|
||||
let networkTypeString = networkType == .wifi ? "WiFi" : networkType == .cellular ? "Cellular" : "Unknown"
|
||||
Logger.shared.log("Quality Selection: Detected network type: \(networkTypeString)", type: "General")
|
||||
|
||||
parseM3U8(url: url) { [weak self] in
|
||||
guard let self = self else { return }
|
||||
if let last = UserDefaults.standard.string(forKey: "lastSelectedQuality"),
|
||||
self.qualities.contains(where: { $0.1 == last }) {
|
||||
self.switchToQuality(urlString: last)
|
||||
|
||||
Logger.shared.log("Quality Selection: Found \(self.qualities.count) available qualities", type: "General")
|
||||
for (index, quality) in self.qualities.enumerated() {
|
||||
Logger.shared.log("Quality Selection: Available [\(index + 1)]: \(quality.0) - \(quality.1)", type: "General")
|
||||
}
|
||||
|
||||
let preferredQuality = UserDefaults.getVideoQualityPreference()
|
||||
Logger.shared.log("Quality Selection: User preference for \(networkTypeString): \(preferredQuality.rawValue)", type: "General")
|
||||
|
||||
if let selectedQuality = VideoQualityPreference.findClosestQuality(preferred: preferredQuality, availableQualities: self.qualities) {
|
||||
Logger.shared.log("Quality Selection: Selected quality: \(selectedQuality.0) (URL: \(selectedQuality.1))", type: "General")
|
||||
self.switchToQuality(urlString: selectedQuality.1)
|
||||
} else {
|
||||
Logger.shared.log("Quality Selection: No matching quality found, using default", type: "General")
|
||||
if let last = UserDefaults.standard.string(forKey: "lastSelectedQuality"),
|
||||
self.qualities.contains(where: { $0.1 == last }) {
|
||||
Logger.shared.log("Quality Selection: Falling back to last selected quality", type: "General")
|
||||
self.switchToQuality(urlString: last)
|
||||
} else if let firstQuality = self.qualities.first {
|
||||
Logger.shared.log("Quality Selection: Falling back to first available quality: \(firstQuality.0)", type: "General")
|
||||
self.switchToQuality(urlString: firstQuality.1)
|
||||
}
|
||||
}
|
||||
|
||||
self.qualityButton.isHidden = false
|
||||
|
|
@ -2338,6 +2370,7 @@ class CustomMediaPlayerViewController: UIViewController, UIGestureRecognizerDele
|
|||
isHLSStream = false
|
||||
qualityButton.isHidden = true
|
||||
updateMenuButtonConstraints()
|
||||
Logger.shared.log("Quality Selection: Non-HLS stream detected, quality selection unavailable", type: "General")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -205,7 +205,11 @@ struct SettingsViewPlayer: View {
|
|||
@AppStorage("skipIntroOutroVisible") private var skipIntroOutroVisible: Bool = true
|
||||
@AppStorage("pipButtonVisible") private var pipButtonVisible: Bool = true
|
||||
|
||||
@AppStorage("videoQualityWiFi") private var wifiQuality: String = VideoQualityPreference.defaultWiFiPreference.rawValue
|
||||
@AppStorage("videoQualityCellular") private var cellularQuality: String = VideoQualityPreference.defaultCellularPreference.rawValue
|
||||
|
||||
private let mediaPlayers = ["Default", "Sora", "VLC", "OutPlayer", "Infuse", "nPlayer", "SenPlayer", "IINA", "TracyPlayer"]
|
||||
private let qualityOptions = VideoQualityPreference.allCases.map { $0.rawValue }
|
||||
|
||||
var body: some View {
|
||||
ScrollView {
|
||||
|
|
@ -261,6 +265,28 @@ struct SettingsViewPlayer: View {
|
|||
)
|
||||
}
|
||||
|
||||
SettingsSection(
|
||||
title: "Video Quality Preferences",
|
||||
footer: "Choose preferred video resolution for WiFi and cellular connections. Higher resolutions use more data but provide better quality. If the exact quality isn't available, the closest option will be selected automatically.\n\nNote: Not all video sources and players support quality selection. This feature works best with HLS streams using the Sora player."
|
||||
) {
|
||||
SettingsPickerRow(
|
||||
icon: "wifi",
|
||||
title: "WiFi Quality",
|
||||
options: qualityOptions,
|
||||
optionToString: { $0 },
|
||||
selection: $wifiQuality
|
||||
)
|
||||
|
||||
SettingsPickerRow(
|
||||
icon: "antenna.radiowaves.left.and.right",
|
||||
title: "Cellular Quality",
|
||||
options: qualityOptions,
|
||||
optionToString: { $0 },
|
||||
selection: $cellularQuality,
|
||||
showDivider: false
|
||||
)
|
||||
}
|
||||
|
||||
SettingsSection(title: "Progress bar Marker Color") {
|
||||
ColorPicker("Segments Color", selection: Binding(
|
||||
get: {
|
||||
|
|
|
|||
Loading…
Reference in a new issue