improved conditional proxying

We don't test every single stream anymore. If we get an 200, 206 or 403 we immediately stop testing streams. Unknown streams are also skipped. This speeds up starting playback, since we don'T have to wait for the network anymore.
This commit is contained in:
Toni Förster 2024-05-22 22:20:51 +02:00
parent d6cfadab9a
commit 30cdaf88e1
No known key found for this signature in database
GPG Key ID: 292F3E5086C83FC7

View File

@ -56,82 +56,101 @@ extension PlayerModel {
} }
} }
func streamsWithInstance(instance _: Instance, streams: [Stream], completion: @escaping ([Stream]) -> Void) { func streamsWithInstance(instance: Instance, streams: [Stream], completion: @escaping ([Stream]) -> Void) {
// Queue for stream processing // Queue for stream processing
let streamProcessingQueue = DispatchQueue(label: "stream.yattee.streamProcessing.Queue", qos: .userInitiated) let streamProcessingQueue = DispatchQueue(label: "stream.yattee.streamProcessing.Queue")
// Queue for accessing the processedStreams array // Queue for accessing the processedStreams array
let processedStreamsQueue = DispatchQueue(label: "stream.yattee.processedStreams.Queue") let processedStreamsQueue = DispatchQueue(label: "stream.yattee.processedStreams.Queue")
// DispatchGroup for managing multiple tasks // DispatchGroup for managing multiple tasks
let streamProcessingGroup = DispatchGroup() let streamProcessingGroup = DispatchGroup()
var processedStreams = [Stream]() var processedStreams = [Stream]()
let instance = instance
var hasForbiddenAsset = false
var hasAllowedAsset = false
for stream in streams { for stream in streams {
streamProcessingQueue.async(group: streamProcessingGroup) { streamProcessingQueue.async(group: streamProcessingGroup) {
let forbiddenAssetTestGroup = DispatchGroup() let forbiddenAssetTestGroup = DispatchGroup()
var hasForbiddenAsset = false if !hasAllowedAsset, !hasForbiddenAsset, !instance.proxiesVideos, stream.format != Stream.Format.unknown {
let (nonHLSAssets, hlsURLs) = self.getAssets(from: [stream])
if let firstStream = nonHLSAssets.first {
let asset = firstStream.0
let url = firstStream.1
let requestRange = firstStream.2
let (nonHLSAssets, hlsURLs) = self.getAssets(from: [stream])
if let randomStream = nonHLSAssets.randomElement() {
let instance = randomStream.0
let asset = randomStream.1
let url = randomStream.2
let requestRange = randomStream.3
// swiftlint:disable:next shorthand_optional_binding
if let asset = asset, let instance = instance, !instance.proxiesVideos {
if instance.app == .invidious { if instance.app == .invidious {
self.testAsset(url: url, range: requestRange, isHLS: false, forbiddenAssetTestGroup: forbiddenAssetTestGroup) { isForbidden in self.testAsset(url: url, range: requestRange, isHLS: false, forbiddenAssetTestGroup: forbiddenAssetTestGroup) { status in
hasForbiddenAsset = isForbidden switch status {
case HTTPStatus.Forbidden:
hasForbiddenAsset = true
case HTTPStatus.PartialContent:
hasAllowedAsset = true
case HTTPStatus.OK:
hasAllowedAsset = true
default:
break
}
} }
} else if instance.app == .piped { } else if instance.app == .piped {
self.testPipedAssets(asset: asset, requestRange: requestRange, isHLS: false, forbiddenAssetTestGroup: forbiddenAssetTestGroup) { isForbidden in self.testPipedAssets(asset: asset!, requestRange: requestRange, isHLS: false, forbiddenAssetTestGroup: forbiddenAssetTestGroup) { status in
hasForbiddenAsset = isForbidden switch status {
case HTTPStatus.Forbidden:
hasForbiddenAsset = true
case HTTPStatus.PartialContent:
hasAllowedAsset = true
case HTTPStatus.OK:
hasAllowedAsset = true
default:
break
}
} }
} }
} } else if let firstHLS = hlsURLs.first {
} else if let randomHLS = hlsURLs.randomElement() { let asset = AVURLAsset(url: firstHLS)
let instance = randomHLS.0 if instance.app == .piped {
let asset = AVURLAsset(url: randomHLS.1) self.testPipedAssets(asset: asset, requestRange: nil, isHLS: true, forbiddenAssetTestGroup: forbiddenAssetTestGroup) { status in
switch status {
if instance?.app == .piped { case HTTPStatus.Forbidden:
self.testPipedAssets(asset: asset, requestRange: nil, isHLS: true, forbiddenAssetTestGroup: forbiddenAssetTestGroup) { isForbidden in hasForbiddenAsset = true
hasForbiddenAsset = isForbidden case HTTPStatus.PartialContent:
hasAllowedAsset = true
case HTTPStatus.OK:
hasAllowedAsset = true
default:
break
}
}
} }
} }
} }
forbiddenAssetTestGroup.wait() forbiddenAssetTestGroup.wait()
// Post-processing code // Post-processing code
if let instance = stream.instance { if instance.app == .invidious, hasForbiddenAsset || instance.proxiesVideos {
if instance.app == .invidious { if let audio = stream.audioAsset {
if hasForbiddenAsset || instance.proxiesVideos { stream.audioAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: audio)
if let audio = stream.audioAsset { }
stream.audioAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: audio) if let video = stream.videoAsset {
} stream.videoAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: video)
if let video = stream.videoAsset { }
stream.videoAsset = InvidiousAPI.proxiedAsset(instance: instance, asset: video) } else if instance.app == .piped, !instance.proxiesVideos, !hasForbiddenAsset {
if let hlsURL = stream.hlsURL {
PipedAPI.nonProxiedAsset(url: hlsURL) { possibleNonProxiedURL in
if let nonProxiedURL = possibleNonProxiedURL {
stream.hlsURL = nonProxiedURL.url
} }
} }
} else if instance.app == .piped, !instance.proxiesVideos, !hasForbiddenAsset { } else {
if let hlsURL = stream.hlsURL { if let audio = stream.audioAsset {
PipedAPI.nonProxiedAsset(url: hlsURL) { possibleNonProxiedURL in PipedAPI.nonProxiedAsset(asset: audio) { nonProxiedAudioAsset in
if let nonProxiedURL = possibleNonProxiedURL { stream.audioAsset = nonProxiedAudioAsset
stream.hlsURL = nonProxiedURL.url
}
} }
} else { }
if let audio = stream.audioAsset { if let video = stream.videoAsset {
PipedAPI.nonProxiedAsset(asset: audio) { nonProxiedAudioAsset in PipedAPI.nonProxiedAsset(asset: video) { nonProxiedVideoAsset in
stream.audioAsset = nonProxiedAudioAsset stream.videoAsset = nonProxiedVideoAsset
}
}
if let video = stream.videoAsset {
PipedAPI.nonProxiedAsset(asset: video) { nonProxiedVideoAsset in
stream.videoAsset = nonProxiedVideoAsset
}
} }
} }
} }
@ -152,21 +171,21 @@ extension PlayerModel {
} }
} }
private func getAssets(from streams: [Stream]) -> (nonHLSAssets: [(Instance?, AVURLAsset?, URL, String?)], hlsURLs: [(Instance?, URL)]) { private func getAssets(from streams: [Stream]) -> (nonHLSAssets: [(AVURLAsset?, URL, String?)], hlsURLs: [URL]) {
var nonHLSAssets = [(Instance?, AVURLAsset?, URL, String?)]() var nonHLSAssets = [(AVURLAsset?, URL, String?)]()
var hlsURLs = [(Instance?, URL)]() var hlsURLs = [URL]()
for stream in streams { for stream in streams {
if stream.isHLS { if stream.isHLS {
if let url = stream.hlsURL?.url { if let url = stream.hlsURL?.url {
hlsURLs.append((stream.instance, url)) hlsURLs.append(url)
} }
} else { } else {
if let asset = stream.audioAsset { if let asset = stream.audioAsset {
nonHLSAssets.append((stream.instance, asset, asset.url, stream.requestRange)) nonHLSAssets.append((asset, asset.url, stream.requestRange))
} }
if let asset = stream.videoAsset { if let asset = stream.videoAsset {
nonHLSAssets.append((stream.instance, asset, asset.url, stream.requestRange)) nonHLSAssets.append((asset, asset.url, stream.requestRange))
} }
} }
} }
@ -174,24 +193,24 @@ extension PlayerModel {
return (nonHLSAssets, hlsURLs) return (nonHLSAssets, hlsURLs)
} }
private func testAsset(url: URL, range: String?, isHLS: Bool, forbiddenAssetTestGroup: DispatchGroup, completion: @escaping (Bool) -> Void) { private func testAsset(url: URL, range: String?, isHLS: Bool, forbiddenAssetTestGroup: DispatchGroup, completion: @escaping (Int) -> Void) {
// In case the range is nil, generate a random one. // In case the range is nil, generate a random one.
let randomEnd = Int.random(in: 200 ... 800) let randomEnd = Int.random(in: 200 ... 800)
let requestRange = range ?? "0-\(randomEnd)" let requestRange = range ?? "0-\(randomEnd)"
forbiddenAssetTestGroup.enter() forbiddenAssetTestGroup.enter()
URLTester.testURLResponse(url: url, range: requestRange, isHLS: isHLS) { statusCode in URLTester.testURLResponse(url: url, range: requestRange, isHLS: isHLS) { statusCode in
completion(statusCode == HTTPStatus.Forbidden) completion(statusCode)
forbiddenAssetTestGroup.leave() forbiddenAssetTestGroup.leave()
} }
} }
private func testPipedAssets(asset: AVURLAsset, requestRange: String?, isHLS: Bool, forbiddenAssetTestGroup: DispatchGroup, completion: @escaping (Bool) -> Void) { private func testPipedAssets(asset: AVURLAsset, requestRange: String?, isHLS: Bool, forbiddenAssetTestGroup: DispatchGroup, completion: @escaping (Int) -> Void) {
PipedAPI.nonProxiedAsset(asset: asset) { possibleNonProxiedAsset in PipedAPI.nonProxiedAsset(asset: asset) { possibleNonProxiedAsset in
if let nonProxiedAsset = possibleNonProxiedAsset { if let nonProxiedAsset = possibleNonProxiedAsset {
self.testAsset(url: nonProxiedAsset.url, range: requestRange, isHLS: isHLS, forbiddenAssetTestGroup: forbiddenAssetTestGroup, completion: completion) self.testAsset(url: nonProxiedAsset.url, range: requestRange, isHLS: isHLS, forbiddenAssetTestGroup: forbiddenAssetTestGroup, completion: completion)
} else { } else {
completion(false) completion(0)
} }
} }
} }