Files
terebi/Sources/App/AutomaticQuality.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
}
}