feat: retain streaming media and add opt-in automatic quality
This commit is contained in:
@@ -0,0 +1,95 @@
|
||||
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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user