1
0
mirror of https://github.com/fHDHR/fHDHR_NextPVR.git synced 2025-12-06 09:16:58 -05:00

Improve Tuner Handling

This commit is contained in:
deathbybandaid 2020-11-12 14:46:47 -05:00
parent 3f3fec7bf4
commit 890cf7c3dd
16 changed files with 801 additions and 430 deletions

View File

@ -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)

View File

@ -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

View 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

View 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()

View 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()

View 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

View 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

View 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(),
}

View File

@ -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

View File

@ -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)

101
fHDHR/http/api/watch.py Normal file
View 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

View File

@ -31,11 +31,10 @@ class Streams_HTML():
fakefile.write(" <th>Channel</th>\n")
fakefile.write(" <th>Method</th>\n")
fakefile.write(" <th>Time Active</th>\n")
fakefile.write(" <th>Options</th>\n")
fakefile.write(" </tr>\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(" <tr>\n")
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>\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")
for line in page_elements["end"]:

View File

@ -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)

93
fHDHR/http/watch/auto.py Normal file
View 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)
"""

View File

@ -1,17 +1,21 @@
from flask import Response, request, stream_with_context, abort
from fHDHR.exceptions import TunerError
class Tuner():
endpoints = ['/tuner<tuner>/<channel>']
endpoints = ['/tuner<tuner_number>/<channel>']
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)]
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":
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"])
"""
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)
"""

View File

@ -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")