Initial commit
This commit is contained in:
commit
be87dcb9a8
13
LICENSE
Normal file
13
LICENSE
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
Version 2, December 2004
|
||||||
|
|
||||||
|
Copyright (C) 2017 Sam Zick <Sam@deathbybandaid.net>
|
||||||
|
|
||||||
|
Everyone is permitted to copy and distribute verbatim or modified
|
||||||
|
copies of this license document, and changing it is allowed as long
|
||||||
|
as the name is changed.
|
||||||
|
|
||||||
|
DO WHAT THE FUCK YOU WANT TO PUBLIC LICENSE
|
||||||
|
TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
|
||||||
|
|
||||||
|
0. You just DO WHAT THE FUCK YOU WANT TO.
|
||||||
22
README.md
Normal file
22
README.md
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
<p align="center">fHDHR web Plugin Watch <img src="docs/images/logo.ico" alt="Logo"/></p>
|
||||||
|
|
||||||
|
|
||||||
|
Welcome to the world of streaming content as a DVR device! We use some fancy python here to achieve a system of:
|
||||||
|
|
||||||
|
**f**un
|
||||||
|
**H**ome
|
||||||
|
**D**istribution
|
||||||
|
**H**iatus
|
||||||
|
**R**ecreation
|
||||||
|
|
||||||
|
fHDHR is labeled as beta until we reach v1.0.0
|
||||||
|
|
||||||
|
Join us in `#fHDHR <irc://irc.freenode.net/#fHDHR>`_ on Freenode.
|
||||||
|
|
||||||
|
# Installation
|
||||||
|
|
||||||
|
1) Review Installation guide located at [Docs](https://github.com/fHDHR/fHDHR/blob/main/docs/README.md)
|
||||||
|
|
||||||
|
2) Insert this plugin into the `plugins` directory of fHDHR using `git clone` or downloading a release zip file.
|
||||||
|
|
||||||
|
3) Adjust your configuration file with the below settings:
|
||||||
5
plugin.json
Normal file
5
plugin.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"name":"webwatch",
|
||||||
|
"version":"v0.6.0-beta",
|
||||||
|
"type":"web"
|
||||||
|
}
|
||||||
0
requirements.txt
Normal file
0
requirements.txt
Normal file
173
stream/__init__.py
Normal file
173
stream/__init__.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
from fHDHR.exceptions import TunerError
|
||||||
|
|
||||||
|
|
||||||
|
def setup(plugin):
|
||||||
|
|
||||||
|
# Check config for ffmpeg path
|
||||||
|
ffmpeg_path = None
|
||||||
|
if plugin.config.dict["webwatch"]["ffmpeg_path"]:
|
||||||
|
# verify path is valid
|
||||||
|
if os.path.isfile(plugin.config.dict["webwatch"]["ffmpeg_path"]):
|
||||||
|
ffmpeg_path = plugin.config.dict["webwatch"]["ffmpeg_path"]
|
||||||
|
else:
|
||||||
|
plugin.logger.warning("Failed to find ffmpeg at %s." % plugin.config.dict["webwatch"]["ffmpeg_path"])
|
||||||
|
|
||||||
|
if not ffmpeg_path:
|
||||||
|
plugin.logger.info("Attempting to find ffmpeg in PATH.")
|
||||||
|
if plugin.config.internal["versions"]["Operating System"]["version"] in ["Linux", "Darwin"]:
|
||||||
|
find_ffmpeg_command = ["which", "ffmpeg"]
|
||||||
|
elif plugin.config.internal["versions"]["Operating System"]["version"] in ["Windows"]:
|
||||||
|
find_ffmpeg_command = ["where", "ffmpeg"]
|
||||||
|
|
||||||
|
ffmpeg_proc = subprocess.Popen(find_ffmpeg_command, stdout=subprocess.PIPE)
|
||||||
|
ffmpeg_path = ffmpeg_proc.stdout.read().decode().strip("\n")
|
||||||
|
ffmpeg_proc.terminate()
|
||||||
|
ffmpeg_proc.communicate()
|
||||||
|
ffmpeg_proc.kill()
|
||||||
|
if not ffmpeg_path:
|
||||||
|
ffmpeg_path = None
|
||||||
|
elif ffmpeg_path.isspace():
|
||||||
|
ffmpeg_path = None
|
||||||
|
|
||||||
|
if ffmpeg_path:
|
||||||
|
plugin.config.dict["webwatch"]["ffmpeg_path"] = ffmpeg_path
|
||||||
|
|
||||||
|
if ffmpeg_path:
|
||||||
|
ffmpeg_command = [ffmpeg_path, "-version", "pipe:stdout"]
|
||||||
|
try:
|
||||||
|
ffmpeg_proc = subprocess.Popen(ffmpeg_command, stdout=subprocess.PIPE)
|
||||||
|
ffmpeg_version = ffmpeg_proc.stdout.read().decode().split("version ")[1].split(" ")[0]
|
||||||
|
except FileNotFoundError:
|
||||||
|
ffmpeg_version = None
|
||||||
|
except PermissionError:
|
||||||
|
ffmpeg_version = None
|
||||||
|
finally:
|
||||||
|
ffmpeg_proc.terminate()
|
||||||
|
ffmpeg_proc.communicate()
|
||||||
|
ffmpeg_proc.kill()
|
||||||
|
|
||||||
|
if not ffmpeg_version:
|
||||||
|
ffmpeg_version = "Missing"
|
||||||
|
plugin.logger.warning("Failed to find ffmpeg.")
|
||||||
|
|
||||||
|
plugin.config.register_version("ffmpeg", ffmpeg_version, "env")
|
||||||
|
|
||||||
|
|
||||||
|
class Plugin_OBJ():
|
||||||
|
|
||||||
|
def __init__(self, fhdhr, plugin_utils, stream_args, tuner):
|
||||||
|
self.fhdhr = fhdhr
|
||||||
|
self.plugin_utils = plugin_utils
|
||||||
|
self.stream_args = stream_args
|
||||||
|
self.tuner = tuner
|
||||||
|
|
||||||
|
if self.plugin_utils.config.internal["versions"]["ffmpeg"]["version"] == "Missing":
|
||||||
|
raise TunerError("806 - Tune Failed: FFMPEG Missing")
|
||||||
|
|
||||||
|
self.bytes_per_read = 1024
|
||||||
|
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():
|
||||||
|
|
||||||
|
chunk = ffmpeg_proc.stdout.read(self.bytes_per_read)
|
||||||
|
if not chunk:
|
||||||
|
break
|
||||||
|
# raise TunerError("807 - No Video Data")
|
||||||
|
yield chunk
|
||||||
|
chunk_size = int(sys.getsizeof(chunk))
|
||||||
|
self.tuner.add_downloaded_size(chunk_size)
|
||||||
|
self.plugin_utils.logger.info("Connection Closed: Tuner Lock Removed")
|
||||||
|
|
||||||
|
except GeneratorExit:
|
||||||
|
self.plugin_utils.logger.info("Connection Closed.")
|
||||||
|
except Exception as e:
|
||||||
|
self.plugin_utils.logger.info("Connection Closed: %s" % e)
|
||||||
|
finally:
|
||||||
|
ffmpeg_proc.terminate()
|
||||||
|
ffmpeg_proc.communicate()
|
||||||
|
ffmpeg_proc.kill()
|
||||||
|
self.plugin_utils.logger.info("Connection Closed: Tuner Lock Removed")
|
||||||
|
if hasattr(self.fhdhr.origins.origins_dict[self.tuner.origin], "close_stream"):
|
||||||
|
self.fhdhr.origins.origins_dict[self.tuner.origin].close_stream(self.tuner.number, self.stream_args)
|
||||||
|
self.tuner.close()
|
||||||
|
# raise TunerError("806 - Tune Failed")
|
||||||
|
|
||||||
|
return generate()
|
||||||
|
|
||||||
|
def ffmpeg_command_assemble(self, stream_args):
|
||||||
|
ffmpeg_command = [
|
||||||
|
self.plugin_utils.config.dict["webwatch"]["ffmpeg_path"],
|
||||||
|
"-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.transcode_profiles(stream_args))
|
||||||
|
ffmpeg_command.extend(self.ffmpeg_loglevel())
|
||||||
|
ffmpeg_command.extend(["pipe:stdout"])
|
||||||
|
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):
|
||||||
|
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.plugin_utils.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):
|
||||||
|
|
||||||
|
ffmpeg_command = []
|
||||||
|
ffmpeg_command.extend([
|
||||||
|
"-c:v", "libvpx",
|
||||||
|
"-c:a", "libvorbis",
|
||||||
|
"-f", "webm"
|
||||||
|
])
|
||||||
|
|
||||||
|
return ffmpeg_command
|
||||||
3
stream/plugin.json
Normal file
3
stream/plugin.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type":"alt_stream"
|
||||||
|
}
|
||||||
13
web/__init__.py
Normal file
13
web/__init__.py
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
|
||||||
|
from .webwatch_html import Watch_HTML
|
||||||
|
from .webwatch_api import WebWatch_Tuner
|
||||||
|
|
||||||
|
|
||||||
|
class Plugin_OBJ():
|
||||||
|
|
||||||
|
def __init__(self, fhdhr, plugin_utils):
|
||||||
|
self.fhdhr = fhdhr
|
||||||
|
self.plugin_utils = plugin_utils
|
||||||
|
|
||||||
|
self.webwatch_html = Watch_HTML(fhdhr, plugin_utils)
|
||||||
|
self.webwatch_api = WebWatch_Tuner(fhdhr, plugin_utils)
|
||||||
3
web/plugin.json
Normal file
3
web/plugin.json
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"type":"web"
|
||||||
|
}
|
||||||
11
web/webwatch.html
Normal file
11
web/webwatch.html
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{% extends "base.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
|
||||||
|
<h4 style="text-align: center;">fHDHR Channel Watch</h4>
|
||||||
|
<video width='50%' height='40%' autoplay controls>
|
||||||
|
<source src="{{ watch_url }}"> Your browser does not support the video tag.
|
||||||
|
</video>
|
||||||
|
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
124
web/webwatch_api.py
Normal file
124
web/webwatch_api.py
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
from flask import Response, request, redirect, abort, session
|
||||||
|
import urllib.parse
|
||||||
|
|
||||||
|
from fHDHR.exceptions import TunerError
|
||||||
|
|
||||||
|
|
||||||
|
class WebWatch_Tuner():
|
||||||
|
endpoints = ["/api/webwatch"]
|
||||||
|
endpoint_name = "api_webwatch"
|
||||||
|
endpoint_methods = ["GET", "POST"]
|
||||||
|
|
||||||
|
def __init__(self, fhdhr, plugin_utils):
|
||||||
|
self.fhdhr = fhdhr
|
||||||
|
|
||||||
|
def __call__(self, *args):
|
||||||
|
return self.get(*args)
|
||||||
|
|
||||||
|
def get(self, *args):
|
||||||
|
|
||||||
|
client_address = request.remote_addr
|
||||||
|
|
||||||
|
accessed_url = request.args.get('accessed', default=request.url, type=str)
|
||||||
|
|
||||||
|
method = request.args.get('method', default="stream", type=str)
|
||||||
|
|
||||||
|
redirect_url = request.args.get('redirect', default=None, type=str)
|
||||||
|
|
||||||
|
origin_methods = self.fhdhr.origins.valid_origins
|
||||||
|
origin = request.args.get('origin', default=None, type=str)
|
||||||
|
if origin and origin not in origin_methods:
|
||||||
|
return "%s Invalid channels origin" % origin
|
||||||
|
|
||||||
|
if method == "stream":
|
||||||
|
|
||||||
|
channel_number = request.args.get('channel', None, type=str)
|
||||||
|
if not channel_number:
|
||||||
|
return "Missing Channel"
|
||||||
|
|
||||||
|
if origin:
|
||||||
|
|
||||||
|
if str(channel_number) in [str(x) for x in self.fhdhr.device.channels.get_channel_list("number", origin)]:
|
||||||
|
chan_obj = self.fhdhr.device.channels.get_channel_obj("number", channel_number, origin)
|
||||||
|
elif str(channel_number) in [str(x) for x in self.fhdhr.device.channels.get_channel_list("id", origin)]:
|
||||||
|
chan_obj = self.fhdhr.device.channels.get_channel_obj("id", channel_number, origin)
|
||||||
|
else:
|
||||||
|
response = Response("Not Found", status=404)
|
||||||
|
response.headers["X-fHDHR-Error"] = "801 - Unknown Channel"
|
||||||
|
self.fhdhr.logger.error(response.headers["X-fHDHR-Error"])
|
||||||
|
abort(response)
|
||||||
|
|
||||||
|
else:
|
||||||
|
|
||||||
|
if str(channel_number) in [str(x) for x in self.fhdhr.device.channels.get_channel_list("id")]:
|
||||||
|
chan_obj = self.fhdhr.device.channels.get_channel_obj("id", channel_number)
|
||||||
|
else:
|
||||||
|
response = Response("Not Found", status=404)
|
||||||
|
response.headers["X-fHDHR-Error"] = "801 - Unknown Channel"
|
||||||
|
self.fhdhr.logger.error(response.headers["X-fHDHR-Error"])
|
||||||
|
abort(response)
|
||||||
|
|
||||||
|
if not chan_obj.dict["enabled"]:
|
||||||
|
response = Response("Service Unavailable", status=503)
|
||||||
|
response.headers["X-fHDHR-Error"] = str("806 - Tune Failed")
|
||||||
|
self.fhdhr.logger.error(response.headers["X-fHDHR-Error"])
|
||||||
|
abort(response)
|
||||||
|
|
||||||
|
origin = chan_obj.origin
|
||||||
|
channel_number = chan_obj.number
|
||||||
|
|
||||||
|
duration = request.args.get('duration', default=0, type=int)
|
||||||
|
|
||||||
|
stream_args = {
|
||||||
|
"channel": channel_number,
|
||||||
|
"origin": origin,
|
||||||
|
"method": "webwatch",
|
||||||
|
"duration": duration,
|
||||||
|
"origin_quality": self.fhdhr.config.dict["streaming"]["origin_quality"],
|
||||||
|
"transcode_quality": self.fhdhr.config.dict["streaming"]["transcode_quality"],
|
||||||
|
"accessed": accessed_url,
|
||||||
|
"client": client_address,
|
||||||
|
"client_id": session["session_id"]
|
||||||
|
}
|
||||||
|
|
||||||
|
try:
|
||||||
|
tunernum = self.fhdhr.device.tuners.first_available(origin, channel_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)
|
||||||
|
self.fhdhr.logger.error(response.headers["X-fHDHR-Error"])
|
||||||
|
abort(response)
|
||||||
|
|
||||||
|
tuner = self.fhdhr.device.tuners.tuners[origin][str(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 %s channel %s was rejected due to %s"
|
||||||
|
% (origin, stream_args["method"], str(stream_args["channel"]), str(e)))
|
||||||
|
response = Response("Service Unavailable", status=503)
|
||||||
|
response.headers["X-fHDHR-Error"] = str(e)
|
||||||
|
self.fhdhr.logger.error(response.headers["X-fHDHR-Error"])
|
||||||
|
tuner.close()
|
||||||
|
abort(response)
|
||||||
|
|
||||||
|
self.fhdhr.logger.info("%s Tuner #%s to be used for stream." % (origin, tunernum))
|
||||||
|
tuner.set_status(stream_args)
|
||||||
|
session["tuner_used"] = tunernum
|
||||||
|
|
||||||
|
try:
|
||||||
|
stream = tuner.get_stream(stream_args, tuner)
|
||||||
|
except TunerError as e:
|
||||||
|
response.headers["X-fHDHR-Error"] = str(e)
|
||||||
|
self.fhdhr.logger.error(response.headers["X-fHDHR-Error"])
|
||||||
|
tuner.close()
|
||||||
|
abort(response)
|
||||||
|
|
||||||
|
return Response(stream.get())
|
||||||
|
|
||||||
|
if redirect_url:
|
||||||
|
return redirect("%s?retmessage=%s" % (redirect_url, urllib.parse.quote("%s Success" % method)))
|
||||||
|
else:
|
||||||
|
return "%s Success" % method
|
||||||
30
web/webwatch_html.py
Normal file
30
web/webwatch_html.py
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
from flask import request, render_template_string, session
|
||||||
|
import pathlib
|
||||||
|
from io import StringIO
|
||||||
|
|
||||||
|
|
||||||
|
class Watch_HTML():
|
||||||
|
endpoints = ["/webwatch", "/webwatch.html"]
|
||||||
|
endpoint_name = "page_webwatch_html"
|
||||||
|
endpoint_access_level = 0
|
||||||
|
pretty_name = "Watch"
|
||||||
|
endpoint_category = "pages"
|
||||||
|
|
||||||
|
def __init__(self, fhdhr, plugin_utils):
|
||||||
|
self.fhdhr = fhdhr
|
||||||
|
|
||||||
|
self.template_file = pathlib.Path(plugin_utils.config.dict["plugin_web_paths"][plugin_utils.namespace]["path"]).joinpath('webwatch.html')
|
||||||
|
self.template = StringIO()
|
||||||
|
self.template.write(open(self.template_file).read())
|
||||||
|
|
||||||
|
def __call__(self, *args):
|
||||||
|
return self.get(*args)
|
||||||
|
|
||||||
|
def get(self, *args):
|
||||||
|
|
||||||
|
origin = self.fhdhr.origins.valid_origins[0]
|
||||||
|
channel_id = [x["id"] for x in self.fhdhr.device.channels.get_channels(origin)][0]
|
||||||
|
|
||||||
|
watch_url = '/api/webwatch?method=stream&channel=%s&origin=%s' % (channel_id, origin)
|
||||||
|
|
||||||
|
return render_template_string(self.template.getvalue(), request=request, session=session, fhdhr=self.fhdhr, watch_url=watch_url)
|
||||||
9
webwatch_conf.json
Normal file
9
webwatch_conf.json
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"webwatch":{
|
||||||
|
"ffmpeg_path":{
|
||||||
|
"value": "none",
|
||||||
|
"config_file": true,
|
||||||
|
"config_web": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user