mirror of
https://github.com/fHDHR/fHDHR_NextPVR.git
synced 2025-12-06 06:46:58 -05:00
Compare commits
8 Commits
fc0708d888
...
4bd2ff971e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4bd2ff971e | ||
|
|
63685f4a0e | ||
|
|
70fe2f3814 | ||
|
|
c0ff51a6db | ||
|
|
8a3f8d919d | ||
|
|
e09bb5d83b | ||
|
|
b72a26c96c | ||
|
|
35e6bb707d |
@ -1,62 +0,0 @@
|
|||||||
[main]
|
|
||||||
# uuid =
|
|
||||||
# cache_dir =
|
|
||||||
# servicename = NextPVR
|
|
||||||
# reponame = fHDHR_NextPVR
|
|
||||||
|
|
||||||
[fhdhr]
|
|
||||||
# address = 0.0.0.0
|
|
||||||
# discovery_address = 0.0.0.0
|
|
||||||
# port = 5004
|
|
||||||
# stream_type = direct
|
|
||||||
# tuner_count = 4
|
|
||||||
# friendlyname = fHDHR-NextPVR
|
|
||||||
# reporting_firmware_name = fHDHR_NextPVR
|
|
||||||
# reporting_manufacturer = BoronDust
|
|
||||||
# reporting_model = fHDHR
|
|
||||||
# reporting_firmware_ver = 20201001
|
|
||||||
# reporting_tuner_type = Antenna
|
|
||||||
# device_auth = fHDHR
|
|
||||||
|
|
||||||
[epg]
|
|
||||||
# images = pass
|
|
||||||
# method = origin
|
|
||||||
# update_frequency = 43200
|
|
||||||
|
|
||||||
[ffmpeg]
|
|
||||||
# path = ffmpeg
|
|
||||||
# bytes_per_read = 1152000
|
|
||||||
|
|
||||||
[vlc]
|
|
||||||
# path = cvlc
|
|
||||||
# bytes_per_read = 1152000
|
|
||||||
|
|
||||||
[direct_stream]
|
|
||||||
# chunksize = 1048576
|
|
||||||
|
|
||||||
[logging]
|
|
||||||
# level = WARNING
|
|
||||||
|
|
||||||
[database]
|
|
||||||
# type = sqlite
|
|
||||||
# driver = None
|
|
||||||
|
|
||||||
[nextpvr]
|
|
||||||
# address = localhost
|
|
||||||
# port = 8866
|
|
||||||
# ssl =
|
|
||||||
# pin =
|
|
||||||
|
|
||||||
[zap2it]
|
|
||||||
# delay = 5
|
|
||||||
# postalcode = None
|
|
||||||
# affiliate_id = gapzap
|
|
||||||
# country = USA
|
|
||||||
# device = -
|
|
||||||
# headendid = lineupId
|
|
||||||
# isoverride = True
|
|
||||||
# languagecode = en
|
|
||||||
# pref =
|
|
||||||
# timespan = 6
|
|
||||||
# timezone =
|
|
||||||
# userid = -
|
|
||||||
@ -55,11 +55,6 @@
|
|||||||
"config_file": true,
|
"config_file": true,
|
||||||
"config_web": true
|
"config_web": true
|
||||||
},
|
},
|
||||||
"stream_type":{
|
|
||||||
"value": "direct",
|
|
||||||
"config_file": true,
|
|
||||||
"config_web": true
|
|
||||||
},
|
|
||||||
"tuner_count":{
|
"tuner_count":{
|
||||||
"value": 4,
|
"value": 4,
|
||||||
"config_file": true,
|
"config_file": true,
|
||||||
|
|||||||
@ -5,10 +5,20 @@
|
|||||||
"config_file": true,
|
"config_file": true,
|
||||||
"config_web": true
|
"config_web": true
|
||||||
},
|
},
|
||||||
"quality": {
|
"origin_quality": {
|
||||||
"value": "none",
|
"value": "none",
|
||||||
"config_file": true,
|
"config_file": true,
|
||||||
"config_web": true
|
"config_web": true
|
||||||
|
},
|
||||||
|
"transcode_quality": {
|
||||||
|
"value": "none",
|
||||||
|
"config_file": true,
|
||||||
|
"config_web": true
|
||||||
|
},
|
||||||
|
"method": {
|
||||||
|
"value": "direct",
|
||||||
|
"config_file": true,
|
||||||
|
"config_web": true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ffmpeg":{
|
"ffmpeg":{
|
||||||
|
|||||||
@ -28,14 +28,28 @@ Here's the `main` section.
|
|||||||
# cache_dir =
|
# cache_dir =
|
||||||
````
|
````
|
||||||
|
|
||||||
|
## streaming
|
||||||
|
|
||||||
|
* `method` can be set to `ffmpeg`, `vlc` or `direct`.
|
||||||
|
* `bytes_per_read` determines how many bytes of the stream to read before sending the data to your client. Increasing this value may cause longer load times, and lowering it may effect `stuttering`.
|
||||||
|
|
||||||
|
|
||||||
|
````
|
||||||
|
[streaming]
|
||||||
|
# method = direct
|
||||||
|
````
|
||||||
|
|
||||||
|
|
||||||
## fhdhr
|
## fhdhr
|
||||||
|
|
||||||
The `fhdhr` contains all the configuration options for interfacing between this script and your media platform.
|
The `fhdhr` contains all the configuration options for interfacing between this script and your media platform.
|
||||||
* `address` and `port` are what we will allow the script to listen on. `0.0.0.0` is the default, and will respond to all.
|
* `address` and `port` are what we will allow the script to listen on. `0.0.0.0` is the default, and will respond to all.
|
||||||
* `discovery_address` may be helpful for making SSDP work properly. If `address` is not `0.0.0.0`, we will use that. If this is not set to a real IP, we won't run SSDP. SSDP is only really helpful for discovering in Plex/Emby. It's a wasted resource since you can manually add the `ip:port` of the script to Plex.
|
* `discovery_address` may be helpful for making SSDP work properly. If `address` is not `0.0.0.0`, we will use that. If this is not set to a real IP, we won't run SSDP. SSDP is only really helpful for discovering in Plex/Emby. It's a wasted resource since you can manually add the `ip:port` of the script to Plex.
|
||||||
* `tuner_count` is a limit of devices able to stream from the script.
|
* `tuner_count` is a limit of devices able to stream from the script. The default is 3, as per Locast's documentation. A 4th is possible, but is not reccomended.
|
||||||
* `friendlyname` is to set the name that Plex sees the script as.
|
* `friendlyname` is to set the name that Plex sees the script as.
|
||||||
* `stream_type` can be set to `ffmpeg`, `vlc` or `direct`.
|
* `reporting_*` are settings that show how the script projects itself as a hardware device.
|
||||||
|
* `device_auth` and `require_auth` are for an unimplemented Authentication feature.
|
||||||
|
* `chanscan_on_start` Scans Origin for new channels at startup.
|
||||||
|
|
||||||
|
|
||||||
````
|
````
|
||||||
@ -43,61 +57,56 @@ The `fhdhr` contains all the configuration options for interfacing between this
|
|||||||
# address = 0.0.0.0
|
# address = 0.0.0.0
|
||||||
# discovery_address = 0.0.0.0
|
# discovery_address = 0.0.0.0
|
||||||
# port = 5004
|
# port = 5004
|
||||||
# stream_type = direct
|
|
||||||
# tuner_count = 4
|
# tuner_count = 4
|
||||||
# friendlyname = fHDHR-NextPVR
|
# friendlyname = fHDHR-Locast
|
||||||
# reporting_firmware_name = fHDHR_NextPVR
|
# reporting_firmware_name = fHDHR_Locast
|
||||||
# reporting_manufacturer = BoronDust
|
# reporting_manufacturer = BoronDust
|
||||||
# reporting_model = fHDHR
|
# reporting_model = fHDHR
|
||||||
# reporting_firmware_ver = 20201001
|
# reporting_firmware_ver = 20201001
|
||||||
# reporting_tuner_type = Antenna
|
# reporting_tuner_type = Antenna
|
||||||
# device_auth = fHDHR
|
# device_auth = fHDHR
|
||||||
|
# require_auth = False
|
||||||
|
# chanscan_on_start = True
|
||||||
````
|
````
|
||||||
|
|
||||||
# EPG
|
# EPG
|
||||||
* `images` can be set to `proxy` or `pass`. If you choose `proxy`, images will be reverse proxied through fHDHR.
|
* `images` can be set to `proxy` or `pass`. If you choose `proxy`, images will be reverse proxied through fHDHR.
|
||||||
* `method` defaults to `origin` and will pull the xmltv data from NextPVR. Other Options include `blocks` which is an hourly schedule with minimal channel information. Another option is `zap2it`, which is another source of EPG information. Channel Numbers may need to be manually mapped.
|
* `method` defaults to `origin` and will pull the xmltv data from Locast. Other Options include `blocks` which is an hourly schedule with minimal channel information. Another option is `zap2it`, which is another source of EPG information. Channel Numbers may need to be manually mapped.
|
||||||
* `update_frequency` * `epg_update_frequency` determines how often we check for new scheduling information. In Seconds.
|
* `update_frequency` determines how often we check for new scheduling information. In Seconds.
|
||||||
|
* `reverse_days` allows Blocks of EPG data to be created prior to the start of the EPG Source data.
|
||||||
|
* `forward_days` allows Blocks of EPG data to be created after the end of the EPG Source data.
|
||||||
|
* `block_size` in seconds, sets the default block size for data before, after and missing timeslots.
|
||||||
|
* `xmltv_offset` allows the final xmltv file to have an offset for users with timezone issues.
|
||||||
|
|
||||||
````
|
````
|
||||||
[epg]
|
[epg]
|
||||||
# images = pass
|
# images = pass
|
||||||
# method = origin
|
# method = origin
|
||||||
# update_frequency = 43200
|
# update_frequency = 43200
|
||||||
|
# reverse_days = -1
|
||||||
|
# forward_days = 7
|
||||||
|
# block_size = 1800
|
||||||
|
# xmltv_offset = +0000
|
||||||
````
|
````
|
||||||
|
|
||||||
## ffmpeg
|
## ffmpeg
|
||||||
|
|
||||||
The `ffmpeg` section includes:
|
The `ffmpeg` section includes:
|
||||||
* `path` is useful if ffmpeg is not in your systems PATH, or you want to manually specify.
|
* `path` is useful if ffmpeg is not in your systems PATH, or you want to manually specify.
|
||||||
* `bytes_per_read` determines how many bytes of the stream to read before sending the data to your client. Increasing this value may cause longer load times, and lowering it may effect `stuttering`.
|
|
||||||
|
|
||||||
````
|
````
|
||||||
[ffmpeg]
|
[ffmpeg]
|
||||||
# path = ffmpeg
|
# path = ffmpeg
|
||||||
# bytes_per_read = 1152000
|
|
||||||
````
|
````
|
||||||
|
|
||||||
## vlc
|
## vlc
|
||||||
|
|
||||||
The `vlc` section includes:
|
The `vlc` section includes:
|
||||||
* `path` is useful if ffmpeg is not in your systems PATH, or you want to manually specify.
|
* `path` is useful if ffmpeg is not in your systems PATH, or you want to manually specify.
|
||||||
* `bytes_per_read` determines how many bytes of the stream to read before sending the data to your client. Increasing this value may cause longer load times, and lowering it may effect `stuttering`.
|
|
||||||
|
|
||||||
````
|
````
|
||||||
[vlc]
|
[vlc]
|
||||||
# path = ffmpeg
|
# path = cvlc
|
||||||
# bytes_per_read = 1152000
|
|
||||||
````
|
|
||||||
|
|
||||||
## direct_stream
|
|
||||||
|
|
||||||
The `direct_stream` section is for when you set the `[fhdhr]stream_type` to `direct`
|
|
||||||
* `chunksize` is how much data to read at a time.
|
|
||||||
|
|
||||||
````
|
|
||||||
[direct_stream]
|
|
||||||
# chunksize = 1024*1024
|
|
||||||
````
|
````
|
||||||
|
|
||||||
# Logging
|
# Logging
|
||||||
@ -117,6 +126,27 @@ TODO: improve documentation here.
|
|||||||
[database]
|
[database]
|
||||||
# type = sqlite
|
# type = sqlite
|
||||||
# driver = None
|
# driver = None
|
||||||
|
user = None
|
||||||
|
pass = None
|
||||||
|
host = None
|
||||||
|
port = None
|
||||||
|
name = None
|
||||||
|
````
|
||||||
|
|
||||||
|
## RMG
|
||||||
|
|
||||||
|
````
|
||||||
|
# enabled = True
|
||||||
|
````
|
||||||
|
|
||||||
|
## SSDP
|
||||||
|
|
||||||
|
````
|
||||||
|
# enabled = True
|
||||||
|
# max_age = 1800
|
||||||
|
# proto = ipv6
|
||||||
|
# iface = None
|
||||||
|
# multicast_address = None
|
||||||
````
|
````
|
||||||
|
|
||||||
## NextPVR
|
## NextPVR
|
||||||
|
|||||||
@ -103,7 +103,7 @@ class Config():
|
|||||||
isdocker = is_docker()
|
isdocker = is_docker()
|
||||||
self.internal["versions"]["Docker"] = isdocker
|
self.internal["versions"]["Docker"] = isdocker
|
||||||
|
|
||||||
if self.dict["fhdhr"]["stream_type"] == "ffmpeg":
|
if self.dict["streaming"]["method"] == "ffmpeg":
|
||||||
try:
|
try:
|
||||||
ffmpeg_command = [self.dict["ffmpeg"]["path"],
|
ffmpeg_command = [self.dict["ffmpeg"]["path"],
|
||||||
"-version",
|
"-version",
|
||||||
@ -121,7 +121,7 @@ class Config():
|
|||||||
print("Failed to find ffmpeg.")
|
print("Failed to find ffmpeg.")
|
||||||
self.internal["versions"]["ffmpeg"] = ffmpeg_version
|
self.internal["versions"]["ffmpeg"] = ffmpeg_version
|
||||||
|
|
||||||
if self.dict["fhdhr"]["stream_type"] == "vlc":
|
if self.dict["streaming"]["method"] == "vlc":
|
||||||
try:
|
try:
|
||||||
vlc_command = [self.dict["vlc"]["path"],
|
vlc_command = [self.dict["vlc"]["path"],
|
||||||
"--version",
|
"--version",
|
||||||
@ -310,7 +310,7 @@ class Config():
|
|||||||
|
|
||||||
self.dict["database"]["path"] = pathlib.Path(cache_dir).joinpath('fhdhr.db')
|
self.dict["database"]["path"] = pathlib.Path(cache_dir).joinpath('fhdhr.db')
|
||||||
|
|
||||||
if self.dict["fhdhr"]["stream_type"] not in ["direct", "ffmpeg", "vlc"]:
|
if self.dict["streaming"]["method"] not in ["direct", "ffmpeg", "vlc"]:
|
||||||
raise fHDHR.exceptions.ConfigurationError("Invalid stream type. Exiting...")
|
raise fHDHR.exceptions.ConfigurationError("Invalid stream type. Exiting...")
|
||||||
|
|
||||||
if not self.dict["fhdhr"]["discovery_address"] and self.dict["fhdhr"]["address"] != "0.0.0.0":
|
if not self.dict["fhdhr"]["discovery_address"] and self.dict["fhdhr"]["address"] != "0.0.0.0":
|
||||||
|
|||||||
@ -190,7 +190,7 @@ class Channel():
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def api_stream_url(self):
|
def api_stream_url(self):
|
||||||
return '/api/tuners?method=%s&channel=%s' % (self.fhdhr.config.dict["fhdhr"]["stream_type"], self.number)
|
return '/api/tuners?method=%s&channel=%s' % (self.fhdhr.config.dict["streaming"]["method"], self.number)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def m3u_url(self):
|
def m3u_url(self):
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import m3u8
|
||||||
|
|
||||||
from fHDHR.exceptions import TunerError
|
from fHDHR.exceptions import TunerError
|
||||||
|
|
||||||
@ -96,12 +97,15 @@ class Tuners():
|
|||||||
raise TunerError("806 - Tune Failed")
|
raise TunerError("806 - Tune Failed")
|
||||||
|
|
||||||
if isinstance(stream_info, str):
|
if isinstance(stream_info, str):
|
||||||
stream_info = {"url": stream_info}
|
stream_info = {"url": stream_info, "headers": None}
|
||||||
stream_args["stream_info"] = stream_info
|
stream_args["stream_info"] = stream_info
|
||||||
|
|
||||||
if not stream_args["stream_info"]["url"]:
|
if not stream_args["stream_info"]["url"]:
|
||||||
raise TunerError("806 - Tune Failed")
|
raise TunerError("806 - Tune Failed")
|
||||||
|
|
||||||
|
if "headers" not in list(stream_args["stream_info"].keys()):
|
||||||
|
stream_args["stream_info"]["headers"] = None
|
||||||
|
|
||||||
if stream_args["stream_info"]["url"].startswith("udp://"):
|
if stream_args["stream_info"]["url"].startswith("udp://"):
|
||||||
stream_args["true_content_type"] = "video/mpeg"
|
stream_args["true_content_type"] = "video/mpeg"
|
||||||
stream_args["content_type"] = "video/mpeg"
|
stream_args["content_type"] = "video/mpeg"
|
||||||
@ -112,7 +116,81 @@ class Tuners():
|
|||||||
|
|
||||||
if stream_args["true_content_type"].startswith(tuple(["application/", "text/"])):
|
if stream_args["true_content_type"].startswith(tuple(["application/", "text/"])):
|
||||||
stream_args["content_type"] = "video/mpeg"
|
stream_args["content_type"] = "video/mpeg"
|
||||||
|
if stream_args["origin_quality"] != -1:
|
||||||
|
stream_args["stream_info"]["url"] = self.m3u8_quality(stream_args)
|
||||||
else:
|
else:
|
||||||
stream_args["content_type"] = stream_args["true_content_type"]
|
stream_args["content_type"] = stream_args["true_content_type"]
|
||||||
|
|
||||||
return stream_args
|
return stream_args
|
||||||
|
|
||||||
|
def m3u8_quality(self, stream_args):
|
||||||
|
|
||||||
|
m3u8_url = stream_args["stream_info"]["url"]
|
||||||
|
quality_profile = stream_args["origin_quality"]
|
||||||
|
|
||||||
|
if not quality_profile:
|
||||||
|
quality_profile = "high"
|
||||||
|
self.fhdhr.logger.info("Origin Quality not set in config. Defaulting to Highest Quality")
|
||||||
|
else:
|
||||||
|
quality_profile = quality_profile.lower()
|
||||||
|
self.fhdhr.logger.info("Origin Quality set in config to %s" % (quality_profile))
|
||||||
|
|
||||||
|
while True:
|
||||||
|
self.fhdhr.logger.info("Opening m3u8 for reading %s" % m3u8_url)
|
||||||
|
|
||||||
|
if stream_args["stream_info"]["headers"]:
|
||||||
|
videoUrlM3u = m3u8.load(m3u8_url, headers=stream_args["stream_info"]["headers"])
|
||||||
|
else:
|
||||||
|
videoUrlM3u = m3u8.load(m3u8_url)
|
||||||
|
|
||||||
|
if len(videoUrlM3u.playlists):
|
||||||
|
self.fhdhr.logger.info("%s m3u8 varients found" % len(videoUrlM3u.playlists))
|
||||||
|
|
||||||
|
# Create list of dicts
|
||||||
|
playlists, playlist_index = {}, 0
|
||||||
|
for playlist_item in videoUrlM3u.playlists:
|
||||||
|
playlist_index += 1
|
||||||
|
playlist_dict = {
|
||||||
|
"url": playlist_item.absolute_uri,
|
||||||
|
"bandwidth": playlist_item.stream_info.bandwidth,
|
||||||
|
}
|
||||||
|
|
||||||
|
if not playlist_item.stream_info.resolution:
|
||||||
|
playlist_dict["width"] = None
|
||||||
|
playlist_dict["height"] = None
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
playlist_dict["width"] = playlist_item.stream_info.resolution[0]
|
||||||
|
playlist_dict["height"] = playlist_item.stream_info.resolution[1]
|
||||||
|
except TypeError:
|
||||||
|
playlist_dict["width"] = None
|
||||||
|
playlist_dict["height"] = None
|
||||||
|
|
||||||
|
playlists[playlist_index] = playlist_dict
|
||||||
|
|
||||||
|
sorted_playlists = sorted(playlists, key=lambda i: (
|
||||||
|
int(playlists[i]['bandwidth']),
|
||||||
|
int(playlists[i]['width'] or 0),
|
||||||
|
int(playlists[i]['height'] or 0)
|
||||||
|
))
|
||||||
|
sorted_playlists = [playlists[x] for x in sorted_playlists]
|
||||||
|
|
||||||
|
if not quality_profile or quality_profile == "high":
|
||||||
|
selected_index = -1
|
||||||
|
elif quality_profile == "medium":
|
||||||
|
selected_index = int((len(sorted_playlists) - 1)/2)
|
||||||
|
elif quality_profile == "low":
|
||||||
|
selected_index = 0
|
||||||
|
|
||||||
|
m3u8_stats = ",".join(
|
||||||
|
["%s %s" % (x, sorted_playlists[selected_index][x])
|
||||||
|
for x in list(sorted_playlists[selected_index].keys())
|
||||||
|
if x != "url" and sorted_playlists[selected_index][x]])
|
||||||
|
self.fhdhr.logger.info("Selected m3u8 details: %s" % m3u8_stats)
|
||||||
|
m3u8_url = sorted_playlists[selected_index]["url"]
|
||||||
|
|
||||||
|
else:
|
||||||
|
self.fhdhr.logger.info("No m3u8 varients found")
|
||||||
|
break
|
||||||
|
|
||||||
|
return m3u8_url
|
||||||
|
|||||||
@ -21,18 +21,7 @@ class Direct_M3U8_Stream():
|
|||||||
if not self.stream_args["duration"] == 0:
|
if not self.stream_args["duration"] == 0:
|
||||||
self.stream_args["time_end"] = self.stream_args["duration"] + time.time()
|
self.stream_args["time_end"] = self.stream_args["duration"] + time.time()
|
||||||
|
|
||||||
self.fhdhr.logger.info("Detected stream URL is m3u8: %s" % self.stream_args["true_content_type"])
|
self.fhdhr.logger.info("Detected stream of m3u8 URL: %s" % self.stream_args["stream_info"]["url"])
|
||||||
|
|
||||||
channel_stream_url = self.stream_args["stream_info"]["url"]
|
|
||||||
while True:
|
|
||||||
|
|
||||||
self.fhdhr.logger.info("Opening m3u8 for reading %s" % channel_stream_url)
|
|
||||||
videoUrlM3u = m3u8.load(channel_stream_url)
|
|
||||||
if len(videoUrlM3u.playlists):
|
|
||||||
self.fhdhr.logger.info("%s m3u8 varients found" % len(videoUrlM3u.playlists))
|
|
||||||
channel_stream_url = videoUrlM3u.playlists[0].absolute_uri
|
|
||||||
else:
|
|
||||||
break
|
|
||||||
|
|
||||||
def generate():
|
def generate():
|
||||||
|
|
||||||
@ -42,7 +31,11 @@ class Direct_M3U8_Stream():
|
|||||||
|
|
||||||
while self.tuner.tuner_lock.locked():
|
while self.tuner.tuner_lock.locked():
|
||||||
|
|
||||||
playlist = m3u8.load(channel_stream_url)
|
if self.stream_args["stream_info"]["headers"]:
|
||||||
|
playlist = m3u8.load(self.stream_args["stream_info"]["url"], headers=self.stream_args["stream_info"]["headers"])
|
||||||
|
else:
|
||||||
|
playlist = m3u8.load(self.stream_args["stream_info"]["url"])
|
||||||
|
|
||||||
segments = playlist.segments
|
segments = playlist.segments
|
||||||
|
|
||||||
if len(played_chunk_urls):
|
if len(played_chunk_urls):
|
||||||
@ -70,12 +63,18 @@ class Direct_M3U8_Stream():
|
|||||||
self.fhdhr.logger.info("Requested Duration Expired.")
|
self.fhdhr.logger.info("Requested Duration Expired.")
|
||||||
self.tuner.close()
|
self.tuner.close()
|
||||||
|
|
||||||
|
if self.stream_args["stream_info"]["headers"]:
|
||||||
|
chunk = self.fhdhr.web.session.get(chunkurl, headers=self.stream_args["stream_info"]["headers"]).content
|
||||||
|
else:
|
||||||
chunk = self.fhdhr.web.session.get(chunkurl).content
|
chunk = self.fhdhr.web.session.get(chunkurl).content
|
||||||
if not chunk:
|
if not chunk:
|
||||||
break
|
break
|
||||||
# raise TunerError("807 - No Video Data")
|
# raise TunerError("807 - No Video Data")
|
||||||
if key:
|
if key:
|
||||||
if key["url"]:
|
if key["url"]:
|
||||||
|
if self.stream_args["stream_info"]["headers"]:
|
||||||
|
keyfile = self.fhdhr.web.session.get(key["url"], headers=self.stream_args["stream_info"]["headers"]).content
|
||||||
|
else:
|
||||||
keyfile = self.fhdhr.web.session.get(key["url"]).content
|
keyfile = self.fhdhr.web.session.get(key["url"]).content
|
||||||
cryptor = AES.new(keyfile, AES.MODE_CBC, keyfile)
|
cryptor = AES.new(keyfile, AES.MODE_CBC, keyfile)
|
||||||
self.fhdhr.logger.info("Decrypting Chunk #%s with key: %s" % (len(played_chunk_urls), key["url"]))
|
self.fhdhr.logger.info("Decrypting Chunk #%s with key: %s" % (len(played_chunk_urls), key["url"]))
|
||||||
|
|||||||
@ -20,6 +20,9 @@ class Direct_Stream():
|
|||||||
|
|
||||||
self.fhdhr.logger.info("Direct Stream of %s URL: %s" % (self.stream_args["true_content_type"], self.stream_args["stream_info"]["url"]))
|
self.fhdhr.logger.info("Direct Stream of %s URL: %s" % (self.stream_args["true_content_type"], self.stream_args["stream_info"]["url"]))
|
||||||
|
|
||||||
|
if self.stream_args["stream_info"]["headers"]:
|
||||||
|
req = self.fhdhr.web.session.get(self.stream_args["stream_info"]["url"], stream=True, headers=self.stream_args["stream_info"]["headers"])
|
||||||
|
else:
|
||||||
req = self.fhdhr.web.session.get(self.stream_args["stream_info"]["url"], stream=True)
|
req = self.fhdhr.web.session.get(self.stream_args["stream_info"]["url"], stream=True)
|
||||||
|
|
||||||
def generate():
|
def generate():
|
||||||
|
|||||||
@ -50,12 +50,26 @@ class FFMPEG_Stream():
|
|||||||
self.fhdhr.config.dict["ffmpeg"]["path"],
|
self.fhdhr.config.dict["ffmpeg"]["path"],
|
||||||
"-i", stream_args["stream_info"]["url"],
|
"-i", stream_args["stream_info"]["url"],
|
||||||
]
|
]
|
||||||
|
ffmpeg_command.extend(self.ffmpeg_headers(stream_args))
|
||||||
ffmpeg_command.extend(self.ffmpeg_duration(stream_args))
|
ffmpeg_command.extend(self.ffmpeg_duration(stream_args))
|
||||||
ffmpeg_command.extend(self.transcode_profiles(stream_args))
|
ffmpeg_command.extend(self.transcode_profiles(stream_args))
|
||||||
ffmpeg_command.extend(self.ffmpeg_loglevel())
|
ffmpeg_command.extend(self.ffmpeg_loglevel())
|
||||||
ffmpeg_command.extend(["pipe:stdout"])
|
ffmpeg_command.extend(["pipe:stdout"])
|
||||||
return ffmpeg_command
|
return ffmpeg_command
|
||||||
|
|
||||||
|
def ffmpeg_headers(self, stream_args):
|
||||||
|
ffmpeg_command = []
|
||||||
|
if stream_args["stream_info"]["headers"]:
|
||||||
|
headers_string = ""
|
||||||
|
if len(list(stream_args["stream_info"]["headers"].keys())) > 1:
|
||||||
|
for x in list(stream_args["stream_info"]["headers"].keys()):
|
||||||
|
headers_string += "%s: %s\r\n" % (x, stream_args["stream_info"]["headers"][x])
|
||||||
|
else:
|
||||||
|
for x in list(stream_args["stream_info"]["headers"].keys()):
|
||||||
|
headers_string += "%s: %s" % (x, stream_args["stream_info"]["headers"][x])
|
||||||
|
ffmpeg_command.extend(["-headers", '\"%s\"' % headers_string])
|
||||||
|
return ffmpeg_command
|
||||||
|
|
||||||
def ffmpeg_duration(self, stream_args):
|
def ffmpeg_duration(self, stream_args):
|
||||||
ffmpeg_command = []
|
ffmpeg_command = []
|
||||||
if stream_args["duration"]:
|
if stream_args["duration"]:
|
||||||
@ -103,30 +117,30 @@ class FFMPEG_Stream():
|
|||||||
16:9 content, not exceeding 320x240 30fps for 4:3 content
|
16:9 content, not exceeding 320x240 30fps for 4:3 content
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if stream_args["transcode"]:
|
if stream_args["transcode_quality"]:
|
||||||
self.fhdhr.logger.info("Client requested a %s transcode for stream." % stream_args["transcode"])
|
self.fhdhr.logger.info("Client requested a %s transcode for stream." % stream_args["transcode_quality"])
|
||||||
stream_args["transcode"] = None
|
stream_args["transcode_quality"] = None
|
||||||
|
|
||||||
ffmpeg_command = []
|
ffmpeg_command = []
|
||||||
|
|
||||||
if not stream_args["transcode"]:
|
if not stream_args["transcode_quality"]:
|
||||||
ffmpeg_command.extend(
|
ffmpeg_command.extend(
|
||||||
[
|
[
|
||||||
"-c", "copy",
|
"-c", "copy",
|
||||||
"-f", "mpegts",
|
"-f", "mpegts",
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
elif stream_args["transcode"] == "heavy":
|
elif stream_args["transcode_quality"] == "heavy":
|
||||||
ffmpeg_command.extend([])
|
ffmpeg_command.extend([])
|
||||||
elif stream_args["transcode"] == "mobile":
|
elif stream_args["transcode_quality"] == "mobile":
|
||||||
ffmpeg_command.extend([])
|
ffmpeg_command.extend([])
|
||||||
elif stream_args["transcode"] == "internet720":
|
elif stream_args["transcode_quality"] == "internet720":
|
||||||
ffmpeg_command.extend([])
|
ffmpeg_command.extend([])
|
||||||
elif stream_args["transcode"] == "internet480":
|
elif stream_args["transcode_quality"] == "internet480":
|
||||||
ffmpeg_command.extend([])
|
ffmpeg_command.extend([])
|
||||||
elif stream_args["transcode"] == "internet360":
|
elif stream_args["transcode_quality"] == "internet360":
|
||||||
ffmpeg_command.extend([])
|
ffmpeg_command.extend([])
|
||||||
elif stream_args["transcode"] == "internet240":
|
elif stream_args["transcode_quality"] == "internet240":
|
||||||
ffmpeg_command.extend([])
|
ffmpeg_command.extend([])
|
||||||
|
|
||||||
return ffmpeg_command
|
return ffmpeg_command
|
||||||
|
|||||||
@ -51,12 +51,17 @@ class VLC_Stream():
|
|||||||
self.fhdhr.config.dict["vlc"]["path"],
|
self.fhdhr.config.dict["vlc"]["path"],
|
||||||
"-I", "dummy", stream_args["stream_info"]["url"],
|
"-I", "dummy", stream_args["stream_info"]["url"],
|
||||||
]
|
]
|
||||||
|
# vlc_command.extend(self.vlc_headers(stream_args))
|
||||||
vlc_command.extend(self.vlc_duration(stream_args))
|
vlc_command.extend(self.vlc_duration(stream_args))
|
||||||
vlc_command.extend(self.vlc_loglevel())
|
vlc_command.extend(self.vlc_loglevel())
|
||||||
vlc_command.extend(["--sout"])
|
vlc_command.extend(["--sout"])
|
||||||
vlc_command.extend(self.transcode_profiles(stream_args))
|
vlc_command.extend(self.transcode_profiles(stream_args))
|
||||||
return vlc_command
|
return vlc_command
|
||||||
|
|
||||||
|
def vlc_headers(self, stream_args):
|
||||||
|
vlc_command = []
|
||||||
|
return vlc_command
|
||||||
|
|
||||||
def vlc_duration(self, stream_args):
|
def vlc_duration(self, stream_args):
|
||||||
vlc_command = []
|
vlc_command = []
|
||||||
if stream_args["duration"]:
|
if stream_args["duration"]:
|
||||||
@ -95,28 +100,28 @@ class VLC_Stream():
|
|||||||
"""
|
"""
|
||||||
vlc_command = []
|
vlc_command = []
|
||||||
|
|
||||||
if stream_args["transcode"]:
|
if stream_args["transcode_quality"]:
|
||||||
self.fhdhr.logger.info("Client requested a %s transcode for stream." % stream_args["transcode"])
|
self.fhdhr.logger.info("Client requested a %s transcode for stream." % stream_args["transcode_quality"])
|
||||||
stream_args["transcode"] = None
|
stream_args["transcode_quality"] = None
|
||||||
|
|
||||||
vlc_transcode_string = "#std{mux=ts,access=file,dst=-}"
|
vlc_transcode_string = "#std{mux=ts,access=file,dst=-}"
|
||||||
return [vlc_transcode_string]
|
return [vlc_transcode_string]
|
||||||
|
|
||||||
'#transcode{vcodec=mp2v,vb=4096,acodec=mp2a,ab=192,scale=1,channels=2,deinterlace}:std{access=file,mux=ts,dst=-"}'
|
'#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"]:
|
if not stream_args["transcode_quality"]:
|
||||||
vlc_command.extend([])
|
vlc_command.extend([])
|
||||||
elif stream_args["transcode"] == "heavy":
|
elif stream_args["transcode_quality"] == "heavy":
|
||||||
vlc_command.extend([])
|
vlc_command.extend([])
|
||||||
elif stream_args["transcode"] == "mobile":
|
elif stream_args["transcode_quality"] == "mobile":
|
||||||
vlc_command.extend([])
|
vlc_command.extend([])
|
||||||
elif stream_args["transcode"] == "internet720":
|
elif stream_args["transcode_quality"] == "internet720":
|
||||||
vlc_command.extend([])
|
vlc_command.extend([])
|
||||||
elif stream_args["transcode"] == "internet480":
|
elif stream_args["transcode_quality"] == "internet480":
|
||||||
vlc_command.extend([])
|
vlc_command.extend([])
|
||||||
elif stream_args["transcode"] == "internet360":
|
elif stream_args["transcode_quality"] == "internet360":
|
||||||
vlc_command.extend([])
|
vlc_command.extend([])
|
||||||
elif stream_args["transcode"] == "internet240":
|
elif stream_args["transcode_quality"] == "internet240":
|
||||||
vlc_command.extend([])
|
vlc_command.extend([])
|
||||||
|
|
||||||
return vlc_command
|
return vlc_command
|
||||||
|
|||||||
@ -11,7 +11,7 @@ from .rmg import fHDHR_RMG
|
|||||||
from .api import fHDHR_API
|
from .api import fHDHR_API
|
||||||
|
|
||||||
|
|
||||||
fHDHR_web_VERSION = "v0.8.0-beta"
|
fHDHR_web_VERSION = "v0.8.1-beta"
|
||||||
|
|
||||||
|
|
||||||
class fHDHR_HTTP_Server():
|
class fHDHR_HTTP_Server():
|
||||||
@ -219,7 +219,8 @@ class fHDHR_HTTP_Server():
|
|||||||
|
|
||||||
self.http = WSGIServer(self.fhdhr.api.address_tuple,
|
self.http = WSGIServer(self.fhdhr.api.address_tuple,
|
||||||
self.fhdhr.app.wsgi_app,
|
self.fhdhr.app.wsgi_app,
|
||||||
log=self.fhdhr.logger)
|
log=self.fhdhr.logger.logger,
|
||||||
|
error_log=self.fhdhr.logger.logger)
|
||||||
try:
|
try:
|
||||||
self.http.serve_forever()
|
self.http.serve_forever()
|
||||||
self.stop()
|
self.stop()
|
||||||
|
|||||||
@ -1,6 +1,9 @@
|
|||||||
from flask import Response, request, redirect
|
from flask import Response, request, redirect
|
||||||
import urllib.parse
|
import urllib.parse
|
||||||
import json
|
import json
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from fHDHR.tools import humanized_time, channel_sort
|
||||||
|
|
||||||
|
|
||||||
class EPG():
|
class EPG():
|
||||||
@ -39,7 +42,76 @@ class EPG():
|
|||||||
epgdict[chan_obj.number]["id"] = chan_obj.dict["origin_id"]
|
epgdict[chan_obj.number]["id"] = chan_obj.dict["origin_id"]
|
||||||
epgdict[chan_obj.number]["thumbnail"] = chan_obj.thumbnail
|
epgdict[chan_obj.number]["thumbnail"] = chan_obj.thumbnail
|
||||||
|
|
||||||
epg_json = json.dumps(epgdict, indent=4)
|
# Sort the channels
|
||||||
|
sorted_channel_list = channel_sort(list(epgdict.keys()))
|
||||||
|
sorted_chan_guide = {}
|
||||||
|
for channel in sorted_channel_list:
|
||||||
|
sorted_chan_guide[channel] = epgdict[channel]
|
||||||
|
|
||||||
|
epg_json = json.dumps(sorted_chan_guide, indent=4)
|
||||||
|
|
||||||
|
return Response(status=200,
|
||||||
|
response=epg_json,
|
||||||
|
mimetype='application/json')
|
||||||
|
|
||||||
|
elif method == "current":
|
||||||
|
|
||||||
|
nowtime = datetime.datetime.utcnow().timestamp()
|
||||||
|
|
||||||
|
chan_guide_list = []
|
||||||
|
|
||||||
|
whatson = self.fhdhr.device.epg.whats_on_allchans(source)
|
||||||
|
|
||||||
|
# Sort the channels
|
||||||
|
sorted_channel_list = channel_sort(list(whatson.keys()))
|
||||||
|
sorted_chan_guide = {}
|
||||||
|
for channel in sorted_channel_list:
|
||||||
|
sorted_chan_guide[channel] = whatson[channel]
|
||||||
|
|
||||||
|
for channel in list(sorted_chan_guide.keys()):
|
||||||
|
if sorted_chan_guide[channel]["listing"][0]["time_end"]:
|
||||||
|
remaining_time = humanized_time(sorted_chan_guide[channel]["listing"][0]["time_end"] - nowtime)
|
||||||
|
else:
|
||||||
|
remaining_time = "N/A"
|
||||||
|
|
||||||
|
chan_dict = {
|
||||||
|
"name": sorted_chan_guide[channel]["name"],
|
||||||
|
"number": sorted_chan_guide[channel]["number"],
|
||||||
|
"chan_thumbnail": sorted_chan_guide[channel]["thumbnail"],
|
||||||
|
"listing_title": sorted_chan_guide[channel]["listing"][0]["title"],
|
||||||
|
"listing_thumbnail": sorted_chan_guide[channel]["listing"][0]["thumbnail"],
|
||||||
|
"listing_description": sorted_chan_guide[channel]["listing"][0]["description"],
|
||||||
|
"listing_remaining_time": str(remaining_time)
|
||||||
|
}
|
||||||
|
|
||||||
|
for time_item in ["time_start", "time_end"]:
|
||||||
|
|
||||||
|
if not sorted_chan_guide[channel]["listing"][0][time_item]:
|
||||||
|
chan_dict["listing_%s" % time_item] = "N/A"
|
||||||
|
elif str(sorted_chan_guide[channel]["listing"][0][time_item]).endswith(tuple(["+0000", "+00:00"])):
|
||||||
|
chan_dict["listing_%s" % time_item] = str(sorted_chan_guide[channel]["listing"][0][time_item])
|
||||||
|
else:
|
||||||
|
chan_dict["listing_%s" % time_item] = str(datetime.datetime.fromtimestamp(sorted_chan_guide[channel]["listing"][0][time_item]))
|
||||||
|
|
||||||
|
if source in ["blocks", "origin", self.fhdhr.config.dict["main"]["dictpopname"]]:
|
||||||
|
chan_obj = self.fhdhr.device.channels.get_channel_obj("origin_id", sorted_chan_guide[channel]["id"])
|
||||||
|
|
||||||
|
chan_dict["name"] = chan_obj.dict["name"]
|
||||||
|
chan_dict["number"] = chan_obj.number
|
||||||
|
chan_dict["chan_thumbnail"] = chan_obj.thumbnail
|
||||||
|
chan_dict["enabled"] = chan_obj.dict["enabled"]
|
||||||
|
chan_dict["m3u_url"] = chan_obj.m3u_url
|
||||||
|
|
||||||
|
chan_dict["listing_thumbnail"] = chan_dict["listing_thumbnail"] or chan_obj.thumbnail
|
||||||
|
else:
|
||||||
|
if not chan_dict["listing_thumbnail"]:
|
||||||
|
chan_dict["listing_thumbnail"] = chan_dict["chan_thumbnail"]
|
||||||
|
if not chan_dict["listing_thumbnail"]:
|
||||||
|
chan_dict["listing_thumbnail"] = "/api/images?method=generate&type=channel&message=%s" % chan_dict["number"]
|
||||||
|
|
||||||
|
chan_guide_list.append(chan_dict)
|
||||||
|
|
||||||
|
epg_json = json.dumps(chan_guide_list, indent=4)
|
||||||
|
|
||||||
return Response(status=200,
|
return Response(status=200,
|
||||||
response=epg_json,
|
response=epg_json,
|
||||||
|
|||||||
@ -16,10 +16,6 @@ class Tuners():
|
|||||||
def __init__(self, fhdhr):
|
def __init__(self, fhdhr):
|
||||||
self.fhdhr = fhdhr
|
self.fhdhr = fhdhr
|
||||||
|
|
||||||
self.quality = self.fhdhr.config.dict["streaming"]["quality"]
|
|
||||||
if self.quality:
|
|
||||||
self.quality = str(self.quality).lower()
|
|
||||||
|
|
||||||
def __call__(self, *args):
|
def __call__(self, *args):
|
||||||
return self.get(*args)
|
return self.get(*args)
|
||||||
|
|
||||||
@ -29,7 +25,7 @@ class Tuners():
|
|||||||
|
|
||||||
accessed_url = request.args.get('accessed', default=request.url, type=str)
|
accessed_url = request.args.get('accessed', default=request.url, type=str)
|
||||||
|
|
||||||
method = request.args.get('method', default=self.fhdhr.config.dict["fhdhr"]["stream_type"], type=str)
|
method = request.args.get('method', default=self.fhdhr.config.dict["streaming"]["method"], type=str)
|
||||||
|
|
||||||
tuner_number = request.args.get('tuner', default=None, type=str)
|
tuner_number = request.args.get('tuner', default=None, type=str)
|
||||||
|
|
||||||
@ -56,12 +52,12 @@ class Tuners():
|
|||||||
|
|
||||||
duration = request.args.get('duration', default=0, type=int)
|
duration = request.args.get('duration', default=0, type=int)
|
||||||
|
|
||||||
transcode = request.args.get('transcode', default=self.quality, type=str)
|
transcode_quality = request.args.get('transcode', default=None, type=str)
|
||||||
valid_transcode_types = [
|
valid_transcode_types = [
|
||||||
None, "high", "medium", "low"
|
None, "high", "medium", "low"
|
||||||
"heavy", "mobile", "internet720", "internet480", "internet360", "internet240"
|
"heavy", "mobile", "internet720", "internet480", "internet360", "internet240"
|
||||||
]
|
]
|
||||||
if transcode not in valid_transcode_types:
|
if transcode_quality not in valid_transcode_types:
|
||||||
response = Response("Service Unavailable", status=503)
|
response = Response("Service Unavailable", status=503)
|
||||||
response.headers["X-fHDHR-Error"] = "802 - Unknown Transcode Profile"
|
response.headers["X-fHDHR-Error"] = "802 - Unknown Transcode Profile"
|
||||||
self.fhdhr.logger.error(response.headers["X-fHDHR-Error"])
|
self.fhdhr.logger.error(response.headers["X-fHDHR-Error"])
|
||||||
@ -71,7 +67,8 @@ class Tuners():
|
|||||||
"channel": channel_number,
|
"channel": channel_number,
|
||||||
"method": method,
|
"method": method,
|
||||||
"duration": duration,
|
"duration": duration,
|
||||||
"transcode": transcode,
|
"origin_quality": self.fhdhr.config.dict["streaming"]["origin_quality"],
|
||||||
|
"transcode_quality": transcode_quality,
|
||||||
"accessed": accessed_url,
|
"accessed": accessed_url,
|
||||||
"client": client_address,
|
"client": client_address,
|
||||||
"client_id": session["session_id"]
|
"client_id": session["session_id"]
|
||||||
|
|||||||
@ -14,7 +14,7 @@ class Auto():
|
|||||||
|
|
||||||
def get(self, channel, *args):
|
def get(self, channel, *args):
|
||||||
|
|
||||||
method = request.args.get('method', default=self.fhdhr.config.dict["fhdhr"]["stream_type"], type=str)
|
method = request.args.get('method', default=self.fhdhr.config.dict["streaming"]["method"], type=str)
|
||||||
|
|
||||||
redirect_url = "/api/tuners?method=%s" % (method)
|
redirect_url = "/api/tuners?method=%s" % (method)
|
||||||
|
|
||||||
@ -36,9 +36,9 @@ class Auto():
|
|||||||
if duration:
|
if duration:
|
||||||
redirect_url += "&duration=%s" % str(duration)
|
redirect_url += "&duration=%s" % str(duration)
|
||||||
|
|
||||||
transcode = request.args.get('transcode', default=None, type=str)
|
transcode_quality = request.args.get('transcode', default=None, type=str)
|
||||||
if transcode:
|
if transcode_quality:
|
||||||
redirect_url += "&transcode=%s" % str(transcode)
|
redirect_url += "&transcode=%s" % str(transcode_quality)
|
||||||
|
|
||||||
redirect_url += "&accessed=%s" % urllib.parse.quote(request.url)
|
redirect_url += "&accessed=%s" % urllib.parse.quote(request.url)
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,7 @@ class Tuner():
|
|||||||
|
|
||||||
def get(self, tuner_number, channel, *args):
|
def get(self, tuner_number, channel, *args):
|
||||||
|
|
||||||
method = request.args.get('method', default=self.fhdhr.config.dict["fhdhr"]["stream_type"], type=str)
|
method = request.args.get('method', default=self.fhdhr.config.dict["streaming"]["method"], type=str)
|
||||||
|
|
||||||
redirect_url = "/api/tuners?method=%s" % (method)
|
redirect_url = "/api/tuners?method=%s" % (method)
|
||||||
|
|
||||||
@ -38,9 +38,9 @@ class Tuner():
|
|||||||
if duration:
|
if duration:
|
||||||
redirect_url += "&duration=%s" % str(duration)
|
redirect_url += "&duration=%s" % str(duration)
|
||||||
|
|
||||||
transcode = request.args.get('transcode', default=None, type=str)
|
transcode_quality = request.args.get('transcode', default=None, type=str)
|
||||||
if transcode:
|
if transcode_quality:
|
||||||
redirect_url += "&transcode=%s" % str(transcode)
|
redirect_url += "&transcode=%s" % str(transcode_quality)
|
||||||
|
|
||||||
redirect_url += "&accessed=%s" % urllib.parse.quote(request.url)
|
redirect_url += "&accessed=%s" % urllib.parse.quote(request.url)
|
||||||
|
|
||||||
|
|||||||
@ -18,7 +18,7 @@ class RMG_Devices_DeviceKey_Media():
|
|||||||
param = request.args.get('method', default=None, type=str)
|
param = request.args.get('method', default=None, type=str)
|
||||||
self.fhdhr.logger.debug("param:%s" % param)
|
self.fhdhr.logger.debug("param:%s" % param)
|
||||||
|
|
||||||
method = self.fhdhr.config.dict["fhdhr"]["stream_type"]
|
method = self.fhdhr.config.dict["streaming"]["method"]
|
||||||
|
|
||||||
redirect_url = "/api/tuners?method=%s" % (method)
|
redirect_url = "/api/tuners?method=%s" % (method)
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user