1
+ import logging
1
2
import unittest
3
+ from functools import partial
2
4
3
- from streamlink .plugins .twitch import Twitch
5
+ from streamlink .plugins .twitch import Twitch , TwitchHLSStream
6
+
7
+ import requests_mock
8
+ from tests .mock import call , patch
9
+
10
+ from streamlink .session import Streamlink
11
+ from tests .resources import text
12
+
13
+
14
+ log = logging .getLogger (__name__ )
4
15
5
16
6
17
class TestPluginTwitch (unittest .TestCase ):
@@ -21,3 +32,158 @@ def test_can_handle_url_negative(self):
21
32
]
22
33
for url in should_not_match :
23
34
self .assertFalse (Twitch .can_handle_url (url ))
35
+
36
+
37
+ class TestTwitchHLSStream (unittest .TestCase ):
38
+ scte35_out = "#EXT-X-DISCONTINUITY\n #EXT-X-SCTE35-OUT\n "
39
+ scte35_out_cont = "#EXT-X-SCTE35-OUT-CONT\n "
40
+ scte35_in = "#EXT-X-DISCONTINUITY\n #EXT-X-SCTE35-IN\n "
41
+ segment = "#EXTINF:1.000,\n stream{0}.ts\n "
42
+
43
+ def getMasterPlaylist (self ):
44
+ with text ("hls/test_master.m3u8" ) as pl :
45
+ return pl .read ()
46
+
47
+ def getPlaylist (self , media_sequence , items ):
48
+ playlist = """
49
+ #EXTM3U
50
+ #EXT-X-VERSION:5
51
+ #EXT-X-TARGETDURATION:1
52
+ #EXT-X-MEDIA-SEQUENCE:{0}
53
+ """ .format (media_sequence )
54
+
55
+ for item in items :
56
+ if type (item ) != int :
57
+ playlist += item
58
+ else :
59
+ playlist += self .segment .format (item )
60
+
61
+ return playlist
62
+
63
+ def start_streamlink (self , kwargs = None ):
64
+ kwargs = kwargs or {}
65
+ log .info ("Executing streamlink" )
66
+ streamlink = Streamlink ()
67
+
68
+ streamlink .set_option ("hls-live-edge" , 4 )
69
+ streamlink .plugins .get ("twitch" ).options .set ("disable-ads" , True )
70
+
71
+ masterStream = TwitchHLSStream .parse_variant_playlist (
72
+ streamlink ,
73
+ "http://mocked/path/master.m3u8" ,
74
+ ** kwargs
75
+ )
76
+ stream = masterStream ["1080p (source)" ].open ()
77
+ data = b"" .join (iter (partial (stream .read , 8192 ), b"" ))
78
+ stream .close ()
79
+ log .info ("End of streamlink execution" )
80
+ return data
81
+
82
+ def mock (self , mocked , method , url , * args , ** kwargs ):
83
+ mocked [url ] = method (url , * args , ** kwargs )
84
+
85
+ def get_result (self , streams , playlists ):
86
+ mocked = {}
87
+ with requests_mock .Mocker () as mock :
88
+ self .mock (mocked , mock .get , "http://mocked/path/master.m3u8" , text = self .getMasterPlaylist ())
89
+ self .mock (mocked , mock .get , "http://mocked/path/playlist.m3u8" , [{"text" : p } for p in playlists ])
90
+ for i , stream in enumerate (streams ):
91
+ self .mock (mocked , mock .get , "http://mocked/path/stream{0}.ts" .format (i ), content = stream )
92
+ return self .start_streamlink (), mocked
93
+
94
+ @patch ("streamlink.plugins.twitch.log" )
95
+ def test_hls_scte35_start_with_end (self , mock_logging ):
96
+ streams = ["[{0}]" .format (i ).encode ("ascii" ) for i in range (12 )]
97
+ playlists = [
98
+ self .getPlaylist (0 , [self .scte35_out , 0 , 1 , 2 , 3 ]),
99
+ self .getPlaylist (4 , [self .scte35_in , 4 , 5 , 6 , 7 ]),
100
+ self .getPlaylist (8 , [8 , 9 , 10 , 11 ]) + "#EXT-X-ENDLIST\n "
101
+ ]
102
+ result , mocked = self .get_result (streams , playlists )
103
+
104
+ expected = b'' .join (streams [4 :12 ])
105
+ self .assertEqual (expected , result )
106
+ for i , _ in enumerate (streams ):
107
+ self .assertTrue (mocked ["http://mocked/path/stream{0}.ts" .format (i )].called )
108
+ mock_logging .info .assert_has_calls ([
109
+ call ("Will skip ad segments" ),
110
+ call ("Will skip ads beginning with segment 0" ),
111
+ call ("Will stop skipping ads beginning with segment 4" )
112
+ ])
113
+
114
+ @patch ("streamlink.plugins.twitch.log" )
115
+ def test_hls_scte35_no_start (self , mock_logging ):
116
+ streams = ["[{0}]" .format (i ).encode ("ascii" ) for i in range (8 )]
117
+ playlists = [
118
+ self .getPlaylist (0 , [0 , 1 , 2 , 3 ]),
119
+ self .getPlaylist (4 , [self .scte35_in , 4 , 5 , 6 , 7 ]) + "#EXT-X-ENDLIST\n "
120
+ ]
121
+ result , mocked = self .get_result (streams , playlists )
122
+
123
+ expected = b'' .join (streams [0 :8 ])
124
+ self .assertEqual (expected , result )
125
+ for i , _ in enumerate (streams ):
126
+ self .assertTrue (mocked ["http://mocked/path/stream{0}.ts" .format (i )].called )
127
+ mock_logging .info .assert_has_calls ([
128
+ call ("Will skip ad segments" )
129
+ ])
130
+
131
+ @patch ("streamlink.plugins.twitch.log" )
132
+ def test_hls_scte35_no_start_with_cont (self , mock_logging ):
133
+ streams = ["[{0}]" .format (i ).encode ("ascii" ) for i in range (8 )]
134
+ playlists = [
135
+ self .getPlaylist (0 , [self .scte35_out_cont , 0 , 1 , 2 , 3 ]),
136
+ self .getPlaylist (4 , [self .scte35_in , 4 , 5 , 6 , 7 ]) + "#EXT-X-ENDLIST\n "
137
+ ]
138
+ result , mocked = self .get_result (streams , playlists )
139
+
140
+ expected = b'' .join (streams [4 :8 ])
141
+ self .assertEqual (expected , result )
142
+ for i , _ in enumerate (streams ):
143
+ self .assertTrue (mocked ["http://mocked/path/stream{0}.ts" .format (i )].called )
144
+ mock_logging .info .assert_has_calls ([
145
+ call ("Will skip ad segments" ),
146
+ call ("Will skip ads beginning with segment 0" ),
147
+ call ("Will stop skipping ads beginning with segment 4" )
148
+ ])
149
+
150
+ @patch ("streamlink.plugins.twitch.log" )
151
+ def test_hls_scte35_no_end (self , mock_logging ):
152
+ streams = ["[{0}]" .format (i ).encode ("ascii" ) for i in range (12 )]
153
+ playlists = [
154
+ self .getPlaylist (0 , [0 , 1 , 2 , 3 ]),
155
+ self .getPlaylist (4 , [self .scte35_out , 4 , 5 , 6 , 7 ]),
156
+ self .getPlaylist (8 , [8 , 9 , 10 , 11 ]) + "#EXT-X-ENDLIST\n "
157
+ ]
158
+ result , mocked = self .get_result (streams , playlists )
159
+
160
+ expected = b'' .join (streams [0 :4 ])
161
+ self .assertEqual (expected , result )
162
+ for i , _ in enumerate (streams ):
163
+ self .assertTrue (mocked ["http://mocked/path/stream{0}.ts" .format (i )].called )
164
+ mock_logging .info .assert_has_calls ([
165
+ call ("Will skip ad segments" ),
166
+ call ("Will skip ads beginning with segment 4" )
167
+ ])
168
+
169
+ @patch ("streamlink.plugins.twitch.log" )
170
+ def test_hls_scte35_in_between (self , mock_logging ):
171
+ streams = ["[{0}]" .format (i ).encode ("ascii" ) for i in range (20 )]
172
+ playlists = [
173
+ self .getPlaylist (0 , [0 , 1 , 2 , 3 ]),
174
+ self .getPlaylist (4 , [4 , 5 , self .scte35_out , 6 , 7 ]),
175
+ self .getPlaylist (8 , [8 , 9 , 10 , 11 ]),
176
+ self .getPlaylist (12 , [12 , 13 , self .scte35_in , 14 , 15 ]),
177
+ self .getPlaylist (16 , [16 , 17 , 18 , 19 ]) + "#EXT-X-ENDLIST\n "
178
+ ]
179
+ result , mocked = self .get_result (streams , playlists )
180
+
181
+ expected = b'' .join (streams [0 :6 ]) + b'' .join (streams [14 :20 ])
182
+ self .assertEqual (expected , result )
183
+ for i , _ in enumerate (streams ):
184
+ self .assertTrue (mocked ["http://mocked/path/stream{0}.ts" .format (i )].called )
185
+ mock_logging .info .assert_has_calls ([
186
+ call ("Will skip ad segments" ),
187
+ call ("Will skip ads beginning with segment 6" ),
188
+ call ("Will stop skipping ads beginning with segment 14" )
189
+ ])
0 commit comments