From 890cf7c3dd39d4563569313e6188cf13f9a08558 Mon Sep 17 00:00:00 2001 From: deathbybandaid Date: Thu, 12 Nov 2020 14:46:47 -0500 Subject: [PATCH] Improve Tuner Handling --- fHDHR/device/__init__.py | 5 +- fHDHR/device/tuners.py | 111 --------- fHDHR/device/tuners/__init__.py | 86 +++++++ fHDHR/device/tuners/stream/__init__.py | 22 ++ fHDHR/device/tuners/stream/direct_stream.py | 112 +++++++++ fHDHR/device/tuners/stream/ffmpeg_stream.py | 127 ++++++++++ fHDHR/device/tuners/stream/vlc_stream.py | 118 ++++++++++ fHDHR/device/tuners/tuner.py | 59 +++++ fHDHR/device/watch.py | 244 -------------------- fHDHR/http/api/__init__.py | 2 + fHDHR/http/api/watch.py | 101 ++++++++ fHDHR/http/pages/streams_html.py | 14 +- fHDHR/http/watch/__init__.py | 4 +- fHDHR/http/watch/auto.py | 93 ++++++++ fHDHR/http/watch/tuner.py | 85 +++++-- fHDHR/http/watch/watch.py | 48 ---- 16 files changed, 801 insertions(+), 430 deletions(-) delete mode 100644 fHDHR/device/tuners.py create mode 100644 fHDHR/device/tuners/__init__.py create mode 100644 fHDHR/device/tuners/stream/__init__.py create mode 100644 fHDHR/device/tuners/stream/direct_stream.py create mode 100644 fHDHR/device/tuners/stream/ffmpeg_stream.py create mode 100644 fHDHR/device/tuners/stream/vlc_stream.py create mode 100644 fHDHR/device/tuners/tuner.py delete mode 100644 fHDHR/device/watch.py create mode 100644 fHDHR/http/api/watch.py create mode 100644 fHDHR/http/watch/auto.py delete mode 100644 fHDHR/http/watch/watch.py diff --git a/fHDHR/device/__init__.py b/fHDHR/device/__init__.py index b14c1c1..e3f7484 100644 --- a/fHDHR/device/__init__.py +++ b/fHDHR/device/__init__.py @@ -1,7 +1,6 @@ from .channels import Channels from .epg import EPG from .tuners import Tuners -from .watch import WatchStream from .images import imageHandler from .station_scan import Station_Scan from .ssdp import SSDPServer @@ -16,9 +15,7 @@ class fHDHR_Device(): self.epg = EPG(fhdhr, self.channels, origin) - self.tuners = Tuners(fhdhr, self.epg) - - self.watch = WatchStream(fhdhr, self.channels, self.tuners) + self.tuners = Tuners(fhdhr, self.epg, self.channels) self.images = imageHandler(fhdhr, self.epg) diff --git a/fHDHR/device/tuners.py b/fHDHR/device/tuners.py deleted file mode 100644 index 241219f..0000000 --- a/fHDHR/device/tuners.py +++ /dev/null @@ -1,111 +0,0 @@ -import threading -import datetime - -from fHDHR.exceptions import TunerError -from fHDHR.tools import humanized_time - - -class Tuner(): - def __init__(self, fhdhr, inum, epg): - self.fhdhr = fhdhr - self.number = inum - self.epg = epg - self.tuner_lock = threading.Lock() - self.set_off_status() - - def grab(self, stream_args): - if self.tuner_lock.locked(): - raise TunerError("Tuner #" + str(self.number) + " is not available.") - - self.fhdhr.logger.info("Tuner #" + str(self.number) + " to be used for stream.") - self.tuner_lock.acquire() - self.status = { - "status": "Active", - "method": stream_args["method"], - "accessed": stream_args["accessed"], - "channel": stream_args["channel"], - "proxied_url": stream_args["channelUri"], - "time_start": datetime.datetime.utcnow(), - } - - def close(self): - self.fhdhr.logger.info("Tuner #" + str(self.number) + " Shutting Down.") - self.set_off_status() - self.tuner_lock.release() - - def get_status(self): - current_status = self.status.copy() - if current_status["status"] == "Active": - current_status["Play Time"] = str( - humanized_time( - int((datetime.datetime.utcnow() - current_status["time_start"]).total_seconds()))) - current_status["time_start"] = str(current_status["time_start"]) - current_status["epg"] = self.epg.whats_on_now(current_status["channel"]) - return current_status - - def set_off_status(self): - self.status = {"status": "Inactive"} - - -class Tuners(): - - def __init__(self, fhdhr, epg): - self.fhdhr = fhdhr - - self.epg = epg - self.max_tuners = int(self.fhdhr.config.dict["fhdhr"]["tuner_count"]) - - self.tuners = {} - - for i in range(1, self.max_tuners + 1): - self.tuners[i] = Tuner(fhdhr, i, epg) - - def tuner_grab(self, stream_args): - tunerselected = None - - if stream_args["tuner"]: - if int(stream_args["tuner"]) not in list(self.tuners.keys()): - raise TunerError("Tuner " + str(stream_args["tuner"]) + " does not exist.") - self.tuners[int(stream_args["tuner"])].grab(stream_args) - tunerselected = int(stream_args["tuner"]) - - else: - - for tunernum in range(1, self.max_tuners + 1): - try: - self.tuners[int(tunernum)].grab(stream_args) - except TunerError: - continue - else: - tunerselected = tunernum - break - - if not tunerselected: - raise TunerError("No Available Tuners.") - else: - return tunerselected - - def tuner_close(self, tunernum): - self.tuners[int(tunernum)].close() - - def status(self): - all_status = {} - for tunernum in range(1, self.max_tuners + 1): - all_status[tunernum] = self.tuners[int(tunernum)].get_status() - return all_status - - def available_tuner_count(self): - available_tuners = 0 - for tunernum in range(1, self.max_tuners + 1): - tuner_status = self.tuners[int(tunernum)].get_status() - if tuner_status["status"] == "Inactive": - available_tuners += 1 - return available_tuners - - def inuse_tuner_count(self): - inuse_tuners = 0 - for tunernum in range(1, self.max_tuners + 1): - tuner_status = self.tuners[int(tunernum)].get_status() - if tuner_status["status"] == "Active": - inuse_tuners += 1 - return inuse_tuners diff --git a/fHDHR/device/tuners/__init__.py b/fHDHR/device/tuners/__init__.py new file mode 100644 index 0000000..dd83ba0 --- /dev/null +++ b/fHDHR/device/tuners/__init__.py @@ -0,0 +1,86 @@ + +from fHDHR.exceptions import TunerError + +from .tuner import Tuner + + +class Tuners(): + + def __init__(self, fhdhr, epg, channels): + self.fhdhr = fhdhr + self.channels = channels + + self.epg = epg + self.max_tuners = int(self.fhdhr.config.dict["fhdhr"]["tuner_count"]) + + self.tuners = {} + + for i in range(1, self.max_tuners + 1): + self.tuners[i] = Tuner(fhdhr, i, epg) + + def tuner_grab(self, tuner_number): + + if int(tuner_number) not in list(self.tuners.keys()): + self.fhdhr.logger.error("Tuner %s does not exist." % str(tuner_number)) + raise TunerError("806 - Tune Failed") + + # TunerError will raise if unavailable + self.tuners[int(tuner_number)].grab() + + return tuner_number + + def first_available(self): + + if not self.available_tuner_count(): + raise TunerError("805 - All Tuners In Use") + + for tunernum in list(self.tuners.keys()): + try: + self.tuners[int(tunernum)].grab() + except TunerError: + continue + else: + return tunernum + + raise TunerError("805 - All Tuners In Use") + + def tuner_close(self, tunernum): + self.tuners[int(tunernum)].close() + + def status(self): + all_status = {} + for tunernum in list(self.tuners.keys()): + all_status[tunernum] = self.tuners[int(tunernum)].get_status() + return all_status + + def available_tuner_count(self): + available_tuners = 0 + for tunernum in list(self.tuners.keys()): + tuner_status = self.tuners[int(tunernum)].get_status() + if tuner_status["status"] == "Inactive": + available_tuners += 1 + return available_tuners + + def inuse_tuner_count(self): + inuse_tuners = 0 + for tunernum in list(self.tuners.keys()): + tuner_status = self.tuners[int(tunernum)].get_status() + if tuner_status["status"] == "Active": + inuse_tuners += 1 + return inuse_tuners + + def get_stream_info(self, stream_args): + + stream_args["channelUri"] = self.channels.get_channel_stream(str(stream_args["channel"])) + if not stream_args["channelUri"]: + raise TunerError("806 - Tune Failed") + + 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/"): + stream_args["content_type"] = "video/mpeg" + else: + stream_args["content_type"] = stream_args["true_content_type"] + + return stream_args diff --git a/fHDHR/device/tuners/stream/__init__.py b/fHDHR/device/tuners/stream/__init__.py new file mode 100644 index 0000000..b7a01f7 --- /dev/null +++ b/fHDHR/device/tuners/stream/__init__.py @@ -0,0 +1,22 @@ + + +from .direct_stream import Direct_Stream +from .ffmpeg_stream import FFMPEG_Stream +from .vlc_stream import VLC_Stream + + +class Stream(): + + def __init__(self, fhdhr, stream_args, tuner): + self.fhdhr = fhdhr + self.stream_args = stream_args + + if stream_args["method"] == "ffmpeg": + self.method = FFMPEG_Stream(fhdhr, stream_args, tuner) + if stream_args["method"] == "vlc": + self.method = VLC_Stream(fhdhr, stream_args, tuner) + elif stream_args["method"] == "direct": + self.method = Direct_Stream(fhdhr, stream_args, tuner) + + def get(self): + return self.method.get() diff --git a/fHDHR/device/tuners/stream/direct_stream.py b/fHDHR/device/tuners/stream/direct_stream.py new file mode 100644 index 0000000..e71bf44 --- /dev/null +++ b/fHDHR/device/tuners/stream/direct_stream.py @@ -0,0 +1,112 @@ + +import time +import re +import urllib.parse + +# from fHDHR.exceptions import TunerError + + +class Direct_Stream(): + + def __init__(self, fhdhr, stream_args, tuner): + self.fhdhr = fhdhr + self.stream_args = stream_args + self.tuner = tuner + + self.chunksize = int(self.fhdhr.config.dict["direct_stream"]['chunksize']) + + def get(self): + + 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"]): + + self.fhdhr.logger.info("Direct Stream of URL: %s" % self.stream_args["channelUri"]) + + req = self.fhdhr.web.session.get(self.stream_args["channelUri"], stream=True) + + def generate(): + try: + while self.tuner.tuner_lock.locked(): + + for chunk in req.iter_content(chunk_size=self.chunksize): + + if (not self.stream_args["duration"] == 0 and + not time.time() < self.stream_args["time_end"]): + req.close() + self.fhdhr.logger.info("Requested Duration Expired.") + self.tuner.close() + + if not chunk: + break + # raise TunerError("807 - No Video Data") + yield chunk + self.fhdhr.logger.info("Connection Closed: Tuner Lock Removed") + + except GeneratorExit: + self.fhdhr.logger.info("Connection Closed.") + except Exception as e: + self.fhdhr.logger.info("Connection Closed: " + str(e)) + finally: + req.close() + self.tuner.close() + # raise TunerError("806 - Tune Failed") + + else: + + 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) + + def generate(): + + try: + + played_chunk_urls = [] + + 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)] + + 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) + + for chunkurl in chunk_urls_play: + + self.fhdhr.logger.info("Passing Through Chunk: %s" % chunkurl) + + 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() + + 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: + self.fhdhr.logger.info("Connection Closed.") + except Exception as e: + self.fhdhr.logger.info("Connection Closed: " + str(e)) + finally: + self.tuner.close() + # raise TunerError("806 - Tune Failed") + + return generate() diff --git a/fHDHR/device/tuners/stream/ffmpeg_stream.py b/fHDHR/device/tuners/stream/ffmpeg_stream.py new file mode 100644 index 0000000..d018747 --- /dev/null +++ b/fHDHR/device/tuners/stream/ffmpeg_stream.py @@ -0,0 +1,127 @@ +import subprocess + +# from fHDHR.exceptions import TunerError + + +class FFMPEG_Stream(): + + def __init__(self, fhdhr, stream_args, tuner): + self.fhdhr = fhdhr + self.stream_args = stream_args + self.tuner = tuner + + self.bytes_per_read = int(self.fhdhr.config.dict["ffmpeg"]["bytes_per_read"]) + self.ffmpeg_command = self.ffmpeg_command_assemble(stream_args) + + def get(self): + + ffmpeg_proc = subprocess.Popen(self.ffmpeg_command, stdout=subprocess.PIPE) + + def generate(): + try: + while self.tuner.tuner_lock.locked(): + + videoData = ffmpeg_proc.stdout.read(self.bytes_per_read) + if not videoData: + break + # raise TunerError("807 - No Video Data") + yield videoData + self.fhdhr.logger.info("Connection Closed: Tuner Lock Removed") + + except GeneratorExit: + self.fhdhr.logger.info("Connection Closed.") + except Exception as e: + self.fhdhr.logger.info("Connection Closed: " + str(e)) + finally: + ffmpeg_proc.terminate() + ffmpeg_proc.communicate() + self.tuner.close() + # raise TunerError("806 - Tune Failed") + + return generate() + + def ffmpeg_command_assemble(self, stream_args): + ffmpeg_command = [ + self.fhdhr.config.dict["ffmpeg"]["ffmpeg_path"], + "-i", stream_args["channelUri"], + ] + ffmpeg_command.extend(self.ffmpeg_duration(stream_args)) + ffmpeg_command.extend(self.transcode_profiles(stream_args)) + ffmpeg_command.extend(self.ffmpeg_loglevel()) + ffmpeg_command.extend(["pipe:stdout"]) + return ffmpeg_command + + def ffmpeg_duration(self, stream_args): + ffmpeg_command = [] + if stream_args["duration"]: + ffmpeg_command.extend(["-t", str(stream_args["duration"])]) + else: + ffmpeg_command.extend( + [ + "-reconnect", "1", + "-reconnect_at_eof", "1", + "-reconnect_streamed", "1", + "-reconnect_delay_max", "2", + ] + ) + + return ffmpeg_command + + def ffmpeg_loglevel(self): + ffmpeg_command = [] + log_level = self.fhdhr.config.dict["logging"]["level"].lower() + + loglevel_dict = { + "debug": "debug", + "info": "info", + "error": "error", + "warning": "warning", + "critical": "fatal", + } + if log_level not in ["info", "debug"]: + ffmpeg_command.extend(["-nostats", "-hide_banner"]) + ffmpeg_command.extend(["-loglevel", loglevel_dict[log_level]]) + return ffmpeg_command + + def transcode_profiles(self, stream_args): + # TODO implement actual profiles here + """ + • heavy: transcode to AVC with the same resolution, frame-rate, and interlacing as the + original stream. For example 1080i60 AVC 1080i60, 720p60 AVC 720p60. → → + • mobile: trancode to AVC progressive not exceeding 1280x720 30fps. + • internet720: transcode to low bitrate AVC progressive not exceeding 1280x720 30fps. + • internet480: transcode to low bitrate AVC progressive not exceeding 848x480 30fps for + 16:9 content, not exceeding 640x480 30fps for 4:3 content. + • internet360: transcode to low bitrate AVC progressive not exceeding 640x360 30fps for + 16:9 content, not exceeding 480x360 30fps for 4:3 content. + • internet240: transcode to low bitrate AVC progressive not exceeding 432x240 30fps for + 16:9 content, not exceeding 320x240 30fps for 4:3 content + """ + + if stream_args["transcode"]: + self.fhdhr.logger.info("Client requested a " + stream_args["transcode"] + " transcode for stream.") + stream_args["transcode"] = None + + ffmpeg_command = [] + + if not stream_args["transcode"]: + ffmpeg_command.extend( + [ + "-c", "copy", + "-f", "mpegts", + ] + ) + elif stream_args["transcode"] == "heavy": + ffmpeg_command.extend([]) + elif stream_args["transcode"] == "mobile": + ffmpeg_command.extend([]) + elif stream_args["transcode"] == "internet720": + ffmpeg_command.extend([]) + elif stream_args["transcode"] == "internet480": + ffmpeg_command.extend([]) + elif stream_args["transcode"] == "internet360": + ffmpeg_command.extend([]) + elif stream_args["transcode"] == "internet240": + ffmpeg_command.extend([]) + + return ffmpeg_command diff --git a/fHDHR/device/tuners/stream/vlc_stream.py b/fHDHR/device/tuners/stream/vlc_stream.py new file mode 100644 index 0000000..acce733 --- /dev/null +++ b/fHDHR/device/tuners/stream/vlc_stream.py @@ -0,0 +1,118 @@ +import subprocess + +# from fHDHR.exceptions import TunerError + + +class VLC_Stream(): + + def __init__(self, fhdhr, stream_args, tuner): + self.fhdhr = fhdhr + self.stream_args = stream_args + self.tuner = tuner + + self.bytes_per_read = int(self.fhdhr.config.dict["vlc"]["bytes_per_read"]) + self.vlc_command = self.vlc_command_assemble(stream_args) + + def get(self): + + vlc_proc = subprocess.Popen(self.vlc_command, stdout=subprocess.PIPE) + + def generate(): + try: + + while self.tuner.tuner_lock.locked(): + + videoData = vlc_proc.stdout.read(self.bytes_per_read) + if not videoData: + break + # raise TunerError("807 - No Video Data") + yield videoData + self.fhdhr.logger.info("Connection Closed: Tuner Lock Removed") + + except GeneratorExit: + self.fhdhr.logger.info("Connection Closed.") + except Exception as e: + self.fhdhr.logger.info("Connection Closed: " + str(e)) + finally: + vlc_proc.terminate() + vlc_proc.communicate() + self.fhdhr.logger.info("Connection Closed: Tuner Lock Removed") + self.tuner.close() + # raise TunerError("806 - Tune Failed") + + return generate() + + def vlc_command_assemble(self, stream_args): + vlc_command = [ + self.fhdhr.config.dict["vlc"]["vlc_path"], + "-I", "dummy", stream_args["channelUri"], + ] + vlc_command.extend(self.vlc_duration(stream_args)) + vlc_command.extend(self.vlc_loglevel()) + vlc_command.extend(["--sout"]) + vlc_command.extend(self.transcode_profiles(stream_args)) + return vlc_command + + def vlc_duration(self, stream_args): + vlc_command = [] + if stream_args["duration"]: + vlc_command.extend(["--run-time=%s" % str(stream_args["duration"])]) + return vlc_command + + def vlc_loglevel(self): + vlc_command = [] + log_level = self.fhdhr.config.dict["logging"]["level"].lower() + + loglevel_dict = { + "debug": "3", + "info": "0", + "error": "1", + "warning": "2", + "critical": "1", + } + vlc_command.extend(["--log-verbose=", loglevel_dict[log_level]]) + if log_level not in ["info", "debug"]: + vlc_command.extend(["--quiet"]) + return vlc_command + + def transcode_profiles(self, stream_args): + # TODO implement actual profiles here + """ + • heavy: transcode to AVC with the same resolution, frame-rate, and interlacing as the + original stream. For example 1080i60 AVC 1080i60, 720p60 AVC 720p60. → → + • mobile: trancode to AVC progressive not exceeding 1280x720 30fps. + • internet720: transcode to low bitrate AVC progressive not exceeding 1280x720 30fps. + • internet480: transcode to low bitrate AVC progressive not exceeding 848x480 30fps for + 16:9 content, not exceeding 640x480 30fps for 4:3 content. + • internet360: transcode to low bitrate AVC progressive not exceeding 640x360 30fps for + 16:9 content, not exceeding 480x360 30fps for 4:3 content. + • internet240: transcode to low bitrate AVC progressive not exceeding 432x240 30fps for + 16:9 content, not exceeding 320x240 30fps for 4:3 content + """ + vlc_command = [] + + if stream_args["transcode"]: + self.fhdhr.logger.info("Client requested a " + stream_args["transcode"] + " transcode for stream.") + stream_args["transcode"] = None + + vlc_transcode_string = "#std{mux=ts,access=file,dst=-}" + return [vlc_transcode_string] + + '#transcode{vcodec=mp2v,vb=4096,acodec=mp2a,ab=192,scale=1,channels=2,deinterlace}:std{access=file,mux=ts,dst=-"}' + + if not stream_args["transcode"]: + vlc_command.extend([]) + elif stream_args["transcode"] == "heavy": + vlc_command.extend([]) + elif stream_args["transcode"] == "mobile": + vlc_command.extend([]) + elif stream_args["transcode"] == "internet720": + vlc_command.extend([]) + elif stream_args["transcode"] == "internet480": + vlc_command.extend([]) + elif stream_args["transcode"] == "internet360": + vlc_command.extend([]) + elif stream_args["transcode"] == "internet240": + vlc_command.extend([]) + + return vlc_command diff --git a/fHDHR/device/tuners/tuner.py b/fHDHR/device/tuners/tuner.py new file mode 100644 index 0000000..f558edb --- /dev/null +++ b/fHDHR/device/tuners/tuner.py @@ -0,0 +1,59 @@ +import threading +import datetime + +from fHDHR.exceptions import TunerError +from fHDHR.tools import humanized_time + +from .stream import Stream + + +class Tuner(): + def __init__(self, fhdhr, inum, epg): + self.fhdhr = fhdhr + + self.number = inum + self.epg = epg + + self.tuner_lock = threading.Lock() + self.set_off_status() + + def grab(self): + if self.tuner_lock.locked(): + self.fhdhr.logger.error("Tuner #" + str(self.number) + " is not available.") + raise TunerError("804 - Tuner In Use") + self.tuner_lock.acquire() + self.status["status"] = "Acquired" + self.fhdhr.logger.info("Tuner #" + str(self.number) + " Acquired.") + + def close(self): + self.set_off_status() + if self.tuner_lock.locked(): + self.tuner_lock.release() + self.fhdhr.logger.info("Tuner #" + str(self.number) + " Released.") + + def get_status(self): + current_status = self.status.copy() + if current_status["status"] == "Active": + current_status["Play Time"] = str( + humanized_time( + int((datetime.datetime.utcnow() - current_status["time_start"]).total_seconds()))) + current_status["time_start"] = str(current_status["time_start"]) + current_status["epg"] = self.epg.whats_on_now(current_status["channel"]) + return current_status + + def set_off_status(self): + self.status = {"status": "Inactive"} + + def get_stream(self, stream_args, tuner): + stream = Stream(self.fhdhr, stream_args, tuner) + return stream.get() + + def set_status(self, stream_args): + self.status = { + "status": "Active", + "method": stream_args["method"], + "accessed": stream_args["accessed"], + "channel": stream_args["channel"], + "proxied_url": stream_args["channelUri"], + "time_start": datetime.datetime.utcnow(), + } diff --git a/fHDHR/device/watch.py b/fHDHR/device/watch.py deleted file mode 100644 index 37e6f3b..0000000 --- a/fHDHR/device/watch.py +++ /dev/null @@ -1,244 +0,0 @@ -import subprocess -import time - -from fHDHR.exceptions import TunerError - - -class WatchStream(): - - def __init__(self, fhdhr, origserv, tuners): - self.fhdhr = fhdhr - - self.origserv = origserv - self.tuners = tuners - - def direct_stream(self, stream_args, tunernum): - - chunksize = int(self.fhdhr.config.dict["direct_stream"]['chunksize']) - - if not stream_args["duration"] == 0: - stream_args["duration"] += time.time() - - req = self.fhdhr.web.session.get(stream_args["channelUri"], stream=True) - - def generate(): - try: - for chunk in req.iter_content(chunk_size=chunksize): - - if not stream_args["duration"] == 0 and not time.time() < stream_args["duration"]: - req.close() - self.fhdhr.logger.info("Requested Duration Expired.") - break - - yield chunk - - except GeneratorExit: - req.close() - self.fhdhr.logger.info("Connection Closed.") - self.tuners.tuner_close(tunernum) - - return generate() - - def ffmpeg_stream(self, stream_args, tunernum): - - bytes_per_read = int(self.fhdhr.config.dict["ffmpeg"]["bytes_per_read"]) - - ffmpeg_command = self.transcode_profiles(stream_args) - - if not stream_args["duration"] == 0: - stream_args["duration"] += time.time() - - ffmpeg_proc = subprocess.Popen(ffmpeg_command, stdout=subprocess.PIPE) - - def generate(): - try: - while True: - - if not stream_args["duration"] == 0 and not time.time() < stream_args["duration"]: - ffmpeg_proc.terminate() - ffmpeg_proc.communicate() - self.fhdhr.logger.info("Requested Duration Expired.") - break - - videoData = ffmpeg_proc.stdout.read(bytes_per_read) - if not videoData: - break - yield videoData - - except GeneratorExit: - ffmpeg_proc.terminate() - ffmpeg_proc.communicate() - self.fhdhr.logger.info("Connection Closed.") - self.tuners.tuner_close(tunernum) - except Exception as e: - ffmpeg_proc.terminate() - ffmpeg_proc.communicate() - self.fhdhr.logger.info("Connection Closed: " + str(e)) - self.tuners.tuner_close(tunernum) - - return generate() - - def vlc_stream(self, stream_args, tunernum): - - bytes_per_read = int(self.fhdhr.config.dict["vlc"]["bytes_per_read"]) - - vlc_command = self.transcode_profiles(stream_args) - - if not stream_args["duration"] == 0: - stream_args["duration"] += time.time() - - vlc_proc = subprocess.Popen(vlc_command, stdout=subprocess.PIPE) - - def generate(): - try: - while True: - - if not stream_args["duration"] == 0 and not time.time() < stream_args["duration"]: - vlc_proc.terminate() - vlc_proc.communicate() - self.fhdhr.logger.info("Requested Duration Expired.") - break - - videoData = vlc_proc.stdout.read(bytes_per_read) - if not videoData: - break - yield videoData - - except GeneratorExit: - vlc_proc.terminate() - vlc_proc.communicate() - self.fhdhr.logger.info("Connection Closed.") - self.tuners.tuner_close(tunernum) - except Exception as e: - vlc_proc.terminate() - vlc_proc.communicate() - self.fhdhr.logger.info("Connection Closed: " + str(e)) - self.tuners.tuner_close(tunernum) - - return generate() - - def get_stream(self, stream_args): - - try: - tunernum = self.tuners.tuner_grab(stream_args) - except TunerError as e: - self.fhdhr.logger.info("A " + stream_args["method"] + " stream request for channel " + - str(stream_args["channel"]) + " was rejected do to " + str(e)) - return - - self.fhdhr.logger.info("Attempting a " + stream_args["method"] + " stream request for channel " + str(stream_args["channel"])) - - if stream_args["method"] == "ffmpeg": - return self.ffmpeg_stream(stream_args, tunernum) - if stream_args["method"] == "vlc": - return self.vlc_stream(stream_args, tunernum) - elif stream_args["method"] == "direct": - return self.direct_stream(stream_args, tunernum) - - def get_stream_info(self, stream_args): - - stream_args["channelUri"] = self.origserv.get_channel_stream(str(stream_args["channel"])) - if not stream_args["channelUri"]: - self.fhdhr.logger.error("Could not Obtain Channel Stream.") - stream_args["content_type"] = "video/mpeg" - else: - channelUri_headers = self.fhdhr.web.session.head(stream_args["channelUri"]).headers - stream_args["content_type"] = channelUri_headers['Content-Type'] - - return stream_args - - def transcode_profiles(self, stream_args): - # TODO implement actual profiles here - """ - • heavy: transcode to AVC with the same resolution, frame-rate, and interlacing as the - original stream. For example 1080i60 AVC 1080i60, 720p60 AVC 720p60. → → - • mobile: trancode to AVC progressive not exceeding 1280x720 30fps. - • internet720: transcode to low bitrate AVC progressive not exceeding 1280x720 30fps. - • internet480: transcode to low bitrate AVC progressive not exceeding 848x480 30fps for - 16:9 content, not exceeding 640x480 30fps for 4:3 content. - • internet360: transcode to low bitrate AVC progressive not exceeding 640x360 30fps for - 16:9 content, not exceeding 480x360 30fps for 4:3 content. - • internet240: transcode to low bitrate AVC progressive not exceeding 432x240 30fps for - 16:9 content, not exceeding 320x240 30fps for 4:3 content - """ - - if stream_args["transcode"]: - self.fhdhr.logger.info("Client requested a " + stream_args["transcode"] + " transcode for stream.") - - log_level = self.fhdhr.config.dict["logging"]["level"].lower() - - if stream_args["method"] == "direct": - return None - - elif stream_args["method"] == "ffmpeg": - ffmpeg_command = [ - self.fhdhr.config.dict["ffmpeg"]["ffmpeg_path"], - "-i", stream_args["channelUri"], - "-c", "copy", - "-f", "mpegts", - ] - - if not stream_args["transcode"]: - ffmpeg_command.extend([]) - elif stream_args["transcode"] == "heavy": - ffmpeg_command.extend([]) - elif stream_args["transcode"] == "mobile": - ffmpeg_command.extend([]) - elif stream_args["transcode"] == "internet720": - ffmpeg_command.extend([]) - elif stream_args["transcode"] == "internet480": - ffmpeg_command.extend([]) - elif stream_args["transcode"] == "internet360": - ffmpeg_command.extend([]) - elif stream_args["transcode"] == "internet240": - ffmpeg_command.extend([]) - - loglevel_dict = { - "debug": "debug", - "info": "info", - "error": "error", - "warning": "warning", - "critical": "fatal", - } - if log_level not in ["info", "debug"]: - ffmpeg_command.extend(["-nostats", "-hide_banner"]) - ffmpeg_command.extend(["-loglevel", loglevel_dict[log_level]]) - - ffmpeg_command.extend(["pipe:stdout"]) - return ffmpeg_command - - elif stream_args["method"] == "vlc": - vlc_command = [ - self.fhdhr.config.dict["vlc"]["vlc_path"], - "-I", "dummy", stream_args["channelUri"], - ] - - loglevel_dict = { - "debug": "3", - "info": "0", - "error": "1", - "warning": "2", - "critical": "1", - } - vlc_command.extend(["--log-verbose=", loglevel_dict[log_level]]) - if log_level not in ["info", "debug"]: - vlc_command.extend(["--quiet"]) - - if not stream_args["transcode"]: - vlc_command.extend([]) - elif stream_args["transcode"] == "heavy": - vlc_command.extend([]) - elif stream_args["transcode"] == "mobile": - vlc_command.extend([]) - elif stream_args["transcode"] == "internet720": - vlc_command.extend([]) - elif stream_args["transcode"] == "internet480": - vlc_command.extend([]) - elif stream_args["transcode"] == "internet360": - vlc_command.extend([]) - elif stream_args["transcode"] == "internet240": - vlc_command.extend([]) - - vlc_command.extend(["--sout", "#std{mux=ts,access=file,dst=-}"]) - - return vlc_command diff --git a/fHDHR/http/api/__init__.py b/fHDHR/http/api/__init__.py index cda1d06..8a04be2 100644 --- a/fHDHR/http/api/__init__.py +++ b/fHDHR/http/api/__init__.py @@ -5,6 +5,7 @@ from .lineup_post import Lineup_Post from .xmltv import xmlTV from .m3u import M3U from .epg import EPG +from .watch import Watch from .debug import Debug_JSON from .images import Images @@ -20,6 +21,7 @@ class fHDHR_API(): self.xmltv = xmlTV(fhdhr) self.m3u = M3U(fhdhr) self.epg = EPG(fhdhr) + self.watch = Watch(fhdhr) self.debug = Debug_JSON(fhdhr) self.lineup_post = Lineup_Post(fhdhr) diff --git a/fHDHR/http/api/watch.py b/fHDHR/http/api/watch.py new file mode 100644 index 0000000..de47557 --- /dev/null +++ b/fHDHR/http/api/watch.py @@ -0,0 +1,101 @@ +from flask import Response, request, redirect, abort, stream_with_context +import urllib.parse + +from fHDHR.exceptions import TunerError + + +class Watch(): + """Methods to create xmltv.xml""" + endpoints = ["/api/watch"] + endpoint_name = "api_watch" + + def __init__(self, fhdhr): + self.fhdhr = fhdhr + + def __call__(self, *args): + return self.get(*args) + + def get(self, *args): + + full_url = request.url + + method = request.args.get('method', default=self.fhdhr.config.dict["fhdhr"]["stream_type"], type=str) + + tuner_number = request.args.get('tuner', None, type=str) + + redirect_url = request.args.get('redirect', default=None, type=str) + + if method in ["direct", "ffmpeg", "vlc"]: + + channel_number = request.args.get('channel', None, type=str) + if not channel_number: + return "Missing Channel" + + if channel_number not in list(self.fhdhr.device.channels.list.keys()): + response = Response("Not Found", status=404) + response.headers["X-fHDHR-Error"] = "801 - Unknown Channel" + abort(response) + + duration = request.args.get('duration', default=0, type=int) + + transcode = request.args.get('transcode', default=None, type=str) + valid_transcode_types = [None, "heavy", "mobile", "internet720", "internet480", "internet360", "internet240"] + if transcode not in valid_transcode_types: + response = Response("Service Unavailable", status=503) + response.headers["X-fHDHR-Error"] = "802 - Unknown Transcode Profile" + abort(response) + + stream_args = { + "channel": channel_number, + "method": method, + "duration": duration, + "transcode": transcode, + "accessed": full_url, + } + + try: + if not tuner_number: + tunernum = self.fhdhr.device.tuners.first_available() + else: + tunernum = self.fhdhr.device.tuners.tuner_grab(tuner_number) + except TunerError as e: + self.fhdhr.logger.info("A %s stream request for channel %s was rejected due to %s" + % (stream_args["method"], str(stream_args["channel"]), str(e))) + response = Response("Service Unavailable", status=503) + response.headers["X-fHDHR-Error"] = str(e) + abort(response) + tuner = self.fhdhr.device.tuners.tuners[int(tunernum)] + + try: + stream_args = self.fhdhr.device.tuners.get_stream_info(stream_args) + except TunerError as e: + self.fhdhr.logger.info("A %s stream request for channel %s was rejected due to %s" + % (stream_args["method"], str(stream_args["channel"]), str(e))) + response = Response("Service Unavailable", status=503) + response.headers["X-fHDHR-Error"] = str(e) + tuner.close() + abort(response) + + self.fhdhr.logger.info("Tuner #" + str(tunernum) + " to be used for stream.") + tuner.set_status(stream_args) + + if stream_args["method"] == "direct": + return Response(tuner.get_stream(stream_args, tuner), content_type=stream_args["content_type"], direct_passthrough=True) + elif stream_args["method"] in ["ffmpeg", "vlc"]: + return Response(stream_with_context(tuner.get_stream(stream_args, tuner)), mimetype=stream_args["content_type"]) + + elif method == "close": + + if not tuner_number or int(tuner_number) not in list(self.fhdhr.device.tuners.tuners.keys()): + return "%s Invalid tuner" % str(tuner_number) + + tuner = self.fhdhr.device.tuners.tuners[int(tuner_number)] + tuner.close() + + else: + return "%s Invalid Method" % method + + if redirect_url: + return redirect(redirect_url + "?retmessage=" + urllib.parse.quote("%s Success" % method)) + else: + return "%s Success" % method diff --git a/fHDHR/http/pages/streams_html.py b/fHDHR/http/pages/streams_html.py index fd17777..e75dfcf 100644 --- a/fHDHR/http/pages/streams_html.py +++ b/fHDHR/http/pages/streams_html.py @@ -31,11 +31,10 @@ class Streams_HTML(): fakefile.write(" Channel\n") fakefile.write(" Method\n") fakefile.write(" Time Active\n") + fakefile.write(" Options\n") fakefile.write(" \n") tuner_status = self.fhdhr.device.tuners.status() - for tuner in list(tuner_status.keys()): - print(tuner_status[tuner]) for tuner in list(tuner_status.keys()): fakefile.write(" \n") fakefile.write(" %s\n" % (str(tuner))) @@ -49,6 +48,17 @@ class Streams_HTML(): fakefile.write(" %s\n" % "N/A") fakefile.write(" %s\n" % "N/A") fakefile.write(" %s\n" % "N/A") + + fakefile.write(" \n") + fakefile.write("
\n") + + if tuner_status[tuner]["status"] in ["Active", "Acquired"]: + fakefile.write( + " \n" % + ("/api/watch?method=close&tuner=" + str(tuner) + "&redirect=%2Fstreams", "Close")) + fakefile.write("
\n") + fakefile.write(" \n") + fakefile.write(" \n") for line in page_elements["end"]: diff --git a/fHDHR/http/watch/__init__.py b/fHDHR/http/watch/__init__.py index 8e87a18..22bd4df 100644 --- a/fHDHR/http/watch/__init__.py +++ b/fHDHR/http/watch/__init__.py @@ -1,5 +1,5 @@ -from .watch import Watch +from .auto import Auto from .tuner import Tuner @@ -8,5 +8,5 @@ class fHDHR_WATCH(): def __init__(self, fhdhr): self.fhdhr = fhdhr - self.watch = Watch(fhdhr) + self.auto = Auto(fhdhr) self.tuner = Tuner(fhdhr) diff --git a/fHDHR/http/watch/auto.py b/fHDHR/http/watch/auto.py new file mode 100644 index 0000000..f442c29 --- /dev/null +++ b/fHDHR/http/watch/auto.py @@ -0,0 +1,93 @@ +from flask import Response, request, stream_with_context, abort + +from fHDHR.exceptions import TunerError + + +class Auto(): + endpoints = ['/auto/'] + endpoint_name = "auto" + + def __init__(self, fhdhr): + self.fhdhr = fhdhr + + def __call__(self, channel, *args): + return self.get(channel, *args) + + def get(self, channel, *args): + + full_url = request.url + + if channel.startswith("v"): + channel_number = channel.replace('v', '') + elif channel.startswith("ch"): + channel_freq = channel.replace('ch', '').split("-")[0] + subchannel = 0 + if "-" in channel: + subchannel = channel.replace('ch', '').split("-")[1] + abort(501, "Not Implemented %s-%s" % (str(channel_freq), str(subchannel))) + + if channel_number not in list(self.fhdhr.device.channels.list.keys()): + response = Response("Not Found", status=404) + response.headers["X-fHDHR-Error"] = "801 - Unknown Channel" + abort(response) + + method = request.args.get('method', default=self.fhdhr.config.dict["fhdhr"]["stream_type"], type=str) + duration = request.args.get('duration', default=0, type=int) + + transcode = request.args.get('transcode', default=None, type=str) + valid_transcode_types = [None, "heavy", "mobile", "internet720", "internet480", "internet360", "internet240"] + if transcode not in valid_transcode_types: + response = Response("Service Unavailable", status=503) + response.headers["X-fHDHR-Error"] = "802 - Unknown Transcode Profile" + abort(response) + + stream_args = { + "channel": channel_number, + "method": method, + "duration": duration, + "transcode": transcode, + "accessed": full_url, + } + + try: + tunernum = self.fhdhr.device.tuners.first_available() + except TunerError as e: + self.fhdhr.logger.info("A %s stream request for channel %s was rejected due to %s" + % (stream_args["method"], str(stream_args["channel"]), str(e))) + response = Response("Service Unavailable", status=503) + response.headers["X-fHDHR-Error"] = str(e) + abort(response) + tuner = self.fhdhr.device.tuners.tuners[int(tunernum)] + + try: + stream_args = self.fhdhr.device.tuners.get_stream_info(stream_args) + except TunerError as e: + self.fhdhr.logger.info("A %s stream request for channel %s was rejected due to %s" + % (stream_args["method"], str(stream_args["channel"]), str(e))) + response = Response("Service Unavailable", status=503) + response.headers["X-fHDHR-Error"] = str(e) + tuner.close() + abort(response) + + self.fhdhr.logger.info("Tuner #" + str(tunernum) + " to be used for stream.") + tuner.set_status(stream_args) + + if stream_args["method"] == "direct": + return Response(tuner.get_stream(stream_args, tuner), content_type=stream_args["content_type"], direct_passthrough=True) + elif stream_args["method"] in ["ffmpeg", "vlc"]: + return Response(stream_with_context(tuner.get_stream(stream_args, tuner)), mimetype=stream_args["content_type"]) + + """ + try: + if stream_args["method"] == "direct": + return Response(tuner.get_stream(stream_args, tuner), content_type=stream_args["content_type"], direct_passthrough=True) + elif stream_args["method"] in ["ffmpeg", "vlc"]: + return Response(stream_with_context(tuner.get_stream(stream_args, tuner)), mimetype=stream_args["content_type"]) + except TunerError as e: + tuner.close() + self.fhdhr.logger.info("A %s stream request for channel %s failed due to %s" + % (stream_args["method"], str(stream_args["channel"]), str(e))) + response = Response("Service Unavailable", status=503) + response.headers["X-fHDHR-Error"] = str(e) + abort(response) + """ diff --git a/fHDHR/http/watch/tuner.py b/fHDHR/http/watch/tuner.py index cb331fc..1a377be 100644 --- a/fHDHR/http/watch/tuner.py +++ b/fHDHR/http/watch/tuner.py @@ -1,17 +1,21 @@ from flask import Response, request, stream_with_context, abort +from fHDHR.exceptions import TunerError + class Tuner(): - endpoints = ['/tuner/'] + endpoints = ['/tuner/'] endpoint_name = "tuner" def __init__(self, fhdhr): self.fhdhr = fhdhr - def __call__(self, tuner, channel, *args): - return self.get(tuner, channel, *args) + def __call__(self, tuner_number, channel, *args): + return self.get(tuner_number, channel, *args) - def get(self, tuner, channel, *args): + def get(self, tuner_number, channel, *args): + + full_url = request.url if channel.startswith("v"): channel_number = channel.replace('v', '') @@ -20,27 +24,70 @@ class Tuner(): subchannel = 0 if "-" in channel: subchannel = channel.replace('ch', '').split("-")[1] - abort(503, "Not Implemented %s-%s" % (str(channel_freq), str(subchannel))) + abort(501, "Not Implemented %s-%s" % (str(channel_freq), str(subchannel))) if channel_number not in list(self.fhdhr.device.channels.list.keys()): - abort(404, "Not Found") + response = Response("Not Found", status=404) + response.headers["X-fHDHR-Error"] = "801 - Unknown Channel" + abort(response) + + method = request.args.get('method', default=self.fhdhr.config.dict["fhdhr"]["stream_type"], type=str) + duration = request.args.get('duration', default=0, type=int) + + transcode = request.args.get('transcode', default=None, type=str) + valid_transcode_types = [None, "heavy", "mobile", "internet720", "internet480", "internet360", "internet240"] + if transcode not in valid_transcode_types: + response = Response("Service Unavailable", status=503) + response.headers["X-fHDHR-Error"] = "802 - Unknown Transcode Profile" + abort(response) - base_url = request.url_root[:-1] stream_args = { "channel": channel_number, - "method": request.args.get('method', default=self.fhdhr.config.dict["fhdhr"]["stream_type"], type=str), - "duration": request.args.get('duration', default=0, type=int), - "transcode": request.args.get('transcode', default=None, type=int), - "accessed": self.fhdhr.device.channels.get_fhdhr_stream_url(base_url, channel_number), - "tuner": tuner + "method": method, + "duration": duration, + "transcode": transcode, + "accessed": full_url, } - stream_args = self.fhdhr.device.watch.get_stream_info(stream_args) - if not stream_args["channelUri"]: - abort(503, "Service Unavailable") + try: + tunernum = self.fhdhr.device.tuners.tuner_grab(tuner_number) + except TunerError as e: + self.fhdhr.logger.info("A %s stream request for channel %s was rejected due to %s" + % (stream_args["method"], str(stream_args["channel"]), str(e))) + response = Response("Service Unavailable", status=503) + response.headers["X-fHDHR-Error"] = str(e) + abort(response) + tuner = self.fhdhr.device.tuners.tuners[int(tunernum)] - if stream_args["channelUri"]: + try: + stream_args = self.fhdhr.device.tuners.get_stream_info(stream_args) + except TunerError as e: + self.fhdhr.logger.info("A %s stream request for channel %s was rejected due to %s" + % (stream_args["method"], str(stream_args["channel"]), str(e))) + response = Response("Service Unavailable", status=503) + response.headers["X-fHDHR-Error"] = str(e) + tuner.close() + abort(response) + + self.fhdhr.logger.info("Tuner #" + str(tunernum) + " to be used for stream.") + tuner.set_status(stream_args) + + if stream_args["method"] == "direct": + return Response(tuner.get_stream(stream_args, tuner), content_type=stream_args["content_type"], direct_passthrough=True) + elif stream_args["method"] in ["ffmpeg", "vlc"]: + return Response(stream_with_context(tuner.get_stream(stream_args, tuner)), mimetype=stream_args["content_type"]) + + """ + try: if stream_args["method"] == "direct": - return Response(self.fhdhr.device.watch.get_stream(stream_args), content_type=stream_args["content_type"], direct_passthrough=True) - elif stream_args["method"] == "ffmpeg": - return Response(stream_with_context(self.fhdhr.device.watch.get_stream(stream_args)), mimetype="video/mpeg") + return Response(tuner.get_stream(stream_args, tuner), content_type=stream_args["content_type"], direct_passthrough=True) + elif stream_args["method"] in ["ffmpeg", "vlc"]: + return Response(stream_with_context(tuner.get_stream(stream_args, tuner)), mimetype=stream_args["content_type"]) + except TunerError as e: + tuner.close() + self.fhdhr.logger.info("A %s stream request for channel %s failed due to %s" + % (stream_args["method"], str(stream_args["channel"]), str(e))) + response = Response("Service Unavailable", status=503) + response.headers["X-fHDHR-Error"] = str(e) + abort(response) + """ diff --git a/fHDHR/http/watch/watch.py b/fHDHR/http/watch/watch.py deleted file mode 100644 index 64b2e47..0000000 --- a/fHDHR/http/watch/watch.py +++ /dev/null @@ -1,48 +0,0 @@ -from flask import Response, request, stream_with_context, abort - - -class Watch(): - endpoints = ['/auto/'] - endpoint_name = "auto" - - def __init__(self, fhdhr): - self.fhdhr = fhdhr - - def __call__(self, channel, *args): - return self.get(channel, *args) - - def get(self, channel, *args): - - if channel.startswith("v"): - channel_number = channel.replace('v', '') - elif channel.startswith("ch"): - channel_freq = channel.replace('ch', '').split("-")[0] - subchannel = 0 - if "-" in channel: - subchannel = channel.replace('ch', '').split("-")[1] - abort(503, "Not Implemented %s-%s" % (str(channel_freq), str(subchannel))) - - if channel_number not in list(self.fhdhr.device.channels.list.keys()): - abort(404, "Not Found") - - base_url = request.url_root[:-1] - stream_args = { - "channel": channel_number, - "method": request.args.get('method', default=self.fhdhr.config.dict["fhdhr"]["stream_type"], type=str), - "duration": request.args.get('duration', default=0, type=int), - "transcode": request.args.get('transcode', default=None, type=int), - "accessed": self.fhdhr.device.channels.get_fhdhr_stream_url(base_url, channel_number), - "tuner": None - } - stream_args = self.fhdhr.device.watch.get_stream_info(stream_args) - - if not stream_args["channelUri"]: - abort(503, "Service Unavailable") - - if stream_args["channelUri"]: - if stream_args["method"] == "direct": - return Response(self.fhdhr.device.watch.get_stream(stream_args), content_type=stream_args["content_type"], direct_passthrough=True) - elif stream_args["method"] == "ffmpeg": - return Response(stream_with_context(self.fhdhr.device.watch.get_stream(stream_args)), mimetype="video/mpeg") - elif stream_args["method"] == "vlc": - return Response(stream_with_context(self.fhdhr.device.watch.get_stream(stream_args)), mimetype="video/mpeg")