From db42e716b58bbd699ff120093ffe47c3c00d425c Mon Sep 17 00:00:00 2001 From: Andrew Kuklewicz Date: Thu, 24 Sep 2026 11:24:23 -0400 Subject: [PATCH] Set target duration to range floor for low average segments instead of an error --- src/containers/hls/lib/playlists.rb | 16 +++++++++------- src/containers/hls/test/playlists_test.rb | 15 +++++++++++++-- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/containers/hls/lib/playlists.rb b/src/containers/hls/lib/playlists.rb index ed9c617a..d001c1ee 100644 --- a/src/containers/hls/lib/playlists.rb +++ b/src/containers/hls/lib/playlists.rb @@ -139,14 +139,16 @@ def measure(playlist) # small TARGETDURATION and must not be dragged up. def harmonize_target_duration playlists = @video_rungs.map(&:playlist) + [@audio.playlist] - target = playlists.map { |pl| required_target_duration(pl) }.max - + longest = playlists.map { |pl| required_target_duration(pl) }.max range = @s::TARGET_DURATION_RANGE - unless range.cover?(target) - raise "EXT-X-TARGETDURATION is #{target}s, outside the allowed range " \ - "#{range.min}-#{range.max}s. Adjust TARGET (and HLS_TIME, which is " \ - "the min_segment floor that evicts grid points near a break and so " \ - "lengthens the adjacent segment)." + + # use the range min as a floor (b/c of breaks, there may be shorter segments; that's fine) + # the boundaries logic should prevent this from happening, but just in case, + # do check no segment exceeds the range max; that's not ok (re: RFC 8216) + target = [longest, range.min].max + if target > range.max + raise "EXT-X-TARGETDURATION is #{target}s, above the allowed maximum " \ + "#{range.max}s. Lower MAX_SEG, which caps segment length." end playlists.each do |pl| diff --git a/src/containers/hls/test/playlists_test.rb b/src/containers/hls/test/playlists_test.rb index ba6955e9..6867d591 100644 --- a/src/containers/hls/test/playlists_test.rb +++ b/src/containers/hls/test/playlists_test.rb @@ -127,15 +127,26 @@ def playlists_for(dir, breaks: []) end end - it "refuses to write a playlist outside the allowed range" do + it "refuses to write a playlist whose segments exceed the allowed maximum" do Dir.mktmpdir do |dir| build_package(dir, video_durations: [14.0, 6.0]) err = _ { playlists_for(dir).write_all }.must_raise RuntimeError - _(err.message).must_match(/outside the allowed range/) + _(err.message).must_match(/above the allowed maximum/) # and must not leave a master behind _(File.exist?(File.join(dir, "index.m3u8"))).must_equal false end end + + it "declares the range floor when every segment is shorter than it" do + Dir.mktmpdir do |dir| + build_package(dir, video_durations: [5.3, 4.9, 3.55], + audio_durations: [5.290667, 4.9, 3.55]) + playlists_for(dir).write_all + %w[720p.m3u8 480p.m3u8 audio.m3u8].each do |name| + _(File.read(File.join(dir, name))).must_include "#EXT-X-TARGETDURATION:6" + end + end + end end describe "EXT-X-VERSION floor" do