96 lines
3.3 KiB
Swift
96 lines
3.3 KiB
Swift
import Foundation
|
|
|
|
// The caller supplies monotonic seconds, keeping the policy independent of timers and extraction.
|
|
struct AutomaticQuality {
|
|
struct Sample {
|
|
var enabled = true
|
|
var network = true
|
|
var playing = true
|
|
var opening = false
|
|
var seeking = false
|
|
var live = false
|
|
var buffering = false
|
|
var position: Double
|
|
var duration: Double
|
|
var ahead: Double?
|
|
var height: Int?
|
|
var heights: [Int]
|
|
}
|
|
|
|
private var lowSince: Double?
|
|
private var healthySince: Double?
|
|
private var stallSince: Double?
|
|
private var lastPosition: Double?
|
|
private var lastProgress: Double?
|
|
private var notBefore = -Double.infinity
|
|
|
|
mutating func resetHealth() {
|
|
lowSince = nil
|
|
healthySince = nil
|
|
stallSince = nil
|
|
lastPosition = nil
|
|
lastProgress = nil
|
|
}
|
|
|
|
mutating func extractionFailed(at now: Double) {
|
|
resetHealth()
|
|
notBefore = max(notBefore, now + 120)
|
|
}
|
|
|
|
mutating func choose(_ sample: Sample, at now: Double) -> Int? {
|
|
guard sample.enabled, sample.network, sample.playing, !sample.opening, !sample.seeking,
|
|
sample.position.isFinite, let height = sample.height, height > 0 else {
|
|
resetHealth()
|
|
return nil
|
|
}
|
|
guard !sample.heights.isEmpty,
|
|
sample.live || !sample.duration.isFinite || sample.duration <= 0 ||
|
|
sample.duration - sample.position > 15 else {
|
|
resetHealth()
|
|
return nil
|
|
}
|
|
if let lastPosition, sample.position < lastPosition - 0.1 {
|
|
resetHealth()
|
|
}
|
|
if lastPosition == nil || sample.position > lastPosition! + 0.01 {
|
|
lastPosition = sample.position
|
|
lastProgress = now
|
|
}
|
|
let stalled = sample.buffering || now - (lastProgress ?? now) >= 3
|
|
let ahead = sample.ahead.flatMap { $0.isFinite && $0 >= 0 ? $0 : nil }
|
|
// Live packet timestamps are not a reliable reserve estimate.
|
|
let knownReserve = !sample.live && ahead != nil
|
|
if stalled {
|
|
stallSince = stallSince ?? now
|
|
} else {
|
|
stallSince = nil
|
|
}
|
|
if knownReserve && ahead! < 4 {
|
|
lowSince = lowSince ?? now
|
|
} else {
|
|
lowSince = nil
|
|
}
|
|
if !stalled && (!knownReserve || ahead! >= 20) {
|
|
healthySince = healthySince ?? now
|
|
} else {
|
|
healthySince = nil
|
|
}
|
|
guard now >= notBefore else { return nil }
|
|
let downshift = stallSince.map({ now - $0 >= 3 }) == true || lowSince.map({ now - $0 >= 6 }) == true
|
|
let upshift = healthySince.map({ now - $0 >= (knownReserve ? 60 : 120) }) == true
|
|
guard height > 1080 || downshift || upshift else { return nil }
|
|
var target: Int?
|
|
for available in sample.heights where available > 0 && available <= 1080 {
|
|
if height > 1080 || downshift {
|
|
if available < height && available > (target ?? 0) { target = available }
|
|
} else if available > height && available < (target ?? Int.max) {
|
|
target = available
|
|
}
|
|
}
|
|
guard let target else { return nil }
|
|
notBefore = now + 30
|
|
resetHealth()
|
|
return target
|
|
}
|
|
}
|