def test_offset_and_duration(self): thread, segments = self.subject([ Playlist(1234, [ Segment(0), Segment(1, duration=0.5), Segment(2, duration=0.5), Segment(3) ], end=True) ], streamoptions={ "start_offset": 1, "duration": 1 }) data = self.await_read(read_all=True) self.assertEqual(data, self.content(segments, cond=lambda s: 0 < s.num < 3), "Respects the offset and duration") self.assertTrue( all([self.called(s) for s in segments.values() if 0 < s.num < 3]), "Downloads second and third segment") self.assertFalse( any([self.called(s) for s in segments.values() if 0 > s.num > 3]), "Skips other segments")
def test_hls_low_latency_has_prefetch_disable_ads_has_preroll( self, mock_log): self.subject([ Playlist(0, [SegmentAd(0), SegmentAd(1), SegmentAd(2), SegmentAd(3)]), Playlist(4, [ Segment(4), Segment(5), Segment(6), Segment(7), SegmentPrefetch(8), SegmentPrefetch(9) ], end=True) ], disable_ads=True, low_latency=True) self.await_read(read_all=True) self.assertEqual(mock_log.info.mock_calls, [ call("Will skip ad segments"), call("Low latency streaming (HLS live edge: 2)"), call("Waiting for pre-roll ads to finish, be patient") ])
def test_filtered_no_timeout(self): thread, reader, writer, segments = self.subject([ Playlist(0, [SegmentFiltered(0), SegmentFiltered(1)]), Playlist(2, [Segment(2), Segment(3)], end=True) ]) self.assertTrue(reader.filter_event.is_set(), "Doesn't let the reader wait if not filtering") self.await_write(2) self.assertFalse(reader.filter_event.is_set(), "Lets the reader wait if filtering") # make reader read (no data available yet) thread.read_wait.set() # once data becomes available, the reader continues reading self.await_write() self.assertTrue(reader.filter_event.is_set(), "Reader is not waiting anymore") thread.read_done.wait() thread.read_done.clear() self.assertFalse(thread.error, "Doesn't time out when filtering") self.assertEqual(b"".join(thread.data), segments[2].content, "Reads next available buffer data") self.await_write() data = self.await_read() self.assertEqual(data, self.content(segments, cond=lambda s: s.num >= 2))
def test_hls_low_latency_has_prefetch_has_preroll(self, mock_log): thread, segments = self.subject([ Playlist(0, [SegmentAd(0), SegmentAd(1), SegmentAd(2), SegmentAd(3)]), Playlist(4, [ Segment(4), Segment(5), Segment(6), Segment(7), SegmentPrefetch(8), SegmentPrefetch(9) ], end=True) ], disable_ads=False, low_latency=True) self.assertEqual(self.await_read(read_all=True), self.content(segments, cond=lambda s: s.num > 1), "Skips first two segments due to reduced live-edge") self.assertFalse( any([self.called(s) for s in segments.values() if s.num < 2]), "Skips first two preroll segments") self.assertTrue( all([self.called(s) for s in segments.values() if s.num >= 2]), "Downloads all remaining segments") self.assertEqual(mock_log.info.mock_calls, [call("Low latency streaming (HLS live edge: 2)")])
def test_filtered_logging(self, mock_log): thread, reader, writer, segments = self.subject([ Playlist(0, [SegmentFiltered(0), SegmentFiltered(1)]), Playlist(2, [Segment(2), Segment(3)]), Playlist(4, [SegmentFiltered(4), SegmentFiltered(5)]), Playlist(6, [Segment(6), Segment(7)], end=True) ]) data = b"" self.assertTrue(reader.filter_event.is_set(), "Doesn't let the reader wait if not filtering") for i in range(2): self.await_write(2) self.assertEqual(len(mock_log.info.mock_calls), i * 2 + 1) self.assertEqual(mock_log.info.mock_calls[i * 2 + 0], call("Filtering out segments and pausing stream output")) self.assertFalse(reader.filter_event.is_set(), "Lets the reader wait if filtering") self.await_write(2) self.assertEqual(len(mock_log.info.mock_calls), i * 2 + 2) self.assertEqual(mock_log.info.mock_calls[i * 2 + 1], call("Resuming stream output")) self.assertTrue(reader.filter_event.is_set(), "Doesn't let the reader wait if not filtering") data += self.await_read() self.assertEqual( data, self.content(segments, cond=lambda s: s.num % 4 > 1), "Correctly filters out segments" ) self.assertTrue(all([self.called(s) for s in segments.values()]), "Downloads all segments")
def test_offsets(self): map1 = TagMap(1, self.id(), {"BYTERANGE": "\"1234@0\""}) map2 = TagMap(2, self.id(), {"BYTERANGE": "\"42@1337\""}) self.mock("GET", self.url(map1), content=map1.content) self.mock("GET", self.url(map2), content=map2.content) s1, s2, s3, s4, s5 = Segment(0), Segment(1), Segment(2), Segment(3), Segment(4) self.subject([ Playlist(0, [ map1, Tag("EXT-X-BYTERANGE", "5@3"), s1, Tag("EXT-X-BYTERANGE", "7"), s2, map2, Tag("EXT-X-BYTERANGE", "11"), s3, Tag("EXT-X-BYTERANGE", "17@13"), s4, Tag("EXT-X-BYTERANGE", "19"), s5, ], end=True) ]) self.await_write(5 * 2) self.await_read(read_all=True) self.assertEqual(self.mocks[self.url(map1)].last_request._request.headers["Range"], "bytes=0-1233") self.assertEqual(self.mocks[self.url(map2)].last_request._request.headers["Range"], "bytes=1337-1378") self.assertEqual(self.mocks[self.url(s1)].last_request._request.headers["Range"], "bytes=3-7") self.assertEqual(self.mocks[self.url(s2)].last_request._request.headers["Range"], "bytes=8-14") self.assertEqual(self.mocks[self.url(s3)].last_request._request.headers["Range"], "bytes=15-25") self.assertEqual(self.mocks[self.url(s4)].last_request._request.headers["Range"], "bytes=13-29") self.assertEqual(self.mocks[self.url(s5)].last_request._request.headers["Range"], "bytes=30-48")
class TestHlsPlaylistReloadTime(TestMixinStreamHLS, unittest.TestCase): segments = [Segment(0, "", 11), Segment(1, "", 7), Segment(2, "", 5), Segment(3, "", 3)] def get_session(self, options=None, reload_time=None, *args, **kwargs): return super(TestHlsPlaylistReloadTime, self).get_session( dict(options or {}, **{"hls-live-edge": 3, "hls-playlist-reload-time": reload_time}) ) def subject(self, *args, **kwargs): thread, _ = super(TestHlsPlaylistReloadTime, self).subject(*args, **kwargs) self.await_read(read_all=True) 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=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=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=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, 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=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=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, 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=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=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") self.assertEqual(time, 8, "uses the live-edge sum if the playlist is missing the targetduration data") def test_hls_playlist_reload_time_no_data(self): time = self.subject([Playlist(0, [], end=True, targetduration=0)], reload_time="default") self.assertEqual(time, 6, "sets reload time to 6 seconds when no data is available")
def test_hls_segment_ignore_names(self): thread, reader, writer, segments = self.subject([ Playlist(0, [Segment(0), Segment(1), Segment(2), Segment(3)], end=True) ], {"hls-segment-ignore-names": [ ".*", "segment0", "segment2", ]}) self.await_write(4) self.assertEqual(self.await_read(), self.content(segments, cond=lambda s: s.num % 2 > 0))
def test_hls_no_disable_ads_has_preroll(self, mock_log): thread, segments = self.subject([ Playlist(0, [SegmentAd(0), SegmentAd(1)]), Playlist(2, [Segment(2), Segment(3)], end=True) ], disable_ads=False, low_latency=False) self.assertEqual(self.await_read(read_all=True), self.content(segments), "Doesn't filter out segments") self.assertTrue(all([self.called(s) for s in segments.values()]), "Downloads all segments") self.assertEqual(mock_log.info.mock_calls, [], "Doesn't log anything")
def test_map(self): discontinuity = Tag("EXT-X-DISCONTINUITY") map1 = TagMap(1, self.id()) map2 = TagMap(2, self.id()) self.mock("GET", self.url(map1), content=map1.content) self.mock("GET", self.url(map2), content=map2.content) thread, segments = self.subject([ Playlist( 0, [map1, Segment(0), Segment(1), Segment(2), Segment(3)]), Playlist(4, [ map1, Segment(4), map2, Segment(5), Segment(6), discontinuity, Segment(7) ], end=True) ]) data = self.await_read(read_all=True, timeout=None) self.assertEqual( data, self.content([ map1, segments[1], map1, segments[2], map1, segments[3], map1, segments[4], map2, segments[5], map2, segments[6], segments[7] ])) self.assertTrue(self.called(map1, once=True), "Downloads first map only once") self.assertTrue(self.called(map2, once=True), "Downloads second map only once")
def test_filtered_timeout(self): thread, reader, writer, segments = self.subject([ Playlist(0, [Segment(0), Segment(1)], end=True) ]) self.await_write() data = self.await_read() self.assertEqual(data, segments[0].content, "Has read the first segment") # simulate a timeout by having an empty buffer # timeout value is set to 0 with self.assertRaises(IOError) as cm: self.await_read() self.assertEqual(str(cm.exception), "Read timeout", "Raises a timeout error when no data is available to read")
def test_unknown_offset(self, mock_log: Mock): thread, _ = self.subject([ Playlist(0, [ Tag("EXT-X-BYTERANGE", "3"), Segment(0), Segment(1) ], end=True) ]) self.await_write(2 - 1) self.thread.close() self.assertEqual(mock_log.error.call_args_list, [ call("Failed to fetch segment 0: Missing BYTERANGE offset") ]) self.assertFalse(self.called(Segment(0)))
def test_reload(self, mock_log): thread, segments = self.subject([ Playlist(1, [Segment(0)]), self.InvalidPlaylist(), self.InvalidPlaylist(), Playlist(2, [Segment(2)], end=True) ]) self.await_write(2) data = self.await_read(read_all=True) self.assertEqual(data, self.content(segments)) self.close() self.await_close() self.assertEqual(mock_log.warning.mock_calls, [ call("Failed to reload playlist: Missing #EXTM3U header"), call("Failed to reload playlist: Missing #EXTM3U header") ])
def test_hls_disable_ads_has_midstream(self, mock_log): thread, segments = self.subject([ Playlist(0, [Segment(0), Segment(1)]), Playlist(2, [SegmentAd(2), SegmentAd(3)]), Playlist(4, [Segment(4), Segment(5)], end=True) ], disable_ads=True, low_latency=False) self.assertEqual( self.await_read(read_all=True), self.content(segments, cond=lambda s: s.num != 2 and s.num != 3), "Filters out mid-stream ad segments") self.assertTrue(all([self.called(s) for s in segments.values()]), "Downloads all segments") self.assertEqual(mock_log.info.mock_calls, [call("Will skip ad segments")])
def test_hls_disable_ads_has_preroll(self, mock_log): thread, segments = self.subject([ Playlist(0, [SegmentAd(0), SegmentAd(1)]), Playlist(2, [SegmentAd(2), SegmentAd(3)]), Playlist(4, [Segment(4), Segment(5)], end=True) ], disable_ads=True, low_latency=False) self.assertEqual(self.await_read(read_all=True), self.content(segments, cond=lambda s: s.num >= 4), "Filters out preroll ad segments") self.assertTrue(all([self.called(s) for s in segments.values()]), "Downloads all segments") self.assertEqual(mock_log.info.mock_calls, [ call("Will skip ad segments"), call("Waiting for pre-roll ads to finish, be patient") ])
def test_unknown_offset_map(self, mock_log: Mock): map1 = TagMap(1, self.id(), {"BYTERANGE": "\"1234\""}) self.mock("GET", self.url(map1), content=map1.content) thread, _ = self.subject([ Playlist(0, [ Segment(0), map1, Segment(1) ], end=True) ]) self.await_write(3 - 1) self.thread.close() self.assertEqual(mock_log.error.call_args_list, [ call("Failed to fetch map for segment 1: Missing BYTERANGE offset") ]) self.assertFalse(self.called(map1))
def test_hls_low_latency_no_prefetch(self, mock_log): self.subject([ Playlist( 0, [Segment(0), Segment(1), Segment(2), Segment(3)]), Playlist( 4, [Segment(4), Segment(5), Segment(6), Segment(7)], end=True) ], disable_ads=False, low_latency=True) self.assertTrue(self.session.get_plugin_option("twitch", "low-latency")) self.assertFalse( self.session.get_plugin_option("twitch", "disable-ads")) self.await_read(read_all=True) self.assertEqual(mock_log.info.mock_calls, [ call("Low latency streaming (HLS live edge: 2)"), call("This is not a low latency stream") ])
def test_hls_low_latency_has_prefetch(self, mock_log): thread, segments = self.subject([ Playlist(0, [ Segment(0), Segment(1), Segment(2), Segment(3), SegmentPrefetch(4), SegmentPrefetch(5) ]), Playlist(4, [ Segment(4), Segment(5), Segment(6), Segment(7), SegmentPrefetch(8), SegmentPrefetch(9) ], end=True) ], disable_ads=False, low_latency=True) self.assertEqual(2, self.session.options.get("hls-live-edge")) self.assertEqual(True, self.session.options.get("hls-segment-stream-data")) self.assertEqual(self.await_read(read_all=True), self.content(segments, cond=lambda s: s.num >= 4), "Skips first four segments due to reduced live-edge") self.assertFalse( any([self.called(s) for s in segments.values() if s.num < 4]), "Doesn't download old segments") self.assertTrue( all([self.called(s) for s in segments.values() if s.num >= 4]), "Downloads all remaining segments") self.assertEqual(mock_log.info.mock_calls, [call("Low latency streaming (HLS live edge: 2)")])
def test_hls_no_low_latency_has_prefetch(self, mock_log): thread, segments = self.subject([ Playlist(0, [ Segment(0), Segment(1), Segment(2), Segment(3), SegmentPrefetch(4), SegmentPrefetch(5) ]), Playlist(4, [ Segment(4), Segment(5), Segment(6), Segment(7), SegmentPrefetch(8), SegmentPrefetch(9) ], end=True) ], disable_ads=False, low_latency=False) self.assertEqual(4, self.session.options.get("hls-live-edge")) self.assertEqual(False, self.session.options.get("hls-segment-stream-data")) self.assertEqual(self.await_read(read_all=True), self.content(segments, cond=lambda s: s.num < 8), "Ignores prefetch segments") self.assertTrue( all([self.called(s) for s in segments.values() if s.num <= 7]), "Ignores prefetch segments") self.assertFalse( any([self.called(s) for s in segments.values() if s.num > 7]), "Ignores prefetch segments") self.assertEqual(mock_log.info.mock_calls, [], "Doesn't log anything")
def test_invalid_offset_reference(self, mock_log: Mock): thread, _ = self.subject([ Playlist(0, [ Tag("EXT-X-BYTERANGE", "3@0"), Segment(0), Segment(1), Tag("EXT-X-BYTERANGE", "5"), Segment(2), Segment(3) ], end=True) ]) self.await_write(4 - 1) self.thread.close() self.assertEqual(mock_log.error.call_args_list, [ call("Failed to fetch segment 2: Missing BYTERANGE offset") ]) self.assertEqual(self.mocks[self.url(Segment(0))].last_request._request.headers["Range"], "bytes=0-2") self.assertFalse(self.called(Segment(2)))
class TestHlsPlaylistReloadTime(TestMixinStreamHLS, unittest.TestCase): segments = [ Segment(0, duration=11), Segment(1, duration=7), Segment(2, duration=5), Segment(3, duration=3) ] def get_session(self, options=None, reload_time=None, *args, **kwargs): return super().get_session(dict(options or {}, **{ "hls-live-edge": 3, "hls-playlist-reload-time": reload_time })) def subject(self, *args, **kwargs): thread, segments = super().subject(start=False, *args, **kwargs) # mock the worker thread's _playlist_reload_time method, so that the main thread can wait on its call playlist_reload_time_called = Event() orig_playlist_reload_time = thread.reader.worker._playlist_reload_time def mocked_playlist_reload_time(*args, **kwargs): playlist_reload_time_called.set() return orig_playlist_reload_time(*args, **kwargs) # immediately kill the writer thread as we don't need it and don't want to wait for its queue polling to end def mocked_futures_get(): return None, None with patch.object(thread.reader.worker, "_playlist_reload_time", side_effect=mocked_playlist_reload_time), \ patch.object(thread.reader.writer, "_futures_get", side_effect=mocked_futures_get): self.start() if not playlist_reload_time_called.wait(timeout=5): # pragma: no cover raise RuntimeError("Missing _playlist_reload_time() call") # wait for the worker thread to terminate, so that deterministic assertions can be done about the reload time thread.reader.worker.join() 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=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=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=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, 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=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=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, 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=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=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") self.assertEqual(time, 8, "uses the live-edge sum if the playlist is missing the targetduration data") def test_hls_playlist_reload_time_no_data(self): time = self.subject([Playlist(0, [], end=True, targetduration=0)], reload_time="default") self.assertEqual(time, 6, "sets reload time to 6 seconds when no data is available")