Each finished broadcast is filed locally — tier, probed encoder, rated, settled, steps spent — and Models/RateHistory turns that into at most a suggestion. The app never applies one: it asks before the next stream, "Use it" sets the rate, "Keep mine" remembers that exact number so the same question does not return until the evidence moves. Nothing leaves the machine. Two rules keep it from nagging. The high-water mark is the highest rate that *finished* a session with zero steps, not the highest seen, so one easy session (a menu screen, a quiet game) cannot set a target the machine cannot hold. And it is asymmetric: raising needs 10% above the current rate across five clean sessions of the same tier and encoder, lowering needs three step-downs in five — fixing damage should not wait for a percentage, and risking it should. The encoder key is real, not decorative: StreamHealth.VideoEncoder is now filled from the probe (FfmpegEncoder), so history recorded against NVENC is not applied to a machine that later runs QSV. At startup, before anything has streamed, the last session on that tier is the honest guess. 672 pass, 0 warnings.
72 lines
3.0 KiB
C#
72 lines
3.0 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using Xunit;
|
|
using ytLive.Models;
|
|
using ytLive.Services;
|
|
|
|
namespace ytLive.Tests;
|
|
|
|
/// <summary>
|
|
/// The rate the app suggests next session, from real sessions — and the two rules that keep it from
|
|
/// becoming a nuisance: five sessions of evidence per tier and encoder, a 10% gap before asking for
|
|
/// more, and a lower bar for backing off.
|
|
/// </summary>
|
|
public class RateHistoryTests
|
|
{
|
|
private static SessionRateResult Session(
|
|
double rated, double settled = -1, int steps = 0, string tier = "1080p60", string encoder = "h264_nvenc")
|
|
=> new(tier, encoder, rated, settled < 0 ? rated : settled, steps);
|
|
|
|
[Fact]
|
|
public void Suggestions_need_five_sessions_of_the_same_tier_and_encoder_and_persist_with_them()
|
|
{
|
|
// Four sessions is not evidence.
|
|
Assert.Null(RateHistory.Suggest(
|
|
Enumerable.Range(0, 4).Select(_ => Session(15)), "1080p60", "h264_nvenc", 12));
|
|
|
|
// Five clean sessions holding 15 against a 12 target: worth asking, and the ask is 10% up.
|
|
var raised = RateHistory.Suggest(
|
|
Enumerable.Range(0, 5).Select(_ => Session(15)), "1080p60", "h264_nvenc", 12);
|
|
Assert.NotNull(raised);
|
|
Assert.True(raised!.Value.Raise);
|
|
Assert.Equal(15, raised.Value.Mbps);
|
|
|
|
// The same evidence means nothing below the threshold (12 → 12.6 is under 10%).
|
|
Assert.Null(RateHistory.Suggest(
|
|
Enumerable.Range(0, 5).Select(_ => Session(13)), "1080p60", "h264_nvenc", 12));
|
|
|
|
// A different encoder is a different machine: no suggestion.
|
|
Assert.Null(RateHistory.Suggest(
|
|
Enumerable.Range(0, 5).Select(_ => Session(15)), "1080p60", "h264_qsv", 12));
|
|
|
|
// Three step-downs out of five asks to back off to the settled median, faster than it asks to
|
|
// climb — fixing damage should not wait for a 10% gap.
|
|
var lowered = RateHistory.Suggest(
|
|
new[]
|
|
{
|
|
Session(12, 9.6, 1), Session(12, 8.0, 2), Session(12), Session(12, 7.2, 1), Session(12),
|
|
},
|
|
"1080p60", "h264_nvenc", 12);
|
|
Assert.NotNull(lowered);
|
|
Assert.False(lowered!.Value.Raise);
|
|
Assert.Equal(9.6, lowered.Value.Mbps); // median of 9.6, 8.0, 12, 7.2, 12
|
|
|
|
// And the storage form survives a round trip through the settings row, which is where the
|
|
// evidence actually lives between runs.
|
|
var db = Path.Combine(Path.GetTempPath(), $"llamacasty-rate-history-{Guid.NewGuid():N}.db");
|
|
using (var store = new LayoutStore(db))
|
|
{
|
|
foreach (var entry in Enumerable.Range(0, 5).Select(_ => Session(15)))
|
|
store.AppendRateHistory(entry);
|
|
|
|
var reloaded = store.LoadRateHistory();
|
|
Assert.Equal(5, reloaded.Count);
|
|
Assert.Equal(Session(15), reloaded[0]);
|
|
Assert.Equal(15, RateHistory.Suggest(reloaded, "1080p60", "h264_nvenc", 12)!.Value.Mbps);
|
|
}
|
|
try { File.Delete(db); } catch (IOException) { }
|
|
}
|
|
}
|