package credits import ( "context" "sync" "time" ) // Whether the server is too busy to be reading media speculatively. // // Deliberately not a telemetry subsystem. The gateway already knows the only thing that // actually matters here — how many televisions are playing something right now — because // every one of them reports playback started, progress and stopped. A scan competes with // those streams for the same disk and the same Emby, so the count is a better predictor of // harm than a CPU figure would be, and it costs nothing to keep. const ( // busyPlaybacks is how many concurrent streams make speculative scanning unwelcome. Two // televisions is an ordinary evening in a household and the NAS copes; a third is the // point at which adding ranged reads of a fourth file stops being free. busyPlaybacks = 2 // playbackStale is how long a playback is believed without a progress report. Reports // arrive every ten seconds, so a minute of silence means the television went away // without saying so — a crash, a power cut, a network drop. Without this the gauge would // latch busy for ever on one lost stop report and speculative scanning would never run // again until the container restarted. playbackStale = 90 * time.Second ) // PlaybackLoad counts what is playing, from the reports televisions already send. type PlaybackLoad struct { mu sync.Mutex active map[string]time.Time } func NewPlaybackLoad() *PlaybackLoad { return &PlaybackLoad{active: map[string]time.Time{}} } // Playing records that a stream is alive. Called from playback started and from each // progress report, which is what makes the staleness sweep work: a live stream keeps // refreshing its own timestamp. func (l *PlaybackLoad) Playing(sessionKey string) { if l == nil || sessionKey == "" { return } l.mu.Lock() l.active[sessionKey] = time.Now() l.mu.Unlock() } // Stopped records that a stream ended. func (l *PlaybackLoad) Stopped(sessionKey string) { if l == nil || sessionKey == "" { return } l.mu.Lock() delete(l.active, sessionKey) l.mu.Unlock() } // Busy is the one question the scheduler asks. func (l *PlaybackLoad) Busy(context.Context) bool { return l.Count() >= busyPlaybacks } // Count is the live stream count, sweeping anything that stopped reporting. The sweep is // here rather than on a ticker because this is the only reader, and a background goroutine // to expire at most a handful of map entries would cost more than it tidies. func (l *PlaybackLoad) Count() int { if l == nil { return 0 } cutoff := time.Now().Add(-playbackStale) l.mu.Lock() defer l.mu.Unlock() for key, seen := range l.active { if seen.Before(cutoff) { delete(l.active, key) } } return len(l.active) }