class StreamIOIterWrapper(io.IOBase): """Wraps a iterator and turn it into a file-like object""" def __init__(self, iterator): self.iterator = iterator self.buffer = Buffer() def read(self, size=-1): if size < 0: size = self.buffer.length while self.buffer.length < size: try: chunk = next(self.iterator) self.buffer.write(chunk) except StopIteration: break return self.buffer.read(size) def close(self): pass
class TestBuffer(unittest.TestCase): def setUp(self): self.buffer = Buffer() def test_write(self): self.buffer.write(b"1" * 8192) self.buffer.write(b"2" * 4096) self.assertEqual(self.buffer.length, 8192 + 4096) def test_read(self): self.buffer.write(b"1" * 8192) self.buffer.write(b"2" * 4096) self.assertEqual(self.buffer.length, 8192 + 4096) self.assertEqual(self.buffer.read(4096), b"1" * 4096) self.assertEqual(self.buffer.read(4096), b"1" * 4096) self.assertEqual(self.buffer.read(), b"2" * 4096) self.assertEqual(self.buffer.read(4096), b"") self.assertEqual(self.buffer.read(), b"") self.assertEqual(self.buffer.length, 0) def test_readwrite(self): self.buffer.write(b"1" * 8192) self.assertEqual(self.buffer.length, 8192) self.assertEqual(self.buffer.read(4096), b"1" * 4096) self.assertEqual(self.buffer.length, 4096) self.buffer.write(b"2" * 4096) self.assertEqual(self.buffer.length, 8192) self.assertEqual(self.buffer.read(1), b"1") self.assertEqual(self.buffer.read(4095), b"1" * 4095) self.assertEqual(self.buffer.read(8192), b"2" * 4096) self.assertEqual(self.buffer.read(8192), b"") self.assertEqual(self.buffer.read(), b"") self.assertEqual(self.buffer.length, 0) def test_close(self): self.buffer.write(b"1" * 8192) self.assertEqual(self.buffer.length, 8192) self.buffer.close() self.buffer.write(b"2" * 8192) self.assertEqual(self.buffer.length, 8192) def test_reuse_input(self): """Objects should be reusable after write()""" original = b"original" tests = [bytearray(original)] try: m = memoryview(bytearray(original)) except NameError: # Python 2.6 does not have "memoryview" pass else: # Python 2.7 doesn't do bytes(memoryview) properly if bytes(m) == original: tests.append(m) for data in tests: self.buffer.write(data) data[:] = b"reused!!" self.assertEqual(self.buffer.read(), original) def test_read_empty(self): self.assertRaises(StopIteration, lambda: next(self.buffer._iterate_chunks(10)))
class AkamaiHDStreamIO(io.IOBase): Version = "2.5.8" FlashVersion = "LNX 11,1,102,63" StreamURLFormat = "{host}/{streamname}" ControlURLFormat = "{host}/control/{streamname}" ControlData = b":)" TokenGenerators = {"c11e59dea648d56e864fc07a19f717b9": Auth3TokenGenerator} StatusComplete = 3 StatusError = 4 Errors = { 1: "Stream not found", 2: "Track not found", 3: "Seek out of bounds", 4: "Authentication failed", 5: "DVR disabled", 6: "Invalid bitrate test" } def __init__(self, session, url, swf=None, seek=None): parsed = urlparse(url) self.session = session self.host = ("{scheme}://{netloc}").format(scheme=parsed.scheme, netloc=parsed.netloc) self.streamname = parsed.path[1:] self.swf = swf self.seek = seek def open(self): self.guid = cache_bust_string(12) self.islive = None self.sessionid = None self.flv = None self.buffer = Buffer() self.completed_handshake = False url = self.StreamURLFormat.format(host=self.host, streamname=self.streamname) params = self._create_params(seek=self.seek) log.debug(f"Opening host={self.host} streamname={self.streamname}") try: res = self.session.http.get(url, stream=True, params=params) self.fd = StreamIOIterWrapper(res.iter_content(8192)) except Exception as err: raise StreamError(str(err)) self.handshake(self.fd) return self def handshake(self, fd): try: self.flv = FLV(fd) except FLVError as err: raise StreamError(str(err)) self.buffer.write(self.flv.header.serialize()) log.debug("Attempting to handshake") for i, tag in enumerate(self.flv): if i == 10: raise StreamError( "No OnEdge metadata in FLV after 10 tags, probably not a AkamaiHD stream" ) self.process_tag(tag, exception=StreamError) if self.completed_handshake: log.debug("Handshake successful") break def process_tag(self, tag, exception=IOError): if isinstance(tag.data, ScriptData) and tag.data.name == "onEdge": self._on_edge(tag.data.value, exception=exception) self.buffer.write(tag.serialize()) def send_token(self, token): headers = {"x-Akamai-Streaming-SessionToken": token} log.debug("Sending new session token") self.send_control("sendingNewToken", headers=headers, swf=self.swf) def send_control(self, cmd, headers=None, **params): if not headers: headers = {} url = self.ControlURLFormat.format(host=self.host, streamname=self.streamname) headers["x-Akamai-Streaming-SessionID"] = self.sessionid params = self._create_params(cmd=cmd, **params) return self.session.http.post(url, headers=headers, params=params, data=self.ControlData, exception=StreamError) def read(self, size=-1): if not (self.flv and self.fd): return b"" if self.buffer.length: return self.buffer.read(size) else: return self.fd.read(size) def _create_params(self, **extra): params = dict(v=self.Version, fp=self.FlashVersion, r=cache_bust_string(5), g=self.guid) params.update(extra) return params def _generate_session_token(self, data64): swfdata = base64.decodestring(bytes(data64, "ascii")) md5 = hashlib.md5() md5.update(swfdata) hash = md5.hexdigest() if hash in self.TokenGenerators: generator = self.TokenGenerators[hash](self) return generator.generate() else: raise StreamError( ("No token generator available for hash '{0}'").format(hash)) def _on_edge(self, data, exception=IOError): def updateattr(attr, key): if key in data: setattr(self, attr, data[key]) log.debug("onEdge data") for key, val in data.items(): if isinstance(val, str): val = val[:50] log.debug(f" {key}={val}") updateattr("islive", "isLive") updateattr("sessionid", "session") updateattr("status", "status") updateattr("streamname", "streamName") if self.status == self.StatusComplete: self.flv = None elif self.status == self.StatusError: errornum = data["errorNumber"] if errornum in self.Errors: msg = self.Errors[errornum] else: msg = "Unknown error" raise exception("onEdge error: " + msg) if not self.completed_handshake: if "data64" in data: sessiontoken = self._generate_session_token(data["data64"]) else: sessiontoken = None self.send_token(sessiontoken) self.completed_handshake = True
class TestBuffer(unittest.TestCase): def setUp(self): self.buffer = Buffer() def test_write(self): self.buffer.write(b"1" * 8192) self.buffer.write(b"2" * 4096) self.assertEqual(self.buffer.length, 8192 + 4096) def test_read(self): self.buffer.write(b"1" * 8192) self.buffer.write(b"2" * 4096) self.assertEqual(self.buffer.length, 8192 + 4096) self.assertEqual(self.buffer.read(4096), b"1" * 4096) self.assertEqual(self.buffer.read(4096), b"1" * 4096) self.assertEqual(self.buffer.read(), b"2" * 4096) self.assertEqual(self.buffer.read(4096), b"") self.assertEqual(self.buffer.read(), b"") self.assertEqual(self.buffer.length, 0) def test_readwrite(self): self.buffer.write(b"1" * 8192) self.assertEqual(self.buffer.length, 8192) self.assertEqual(self.buffer.read(4096), b"1" * 4096) self.assertEqual(self.buffer.length, 4096) self.buffer.write(b"2" * 4096) self.assertEqual(self.buffer.length, 8192) self.assertEqual(self.buffer.read(1), b"1") self.assertEqual(self.buffer.read(4095), b"1" * 4095) self.assertEqual(self.buffer.read(8192), b"2" * 4096) self.assertEqual(self.buffer.read(8192), b"") self.assertEqual(self.buffer.read(), b"") self.assertEqual(self.buffer.length, 0) def test_close(self): self.buffer.write(b"1" * 8192) self.assertEqual(self.buffer.length, 8192) self.buffer.close() self.buffer.write(b"2" * 8192) self.assertEqual(self.buffer.length, 8192) def test_reuse_input(self): """Objects should be reusable after write()""" original = b"original" tests = [bytearray(original)] try: m = memoryview(bytearray(original)) except NameError: # Python 2.6 does not have "memoryview" pass else: # Python 2.7 doesn't do bytes(memoryview) properly if bytes(m) == original: tests.append(m) for data in tests: self.buffer.write(data) data[:] = b"reused!!" self.assertEqual(self.buffer.read(), original) def test_read_empty(self): self.assertRaises( StopIteration, lambda: next(self.buffer._iterate_chunks(10)))
class TestBuffer(unittest.TestCase): def setUp(self): self.buffer = Buffer() def test_write(self): self.buffer.write(b"1" * 8192) self.buffer.write(b"2" * 4096) self.assertEqual(self.buffer.length, 8192 + 4096) def test_read(self): self.buffer.write(b"1" * 8192) self.buffer.write(b"2" * 4096) self.assertEqual(self.buffer.length, 8192 + 4096) self.assertEqual(self.buffer.read(4096), b"1" * 4096) self.assertEqual(self.buffer.read(4096), b"1" * 4096) self.assertEqual(self.buffer.read(), b"2" * 4096) self.assertEqual(self.buffer.read(4096), b"") self.assertEqual(self.buffer.read(), b"") self.assertEqual(self.buffer.length, 0) def test_readwrite(self): self.buffer.write(b"1" * 8192) self.assertEqual(self.buffer.length, 8192) self.assertEqual(self.buffer.read(4096), b"1" * 4096) self.assertEqual(self.buffer.length, 4096) self.buffer.write(b"2" * 4096) self.assertEqual(self.buffer.length, 8192) self.assertEqual(self.buffer.read(1), b"1") self.assertEqual(self.buffer.read(4095), b"1" * 4095) self.assertEqual(self.buffer.read(8192), b"2" * 4096) self.assertEqual(self.buffer.read(8192), b"") self.assertEqual(self.buffer.read(), b"") self.assertEqual(self.buffer.length, 0) def test_close(self): self.buffer.write(b"1" * 8192) self.assertEqual(self.buffer.length, 8192) self.buffer.close() self.buffer.write(b"2" * 8192) self.assertEqual(self.buffer.length, 8192) def test_reuse_input(self): """Objects should be reusable after write()""" original = b"original" tests = [bytearray(original), memoryview(bytearray(original))] for data in tests: self.buffer.write(data) data[:] = b"reused!!" self.assertEqual(self.buffer.read(), original) def test_read_empty(self): self.assertRaises( StopIteration, lambda: next(self.buffer._iterate_chunks(10)))