Repository: streamlink/streamlink

Issue:
Change default HLS playlist reload time to 6 seconds rather than 15 to meet HLS spec recommendation
### Checklist

- [X] This is a feature request and not a different kind of issue
- [X] [I have read the contribution guidelines](https://github.com/streamlink/streamlink/blob/master/CONTRIBUTING.md#contributing-to-streamlink)
- [X] [I have checked the list of open and recently closed plugin requests](https://github.com/streamlink/streamlink/issues?q=is%3Aissue+label%3A%22feature+request%22)

### Description

The current default playlist reload time for HLS is set to 15 seconds if there is no higher precedent reload time overriding it.
https://github.com/streamlink/streamlink/blob/e2c9032f9e879ca98460fa319e215b32165bd9e1/src/streamlink/stream/hls.py#L220
[It seems like this default has been in place since HLS was originally implemented 9 years ago.](https://github.com/streamlink/streamlink/commit/7909377651f8d85c76d9016b4c7e61d17a7a6d75)
[The current recommended target duration for HLS is 6 seconds](https://developer.apple.com/documentation/http_live_streaming/hls_authoring_specification_for_apple_devices#2969514). I think using this value as the default fallback makes more sense than 15 seconds. With 15 second reload times, it's somewhat likely for segments to expire between playlist reloads if the default reload time is selected for whatever reason.


Discussion:
The default playlist reload time is derived from the `EXT-X-TARGETDURATION` tag, which is mandatory in every HLS media playlist:
https://datatracker.ietf.org/doc/html/rfc8216#section-4.3.3.1

The value of 15 is the default before the actual reload time gets computed and it's a last resort value when there's no target duration (an invalid HLS media playlist) **and** when there aren't any segments in the playlist. If there are segments in the media playlist and there's no target duration or if it's zero, then the sum of segment durations from the number of segments of the current live edge value is used.
https://github.com/streamlink/streamlink/blob/e2c9032f9e879ca98460fa319e215b32165bd9e1/tests/streams/test_hls.py#L275-L354

There is no need to change this value unless it's somehow important to handle an edge case of an edge case. If you want to change it, then please send a pull request and fix up the test cases with new logical values.

Resolving patch:
diff --git a/src/streamlink/stream/hls.py b/src/streamlink/stream/hls.py
--- a/src/streamlink/stream/hls.py
+++ b/src/streamlink/stream/hls.py
@@ -217,7 +217,7 @@ def __init__(self, *args, **kwargs):
         self.playlist_end: Optional[Sequence.num] = None
         self.playlist_sequence: int = -1
         self.playlist_sequences: List[Sequence] = []
-        self.playlist_reload_time: float = 15
+        self.playlist_reload_time: float = 6
         self.playlist_reload_time_override = self.session.options.get("hls-playlist-reload-time")
         self.playlist_reload_retries = self.session.options.get("hls-playlist-reload-attempts")
         self.live_edge = self.session.options.get("hls-live-edge")


Test patch:
diff --git a/tests/streams/test_hls.py b/tests/streams/test_hls.py
--- a/tests/streams/test_hls.py
+++ b/tests/streams/test_hls.py
@@ -310,40 +310,40 @@ def mocked_playlist_reload_time(*args, **kwargs):
             return thread.reader.worker.playlist_reload_time
 
     def test_hls_playlist_reload_time_default(self):
-        time = self.subject([Playlist(0, self.segments, end=True, targetduration=6)], reload_time="default")
-        self.assertEqual(time, 6, "default sets the reload time to the playlist's target duration")
+        time = self.subject([Playlist(0, self.segments, end=True, targetduration=4)], reload_time="default")
+        self.assertEqual(time, 4, "default sets the reload time to the playlist's target duration")
 
     def test_hls_playlist_reload_time_segment(self):
-        time = self.subject([Playlist(0, self.segments, end=True, targetduration=6)], reload_time="segment")
+        time = self.subject([Playlist(0, self.segments, end=True, targetduration=4)], reload_time="segment")
         self.assertEqual(time, 3, "segment sets the reload time to the playlist's last segment")
 
     def test_hls_playlist_reload_time_segment_no_segments(self):
-        time = self.subject([Playlist(0, [], end=True, targetduration=6)], reload_time="segment")
-        self.assertEqual(time, 6, "segment sets the reload time to the targetduration if no segments are available")
+        time = self.subject([Playlist(0, [], end=True, targetduration=4)], reload_time="segment")
+        self.assertEqual(time, 4, "segment sets the reload time to the targetduration if no segments are available")
 
     def test_hls_playlist_reload_time_segment_no_segments_no_targetduration(self):
         time = self.subject([Playlist(0, [], end=True, targetduration=0)], reload_time="segment")
-        self.assertEqual(time, 15, "sets reload time to 15 seconds when no segments and no targetduration are available")
+        self.assertEqual(time, 6, "sets reload time to 6 seconds when no segments and no targetduration are available")
 
     def test_hls_playlist_reload_time_live_edge(self):
-        time = self.subject([Playlist(0, self.segments, end=True, targetduration=6)], reload_time="live-edge")
+        time = self.subject([Playlist(0, self.segments, end=True, targetduration=4)], reload_time="live-edge")
         self.assertEqual(time, 8, "live-edge sets the reload time to the sum of the number of segments of the live-edge")
 
     def test_hls_playlist_reload_time_live_edge_no_segments(self):
-        time = self.subject([Playlist(0, [], end=True, targetduration=6)], reload_time="live-edge")
-        self.assertEqual(time, 6, "live-edge sets the reload time to the targetduration if no segments are available")
+        time = self.subject([Playlist(0, [], end=True, targetduration=4)], reload_time="live-edge")
+        self.assertEqual(time, 4, "live-edge sets the reload time to the targetduration if no segments are available")
 
     def test_hls_playlist_reload_time_live_edge_no_segments_no_targetduration(self):
         time = self.subject([Playlist(0, [], end=True, targetduration=0)], reload_time="live-edge")
-        self.assertEqual(time, 15, "sets reload time to 15 seconds when no segments and no targetduration are available")
+        self.assertEqual(time, 6, "sets reload time to 6 seconds when no segments and no targetduration are available")
 
     def test_hls_playlist_reload_time_number(self):
-        time = self.subject([Playlist(0, self.segments, end=True, targetduration=6)], reload_time="4")
-        self.assertEqual(time, 4, "number values override the reload time")
+        time = self.subject([Playlist(0, self.segments, end=True, targetduration=4)], reload_time="2")
+        self.assertEqual(time, 2, "number values override the reload time")
 
     def test_hls_playlist_reload_time_number_invalid(self):
-        time = self.subject([Playlist(0, self.segments, end=True, targetduration=6)], reload_time="0")
-        self.assertEqual(time, 6, "invalid number values set the reload time to the playlist's targetduration")
+        time = self.subject([Playlist(0, self.segments, end=True, targetduration=4)], reload_time="0")
+        self.assertEqual(time, 4, "invalid number values set the reload time to the playlist's targetduration")
 
     def test_hls_playlist_reload_time_no_target_duration(self):
         time = self.subject([Playlist(0, self.segments, end=True, targetduration=0)], reload_time="default")
@@ -351,7 +351,7 @@ def test_hls_playlist_reload_time_no_target_duration(self):
 
     def test_hls_playlist_reload_time_no_data(self):
         time = self.subject([Playlist(0, [], end=True, targetduration=0)], reload_time="default")
-        self.assertEqual(time, 15, "sets reload time to 15 seconds when no data is available")
+        self.assertEqual(time, 6, "sets reload time to 6 seconds when no data is available")
 
 
 @patch('streamlink.stream.hls.FFMPEGMuxer.is_usable', Mock(return_value=True))