From f9883ab0648fa64283cdc2c6e4e65ee5201abac6 Mon Sep 17 00:00:00 2001 From: deathbybandaid Date: Fri, 13 Nov 2020 14:08:55 -0500 Subject: [PATCH] Improve Direct Streaming --- fHDHR/device/tuners/__init__.py | 2 +- fHDHR/device/tuners/stream/direct_stream.py | 87 +++++++++++++-------- requirements.txt | 2 + 3 files changed, 59 insertions(+), 32 deletions(-) diff --git a/fHDHR/device/tuners/__init__.py b/fHDHR/device/tuners/__init__.py index dd83ba0..d42903b 100644 --- a/fHDHR/device/tuners/__init__.py +++ b/fHDHR/device/tuners/__init__.py @@ -78,7 +78,7 @@ class Tuners(): channelUri_headers = self.fhdhr.web.session.head(stream_args["channelUri"]).headers stream_args["true_content_type"] = channelUri_headers['Content-Type'] - if stream_args["true_content_type"].startswith("application/"): + if stream_args["true_content_type"].startswith(tuple(["application/", "text/"])): stream_args["content_type"] = "video/mpeg" else: stream_args["content_type"] = stream_args["true_content_type"] diff --git a/fHDHR/device/tuners/stream/direct_stream.py b/fHDHR/device/tuners/stream/direct_stream.py index e71bf44..b62189e 100644 --- a/fHDHR/device/tuners/stream/direct_stream.py +++ b/fHDHR/device/tuners/stream/direct_stream.py @@ -1,7 +1,7 @@ - import time -import re -import urllib.parse +import m3u8 + +from Crypto.Cipher import AES # from fHDHR.exceptions import TunerError @@ -20,14 +20,18 @@ class Direct_Stream(): if not self.stream_args["duration"] == 0: self.stream_args["time_end"] = self.stream_args["duration"] + time.time() - if not re.match('^(.*m3u8)[\n\r]*$', self.stream_args["channelUri"]): + if not self.stream_args["true_content_type"].startswith(tuple(["application/", "text/"])): - self.fhdhr.logger.info("Direct Stream of URL: %s" % self.stream_args["channelUri"]) + self.fhdhr.logger.info("Direct Stream of %s URL: %s" % (self.stream_args["true_content_type"], self.stream_args["channelUri"])) req = self.fhdhr.web.session.get(self.stream_args["channelUri"], stream=True) def generate(): + try: + + chunk_counter = 1 + while self.tuner.tuner_lock.locked(): for chunk in req.iter_content(chunk_size=self.chunksize): @@ -41,7 +45,10 @@ class Direct_Stream(): if not chunk: break # raise TunerError("807 - No Video Data") + + self.fhdhr.logger.info("Passing Through Chunk #%s with size %s" % (chunk_counter, self.chunksize)) yield chunk + self.fhdhr.logger.info("Connection Closed: Tuner Lock Removed") except GeneratorExit: @@ -57,15 +64,14 @@ class Direct_Stream(): self.fhdhr.logger.info("Detected stream URL is m3u8: %s" % self.stream_args["true_content_type"]) - # Determine if this m3u8 contains variants or chunks channelUri = self.stream_args["channelUri"] - self.fhdhr.logger.info("Opening m3u8 URL: %s" % channelUri) - m3u8_get = self.fhdhr.web.session.get(self.stream_args["channelUri"]) - m3u8_content = m3u8_get.text - variants = [urllib.parse.urljoin(self.stream_args["channelUri"], line) for line in m3u8_content.split('\n') if re.match('^(.*m3u8)[\n\r]*$', line)] - if len(variants): - channelUri = variants[0] - self.fhdhr.logger.info("m3u8 contained variants. Using URL: %s" % channelUri) + while True: + + videoUrlM3u = m3u8.load(channelUri) + if len(videoUrlM3u.playlists): + channelUri = videoUrlM3u.playlists[0].absolute_uri + else: + break def generate(): @@ -75,30 +81,49 @@ class Direct_Stream(): while self.tuner.tuner_lock.locked(): - m3u8_get = self.fhdhr.web.session.get(channelUri) - m3u8_content = m3u8_get.text - chunk_urls_detect = [urllib.parse.urljoin(channelUri, line) for line in m3u8_content.split('\n') if re.match('^(.*ts)[\n\r]*$', line)] + playlist = m3u8.load(channelUri) + segments = playlist.segments + + if len(played_chunk_urls): + newsegments = 0 + for segment in segments: + if segment.absolute_uri not in played_chunk_urls: + newsegments += 1 + self.fhdhr.logger.info("Refreshing m3u8, Loaded %s new segments." % str(newsegments)) + else: + self.fhdhr.logger.info("Loaded %s segments." % str(len(segments))) + + if playlist.keys != [None]: + keys = [{"url": key.uri, "method": key.method, "iv": key.iv} for key in playlist.keys if key] + else: + keys = [None for i in range(0, len(segments))] + + for segment, key in zip(segments, keys): + chunkurl = segment.absolute_uri - chunk_urls_play = [] - for chunkurl in chunk_urls_detect: if chunkurl not in played_chunk_urls: - chunk_urls_play.append(chunkurl) - played_chunk_urls.append(chunkurl) + played_chunk_urls.append(chunkurl) - for chunkurl in chunk_urls_play: + if (not self.stream_args["duration"] == 0 and + not time.time() < self.stream_args["time_end"]): + self.fhdhr.logger.info("Requested Duration Expired.") + self.tuner.close() - self.fhdhr.logger.info("Passing Through Chunk: %s" % chunkurl) + chunk = self.fhdhr.web.session.get(chunkurl).content + if not chunk: + break + # raise TunerError("807 - No Video Data") + if key: + keyfile = self.fhdhr.web.session.get(key["url"]).content + cryptor = AES.new(keyfile, AES.MODE_CBC, keyfile) + chunk = cryptor.decrypt(chunk) - if (not self.stream_args["duration"] == 0 and - not time.time() < self.stream_args["time_end"]): - self.fhdhr.logger.info("Requested Duration Expired.") - self.tuner.close() + self.fhdhr.logger.info("Passing Through Chunk: %s" % chunkurl) + yield chunk + + if playlist.target_duration: + time.sleep(int(playlist.target_duration)) - chunk = self.fhdhr.web.session.get(chunkurl).content - if not chunk: - break - # raise TunerError("807 - No Video Data") - yield chunk self.fhdhr.logger.info("Connection Closed: Tuner Lock Removed") except GeneratorExit: diff --git a/requirements.txt b/requirements.txt index 82d06fc..5b0f3cb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -4,3 +4,5 @@ flask image xmltodict sqlalchemy +pycrypto +m3u8