diff options
Diffstat (limited to 'server')
| -rw-r--r-- | server/internal/skips/kinds.go | 51 | ||||
| -rw-r--r-- | server/internal/skips/kinds_test.go | 27 |
2 files changed, 69 insertions, 9 deletions
diff --git a/server/internal/skips/kinds.go b/server/internal/skips/kinds.go index e70ce05..e2518dd 100644 --- a/server/internal/skips/kinds.go +++ b/server/internal/skips/kinds.go @@ -38,10 +38,10 @@ const ( // confirm anything until a second fetch occasion has passed. func Classify(breaks []AdBreak, servedMs, declaredMs int64) []AdBreak { out := make([]AdBreak, 0, len(breaks)) - // If there's no declared length, we can't compare, so treat conservatively as inserted ads. + // If we can't judge (missing served or declared length), treat conservatively as inserted ads. // If both lengths are known and the file is longer than declared by more than encoding slop, // it contains inserted ads. - inserted := declaredMs <= 0 || (servedMs > 0 && declaredMs > 0 && servedMs-declaredMs > durationSlopMs) + inserted := servedMs <= 0 || declaredMs <= 0 || (servedMs > 0 && declaredMs > 0 && servedMs-declaredMs > durationSlopMs) for _, b := range breaks { b.Kind = KindAd @@ -53,15 +53,48 @@ func Classify(breaks []AdBreak, servedMs, declaredMs int64) []AdBreak { return out } -// furnitureKindAt names a break by where it sits. Opening first: a very short episode could -// satisfy both tests, and its first break is its opening. +// furnitureKindAt names a break by where it sits. +// +// For normal-length episodes, the opening and closing windows don't overlap. For very short +// episodes, they do: a break may be close to both the start and the end. In that case, classify +// by the nearest edge. func furnitureKindAt(b AdBreak, servedMs int64) BreakKind { - switch { - case b.StartMs <= edgeWindowMs: + if servedMs <= 0 { + return KindCue // shouldn't happen but be safe + } + + // Use a window size proportional to the episode length for very short episodes. + // For long episodes (>= 240s), use the fixed edgeWindowMs (120s). + // For short episodes, use a quarter of the episode length as the window. + windowMs := int64(edgeWindowMs) + if servedMs < 2*edgeWindowMs { + // For short episodes (< 240s), use a quarter of the episode as the window + windowMs = servedMs / 4 + } + + distToStart := b.StartMs + distToEnd := servedMs - b.EndMs + + inOpeningWindow := distToStart <= windowMs + inClosingWindow := distToEnd <= windowMs + + if inOpeningWindow && inClosingWindow { + // In the overlap - use nearest edge + if distToStart < distToEnd { + return KindIntro + } else if distToEnd < distToStart { + return KindOutro + } else { + // Exactly in the middle - treat as cue (interior break) + return KindCue + } + } + + if inOpeningWindow { return KindIntro - case servedMs > 0 && servedMs-b.EndMs <= edgeWindowMs: + } + if inClosingWindow { return KindOutro - default: - return KindCue } + return KindCue } diff --git a/server/internal/skips/kinds_test.go b/server/internal/skips/kinds_test.go index f9fc6f0..544b9ea 100644 --- a/server/internal/skips/kinds_test.go +++ b/server/internal/skips/kinds_test.go @@ -109,3 +109,30 @@ func TestClassifyLeavesEverythingButTheKindAlone(t *testing.T) { t.Errorf("classify altered the range: %+v, want %+v with only Kind set", got, in) } } + +// Finding 1: A zero or invalid servedMs falls through to furniture instead of the mandated "ad" fallback. +func TestAZeroServedLengthIsTreatedAsAdvertising(t *testing.T) { + got := Classify([]AdBreak{{StartMs: 50_000, EndMs: 55_000}}, 0, 100_000) + + if got[0].Kind != KindAd { + t.Errorf("kind %q, want %q when served length is zero", got[0].Kind, KindAd) + } +} + +// Finding 2: In a short episode, interior and closing breaks are misclassified as "intro". +func TestAShortEpisodeClassifiesBreaksByPosition(t *testing.T) { + // Episode is 100 seconds (less than 2*edgeWindowMs = 240s). + // With breaks at 0s, 50s, and 90s, they should be distinguished as intro, cue, and outro. + got := Classify([]AdBreak{ + {StartMs: 0, EndMs: 5_000}, + {StartMs: 50_000, EndMs: 55_000}, + {StartMs: 90_000, EndMs: 95_000}, + }, 100_000, 100_000) + + want := []BreakKind{KindIntro, KindCue, KindOutro} + for i, k := range kindsOf(got) { + if k != want[i] { + t.Errorf("break %d at %dms is %q, want %q in short episode", i, got[i].StartMs, k, want[i]) + } + } +} |
