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:
bastimeyer: What I meant with "fix up the test cases with new logical values" is that the other values should be changed as well, because 6 is already used as targetduration for most of the other test cases and this makes the test assertions ambiguous.

```diff
diff --git a/src/streamlink/stream/hls.py b/src/streamlink/stream/hls.py
index e6babb9c..13a87664 100644
--- a/src/streamlink/stream/hls.py
+++ b/src/streamlink/stream/hls.py
@@ -217,7 +217,7 @@ class HLSStreamWorker(SegmentedStreamWorker):
         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")
diff --git a/tests/streams/test_hls.py b/tests/streams/test_hls.py
index 1970fe5f..f0b58632 100644
--- a/tests/streams/test_hls.py
+++ b/tests/streams/test_hls.py
@@ -310,40 +310,40 @@ class TestHlsPlaylistReloadTime(TestMixinStreamHLS, unittest.TestCase):
             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 @@ class TestHlsPlaylistReloadTime(TestMixinStreamHLS, unittest.TestCase):
 
     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))
```
Hakkin: Alright, updated with suggested tests.

Patch:
diff --git a/src/streamlink/stream/hls.py b/src/streamlink/stream/hls.py
index e6babb9c..13a87664 100644
--- a/src/streamlink/stream/hls.py
+++ b/src/streamlink/stream/hls.py
@@ -217,7 +217,7 @@ class HLSStreamWorker(SegmentedStreamWorker):
         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
index 1970fe5f..7ecdba16 100644
--- a/tests/streams/test_hls.py
+++ b/tests/streams/test_hls.py
@@ -310,40 +310,40 @@ class TestHlsPlaylistReloadTime(TestMixinStreamHLS, unittest.TestCase):
             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 @@ class TestHlsPlaylistReloadTime(TestMixinStreamHLS, unittest.TestCase):
 
     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))