mirror of
https://github.com/fHDHR/fHDHR_NextPVR.git
synced 2025-12-06 13:36:59 -05:00
Improve Tuner Handling
This commit is contained in:
parent
3f3fec7bf4
commit
890cf7c3dd
@ -1,7 +1,6 @@
|
|||||||
from .channels import Channels
|
from .channels import Channels
|
||||||
from .epg import EPG
|
from .epg import EPG
|
||||||
from .tuners import Tuners
|
from .tuners import Tuners
|
||||||
from .watch import WatchStream
|
|
||||||
from .images import imageHandler
|
from .images import imageHandler
|
||||||
from .station_scan import Station_Scan
|
from .station_scan import Station_Scan
|
||||||
from .ssdp import SSDPServer
|
from .ssdp import SSDPServer
|
||||||
@ -16,9 +15,7 @@ class fHDHR_Device():
|
|||||||
|
|
||||||
self.epg = EPG(fhdhr, self.channels, origin)
|
self.epg = EPG(fhdhr, self.channels, origin)
|
||||||
|
|
||||||
self.tuners = Tuners(fhdhr, self.epg)
|
self.tuners = Tuners(fhdhr, self.epg, self.channels)
|
||||||
|
|
||||||
self.watch = WatchStream(fhdhr, self.channels, self.tuners)
|
|
||||||
|
|
||||||
self.images = imageHandler(fhdhr, self.epg)
|
self.images = imageHandler(fhdhr, self.epg)
|
||||||
|
|
||||||
|
|||||||
@ -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
|
|
||||||
86
fHDHR/device/tuners/__init__.py
Normal file
86
fHDHR/device/tuners/__init__.py
Normal file
@ -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
|
||||||
22
fHDHR/device/tuners/stream/__init__.py
Normal file
22
fHDHR/device/tuners/stream/__init__.py
Normal file
@ -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()
|
||||||
112
fHDHR/device/tuners/stream/direct_stream.py
Normal file
112
fHDHR/device/tuners/stream/direct_stream.py
Normal file
@ -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()
|
||||||
127
fHDHR/device/tuners/stream/ffmpeg_stream.py
Normal file
127
fHDHR/device/tuners/stream/ffmpeg_stream.py
Normal file
@ -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
|
||||||
118
fHDHR/device/tuners/stream/vlc_stream.py
Normal file
118
fHDHR/device/tuners/stream/vlc_stream.py
Normal file
@ -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
|
||||||
59
fHDHR/device/tuners/tuner.py
Normal file
59
fHDHR/device/tuners/tuner.py
Normal file
@ -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(),
|
||||||
|
}
|
||||||
@ -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
|
|
||||||
@ -5,6 +5,7 @@ from .lineup_post import Lineup_Post
|
|||||||
from .xmltv import xmlTV
|
from .xmltv import xmlTV
|
||||||
from .m3u import M3U
|
from .m3u import M3U
|
||||||
from .epg import EPG
|
from .epg import EPG
|
||||||
|
from .watch import Watch
|
||||||
from .debug import Debug_JSON
|
from .debug import Debug_JSON
|
||||||
|
|
||||||
from .images import Images
|
from .images import Images
|
||||||
@ -20,6 +21,7 @@ class fHDHR_API():
|
|||||||
self.xmltv = xmlTV(fhdhr)
|
self.xmltv = xmlTV(fhdhr)
|
||||||
self.m3u = M3U(fhdhr)
|
self.m3u = M3U(fhdhr)
|
||||||
self.epg = EPG(fhdhr)
|
self.epg = EPG(fhdhr)
|
||||||
|
self.watch = Watch(fhdhr)
|
||||||
self.debug = Debug_JSON(fhdhr)
|
self.debug = Debug_JSON(fhdhr)
|
||||||
self.lineup_post = Lineup_Post(fhdhr)
|
self.lineup_post = Lineup_Post(fhdhr)
|
||||||
|
|
||||||
|
|||||||
101
fHDHR/http/api/watch.py
Normal file
101
fHDHR/http/api/watch.py
Normal file
@ -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
|
||||||
@ -31,11 +31,10 @@ class Streams_HTML():
|
|||||||
fakefile.write(" <th>Channel</th>\n")
|
fakefile.write(" <th>Channel</th>\n")
|
||||||
fakefile.write(" <th>Method</th>\n")
|
fakefile.write(" <th>Method</th>\n")
|
||||||
fakefile.write(" <th>Time Active</th>\n")
|
fakefile.write(" <th>Time Active</th>\n")
|
||||||
|
fakefile.write(" <th>Options</th>\n")
|
||||||
fakefile.write(" </tr>\n")
|
fakefile.write(" </tr>\n")
|
||||||
|
|
||||||
tuner_status = self.fhdhr.device.tuners.status()
|
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()):
|
for tuner in list(tuner_status.keys()):
|
||||||
fakefile.write(" <tr>\n")
|
fakefile.write(" <tr>\n")
|
||||||
fakefile.write(" <td>%s</td>\n" % (str(tuner)))
|
fakefile.write(" <td>%s</td>\n" % (str(tuner)))
|
||||||
@ -49,6 +48,17 @@ class Streams_HTML():
|
|||||||
fakefile.write(" <td>%s</td>\n" % "N/A")
|
fakefile.write(" <td>%s</td>\n" % "N/A")
|
||||||
fakefile.write(" <td>%s</td>\n" % "N/A")
|
fakefile.write(" <td>%s</td>\n" % "N/A")
|
||||||
fakefile.write(" <td>%s</td>\n" % "N/A")
|
fakefile.write(" <td>%s</td>\n" % "N/A")
|
||||||
|
|
||||||
|
fakefile.write(" <td>\n")
|
||||||
|
fakefile.write(" <div>\n")
|
||||||
|
|
||||||
|
if tuner_status[tuner]["status"] in ["Active", "Acquired"]:
|
||||||
|
fakefile.write(
|
||||||
|
" <button onclick=\"OpenLink('%s')\">%s</a></button>\n" %
|
||||||
|
("/api/watch?method=close&tuner=" + str(tuner) + "&redirect=%2Fstreams", "Close"))
|
||||||
|
fakefile.write(" </div>\n")
|
||||||
|
fakefile.write(" </td>\n")
|
||||||
|
|
||||||
fakefile.write(" </tr>\n")
|
fakefile.write(" </tr>\n")
|
||||||
|
|
||||||
for line in page_elements["end"]:
|
for line in page_elements["end"]:
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
|
|
||||||
from .watch import Watch
|
from .auto import Auto
|
||||||
from .tuner import Tuner
|
from .tuner import Tuner
|
||||||
|
|
||||||
|
|
||||||
@ -8,5 +8,5 @@ class fHDHR_WATCH():
|
|||||||
def __init__(self, fhdhr):
|
def __init__(self, fhdhr):
|
||||||
self.fhdhr = fhdhr
|
self.fhdhr = fhdhr
|
||||||
|
|
||||||
self.watch = Watch(fhdhr)
|
self.auto = Auto(fhdhr)
|
||||||
self.tuner = Tuner(fhdhr)
|
self.tuner = Tuner(fhdhr)
|
||||||
|
|||||||
93
fHDHR/http/watch/auto.py
Normal file
93
fHDHR/http/watch/auto.py
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
from flask import Response, request, stream_with_context, abort
|
||||||
|
|
||||||
|
from fHDHR.exceptions import TunerError
|
||||||
|
|
||||||
|
|
||||||
|
class Auto():
|
||||||
|
endpoints = ['/auto/<channel>']
|
||||||
|
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)
|
||||||
|
"""
|
||||||
@ -1,17 +1,21 @@
|
|||||||
from flask import Response, request, stream_with_context, abort
|
from flask import Response, request, stream_with_context, abort
|
||||||
|
|
||||||
|
from fHDHR.exceptions import TunerError
|
||||||
|
|
||||||
|
|
||||||
class Tuner():
|
class Tuner():
|
||||||
endpoints = ['/tuner<tuner>/<channel>']
|
endpoints = ['/tuner<tuner_number>/<channel>']
|
||||||
endpoint_name = "tuner"
|
endpoint_name = "tuner"
|
||||||
|
|
||||||
def __init__(self, fhdhr):
|
def __init__(self, fhdhr):
|
||||||
self.fhdhr = fhdhr
|
self.fhdhr = fhdhr
|
||||||
|
|
||||||
def __call__(self, tuner, channel, *args):
|
def __call__(self, tuner_number, channel, *args):
|
||||||
return self.get(tuner, 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"):
|
if channel.startswith("v"):
|
||||||
channel_number = channel.replace('v', '')
|
channel_number = channel.replace('v', '')
|
||||||
@ -20,27 +24,70 @@ class Tuner():
|
|||||||
subchannel = 0
|
subchannel = 0
|
||||||
if "-" in channel:
|
if "-" in channel:
|
||||||
subchannel = channel.replace('ch', '').split("-")[1]
|
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()):
|
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 = {
|
stream_args = {
|
||||||
"channel": channel_number,
|
"channel": channel_number,
|
||||||
"method": request.args.get('method', default=self.fhdhr.config.dict["fhdhr"]["stream_type"], type=str),
|
"method": method,
|
||||||
"duration": request.args.get('duration', default=0, type=int),
|
"duration": duration,
|
||||||
"transcode": request.args.get('transcode', default=None, type=int),
|
"transcode": transcode,
|
||||||
"accessed": self.fhdhr.device.channels.get_fhdhr_stream_url(base_url, channel_number),
|
"accessed": full_url,
|
||||||
"tuner": tuner
|
|
||||||
}
|
}
|
||||||
stream_args = self.fhdhr.device.watch.get_stream_info(stream_args)
|
|
||||||
|
|
||||||
if not stream_args["channelUri"]:
|
try:
|
||||||
abort(503, "Service Unavailable")
|
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["channelUri"]:
|
|
||||||
if stream_args["method"] == "direct":
|
if stream_args["method"] == "direct":
|
||||||
return Response(self.fhdhr.device.watch.get_stream(stream_args), content_type=stream_args["content_type"], direct_passthrough=True)
|
return Response(tuner.get_stream(stream_args, tuner), content_type=stream_args["content_type"], direct_passthrough=True)
|
||||||
elif stream_args["method"] == "ffmpeg":
|
elif stream_args["method"] in ["ffmpeg", "vlc"]:
|
||||||
return Response(stream_with_context(self.fhdhr.device.watch.get_stream(stream_args)), mimetype="video/mpeg")
|
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)
|
||||||
|
"""
|
||||||
|
|||||||
@ -1,48 +0,0 @@
|
|||||||
from flask import Response, request, stream_with_context, abort
|
|
||||||
|
|
||||||
|
|
||||||
class Watch():
|
|
||||||
endpoints = ['/auto/<channel>']
|
|
||||||
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")
|
|
||||||
Loading…
Reference in New Issue
Block a user