added stream structural requirements, updated context menu logic
This commit is contained in:
parent
353acf7ae6
commit
fddcc3a266
@ -4,6 +4,7 @@ import builtins
|
|||||||
import threading
|
import threading
|
||||||
import re
|
import re
|
||||||
import secrets
|
import secrets
|
||||||
|
import subprocess
|
||||||
|
|
||||||
# Lib imports
|
# Lib imports
|
||||||
from flask import session
|
from flask import session
|
||||||
@ -14,6 +15,9 @@ from core.utils import Logger
|
|||||||
from core.utils import MessageHandler # Get simple message processor
|
from core.utils import MessageHandler # Get simple message processor
|
||||||
|
|
||||||
|
|
||||||
|
class BuiltinsException(Exception):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
# NOTE: Threads WILL NOT die with parent's destruction.
|
# NOTE: Threads WILL NOT die with parent's destruction.
|
||||||
def threaded_wrapper(fn):
|
def threaded_wrapper(fn):
|
||||||
@ -42,22 +46,54 @@ def _get_file_size(file):
|
|||||||
|
|
||||||
# NOTE: Just reminding myself we can add to builtins two different ways...
|
# NOTE: Just reminding myself we can add to builtins two different ways...
|
||||||
# __builtins__.update({"event_system": Builtins()})
|
# __builtins__.update({"event_system": Builtins()})
|
||||||
builtins.app_name = "WebFM"
|
builtins.app_name = "WebFM"
|
||||||
builtins.threaded = threaded_wrapper
|
builtins.threaded = threaded_wrapper
|
||||||
builtins.daemon_threaded = daemon_threaded_wrapper
|
builtins.daemon_threaded = daemon_threaded_wrapper
|
||||||
builtins.sizeof_fmt = sizeof_fmt_def
|
builtins.sizeof_fmt = sizeof_fmt_def
|
||||||
builtins.get_file_size = _get_file_size
|
builtins.get_file_size = _get_file_size
|
||||||
builtins.ROOT_FILE_PTH = os.path.dirname(os.path.realpath(__file__))
|
builtins.ROOT_FILE_PTH = os.path.dirname(os.path.realpath(__file__))
|
||||||
builtins.BG_IMGS_PATH = ROOT_FILE_PTH + "/static/imgs/backgrounds/"
|
builtins.BG_IMGS_PATH = ROOT_FILE_PTH + "/static/imgs/backgrounds/"
|
||||||
builtins.BG_FILE_TYPE = (".webm", ".mp4", ".gif", ".jpg", ".png", ".webp")
|
builtins.BG_FILE_TYPE = (".webm", ".mp4", ".gif", ".jpg", ".png", ".webp")
|
||||||
builtins.valid_fname_pat = re.compile(r"[a-z0-9A-Z-_\[\]\(\)\| ]{4,20}")
|
builtins.valid_fname_pat = re.compile(r"[a-z0-9A-Z-_\[\]\(\)\| ]{4,20}")
|
||||||
builtins.logger = Logger().get_logger()
|
builtins.logger = Logger().get_logger()
|
||||||
builtins.json_message = MessageHandler()
|
builtins.json_message = MessageHandler()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
# NOTE: Need threads defined before instantiating
|
||||||
|
|
||||||
|
def _start_rtsp_server():
|
||||||
|
PATH = f"{ROOT_FILE_PTH}/utils/rtsp-server"
|
||||||
|
RAMFS = "/dev/shm/webfm"
|
||||||
|
SYMLINK = f"{PATH}/m3u8_stream_files"
|
||||||
|
|
||||||
|
if not os.path.exists(PATH) or not os.path.exists(f"{PATH}/rtsp-simple-server"):
|
||||||
|
msg = f"\n\nAlert: Reference --> https://github.com/aler9/rtsp-simple-server/releases" + \
|
||||||
|
f"\nPlease insure {PATH} exists and rtsp-simple-server binary is there.\n\n"
|
||||||
|
raise BuiltinsException(msg)
|
||||||
|
|
||||||
|
if not os.path.exists(RAMFS):
|
||||||
|
os.mkdir(RAMFS)
|
||||||
|
if not os.path.exists(f"{PATH}/m3u8_stream_files"):
|
||||||
|
os.symlink(RAMFS, SYMLINK)
|
||||||
|
|
||||||
|
@daemon_threaded
|
||||||
|
def _start_rtsp_server_threaded():
|
||||||
|
os.chdir(PATH)
|
||||||
|
command = ["./rtsp-simple-server", "./rtsp-simple-server.yml"]
|
||||||
|
process = subprocess.Popen(command)
|
||||||
|
process.wait()
|
||||||
|
|
||||||
|
_start_rtsp_server_threaded()
|
||||||
|
|
||||||
|
_start_rtsp_server()
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# NOTE: Need threads defined befor instantiating
|
|
||||||
from core.utils.shellfm.windows.controller import WindowController # Get file manager controller
|
from core.utils.shellfm.windows.controller import WindowController # Get file manager controller
|
||||||
window_controllers = {}
|
window_controllers = {}
|
||||||
|
processes = {}
|
||||||
|
|
||||||
def _get_view():
|
def _get_view():
|
||||||
controller = None
|
controller = None
|
||||||
try:
|
try:
|
||||||
@ -85,11 +121,64 @@ def _get_view():
|
|||||||
view.logger = logger
|
view.logger = logger
|
||||||
|
|
||||||
session['win_controller_id'] = id
|
session['win_controller_id'] = id
|
||||||
window_controllers.update( {id: controller } )
|
window_controllers.update( {id: controller} )
|
||||||
controller = window_controllers[ session["win_controller_id"] ].get_window_by_index(0).get_tab_by_index(0)
|
controller = window_controllers[ session["win_controller_id"] ].get_window_by_index(0).get_tab_by_index(0)
|
||||||
|
|
||||||
return controller
|
return controller
|
||||||
|
|
||||||
|
|
||||||
|
def _get_stream(video_path=None, stream_target=None):
|
||||||
|
process = None
|
||||||
|
try:
|
||||||
|
window = window_controllers[ session["win_controller_id"] ].get_window_by_index(0)
|
||||||
|
tab = window.get_tab_by_index(0)
|
||||||
|
id = f"{window.get_id()}{tab.get_id()}"
|
||||||
|
process = processes[id]
|
||||||
|
except Exception as e:
|
||||||
|
if video_path and stream_target:
|
||||||
|
# NOTE: Yes, technically we should check if cuda is supported.
|
||||||
|
# Yes, the process probably should give us info we can process when failure occures.
|
||||||
|
command = [
|
||||||
|
"ffmpeg", "-nostdin", "-fflags", "+genpts", "-hwaccel", "cuda",
|
||||||
|
"-stream_loop", "1", "-i", video_path, "-strict", "experimental",
|
||||||
|
"-vcodec", "copy", "-acodec", "copy", "-f", "rtsp",
|
||||||
|
"-rtsp_transport", "udp", stream_target
|
||||||
|
]
|
||||||
|
|
||||||
builtins.get_view = _get_view
|
# command = [
|
||||||
|
# "ffmpeg", "-nostdin", "-fflags", "+genpts", "-i", video_path,
|
||||||
|
# "-strict", "experimental", "-f",
|
||||||
|
# "rtsp", "-rtsp_transport", "udp", stream_target
|
||||||
|
# ]
|
||||||
|
|
||||||
|
proc = subprocess.Popen(command, shell=False, stdin=None, stdout=None, stderr=None)
|
||||||
|
window = window_controllers[ session["win_controller_id"] ].get_window_by_index(0)
|
||||||
|
tab = window.get_tab_by_index(0)
|
||||||
|
id = f"{window.get_id()}{tab.get_id()}"
|
||||||
|
|
||||||
|
processes.update( {id: proc} )
|
||||||
|
process = processes[id]
|
||||||
|
|
||||||
|
return process
|
||||||
|
|
||||||
|
def _kill_stream(process):
|
||||||
|
try:
|
||||||
|
if process.poll() == None:
|
||||||
|
process.terminate()
|
||||||
|
while process.poll() == None:
|
||||||
|
...
|
||||||
|
|
||||||
|
window = window_controllers[ session["win_controller_id"] ].get_window_by_index(0)
|
||||||
|
tab = window.get_tab_by_index(0)
|
||||||
|
id = f"{window.get_id()}{tab.get_id()}"
|
||||||
|
del processes[id]
|
||||||
|
except Exception as e:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
builtins.get_view = _get_view
|
||||||
|
builtins.get_stream = _get_stream
|
||||||
|
builtins.kill_stream = _kill_stream
|
||||||
|
@ -1,4 +1,6 @@
|
|||||||
# Python imports
|
# Python imports
|
||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
# Lib imports
|
# Lib imports
|
||||||
from flask import Flask
|
from flask import Flask
|
||||||
@ -11,7 +13,6 @@ from flask_login import login_user
|
|||||||
from flask_login import logout_user
|
from flask_login import logout_user
|
||||||
from flask_login import LoginManager
|
from flask_login import LoginManager
|
||||||
|
|
||||||
|
|
||||||
app = Flask(__name__)
|
app = Flask(__name__)
|
||||||
app.config.from_object("core.config.ProductionConfig")
|
app.config.from_object("core.config.ProductionConfig")
|
||||||
# app.config.from_object("core.config.DevelopmentConfig")
|
# app.config.from_object("core.config.DevelopmentConfig")
|
||||||
@ -40,6 +41,7 @@ app.jinja_env.globals['oidc_isAdmin'] = oidc_isAdmin
|
|||||||
app.jinja_env.globals['TITLE'] = app.config["TITLE"]
|
app.jinja_env.globals['TITLE'] = app.config["TITLE"]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
from core.models import db
|
from core.models import db
|
||||||
from core.models import User
|
from core.models import User
|
||||||
from core.models import Favorites
|
from core.models import Favorites
|
||||||
|
@ -41,6 +41,8 @@ def delete_item(_hash = None):
|
|||||||
msg = "[Error] Unable to delete the file/folder...."
|
msg = "[Error] Unable to delete the file/folder...."
|
||||||
return json_message.create("danger", msg)
|
return json_message.create("danger", msg)
|
||||||
|
|
||||||
|
msg = "Can't manage the request type..."
|
||||||
|
return json_message.create("danger", msg)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/create/<_type>', methods=['GET', 'POST'])
|
@app.route('/api/create/<_type>', methods=['GET', 'POST'])
|
||||||
@ -77,9 +79,9 @@ def create_item(_type = None):
|
|||||||
|
|
||||||
msg = "[Success] created the file/dir..."
|
msg = "[Success] created the file/dir..."
|
||||||
return json_message.create("success", msg)
|
return json_message.create("success", msg)
|
||||||
else:
|
|
||||||
msg = "Can't manage the request type..."
|
msg = "Can't manage the request type..."
|
||||||
return json_message.create("danger", msg)
|
return json_message.create("danger", msg)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/upload', methods=['GET', 'POST'])
|
@app.route('/api/upload', methods=['GET', 'POST'])
|
||||||
@ -109,6 +111,6 @@ def upload():
|
|||||||
|
|
||||||
msg = "[Success] Uploaded file(s)..."
|
msg = "[Success] Uploaded file(s)..."
|
||||||
return json_message.create("success", msg)
|
return json_message.create("success", msg)
|
||||||
else:
|
|
||||||
msg = "Can't manage the request type..."
|
msg = "Can't manage the request type..."
|
||||||
return json_message.create("danger", msg)
|
return json_message.create("danger", msg)
|
||||||
|
@ -11,7 +11,7 @@ from core import Favorites # Get from __init__
|
|||||||
|
|
||||||
|
|
||||||
@app.route('/api/list-favorites', methods=['GET', 'POST'])
|
@app.route('/api/list-favorites', methods=['GET', 'POST'])
|
||||||
def listFavorites():
|
def list_favorites():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
list = db.session.query(Favorites).all()
|
list = db.session.query(Favorites).all()
|
||||||
faves = []
|
faves = []
|
||||||
@ -19,12 +19,12 @@ def listFavorites():
|
|||||||
faves.append([fave.link, fave.id])
|
faves.append([fave.link, fave.id])
|
||||||
|
|
||||||
return json_message.faves_list(faves)
|
return json_message.faves_list(faves)
|
||||||
else:
|
|
||||||
msg = "Can't manage the request type..."
|
msg = "Can't manage the request type..."
|
||||||
return json_message.create("danger", msg)
|
return json_message.create("danger", msg)
|
||||||
|
|
||||||
@app.route('/api/load-favorite/<_id>', methods=['GET', 'POST'])
|
@app.route('/api/load-favorite/<_id>', methods=['GET', 'POST'])
|
||||||
def loadFavorite(_id):
|
def load_favorite(_id):
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
try:
|
try:
|
||||||
ID = int(_id)
|
ID = int(_id)
|
||||||
@ -36,13 +36,13 @@ def loadFavorite(_id):
|
|||||||
print(repr(e))
|
print(repr(e))
|
||||||
msg = "Incorrect Favorites ID..."
|
msg = "Incorrect Favorites ID..."
|
||||||
return json_message.create("danger", msg)
|
return json_message.create("danger", msg)
|
||||||
else:
|
|
||||||
msg = "Can't manage the request type..."
|
msg = "Can't manage the request type..."
|
||||||
return json_message.create("danger", msg)
|
return json_message.create("danger", msg)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/manage-favorites/<_action>', methods=['GET', 'POST'])
|
@app.route('/api/manage-favorites/<_action>', methods=['GET', 'POST'])
|
||||||
def manageFavorites(_action):
|
def manage_favorites(_action):
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
ACTION = _action.strip()
|
ACTION = _action.strip()
|
||||||
view = get_view()
|
view = get_view()
|
||||||
@ -62,6 +62,6 @@ def manageFavorites(_action):
|
|||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return json_message.create("success", msg)
|
return json_message.create("success", msg)
|
||||||
else:
|
|
||||||
msg = "Can't manage the request type..."
|
msg = "Can't manage the request type..."
|
||||||
return json_message.create("danger", msg)
|
return json_message.create("danger", msg)
|
||||||
|
@ -18,7 +18,7 @@ tmdb = scraper.get_tmdb_scraper()
|
|||||||
|
|
||||||
|
|
||||||
@app.route('/api/get-background-poster-trailer', methods=['GET', 'POST'])
|
@app.route('/api/get-background-poster-trailer', methods=['GET', 'POST'])
|
||||||
def getPosterTrailer():
|
def get_poster_trailer():
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
info = {}
|
info = {}
|
||||||
view = get_view()
|
view = get_view()
|
||||||
@ -69,6 +69,8 @@ def getPosterTrailer():
|
|||||||
|
|
||||||
return info
|
return info
|
||||||
|
|
||||||
|
msg = "Can't manage the request type..."
|
||||||
|
return json_message.create("danger", msg)
|
||||||
|
|
||||||
@app.route('/backgrounds', methods=['GET', 'POST'])
|
@app.route('/backgrounds', methods=['GET', 'POST'])
|
||||||
def backgrounds():
|
def backgrounds():
|
||||||
@ -81,10 +83,10 @@ def backgrounds():
|
|||||||
return json_message.backgrounds(files)
|
return json_message.backgrounds(files)
|
||||||
|
|
||||||
@app.route('/api/get-thumbnails', methods=['GET', 'POST'])
|
@app.route('/api/get-thumbnails', methods=['GET', 'POST'])
|
||||||
def getThumbnails():
|
def get_thumbnails():
|
||||||
if request.method == 'GET':
|
if request.method == 'GET':
|
||||||
view = get_view()
|
view = get_view()
|
||||||
return json_message.thumbnails( view.get_video_icons() )
|
return json_message.thumbnails( view.get_video_icons() )
|
||||||
else:
|
|
||||||
msg = "Can't manage the request type..."
|
msg = "Can't manage the request type..."
|
||||||
return json_message.create("danger", msg)
|
return json_message.create("danger", msg)
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
# Python imports
|
# Python imports
|
||||||
import os
|
import os
|
||||||
|
# import subprocess
|
||||||
|
import uuid
|
||||||
|
|
||||||
# Lib imports
|
# Lib imports
|
||||||
from flask import redirect
|
from flask import redirect
|
||||||
@ -30,7 +32,7 @@ def home():
|
|||||||
|
|
||||||
|
|
||||||
@app.route('/api/list-files/<_hash>', methods=['GET', 'POST'])
|
@app.route('/api/list-files/<_hash>', methods=['GET', 'POST'])
|
||||||
def listFiles(_hash = None):
|
def list_files(_hash = None):
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
view = get_view()
|
view = get_view()
|
||||||
dot_dots = view.get_dot_dots()
|
dot_dots = view.get_dot_dots()
|
||||||
@ -64,13 +66,13 @@ def listFiles(_hash = None):
|
|||||||
in_fave = "true" if fave else "false"
|
in_fave = "true" if fave else "false"
|
||||||
files.update({'in_fave': in_fave})
|
files.update({'in_fave': in_fave})
|
||||||
return files
|
return files
|
||||||
else:
|
|
||||||
msg = "Can't manage the request type..."
|
msg = "Can't manage the request type..."
|
||||||
return json_message.create("danger", msg)
|
return json_message.create("danger", msg)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/file-manager-action/<_type>/<_hash>', methods=['GET', 'POST'])
|
@app.route('/api/file-manager-action/<_type>/<_hash>', methods=['GET', 'POST'])
|
||||||
def fileManagerAction(_type, _hash = None):
|
def file_manager_action(_type, _hash = None):
|
||||||
view = get_view()
|
view = get_view()
|
||||||
|
|
||||||
if _type == "reset-path" and _hash == "None":
|
if _type == "reset-path" and _hash == "None":
|
||||||
@ -86,20 +88,39 @@ def fileManagerAction(_type, _hash = None):
|
|||||||
if _type == "files":
|
if _type == "files":
|
||||||
logger.debug(f"Downloading:\n\tDirectory: {folder}\n\tFile: {file}")
|
logger.debug(f"Downloading:\n\tDirectory: {folder}\n\tFile: {file}")
|
||||||
return send_from_directory(directory=folder, filename=file)
|
return send_from_directory(directory=folder, filename=file)
|
||||||
|
|
||||||
if _type == "remux":
|
if _type == "remux":
|
||||||
# NOTE: Need to actually implimint a websocket to communicate back to client that remux has completed.
|
# NOTE: Need to actually implimint a websocket to communicate back to client that remux has completed.
|
||||||
# As is, the remux thread hangs until completion and client tries waiting until server reaches connection timeout.
|
# As is, the remux thread hangs until completion and client tries waiting until server reaches connection timeout.
|
||||||
# I.E....this is stupid but for now works better than nothing
|
# I.E....this is stupid but for now works better than nothing
|
||||||
good_result = view.remux_video(_hash, fpath)
|
good_result = view.remux_video(_hash, fpath)
|
||||||
if good_result:
|
if not good_result:
|
||||||
return '{"path":"static/remuxs/' + _hash + '.mp4"}'
|
|
||||||
else:
|
|
||||||
msg = "Remuxing: Remux failed or took too long; please, refresh the page and try again..."
|
msg = "Remuxing: Remux failed or took too long; please, refresh the page and try again..."
|
||||||
return json_message.create("success", msg)
|
return json_message.create("warning", msg)
|
||||||
|
|
||||||
if _type == "remux":
|
return '{"path":"static/remuxs/' + _hash + '.mp4"}'
|
||||||
stream_target = view.remux_video(_hash, fpath)
|
|
||||||
|
|
||||||
|
if _type == "stream":
|
||||||
|
process = get_stream()
|
||||||
|
if process:
|
||||||
|
if not kill_stream(process):
|
||||||
|
msg = "Couldn't stop an existing stream!"
|
||||||
|
return json_message.create("danger", msg)
|
||||||
|
|
||||||
|
_sub_uuid = uuid.uuid4().hex
|
||||||
|
_video_path = fpath
|
||||||
|
_stub = f"{_hash}{_sub_uuid}"
|
||||||
|
_rtsp_path = f"rtsp://www.{app_name.lower()}.com:8554/{_stub}"
|
||||||
|
_webrtc_path = f"http://www.{app_name.lower()}.com:8889/{_stub}/"
|
||||||
|
_stream_target = _rtsp_path
|
||||||
|
|
||||||
|
stream = get_stream(_video_path, _stream_target)
|
||||||
|
if stream.poll():
|
||||||
|
msg = "Streaming: Setting up stream failed! Please try again..."
|
||||||
|
return json_message.create("danger", msg)
|
||||||
|
|
||||||
|
_stream_target = _webrtc_path
|
||||||
|
return {"stream": _stream_target}
|
||||||
|
|
||||||
# NOTE: Positionally protecting actions further down that are privlidged
|
# NOTE: Positionally protecting actions further down that are privlidged
|
||||||
# Be aware of ordering!
|
# Be aware of ordering!
|
||||||
@ -116,3 +137,20 @@ def fileManagerAction(_type, _hash = None):
|
|||||||
msg = "Opened media..."
|
msg = "Opened media..."
|
||||||
view.open_file_locally(fpath)
|
view.open_file_locally(fpath)
|
||||||
return json_message.create("success", msg)
|
return json_message.create("success", msg)
|
||||||
|
|
||||||
|
|
||||||
|
@app.route('/api/stop-current-stream', methods=['GET', 'POST'])
|
||||||
|
def stop_current_stream():
|
||||||
|
type = "success"
|
||||||
|
msg = "Stopped found stream process..."
|
||||||
|
process = get_stream()
|
||||||
|
|
||||||
|
if process:
|
||||||
|
if not kill_stream(process):
|
||||||
|
type = "danger"
|
||||||
|
msg = "Couldn't stop an existing stream!"
|
||||||
|
else:
|
||||||
|
type = "warning"
|
||||||
|
msg = "No stream process found. Nothing to stop..."
|
||||||
|
|
||||||
|
return json_message.create(type, msg)
|
||||||
|
@ -1,3 +1,8 @@
|
|||||||
|
#ctxDownload,
|
||||||
|
#img2Tab {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.menu {
|
.menu {
|
||||||
width: 165px;
|
width: 165px;
|
||||||
z-index: 999;
|
z-index: 999;
|
||||||
@ -7,15 +12,17 @@
|
|||||||
display: none;
|
display: none;
|
||||||
transition: 0.2s display ease-in;
|
transition: 0.2s display ease-in;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu .menu-options {
|
.menu .menu-options {
|
||||||
list-style: none;
|
list-style: none;
|
||||||
padding: 10px 0;
|
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
|
padding: unset !important;
|
||||||
|
margin: unset !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.menu .menu-options .menu-option {
|
.menu .menu-options .menu-option {
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
z-index: 1;
|
padding: 10px 20px 10px 20px;
|
||||||
padding: 10px 40px 10px 20px;
|
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -28,9 +35,11 @@ button {
|
|||||||
background: grey;
|
background: grey;
|
||||||
border: none;
|
border: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
button .next {
|
button .next {
|
||||||
color: green;
|
color: green;
|
||||||
}
|
}
|
||||||
|
|
||||||
button[disabled="false"]:hover .next {
|
button[disabled="false"]:hover .next {
|
||||||
color: red;
|
color: red;
|
||||||
animation: move 0.5s;
|
animation: move 0.5s;
|
||||||
|
@ -29,6 +29,8 @@ const loadBackground = () => {
|
|||||||
setCookie('bgSlug', path);
|
setCookie('bgSlug', path);
|
||||||
setBackgroundElement(bgElm, path);
|
setBackgroundElement(bgElm, path);
|
||||||
} else {
|
} else {
|
||||||
|
// NOTE: Probably in IFRAME and unloaded the background...
|
||||||
|
if (!bgElm) return ;
|
||||||
setBackgroundElement(bgElm, bgPath);
|
setBackgroundElement(bgElm, bgPath);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,16 @@
|
|||||||
const menu = document.querySelector(".menu");
|
const img2TabElm = document.getElementById("img2Tab");
|
||||||
let menuVisible = false;
|
const ctxDownloadElm = document.getElementById("ctxDownload");
|
||||||
let active_card = null;
|
const menu = document.querySelector(".menu");
|
||||||
|
|
||||||
|
let menuVisible = false;
|
||||||
|
let img2TabSrc = null;
|
||||||
|
let active_card = null;
|
||||||
|
|
||||||
|
const img2Tab = () => {
|
||||||
|
if (img2TabSrc !== null) {
|
||||||
|
window.open(img2TabSrc,'_blank');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const toggleMenu = command => {
|
const toggleMenu = command => {
|
||||||
menu.style.display = command === "show" ? "block" : "none";
|
menu.style.display = command === "show" ? "block" : "none";
|
||||||
@ -24,6 +34,16 @@ document.body.addEventListener("contextmenu", e => {
|
|||||||
|
|
||||||
let target = e.target;
|
let target = e.target;
|
||||||
let elm = target;
|
let elm = target;
|
||||||
|
|
||||||
|
ctxDownloadElm.style.display = (elm.nodeName === "IMG" ||
|
||||||
|
(elm.hasAttribute("ftype") &&
|
||||||
|
(elm.getAttribute("ftype") === "file" ||
|
||||||
|
elm.getAttribute("ftype") === "video" ||
|
||||||
|
elm.getAttribute("ftype") === "image")
|
||||||
|
)) ? "block" : "none";
|
||||||
|
img2TabElm.style.display = (elm.nodeName === "IMG") ? "block" : "none";
|
||||||
|
img2TabSrc = (elm.nodeName === "IMG") ? elm.src : null;
|
||||||
|
|
||||||
while (elm.nodeName != "BODY") {
|
while (elm.nodeName != "BODY") {
|
||||||
if (!elm.classList.contains("card")) {
|
if (!elm.classList.contains("card")) {
|
||||||
elm = elm.parentElement;
|
elm = elm.parentElement;
|
||||||
|
@ -41,10 +41,10 @@ const openFile = (eve) => {
|
|||||||
|
|
||||||
if (ftype === "dir") {
|
if (ftype === "dir") {
|
||||||
listFilesAjax(hash);
|
listFilesAjax(hash);
|
||||||
} else if (ftype === "video") {
|
} else if (ftype === "video" || ftype === "stream") {
|
||||||
showFile(title, hash, extension, "video", target);
|
showFile(title, hash, extension, ftype, target);
|
||||||
} else {
|
} else {
|
||||||
showFile(title, hash, extension, "file");
|
showFile(title, hash, extension, ftype);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -63,11 +63,13 @@ const updateHTMLDirList = async (data) => {
|
|||||||
|
|
||||||
// Set faves state
|
// Set faves state
|
||||||
let tggl_faves_btn = document.getElementById("tggl-faves-btn");
|
let tggl_faves_btn = document.getElementById("tggl-faves-btn");
|
||||||
if (isInFaves == "true")
|
if (isInFaves == "true") {
|
||||||
tggl_faves_btn.classList.add("btn-info");
|
tggl_faves_btn.classList.remove("btn-secondary");
|
||||||
else
|
tggl_faves_btn.classList.add("btn-warning");
|
||||||
tggl_faves_btn.classList.remove("btn-info");
|
} else {
|
||||||
|
tggl_faves_btn.classList.add("btn-secondary");
|
||||||
|
tggl_faves_btn.classList.remove("btn-warning");
|
||||||
|
}
|
||||||
|
|
||||||
renderFilesList(data.list);
|
renderFilesList(data.list);
|
||||||
loadBackgroundPoster();
|
loadBackgroundPoster();
|
||||||
|
27
src/core/static/js/webfm/react-ui-logic.js
vendored
27
src/core/static/js/webfm/react-ui-logic.js
vendored
@ -55,14 +55,16 @@ class FilesList extends React.Component {
|
|||||||
const name = file[0];
|
const name = file[0];
|
||||||
if (name == "000.jpg") { continue }
|
if (name == "000.jpg") { continue }
|
||||||
|
|
||||||
const hash = file[1];
|
const hash = file[1];
|
||||||
const fsize = file[2];
|
const fsize = file[2];
|
||||||
let extension = re.exec( name.toLowerCase() )[1] ? name : "file.dir";
|
let extension = re.exec( name.toLowerCase() )[1] ? name : "file.dir";
|
||||||
let data = setFileIconType(extension);
|
let data = setFileIconType(extension);
|
||||||
let icon = data[0];
|
let icon = data[0];
|
||||||
let filetype = data[1];
|
let filetype = data[1];
|
||||||
let card_header = null;
|
let card_header = null;
|
||||||
let card_body = null;
|
let card_body = null;
|
||||||
|
let download_button = null;
|
||||||
|
let stream_button = null;
|
||||||
|
|
||||||
if (filetype === "video") {
|
if (filetype === "video") {
|
||||||
card_header = name;
|
card_header = name;
|
||||||
@ -70,6 +72,10 @@ class FilesList extends React.Component {
|
|||||||
<img class="card-img-top card-image" src="" alt={name} />
|
<img class="card-img-top card-image" src="" alt={name} />
|
||||||
<span class="float-right">{fsize}</span>
|
<span class="float-right">{fsize}</span>
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
|
stream_button = <React.Fragment>
|
||||||
|
<input hash={hash} onClick={this.openThis} ftype="stream" class="btn btn-dark btn-sm float-end" title={name} type="button" value="Stream"/>
|
||||||
|
</React.Fragment>;
|
||||||
|
|
||||||
} else if (filetype === "image") {
|
} else if (filetype === "image") {
|
||||||
card_header = name;
|
card_header = name;
|
||||||
card_body = <React.Fragment>
|
card_body = <React.Fragment>
|
||||||
@ -82,7 +88,11 @@ class FilesList extends React.Component {
|
|||||||
{name}
|
{name}
|
||||||
<span>{fsize}</span>
|
<span>{fsize}</span>
|
||||||
</React.Fragment>;
|
</React.Fragment>;
|
||||||
|
}
|
||||||
|
if (filetype !== "dir") {
|
||||||
|
download_button = <React.Fragment>
|
||||||
|
<a href={"api/file-manager-action/files/" + hash} download class="btn btn-dark btn-sm float-start">Download</a>
|
||||||
|
</React.Fragment>;
|
||||||
}
|
}
|
||||||
|
|
||||||
final.push(
|
final.push(
|
||||||
@ -95,9 +105,10 @@ class FilesList extends React.Component {
|
|||||||
{card_body}
|
{card_body}
|
||||||
</div>
|
</div>
|
||||||
<div class="card-footer">
|
<div class="card-footer">
|
||||||
<a href={"api/file-manager-action/files/" + hash} download class="btn btn-dark btn-sm float-start">Download</a>
|
{download_button}
|
||||||
<input hash={hash} onClick={this.openThisLocally} ftype={filetype} class="btn btn-dark btn-sm float-start" type="button" value="Open Locally"/>
|
<input hash={hash} onClick={this.openThisLocally} ftype={filetype} class="btn btn-dark btn-sm float-start" type="button" value="Open Locally"/>
|
||||||
<input hash={hash} onClick={this.openThis} ftype={filetype} class="btn btn-dark btn-sm float-end" title={name} type="button" value="Open"/>
|
<input hash={hash} onClick={this.openThis} ftype={filetype} class="btn btn-dark btn-sm float-end" title={name} type="button" value="Open"/>
|
||||||
|
{stream_button}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
@ -49,7 +49,7 @@ const scrollFilesToTop = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const closeFile = () => {
|
const closeFile = async () => {
|
||||||
const video = document.getElementById("video");
|
const video = document.getElementById("video");
|
||||||
const trailerPlayer = document.getElementById("trailerPlayer")
|
const trailerPlayer = document.getElementById("trailerPlayer")
|
||||||
let title = document.getElementById("selectedFile");
|
let title = document.getElementById("selectedFile");
|
||||||
@ -65,6 +65,9 @@ const closeFile = () => {
|
|||||||
|
|
||||||
trailerPlayer.src = "#";
|
trailerPlayer.src = "#";
|
||||||
trailerPlayer.style.display = "none";
|
trailerPlayer.style.display = "none";
|
||||||
|
|
||||||
|
// FIXME: Yes, a wasted call every time there is no stream.
|
||||||
|
await fetchData("api/stop-current-stream");
|
||||||
clearSelectedActiveMedia();
|
clearSelectedActiveMedia();
|
||||||
clearModalFades();
|
clearModalFades();
|
||||||
}
|
}
|
||||||
@ -79,15 +82,16 @@ const showFile = async (title, hash, extension, type, target=null) => {
|
|||||||
let titleElm = document.getElementById("selectedFile");
|
let titleElm = document.getElementById("selectedFile");
|
||||||
titleElm.innerText = title;
|
titleElm.innerText = title;
|
||||||
|
|
||||||
if (type === "video") {
|
// FIXME: Yes, a wasted call every time there is no stream.
|
||||||
setupVideo(hash, extension);
|
await fetchData("api/stop-current-stream");
|
||||||
|
if (type === "video" || type === "stream") {
|
||||||
|
isStream = (type === "stream")
|
||||||
|
setupVideo(hash, extension, isStream);
|
||||||
setSelectedActiveMedia(target);
|
setSelectedActiveMedia(target);
|
||||||
}
|
} else if (type === "trailer") {
|
||||||
if (type === "file") {
|
|
||||||
setupFile(hash, extension);
|
|
||||||
}
|
|
||||||
if (type === "trailer") {
|
|
||||||
launchTrailer(hash);
|
launchTrailer(hash);
|
||||||
|
} else {
|
||||||
|
setupFile(hash, extension);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,7 +104,7 @@ const launchTrailer = (link) => {
|
|||||||
modal.show();
|
modal.show();
|
||||||
}
|
}
|
||||||
|
|
||||||
const setupVideo = async (hash, extension) => {
|
const setupVideo = async (hash, extension, isStream=false) => {
|
||||||
let modal = new bootstrap.Modal(document.getElementById('file-view-modal'), { keyboard: false });
|
let modal = new bootstrap.Modal(document.getElementById('file-view-modal'), { keyboard: false });
|
||||||
let video = document.getElementById("video");
|
let video = document.getElementById("video");
|
||||||
video.poster = "static/imgs/icons/loading.gif";
|
video.poster = "static/imgs/icons/loading.gif";
|
||||||
@ -109,13 +113,23 @@ const setupVideo = async (hash, extension) => {
|
|||||||
video_path = "api/file-manager-action/files/" + hash;
|
video_path = "api/file-manager-action/files/" + hash;
|
||||||
|
|
||||||
clearSelectedActiveMedia();
|
clearSelectedActiveMedia();
|
||||||
try {
|
try {
|
||||||
if ((/\.(avi|mkv|wmv|flv|f4v|mov|m4v|mpg|mpeg|mp4|webm|mp3|flac|ogg)$/i).test(extension)) {
|
if ((/\.(avi|mkv|wmv|flv|f4v|mov|m4v|mpg|mpeg|mp4|webm|mp3|flac|ogg)$/i).test(extension)) {
|
||||||
if ((/\.(avi|mkv|wmv|flv|f4v)$/i).test(extension)) {
|
if ((/\.(avi|mkv|wmv|flv|f4v)$/i).test(extension)) {
|
||||||
data = await fetchData( "api/file-manager-action/remux/" + hash );
|
if (isStream) {
|
||||||
if ( data.hasOwnProperty('path') ) {
|
data = await fetchData( "api/file-manager-action/stream/" + hash );
|
||||||
video_path = data.path;
|
if (data.hasOwnProperty('stream')) {
|
||||||
|
video_path = data.stream;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
|
data = await fetchData( "api/file-manager-action/remux/" + hash );
|
||||||
|
if (data.hasOwnProperty('path')) {
|
||||||
|
video_path = data.path;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.hasOwnProperty('path') === null &&
|
||||||
|
data.hasOwnProperty('stream') === null) {
|
||||||
displayMessage(data.message.text, data.message.type);
|
displayMessage(data.message.text, data.message.type);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -254,14 +268,16 @@ const updateBackground = (srcLink, isvideo = true) => {
|
|||||||
|
|
||||||
|
|
||||||
const manageFavorites = (elm) => {
|
const manageFavorites = (elm) => {
|
||||||
const classType = "btn-info";
|
const classType = "btn-warning";
|
||||||
const hasClass = elm.classList.contains(classType);
|
const hasClass = elm.classList.contains(classType);
|
||||||
if (hasClass) {
|
if (hasClass) {
|
||||||
elm.classList.remove(classType);
|
|
||||||
action = "delete";
|
action = "delete";
|
||||||
|
elm.classList.remove(classType);
|
||||||
|
elm.classList.add("btn-secondary");
|
||||||
} else {
|
} else {
|
||||||
elm.classList.add(classType);
|
|
||||||
action = "add";
|
action = "add";
|
||||||
|
elm.classList.add(classType);
|
||||||
|
elm.classList.remove("btn-secondary");
|
||||||
}
|
}
|
||||||
|
|
||||||
manageFavoritesAjax(action);
|
manageFavoritesAjax(action);
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
<div id="navbarTogglerFooter" class="row collapse navbar-collapse">
|
<div id="navbarTogglerFooter" class="row collapse navbar-collapse">
|
||||||
<ul class="navbar-nav">
|
<ul class="navbar-nav">
|
||||||
<li class="nav-item mx-3">
|
<li class="nav-item mx-3">
|
||||||
<span id="tggl-faves-btn" class="btn btn-info btn-sm" title="Add/Delete from favorites..." >☆</span>
|
<span id="tggl-faves-btn" class="btn btn-secondary btn-sm" title="Add/Delete from favorites..." >☆</span>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item mx-auto">
|
<li class="nav-item mx-auto">
|
||||||
<span id="path">{{current_directory}}</span>
|
<span id="path">{{current_directory}}</span>
|
||||||
|
9
src/core/templates/context-menu.html
Normal file
9
src/core/templates/context-menu.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<div class="menu">
|
||||||
|
<ul class="menu-options">
|
||||||
|
<li class="menu-option" onclick="goUpADirectory()">Back</li>
|
||||||
|
<li class="menu-option" onclick="goHome()">Home Directory</li>
|
||||||
|
<li id="ctxDownload" class="menu-option" onclick="downloadItem()">Download</li>
|
||||||
|
<li id="img2Tab" class="menu-option" onclick="img2Tab()">Image To New Tab</li>
|
||||||
|
<li class="menu-option" onclick="deleteItem()">Delete</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
@ -40,15 +40,7 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
<body>
|
<body>
|
||||||
<video id="bg" src="" poster="" autoplay loop> </video>
|
<video id="bg" src="" poster="" autoplay loop> </video>
|
||||||
|
{% include "context-menu.html" %}
|
||||||
<div class="menu">
|
|
||||||
<ul class="menu-options">
|
|
||||||
<li class="menu-option" onclick="goUpADirectory()">Back</li>
|
|
||||||
<li class="menu-option" onclick="goHome()">Home Directory</li>
|
|
||||||
<li class="menu-option" onclick="downloadItem()">Download</li>
|
|
||||||
<li class="menu-option" onclick="deleteItem()">Delete</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% block body_header %}
|
{% block body_header %}
|
||||||
{% include "body-header.html" %}
|
{% include "body-header.html" %}
|
||||||
|
400
src/core/utils/rtsp-server/rtsp-simple-server.yml
Normal file
400
src/core/utils/rtsp-server/rtsp-simple-server.yml
Normal file
@ -0,0 +1,400 @@
|
|||||||
|
|
||||||
|
###############################################
|
||||||
|
# General parameters
|
||||||
|
|
||||||
|
# Sets the verbosity of the program; available values are "error", "warn", "info", "debug".
|
||||||
|
logLevel: info
|
||||||
|
# Destinations of log messages; available values are "stdout", "file" and "syslog".
|
||||||
|
logDestinations: [stdout]
|
||||||
|
# If "file" is in logDestinations, this is the file which will receive the logs.
|
||||||
|
logFile: rtsp-simple-server.log
|
||||||
|
|
||||||
|
# Timeout of read operations.
|
||||||
|
readTimeout: 10s
|
||||||
|
# Timeout of write operations.
|
||||||
|
writeTimeout: 10s
|
||||||
|
# Number of read buffers.
|
||||||
|
# A higher value allows a wider throughput, a lower value allows to save RAM.
|
||||||
|
readBufferCount: 2048
|
||||||
|
|
||||||
|
# HTTP URL to perform external authentication.
|
||||||
|
# Every time a user wants to authenticate, the server calls this URL
|
||||||
|
# with the POST method and a body containing:
|
||||||
|
# {
|
||||||
|
# "ip": "ip",
|
||||||
|
# "user": "user",
|
||||||
|
# "password": "password",
|
||||||
|
# "path": "path",
|
||||||
|
# "protocol": "rtsp|rtmp|hls|webrtc",
|
||||||
|
# "id": "id",
|
||||||
|
# "action": "read|publish",
|
||||||
|
# "query": "query"
|
||||||
|
# }
|
||||||
|
# If the response code is 20x, authentication is accepted, otherwise
|
||||||
|
# it is discarded.
|
||||||
|
externalAuthenticationURL:
|
||||||
|
|
||||||
|
# Enable the HTTP API.
|
||||||
|
api: no
|
||||||
|
# Address of the API listener.
|
||||||
|
apiAddress: 127.0.0.1:9997
|
||||||
|
|
||||||
|
# Enable Prometheus-compatible metrics.
|
||||||
|
metrics: no
|
||||||
|
# Address of the metrics listener.
|
||||||
|
metricsAddress: 127.0.0.1:9998
|
||||||
|
|
||||||
|
# Enable pprof-compatible endpoint to monitor performances.
|
||||||
|
pprof: no
|
||||||
|
# Address of the pprof listener.
|
||||||
|
pprofAddress: 127.0.0.1:9999
|
||||||
|
|
||||||
|
# Command to run when a client connects to the server.
|
||||||
|
# This is terminated with SIGINT when a client disconnects from the server.
|
||||||
|
# The following environment variables are available:
|
||||||
|
# * RTSP_PORT: server port
|
||||||
|
runOnConnect:
|
||||||
|
# Restart the command if it exits suddenly.
|
||||||
|
runOnConnectRestart: no
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
# RTSP parameters
|
||||||
|
|
||||||
|
# Disable support for the RTSP protocol.
|
||||||
|
rtspDisable: no
|
||||||
|
# List of enabled RTSP transport protocols.
|
||||||
|
# UDP is the most performant, but doesn't work when there's a NAT/firewall between
|
||||||
|
# server and clients, and doesn't support encryption.
|
||||||
|
# UDP-multicast allows to save bandwidth when clients are all in the same LAN.
|
||||||
|
# TCP is the most versatile, and does support encryption.
|
||||||
|
# The handshake is always performed with TCP.
|
||||||
|
protocols: [udp, multicast, tcp]
|
||||||
|
# Encrypt handshakes and TCP streams with TLS (RTSPS).
|
||||||
|
# Available values are "no", "strict", "optional".
|
||||||
|
encryption: "no"
|
||||||
|
# Address of the TCP/RTSP listener. This is needed only when encryption is "no" or "optional".
|
||||||
|
rtspAddress: :8554
|
||||||
|
# Address of the TCP/TLS/RTSPS listener. This is needed only when encryption is "strict" or "optional".
|
||||||
|
rtspsAddress: :8322
|
||||||
|
# Address of the UDP/RTP listener. This is needed only when "udp" is in protocols.
|
||||||
|
rtpAddress: :8000
|
||||||
|
# Address of the UDP/RTCP listener. This is needed only when "udp" is in protocols.
|
||||||
|
rtcpAddress: :8001
|
||||||
|
# IP range of all UDP-multicast listeners. This is needed only when "multicast" is in protocols.
|
||||||
|
multicastIPRange: 224.1.0.0/16
|
||||||
|
# Port of all UDP-multicast/RTP listeners. This is needed only when "multicast" is in protocols.
|
||||||
|
multicastRTPPort: 8002
|
||||||
|
# Port of all UDP-multicast/RTCP listeners. This is needed only when "multicast" is in protocols.
|
||||||
|
multicastRTCPPort: 8003
|
||||||
|
# Path to the server key. This is needed only when encryption is "strict" or "optional".
|
||||||
|
# This can be generated with:
|
||||||
|
# openssl genrsa -out server.key 2048
|
||||||
|
# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
|
||||||
|
serverKey: server.key
|
||||||
|
# Path to the server certificate. This is needed only when encryption is "strict" or "optional".
|
||||||
|
serverCert: server.crt
|
||||||
|
# Authentication methods.
|
||||||
|
authMethods: [basic, digest]
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
# RTMP parameters
|
||||||
|
|
||||||
|
# Disable support for the RTMP protocol.
|
||||||
|
rtmpDisable: yes
|
||||||
|
# Address of the RTMP listener. This is needed only when encryption is "no" or "optional".
|
||||||
|
rtmpAddress: :1935
|
||||||
|
# Encrypt connections with TLS (RTMPS).
|
||||||
|
# Available values are "no", "strict", "optional".
|
||||||
|
rtmpEncryption: "no"
|
||||||
|
# Address of the RTMPS listener. This is needed only when encryption is "strict" or "optional".
|
||||||
|
rtmpsAddress: :1936
|
||||||
|
# Path to the server key. This is needed only when encryption is "strict" or "optional".
|
||||||
|
# This can be generated with:
|
||||||
|
# openssl genrsa -out server.key 2048
|
||||||
|
# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
|
||||||
|
rtmpServerKey: server.key
|
||||||
|
# Path to the server certificate. This is needed only when encryption is "strict" or "optional".
|
||||||
|
rtmpServerCert: server.crt
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
# HLS parameters
|
||||||
|
|
||||||
|
# Disable support for the HLS protocol.
|
||||||
|
hlsDisable: yes
|
||||||
|
# Address of the HLS listener.
|
||||||
|
hlsAddress: :8888
|
||||||
|
# Enable TLS/HTTPS on the HLS server.
|
||||||
|
# This is required for Low-Latency HLS.
|
||||||
|
hlsEncryption: no
|
||||||
|
# Path to the server key. This is needed only when encryption is yes.
|
||||||
|
# This can be generated with:
|
||||||
|
# openssl genrsa -out server.key 2048
|
||||||
|
# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
|
||||||
|
hlsServerKey: server.key
|
||||||
|
# Path to the server certificate.
|
||||||
|
hlsServerCert: server.crt
|
||||||
|
# By default, HLS is generated only when requested by a user.
|
||||||
|
# This option allows to generate it always, avoiding the delay between request and generation.
|
||||||
|
hlsAlwaysRemux: no
|
||||||
|
# Variant of the HLS protocol to use. Available options are:
|
||||||
|
# * mpegts - uses MPEG-TS segments, for maximum compatibility.
|
||||||
|
# * fmp4 - uses fragmented MP4 segments, more efficient.
|
||||||
|
# * lowLatency - uses Low-Latency HLS.
|
||||||
|
hlsVariant: mpegts
|
||||||
|
# Number of HLS segments to keep on the server.
|
||||||
|
# Segments allow to seek through the stream.
|
||||||
|
# Their number doesn't influence latency.
|
||||||
|
hlsSegmentCount: 7
|
||||||
|
# Minimum duration of each segment.
|
||||||
|
# A player usually puts 3 segments in a buffer before reproducing the stream.
|
||||||
|
# The final segment duration is also influenced by the interval between IDR frames,
|
||||||
|
# since the server changes the duration in order to include at least one IDR frame
|
||||||
|
# in each segment.
|
||||||
|
hlsSegmentDuration: 1s
|
||||||
|
# Minimum duration of each part.
|
||||||
|
# A player usually puts 3 parts in a buffer before reproducing the stream.
|
||||||
|
# Parts are used in Low-Latency HLS in place of segments.
|
||||||
|
# Part duration is influenced by the distance between video/audio samples
|
||||||
|
# and is adjusted in order to produce segments with a similar duration.
|
||||||
|
hlsPartDuration: 200ms
|
||||||
|
# Maximum size of each segment.
|
||||||
|
# This prevents RAM exhaustion.
|
||||||
|
hlsSegmentMaxSize: 50M
|
||||||
|
# Value of the Access-Control-Allow-Origin header provided in every HTTP response.
|
||||||
|
# This allows to play the HLS stream from an external website.
|
||||||
|
hlsAllowOrigin: '*'
|
||||||
|
# List of IPs or CIDRs of proxies placed before the HLS server.
|
||||||
|
# If the server receives a request from one of these entries, IP in logs
|
||||||
|
# will be taken from the X-Forwarded-For header.
|
||||||
|
hlsTrustedProxies: []
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
# WebRTC parameters
|
||||||
|
|
||||||
|
# Disable support for the WebRTC protocol.
|
||||||
|
webrtcDisable: no
|
||||||
|
# Address of the WebRTC listener.
|
||||||
|
webrtcAddress: :8889
|
||||||
|
# Enable TLS/HTTPS on the WebRTC server.
|
||||||
|
webrtcEncryption: no
|
||||||
|
# Path to the server key.
|
||||||
|
# This can be generated with:
|
||||||
|
# openssl genrsa -out server.key 2048
|
||||||
|
# openssl req -new -x509 -sha256 -key server.key -out server.crt -days 3650
|
||||||
|
webrtcServerKey: server.key
|
||||||
|
# Path to the server certificate.
|
||||||
|
webrtcServerCert: server.crt
|
||||||
|
# Value of the Access-Control-Allow-Origin header provided in every HTTP response.
|
||||||
|
# This allows to play the WebRTC stream from an external website.
|
||||||
|
webrtcAllowOrigin: '*'
|
||||||
|
# List of IPs or CIDRs of proxies placed before the WebRTC server.
|
||||||
|
# If the server receives a request from one of these entries, IP in logs
|
||||||
|
# will be taken from the X-Forwarded-For header.
|
||||||
|
webrtcTrustedProxies: []
|
||||||
|
# List of ICE servers, in format type:user:pass:host:port or type:host:port.
|
||||||
|
# type can be "stun", "turn" or "turns".
|
||||||
|
# STUN servers are used to get the public IP of both server and clients.
|
||||||
|
# TURN/TURNS servers are used as relay when a direct connection between server and clients is not possible.
|
||||||
|
# if user is "AUTH_SECRET", then authentication is secret based.
|
||||||
|
# the secret must be inserted into the pass field.
|
||||||
|
webrtcICEServers: [stun:stun.l.google.com:19302]
|
||||||
|
# List of public IP addresses that are to be used as a host.
|
||||||
|
# This is used typically for servers that are behind 1:1 D-NAT.
|
||||||
|
webrtcICEHostNAT1To1IPs: []
|
||||||
|
# Address of a ICE UDP listener in format host:port.
|
||||||
|
# If filled, ICE traffic will come through a single UDP port,
|
||||||
|
# allowing the deployment of the server inside a container or behind a NAT.
|
||||||
|
webrtcICEUDPMuxAddress:
|
||||||
|
# Address of a ICE TCP listener in format host:port.
|
||||||
|
# If filled, ICE traffic will come through a single TCP port,
|
||||||
|
# allowing the deployment of the server inside a container or behind a NAT.
|
||||||
|
# At the moment, setting this parameter forces usage of the TCP protocol,
|
||||||
|
# which is not optimal for WebRTC.
|
||||||
|
webrtcICETCPMuxAddress:
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
# Path parameters
|
||||||
|
|
||||||
|
# These settings are path-dependent, and the map key is the name of the path.
|
||||||
|
# It's possible to use regular expressions by using a tilde as prefix.
|
||||||
|
# For example, "~^(test1|test2)$" will match both "test1" and "test2".
|
||||||
|
# For example, "~^prefix" will match all paths that start with "prefix".
|
||||||
|
# The settings under the path "all" are applied to all paths that do not match
|
||||||
|
# another entry.
|
||||||
|
paths:
|
||||||
|
all:
|
||||||
|
# Source of the stream. This can be:
|
||||||
|
# * publisher -> the stream is published by a RTSP or RTMP client
|
||||||
|
# * rtsp://existing-url -> the stream is pulled from another RTSP server / camera
|
||||||
|
# * rtsps://existing-url -> the stream is pulled from another RTSP server / camera with RTSPS
|
||||||
|
# * rtmp://existing-url -> the stream is pulled from another RTMP server / camera
|
||||||
|
# * rtmps://existing-url -> the stream is pulled from another RTMP server / camera with RTMPS
|
||||||
|
# * http://existing-url/stream.m3u8 -> the stream is pulled from another HLS server
|
||||||
|
# * https://existing-url/stream.m3u8 -> the stream is pulled from another HLS server with HTTPS
|
||||||
|
# * redirect -> the stream is provided by another path or server
|
||||||
|
# * rpiCamera -> the stream is provided by a Raspberry Pi Camera
|
||||||
|
source: publisher
|
||||||
|
|
||||||
|
# If the source is an RTSP or RTSPS URL, this is the protocol that will be used to
|
||||||
|
# pull the stream. available values are "automatic", "udp", "multicast", "tcp".
|
||||||
|
sourceProtocol: automatic
|
||||||
|
|
||||||
|
# Tf the source is an RTSP or RTSPS URL, this allows to support sources that
|
||||||
|
# don't provide server ports or use random server ports. This is a security issue
|
||||||
|
# and must be used only when interacting with sources that require it.
|
||||||
|
sourceAnyPortEnable: no
|
||||||
|
|
||||||
|
# If the source is a RTSPS, RTMPS or HTTPS URL, and the source certificate is self-signed
|
||||||
|
# or invalid, you can provide the fingerprint of the certificate in order to
|
||||||
|
# validate it anyway. It can be obtained by running:
|
||||||
|
# openssl s_client -connect source_ip:source_port </dev/null 2>/dev/null | sed -n '/BEGIN/,/END/p' > server.crt
|
||||||
|
# openssl x509 -in server.crt -noout -fingerprint -sha256 | cut -d "=" -f2 | tr -d ':'
|
||||||
|
sourceFingerprint:
|
||||||
|
|
||||||
|
# If the source is an RTSP or RTMP URL, it will be pulled only when at least
|
||||||
|
# one reader is connected, saving bandwidth.
|
||||||
|
sourceOnDemand: no
|
||||||
|
# If sourceOnDemand is "yes", readers will be put on hold until the source is
|
||||||
|
# ready or until this amount of time has passed.
|
||||||
|
sourceOnDemandStartTimeout: 10s
|
||||||
|
# If sourceOnDemand is "yes", the source will be closed when there are no
|
||||||
|
# readers connected and this amount of time has passed.
|
||||||
|
sourceOnDemandCloseAfter: 10s
|
||||||
|
|
||||||
|
# If the source is "redirect", this is the RTSP URL which clients will be
|
||||||
|
# redirected to.
|
||||||
|
sourceRedirect:
|
||||||
|
|
||||||
|
# If the source is "publisher" and a client is publishing, do not allow another
|
||||||
|
# client to disconnect the former and publish in its place.
|
||||||
|
disablePublisherOverride: no
|
||||||
|
|
||||||
|
# If the source is "publisher" and no one is publishing, redirect readers to this
|
||||||
|
# path. It can be can be a relative path (i.e. /otherstream) or an absolute RTSP URL.
|
||||||
|
fallback:
|
||||||
|
|
||||||
|
# If the source is "rpiCamera", these are the Raspberry Pi Camera parameters.
|
||||||
|
# ID of the camera
|
||||||
|
rpiCameraCamID: 0
|
||||||
|
# width of frames
|
||||||
|
rpiCameraWidth: 1920
|
||||||
|
# height of frames
|
||||||
|
rpiCameraHeight: 1080
|
||||||
|
# flip horizontally
|
||||||
|
rpiCameraHFlip: false
|
||||||
|
# flip vertically
|
||||||
|
rpiCameraVFlip: false
|
||||||
|
# brightness [-1, 1]
|
||||||
|
rpiCameraBrightness: 0
|
||||||
|
# contrast [0, 16]
|
||||||
|
rpiCameraContrast: 1
|
||||||
|
# saturation [0, 16]
|
||||||
|
rpiCameraSaturation: 1
|
||||||
|
# sharpness [0, 16]
|
||||||
|
rpiCameraSharpness: 1
|
||||||
|
# exposure mode.
|
||||||
|
# values: normal, short, long, custom
|
||||||
|
rpiCameraExposure: normal
|
||||||
|
# auto-white-balance mode.
|
||||||
|
# values: auto, incandescent, tungsten, fluorescent, indoor, daylight, cloudy, custom
|
||||||
|
rpiCameraAWB: auto
|
||||||
|
# denoise operating mode.
|
||||||
|
# values: auto, off, cdn_off, cdn_fast, cdn_hq
|
||||||
|
rpiCameraDenoise: auto
|
||||||
|
# fixed shutter speed, in microseconds.
|
||||||
|
rpiCameraShutter: 0
|
||||||
|
# metering mode of the AEC/AGC algorithm.
|
||||||
|
# values: centre, spot, matrix, custom
|
||||||
|
rpiCameraMetering: centre
|
||||||
|
# fixed gain
|
||||||
|
rpiCameraGain: 0
|
||||||
|
# EV compensation of the image [-10, 10]
|
||||||
|
rpiCameraEV: 0
|
||||||
|
# Region of interest, in format x,y,width,height
|
||||||
|
rpiCameraROI:
|
||||||
|
# tuning file
|
||||||
|
rpiCameraTuningFile:
|
||||||
|
# sensor mode, in format [width]:[height]:[bit-depth]:[packing]
|
||||||
|
# bit-depth and packing are optional.
|
||||||
|
rpiCameraMode:
|
||||||
|
# frames per second
|
||||||
|
rpiCameraFPS: 30
|
||||||
|
# period between IDR frames
|
||||||
|
rpiCameraIDRPeriod: 60
|
||||||
|
# bitrate
|
||||||
|
rpiCameraBitrate: 1000000
|
||||||
|
# H264 profile
|
||||||
|
rpiCameraProfile: main
|
||||||
|
# H264 level
|
||||||
|
rpiCameraLevel: '4.1'
|
||||||
|
|
||||||
|
# Username required to publish.
|
||||||
|
# SHA256-hashed values can be inserted with the "sha256:" prefix.
|
||||||
|
publishUser:
|
||||||
|
# Password required to publish.
|
||||||
|
# SHA256-hashed values can be inserted with the "sha256:" prefix.
|
||||||
|
publishPass:
|
||||||
|
# IPs or networks (x.x.x.x/24) allowed to publish.
|
||||||
|
publishIPs: []
|
||||||
|
|
||||||
|
# Username required to read.
|
||||||
|
# SHA256-hashed values can be inserted with the "sha256:" prefix.
|
||||||
|
readUser:
|
||||||
|
# password required to read.
|
||||||
|
# SHA256-hashed values can be inserted with the "sha256:" prefix.
|
||||||
|
readPass:
|
||||||
|
# IPs or networks (x.x.x.x/24) allowed to read.
|
||||||
|
readIPs: []
|
||||||
|
|
||||||
|
# Command to run when this path is initialized.
|
||||||
|
# This can be used to publish a stream and keep it always opened.
|
||||||
|
# This is terminated with SIGINT when the program closes.
|
||||||
|
# The following environment variables are available:
|
||||||
|
# * RTSP_PATH: path name
|
||||||
|
# * RTSP_PORT: server port
|
||||||
|
# * G1, G2, ...: regular expression groups, if path name is
|
||||||
|
# a regular expression.
|
||||||
|
runOnInit:
|
||||||
|
# Restart the command if it exits suddenly.
|
||||||
|
runOnInitRestart: no
|
||||||
|
|
||||||
|
# Command to run when this path is requested.
|
||||||
|
# This can be used to publish a stream on demand.
|
||||||
|
# This is terminated with SIGINT when the path is not requested anymore.
|
||||||
|
# The following environment variables are available:
|
||||||
|
# * RTSP_PATH: path name
|
||||||
|
# * RTSP_PORT: server port
|
||||||
|
# * G1, G2, ...: regular expression groups, if path name is
|
||||||
|
# a regular expression.
|
||||||
|
runOnDemand:
|
||||||
|
# Restart the command if it exits suddenly.
|
||||||
|
runOnDemandRestart: no
|
||||||
|
# Readers will be put on hold until the runOnDemand command starts publishing
|
||||||
|
# or until this amount of time has passed.
|
||||||
|
runOnDemandStartTimeout: 10s
|
||||||
|
# The command will be closed when there are no
|
||||||
|
# readers connected and this amount of time has passed.
|
||||||
|
runOnDemandCloseAfter: 10s
|
||||||
|
|
||||||
|
# Command to run when the stream is ready to be read, whether it is
|
||||||
|
# published by a client or pulled from a server / camera.
|
||||||
|
# This is terminated with SIGINT when the stream is not ready anymore.
|
||||||
|
# The following environment variables are available:
|
||||||
|
# * RTSP_PATH: path name
|
||||||
|
# * RTSP_PORT: server port
|
||||||
|
# * G1, G2, ...: regular expression groups, if path name is
|
||||||
|
# a regular expression.
|
||||||
|
runOnReady:
|
||||||
|
# Restart the command if it exits suddenly.
|
||||||
|
runOnReadyRestart: no
|
||||||
|
|
||||||
|
# Command to run when a clients starts reading.
|
||||||
|
# This is terminated with SIGINT when a client stops reading.
|
||||||
|
# The following environment variables are available:
|
||||||
|
# * RTSP_PATH: path name
|
||||||
|
# * RTSP_PORT: server port
|
||||||
|
# * G1, G2, ...: regular expression groups, if path name is
|
||||||
|
# a regular expression.
|
||||||
|
runOnRead:
|
||||||
|
# Restart the command if it exits suddenly.
|
||||||
|
runOnReadRestart: no
|
@ -13,6 +13,7 @@ from os import path
|
|||||||
class Settings:
|
class Settings:
|
||||||
logger = None
|
logger = None
|
||||||
USR_WEBFM = "/usr/share/webfm"
|
USR_WEBFM = "/usr/share/webfm"
|
||||||
|
SHIM_PATH = "/dev/shm/webfm"
|
||||||
USER_HOME = path.expanduser('~')
|
USER_HOME = path.expanduser('~')
|
||||||
CONFIG_PATH = f"{USER_HOME}/.config/webfm"
|
CONFIG_PATH = f"{USER_HOME}/.config/webfm"
|
||||||
CONFIG_FILE = f"{CONFIG_PATH}/settings.json"
|
CONFIG_FILE = f"{CONFIG_PATH}/settings.json"
|
||||||
@ -23,14 +24,19 @@ class Settings:
|
|||||||
DEFAULT_ICON = f"{DEFAULT_ICONS}/text.png"
|
DEFAULT_ICON = f"{DEFAULT_ICONS}/text.png"
|
||||||
FFMPG_THUMBNLR = f"{CONFIG_PATH}/ffmpegthumbnailer" # Thumbnail generator binary
|
FFMPG_THUMBNLR = f"{CONFIG_PATH}/ffmpegthumbnailer" # Thumbnail generator binary
|
||||||
BLENDER_THUMBNLR = f"{CONFIG_PATH}/blender-thumbnailer" # Blender thumbnail generator binary
|
BLENDER_THUMBNLR = f"{CONFIG_PATH}/blender-thumbnailer" # Blender thumbnail generator binary
|
||||||
REMUX_FOLDER = f"{USER_HOME}/.remuxs" # Remuxed files folder
|
# REMUX_FOLDER = f"{USER_HOME}/.remuxs" # Remuxed files folder
|
||||||
|
REMUX_FOLDER = f"{SHIM_PATH}/.remuxs" # Remuxed files folder
|
||||||
|
|
||||||
ICON_DIRS = ["/usr/share/icons", f"{USER_HOME}/.icons" "/usr/share/pixmaps"]
|
ICON_DIRS = ["/usr/share/icons", f"{USER_HOME}/.icons" "/usr/share/pixmaps"]
|
||||||
BASE_THUMBS_PTH = f"{USER_HOME}/.thumbnails" # Used for thumbnail generation
|
# BASE_THUMBS_PTH = f"{USER_HOME}/.thumbnails" # Used for thumbnail generation
|
||||||
|
BASE_THUMBS_PTH = f"{SHIM_PATH}/.thumbnails" # Used for thumbnail generation
|
||||||
ABS_THUMBS_PTH = f"{BASE_THUMBS_PTH}/normal" # Used for thumbnail generation
|
ABS_THUMBS_PTH = f"{BASE_THUMBS_PTH}/normal" # Used for thumbnail generation
|
||||||
STEAM_ICONS_PTH = f"{BASE_THUMBS_PTH}/steam_icons"
|
STEAM_ICONS_PTH = f"{BASE_THUMBS_PTH}/steam_icons"
|
||||||
|
|
||||||
# Dir structure check
|
# Dir structure check
|
||||||
|
if not path.isdir(SHIM_PATH):
|
||||||
|
os.mkdir(SHIM_PATH)
|
||||||
|
|
||||||
if not path.isdir(REMUX_FOLDER):
|
if not path.isdir(REMUX_FOLDER):
|
||||||
os.mkdir(REMUX_FOLDER)
|
os.mkdir(REMUX_FOLDER)
|
||||||
|
|
||||||
@ -55,9 +61,9 @@ class Settings:
|
|||||||
STEAM_CDN_URL = config["steam_cdn_url"]
|
STEAM_CDN_URL = config["steam_cdn_url"]
|
||||||
FFMPG_THUMBNLR = FFMPG_THUMBNLR if config["thumbnailer_path"] == "" else config["thumbnailer_path"]
|
FFMPG_THUMBNLR = FFMPG_THUMBNLR if config["thumbnailer_path"] == "" else config["thumbnailer_path"]
|
||||||
BLENDER_THUMBNLR = BLENDER_THUMBNLR if config["blender_thumbnailer_path"] == "" else config["blender_thumbnailer_path"]
|
BLENDER_THUMBNLR = BLENDER_THUMBNLR if config["blender_thumbnailer_path"] == "" else config["blender_thumbnailer_path"]
|
||||||
HIDE_HIDDEN_FILES = True if config["hide_hidden_files"] == "true" else False
|
HIDE_HIDDEN_FILES = True if config["hide_hidden_files"] in ["true", ""] else False
|
||||||
go_past_home = True if config["go_past_home"] == "true" else False
|
go_past_home = True if config["go_past_home"] in ["true", ""] else False
|
||||||
lock_folder = True if config["lock_folder"] == "true" else False
|
lock_folder = False if config["lock_folder"] in ["false", ""] else True
|
||||||
locked_folders = config["locked_folders"].split("::::")
|
locked_folders = config["locked_folders"].split("::::")
|
||||||
mplayer_options = config["mplayer_options"].split()
|
mplayer_options = config["mplayer_options"].split()
|
||||||
music_app = config["music_app"]
|
music_app = config["music_app"]
|
||||||
@ -76,7 +82,6 @@ class Settings:
|
|||||||
|
|
||||||
# Filters
|
# Filters
|
||||||
filters = settings["filters"]
|
filters = settings["filters"]
|
||||||
fmeshs = tuple(filters["meshs"])
|
|
||||||
fcode = tuple(filters["code"])
|
fcode = tuple(filters["code"])
|
||||||
fvideos = tuple(filters["videos"])
|
fvideos = tuple(filters["videos"])
|
||||||
foffice = tuple(filters["office"])
|
foffice = tuple(filters["office"])
|
||||||
|
Loading…
Reference in New Issue
Block a user