1
0
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:
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 .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)

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 .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
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>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"]:

View File

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

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