develop #2
@ -29,7 +29,7 @@ class Config(object):
|
|||||||
LOGIN_PATH = "OIDC" # Value can be OIDC or FLASK_LOGIN
|
LOGIN_PATH = "OIDC" # Value can be OIDC or FLASK_LOGIN
|
||||||
OIDC_TOKEN_TYPE_HINT = 'access_token'
|
OIDC_TOKEN_TYPE_HINT = 'access_token'
|
||||||
APP_REDIRECT_URI = "https%3A%2F%2Fwww.webfm.com%2F" # This path is submitted as the redirect URI in certain code flows
|
APP_REDIRECT_URI = "https%3A%2F%2Fwww.webfm.com%2F" # This path is submitted as the redirect URI in certain code flows
|
||||||
OIDC_CLIENT_SECRETS = ROOT_FILE_PTH + '/client_secrets.json'
|
OIDC_CLIENT_SECRETS = f'{ROOT_FILE_PTH}/client_secrets.json'
|
||||||
OIDC_ID_TOKEN_COOKIE_SECURE = True
|
OIDC_ID_TOKEN_COOKIE_SECURE = True
|
||||||
OIDC_REQUIRE_VERIFIED_EMAIL = False
|
OIDC_REQUIRE_VERIFIED_EMAIL = False
|
||||||
OIDC_USER_INFO_ENABLED = True
|
OIDC_USER_INFO_ENABLED = True
|
||||||
@ -38,14 +38,14 @@ class Config(object):
|
|||||||
'https://www.ssoapps.com/auth/realms/apps'
|
'https://www.ssoapps.com/auth/realms/apps'
|
||||||
]
|
]
|
||||||
|
|
||||||
STATIC_FPTH = ROOT_FILE_PTH + "/static"
|
STATIC_FPTH = f"{ROOT_FILE_PTH}/static"
|
||||||
REL_THUMBS_PTH = "static/imgs/thumbnails" # Used for flask thumbnail return
|
REL_THUMBS_PTH = "static/imgs/thumbnails" # Used for flask thumbnail return
|
||||||
|
|
||||||
# We are overiding some of the the shellmen view settings with these to make it all work with flask.
|
# We are overiding some of the the shellmen view settings with these to make it all work with flask.
|
||||||
# These are passed along to the shellmen view from the Routes file upon the window controller creation.
|
# These are passed along to the shellmen view from the Routes file upon the window controller creation.
|
||||||
ABS_THUMBS_PTH = STATIC_FPTH + "/imgs/thumbnails" # Used for thumbnail generation
|
ABS_THUMBS_PTH = f"{STATIC_FPTH}/imgs/thumbnails" # Used for thumbnail generation
|
||||||
REMUX_FOLDER = STATIC_FPTH + "/remuxs" # Remuxed files folder
|
REMUX_FOLDER = f"{STATIC_FPTH}/remuxs" # Remuxed files folder
|
||||||
FFMPG_THUMBNLR = STATIC_FPTH + "/ffmpegthumbnailer" # Thumbnail generator binary
|
FFMPG_THUMBNLR = f"{STATIC_FPTH}/ffmpegthumbnailer" # Thumbnail generator binary
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -67,11 +67,11 @@ def listFiles(_hash = None):
|
|||||||
msg = "Log in with an Admin privlidged user to view the requested path!"
|
msg = "Log in with an Admin privlidged user to view the requested path!"
|
||||||
is_locked = view.is_folder_locked(_hash)
|
is_locked = view.is_folder_locked(_hash)
|
||||||
if is_locked and not oidc.user_loggedin:
|
if is_locked and not oidc.user_loggedin:
|
||||||
return msgHandler.createMessageJSON("danger", msg)
|
return msgHandler.create_JSON_message("danger", msg)
|
||||||
elif is_locked and oidc.user_loggedin:
|
elif is_locked and oidc.user_loggedin:
|
||||||
isAdmin = oidc.user_getfield("isAdmin")
|
isAdmin = oidc.user_getfield("isAdmin")
|
||||||
if isAdmin != "yes" :
|
if isAdmin != "yes" :
|
||||||
return msgHandler.createMessageJSON("danger", msg)
|
return msgHandler.create_JSON_message("danger", msg)
|
||||||
|
|
||||||
if dot_dots[0][1] != _hash and dot_dots[1][1] != _hash:
|
if dot_dots[0][1] != _hash and dot_dots[1][1] != _hash:
|
||||||
path = view.get_path_part_from_hash(_hash)
|
path = view.get_path_part_from_hash(_hash)
|
||||||
@ -80,7 +80,7 @@ def listFiles(_hash = None):
|
|||||||
error_msg = view.get_error_message()
|
error_msg = view.get_error_message()
|
||||||
if error_msg != None:
|
if error_msg != None:
|
||||||
view.unset_error_message()
|
view.unset_error_message()
|
||||||
return msgHandler.createMessageJSON("danger", error_msg)
|
return msgHandler.create_JSON_message("danger", error_msg)
|
||||||
|
|
||||||
|
|
||||||
sub_path = view.get_current_sub_path()
|
sub_path = view.get_current_sub_path()
|
||||||
@ -92,7 +92,7 @@ def listFiles(_hash = None):
|
|||||||
date = current_directory[startIndex:endIndex]
|
date = current_directory[startIndex:endIndex]
|
||||||
video_data = tmdb.search(title, date)
|
video_data = tmdb.search(title, date)
|
||||||
background_url = video_data[0]["backdrop_path"]
|
background_url = video_data[0]["backdrop_path"]
|
||||||
background_pth = view.get_current_directory() + "/" + "000.jpg"
|
background_pth = f"{view.get_current_directory()}/000.jpg"
|
||||||
|
|
||||||
if not os.path.isfile(background_pth):
|
if not os.path.isfile(background_pth):
|
||||||
r = requests.get(background_url, stream = True)
|
r = requests.get(background_url, stream = True)
|
||||||
@ -116,20 +116,17 @@ def listFiles(_hash = None):
|
|||||||
return files
|
return files
|
||||||
else:
|
else:
|
||||||
msg = "Can't manage the request type..."
|
msg = "Can't manage the request type..."
|
||||||
return msgHandler.createMessageJSON("danger", msg)
|
return msgHandler.create_JSON_message("danger", msg)
|
||||||
|
|
||||||
@app.route('/api/get-posters', methods=['GET', 'POST'])
|
@app.route('/api/get-posters', methods=['GET', 'POST'])
|
||||||
def getPosters():
|
def getPosters():
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
view = get_window_controller().get_window(1).get_view(0)
|
view = get_window_controller().get_window(1).get_view(0)
|
||||||
videos = view.get_videos()
|
videos = view.get_videos()
|
||||||
|
|
||||||
print(videos)
|
|
||||||
# tmdb.search("Matrix")
|
|
||||||
return videos
|
return videos
|
||||||
else:
|
else:
|
||||||
msg = "Can't manage the request type..."
|
msg = "Can't manage the request type..."
|
||||||
return msgHandler.createMessageJSON("danger", msg)
|
return msgHandler.create_JSON_message("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 fileManagerAction(_type, _hash = None):
|
||||||
@ -138,7 +135,7 @@ def fileManagerAction(_type, _hash = None):
|
|||||||
if _type == "reset-path" and _hash == "None":
|
if _type == "reset-path" and _hash == "None":
|
||||||
view.set_to_home()
|
view.set_to_home()
|
||||||
msg = "Returning to home directory..."
|
msg = "Returning to home directory..."
|
||||||
return msgHandler.createMessageJSON("success", msg)
|
return msgHandler.create_JSON_message("success", msg)
|
||||||
|
|
||||||
folder = view.get_current_directory()
|
folder = view.get_current_directory()
|
||||||
file = view.get_path_part_from_hash(_hash)
|
file = view.get_path_part_from_hash(_hash)
|
||||||
@ -146,34 +143,39 @@ def fileManagerAction(_type, _hash = None):
|
|||||||
logger.debug(fpath)
|
logger.debug(fpath)
|
||||||
|
|
||||||
if _type == "files":
|
if _type == "files":
|
||||||
return send_from_directory(folder, file)
|
logger.debug(f"Downloading:\n\tDirectory: {folder}\n\tFile: {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.remuxVideo(_hash, fpath)
|
good_result = view.remux_video(_hash, fpath)
|
||||||
if good_result:
|
if good_result:
|
||||||
return '{"path":"static/remuxs/' + _hash + '.mp4"}'
|
return '{"path":"static/remuxs/' + _hash + '.mp4"}'
|
||||||
else:
|
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 msgHandler.createMessageJSON("success", msg)
|
return msgHandler.create_JSON_message("success", msg)
|
||||||
if _type == "run-locally":
|
|
||||||
msg = "Opened media..."
|
if _type == "remux":
|
||||||
view.openFilelocally(fpath)
|
stream_target = view.remux_video(_hash, fpath)
|
||||||
return msgHandler.createMessageJSON("success", msg)
|
|
||||||
|
|
||||||
|
|
||||||
# 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!
|
||||||
msg = "Log in with an Admin privlidged user to do this action!"
|
msg = "Log in with an Admin privlidged user to do this action!"
|
||||||
if not oidc.user_loggedin:
|
if not oidc.user_loggedin:
|
||||||
return msgHandler.createMessageJSON("danger", msg)
|
return msgHandler.create_JSON_message("danger", msg)
|
||||||
elif oidc.user_loggedin:
|
elif oidc.user_loggedin:
|
||||||
isAdmin = oidc.user_getfield("isAdmin")
|
isAdmin = oidc.user_getfield("isAdmin")
|
||||||
if isAdmin != "yes" :
|
if isAdmin != "yes" :
|
||||||
return msgHandler.createMessageJSON("danger", msg)
|
return msgHandler.create_JSON_message("danger", msg)
|
||||||
|
|
||||||
|
|
||||||
|
if _type == "run-locally":
|
||||||
|
msg = "Opened media..."
|
||||||
|
view.open_file_locally(fpath)
|
||||||
|
return msgHandler.create_JSON_message("success", msg)
|
||||||
|
|
||||||
if _type == "delete":
|
if _type == "delete":
|
||||||
try:
|
try:
|
||||||
msg = f"[Success] Deleted the file/folder -->: {file} !"
|
msg = f"[Success] Deleted the file/folder -->: {file} !"
|
||||||
@ -181,10 +183,10 @@ def fileManagerAction(_type, _hash = None):
|
|||||||
os.unlink(fpath)
|
os.unlink(fpath)
|
||||||
else:
|
else:
|
||||||
shutil.rmtree(fpath)
|
shutil.rmtree(fpath)
|
||||||
return msgHandler.createMessageJSON("success", msg)
|
return msgHandler.create_JSON_message("success", msg)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
msg = "[Error] Unable to delete the file/folder...."
|
msg = "[Error] Unable to delete the file/folder...."
|
||||||
return msgHandler.createMessageJSON("danger", msg)
|
return msgHandler.create_JSON_message("danger", msg)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/list-favorites', methods=['GET', 'POST'])
|
@app.route('/api/list-favorites', methods=['GET', 'POST'])
|
||||||
@ -198,7 +200,7 @@ def listFavorites():
|
|||||||
return '{"faves_list":' + json.dumps(faves) + '}'
|
return '{"faves_list":' + json.dumps(faves) + '}'
|
||||||
else:
|
else:
|
||||||
msg = "Can't manage the request type..."
|
msg = "Can't manage the request type..."
|
||||||
return msgHandler.createMessageJSON("danger", msg)
|
return msgHandler.create_JSON_message("danger", msg)
|
||||||
|
|
||||||
@app.route('/api/load-favorite/<_id>', methods=['GET', 'POST'])
|
@app.route('/api/load-favorite/<_id>', methods=['GET', 'POST'])
|
||||||
def loadFavorite(_id):
|
def loadFavorite(_id):
|
||||||
@ -212,10 +214,10 @@ def loadFavorite(_id):
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(repr(e))
|
print(repr(e))
|
||||||
msg = "Incorrect Favorites ID..."
|
msg = "Incorrect Favorites ID..."
|
||||||
return msgHandler.createMessageJSON("danger", msg)
|
return msgHandler.create_JSON_message("danger", msg)
|
||||||
else:
|
else:
|
||||||
msg = "Can't manage the request type..."
|
msg = "Can't manage the request type..."
|
||||||
return msgHandler.createMessageJSON("danger", msg)
|
return msgHandler.create_JSON_message("danger", msg)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/manage-favorites/<_action>', methods=['GET', 'POST'])
|
@app.route('/api/manage-favorites/<_action>', methods=['GET', 'POST'])
|
||||||
@ -235,13 +237,13 @@ def manageFavorites(_action):
|
|||||||
msg = "Deleted from Favorites successfully..."
|
msg = "Deleted from Favorites successfully..."
|
||||||
else:
|
else:
|
||||||
msg = "Couldn't handle action for favorites item..."
|
msg = "Couldn't handle action for favorites item..."
|
||||||
return msgHandler.createMessageJSON("danger", msg)
|
return msgHandler.create_JSON_message("danger", msg)
|
||||||
|
|
||||||
db.session.commit()
|
db.session.commit()
|
||||||
return msgHandler.createMessageJSON("success", msg)
|
return msgHandler.create_JSON_message("success", msg)
|
||||||
else:
|
else:
|
||||||
msg = "Can't manage the request type..."
|
msg = "Can't manage the request type..."
|
||||||
return msgHandler.createMessageJSON("danger", msg)
|
return msgHandler.create_JSON_message("danger", msg)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/api/create/<_type>', methods=['GET', 'POST'])
|
@app.route('/api/create/<_type>', methods=['GET', 'POST'])
|
||||||
@ -249,42 +251,42 @@ def create_item(_type = None):
|
|||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
msg = "Log in with an Admin privlidged user to upload files!"
|
msg = "Log in with an Admin privlidged user to upload files!"
|
||||||
if not oidc.user_loggedin:
|
if not oidc.user_loggedin:
|
||||||
return msgHandler.createMessageJSON("danger", msg)
|
return msgHandler.create_JSON_message("danger", msg)
|
||||||
elif oidc.user_loggedin:
|
elif oidc.user_loggedin:
|
||||||
isAdmin = oidc.user_getfield("isAdmin")
|
isAdmin = oidc.user_getfield("isAdmin")
|
||||||
if isAdmin != "yes" :
|
if isAdmin != "yes" :
|
||||||
return msgHandler.createMessageJSON("danger", msg)
|
return msgHandler.create_JSON_message("danger", msg)
|
||||||
|
|
||||||
TYPE = _type.strip()
|
TYPE = _type.strip()
|
||||||
FNAME = str(request.values['fname']).strip()
|
FNAME = str(request.values['fname']).strip()
|
||||||
|
|
||||||
if not re.fullmatch(valid_fname_pat, FNAME):
|
if not re.fullmatch(valid_fname_pat, FNAME):
|
||||||
msg = "A new item name can only contain alphanumeric, -, _, |, [], (), or spaces and must be minimum of 4 and max of 20 characters..."
|
msg = "A new item name can only contain alphanumeric, -, _, |, [], (), or spaces and must be minimum of 4 and max of 20 characters..."
|
||||||
return msgHandler.createMessageJSON("danger", msg)
|
return msgHandler.create_JSON_message("danger", msg)
|
||||||
|
|
||||||
view = get_window_controller().get_window(1).get_view(0)
|
view = get_window_controller().get_window(1).get_view(0)
|
||||||
folder = view.get_current_directory()
|
folder = view.get_current_directory()
|
||||||
new_item = folder + '/' + FNAME
|
new_item = f"{folder}/{FNAME}"
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if TYPE == "dir":
|
if TYPE == "dir":
|
||||||
os.mkdir(new_item)
|
os.mkdir(new_item)
|
||||||
elif TYPE == "file":
|
elif TYPE == "file":
|
||||||
open(new_item + ".txt", 'a').close()
|
open(f"{new_item}.txt", 'a').close()
|
||||||
else:
|
else:
|
||||||
msg = "Couldn't handle action type for api create..."
|
msg = "Couldn't handle action type for api create..."
|
||||||
return msgHandler.createMessageJSON("danger", msg)
|
return msgHandler.create_JSON_message("danger", msg)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(repr(e))
|
print(repr(e))
|
||||||
msg = "Couldn't create file/folder. An unexpected error occured..."
|
msg = "Couldn't create file/folder. An unexpected error occured..."
|
||||||
return msgHandler.createMessageJSON("danger", msg)
|
return msgHandler.create_JSON_message("danger", msg)
|
||||||
|
|
||||||
|
|
||||||
msg = "[Success] created the file/dir..."
|
msg = "[Success] created the file/dir..."
|
||||||
return msgHandler.createMessageJSON("success", msg)
|
return msgHandler.create_JSON_message("success", msg)
|
||||||
else:
|
else:
|
||||||
msg = "Can't manage the request type..."
|
msg = "Can't manage the request type..."
|
||||||
return msgHandler.createMessageJSON("danger", msg)
|
return msgHandler.create_JSON_message("danger", msg)
|
||||||
|
|
||||||
|
|
||||||
@app.route('/upload', methods=['GET', 'POST'])
|
@app.route('/upload', methods=['GET', 'POST'])
|
||||||
@ -292,15 +294,15 @@ def upload():
|
|||||||
if request.method == 'POST' and len(request.files) > 0:
|
if request.method == 'POST' and len(request.files) > 0:
|
||||||
msg = "Log in with an Admin privlidged user to upload files!"
|
msg = "Log in with an Admin privlidged user to upload files!"
|
||||||
if not oidc.user_loggedin:
|
if not oidc.user_loggedin:
|
||||||
return msgHandler.createMessageJSON("danger", msg)
|
return msgHandler.create_JSON_message("danger", msg)
|
||||||
elif oidc.user_loggedin:
|
elif oidc.user_loggedin:
|
||||||
isAdmin = oidc.user_getfield("isAdmin")
|
isAdmin = oidc.user_getfield("isAdmin")
|
||||||
if isAdmin != "yes" :
|
if isAdmin != "yes" :
|
||||||
return msgHandler.createMessageJSON("danger", msg)
|
return msgHandler.create_JSON_message("danger", msg)
|
||||||
|
|
||||||
view = get_window_controller().get_window(1).get_view(0)
|
view = get_window_controller().get_window(1).get_view(0)
|
||||||
folder = view.get_current_directory()
|
folder = view.get_current_directory()
|
||||||
UPLOADS_PTH = folder + '/'
|
UPLOADS_PTH = f'{folder}/'
|
||||||
files = UploadSet('files', ALL, default_dest=lambda x: UPLOADS_PTH)
|
files = UploadSet('files', ALL, default_dest=lambda x: UPLOADS_PTH)
|
||||||
configure_uploads(app, files)
|
configure_uploads(app, files)
|
||||||
|
|
||||||
@ -310,10 +312,10 @@ def upload():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(repr(e))
|
print(repr(e))
|
||||||
msg = "[Error] Failed to upload some or all of the file(s)..."
|
msg = "[Error] Failed to upload some or all of the file(s)..."
|
||||||
return msgHandler.createMessageJSON("danger", msg)
|
return msgHandler.create_JSON_message("danger", msg)
|
||||||
|
|
||||||
msg = "[Success] Uploaded file(s)..."
|
msg = "[Success] Uploaded file(s)..."
|
||||||
return msgHandler.createMessageJSON("success", msg)
|
return msgHandler.create_JSON_message("success", msg)
|
||||||
else:
|
else:
|
||||||
msg = "Can't manage the request type..."
|
msg = "Can't manage the request type..."
|
||||||
return msgHandler.createMessageJSON("danger", msg)
|
return msgHandler.create_JSON_message("danger", msg)
|
||||||
|
@ -10,5 +10,5 @@ class MessageHandler:
|
|||||||
print("MessageHandler initialized...")
|
print("MessageHandler initialized...")
|
||||||
|
|
||||||
|
|
||||||
def createMessageJSON(self, type, text):
|
def create_JSON_message(self, type, text):
|
||||||
return '{"message": { "type": "' + type + '", "text": "' + text + '" } }'
|
return '{"message": { "type": "' + type + '", "text": "' + text + '" } }'
|
||||||
|
@ -11,7 +11,7 @@ class WindowController:
|
|||||||
if window.id == win_id:
|
if window.id == win_id:
|
||||||
return window
|
return window
|
||||||
|
|
||||||
raise("No Window by ID {} found!".format(win_id))
|
raise(f"No Window by ID {win_id} found!")
|
||||||
|
|
||||||
def get_windows(self):
|
def get_windows(self):
|
||||||
return self.windows
|
return self.windows
|
||||||
@ -19,7 +19,7 @@ class WindowController:
|
|||||||
def add_window(self):
|
def add_window(self):
|
||||||
window = Window()
|
window = Window()
|
||||||
window.id = len(self.windows) + 1
|
window.id = len(self.windows) + 1
|
||||||
window.name = "window_" + str(window.id)
|
window.name = f"window_{window.id}"
|
||||||
window.create_view()
|
window.create_view()
|
||||||
self.windows.append(window)
|
self.windows.append(window)
|
||||||
|
|
||||||
@ -48,10 +48,10 @@ class WindowController:
|
|||||||
def list_windows(self):
|
def list_windows(self):
|
||||||
for window in self.windows:
|
for window in self.windows:
|
||||||
print("\n[ Window ]")
|
print("\n[ Window ]")
|
||||||
print("ID: " + str(window.id))
|
print("ID: {window.id}")
|
||||||
print("Name: " + window.name)
|
print("Name: {window.name}")
|
||||||
print("Nickname: " + window.nickname)
|
print("Nickname: {window.nickname}")
|
||||||
print("View Count: " + str( len(window.views) ))
|
print("View Count: {len(window.views)}")
|
||||||
|
|
||||||
|
|
||||||
def list_views_from_window(self, win_id):
|
def list_views_from_window(self, win_id):
|
||||||
|
@ -157,7 +157,7 @@ class View(Settings, Launcher, Path):
|
|||||||
if not os.path.exists(hashImgPth) :
|
if not os.path.exists(hashImgPth) :
|
||||||
fullPath = join(current_directory, video[0])
|
fullPath = join(current_directory, video[0])
|
||||||
self.logger.debug(f"Hash Path: {hashImgPth}\nFile Path: {fullPath}")
|
self.logger.debug(f"Hash Path: {hashImgPth}\nFile Path: {fullPath}")
|
||||||
self.generateVideoThumbnail(fullPath, hashImgPth)
|
self.generate_video_thumbnail(fullPath, hashImgPth)
|
||||||
|
|
||||||
return videos_set
|
return videos_set
|
||||||
|
|
||||||
|
@ -9,7 +9,7 @@ import os, subprocess, threading
|
|||||||
|
|
||||||
|
|
||||||
class Launcher:
|
class Launcher:
|
||||||
def openFilelocally(self, file):
|
def open_file_locally(self, file):
|
||||||
lowerName = file.lower()
|
lowerName = file.lower()
|
||||||
command = []
|
command = []
|
||||||
|
|
||||||
@ -38,14 +38,25 @@ class Launcher:
|
|||||||
subprocess.Popen(command, start_new_session=True, stdout=DEVNULL, stderr=DEVNULL, close_fds=True)
|
subprocess.Popen(command, start_new_session=True, stdout=DEVNULL, stderr=DEVNULL, close_fds=True)
|
||||||
|
|
||||||
|
|
||||||
def remuxVideo(self, hash, file):
|
def create_stream(self, hash, file):
|
||||||
remux_vid_pth = self.REMUX_FOLDER + "/" + hash + ".mp4"
|
# ffmpeg -re -stream_loop -1 -i "<video file path>" -f rtsp -rtsp_transport udp rtsp://localhost:8554/live.stream
|
||||||
|
command = ["ffmpeg", "-re", "-stream_loop", "-1", "-i", file, "-f", "rtsp", "-rtsp_transport", "udp", "rtsp://www.webfm.com:8554/{hash}.stream",]
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(command)
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.debug(message)
|
||||||
|
self.logger.debug(e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def remux_video(self, hash, file):
|
||||||
|
remux_vid_pth = f"{self.REMUX_FOLDER}/{hash}.mp4"
|
||||||
self.logger.debug(remux_vid_pth)
|
self.logger.debug(remux_vid_pth)
|
||||||
|
|
||||||
if not os.path.isfile(remux_vid_pth):
|
if not os.path.isfile(remux_vid_pth):
|
||||||
self.check_remux_space()
|
self.check_remux_space()
|
||||||
|
|
||||||
command = ["ffmpeg", "-i", file, "-hide_banner", "-movflags", "+faststart"]
|
command = ["ffmpeg", "-i", file, "-hide_banner", "-movflags", "+faststart", "-sws_flags", "fast_bilinear",]
|
||||||
if file.endswith("mkv"):
|
if file.endswith("mkv"):
|
||||||
command += ["-codec", "copy", "-strict", "-2"]
|
command += ["-codec", "copy", "-strict", "-2"]
|
||||||
if file.endswith("avi"):
|
if file.endswith("avi"):
|
||||||
@ -67,7 +78,7 @@ class Launcher:
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
|
|
||||||
def generateVideoThumbnail(self, fullPath, hashImgPth):
|
def generate_video_thumbnail(self, fullPath, hashImgPth):
|
||||||
try:
|
try:
|
||||||
proc = subprocess.Popen([self.FFMPG_THUMBNLR, "-t", "65%", "-s", "300", "-c", "jpg", "-i", fullPath, "-o", hashImgPth])
|
proc = subprocess.Popen([self.FFMPG_THUMBNLR, "-t", "65%", "-s", "300", "-c", "jpg", "-i", fullPath, "-o", hashImgPth])
|
||||||
proc.wait()
|
proc.wait()
|
||||||
@ -83,7 +94,7 @@ class Launcher:
|
|||||||
self.logger.debug(e)
|
self.logger.debug(e)
|
||||||
return
|
return
|
||||||
|
|
||||||
usage = self.getRemuxFolderUsage(self.REMUX_FOLDER)
|
usage = self.get_remux_folder_usage(self.REMUX_FOLDER)
|
||||||
if usage > limit:
|
if usage > limit:
|
||||||
files = os.listdir(self.REMUX_FOLDER)
|
files = os.listdir(self.REMUX_FOLDER)
|
||||||
for file in files:
|
for file in files:
|
||||||
@ -91,7 +102,7 @@ class Launcher:
|
|||||||
os.unlink(fp)
|
os.unlink(fp)
|
||||||
|
|
||||||
|
|
||||||
def getRemuxFolderUsage(self, start_path = "."):
|
def get_remux_folder_usage(self, start_path = "."):
|
||||||
total_size = 0
|
total_size = 0
|
||||||
for dirpath, dirnames, filenames in os.walk(start_path):
|
for dirpath, dirnames, filenames in os.walk(start_path):
|
||||||
for f in filenames:
|
for f in filenames:
|
||||||
|
@ -14,21 +14,21 @@ class Settings:
|
|||||||
logger = None
|
logger = None
|
||||||
|
|
||||||
USER_HOME = path.expanduser('~')
|
USER_HOME = path.expanduser('~')
|
||||||
CONFIG_PATH = USER_HOME + "/.config/webfm"
|
CONFIG_PATH = f"{USER_HOME}/.config/webfm"
|
||||||
CONFIG_FILE = CONFIG_PATH + "/settings.json"
|
CONFIG_FILE = f"{CONFIG_PATH}/settings.json"
|
||||||
HIDE_HIDDEN_FILES = True
|
HIDE_HIDDEN_FILES = True
|
||||||
|
|
||||||
GTK_ORIENTATION = 1 # HORIZONTAL (0) VERTICAL (1)
|
GTK_ORIENTATION = 1 # HORIZONTAL (0) VERTICAL (1)
|
||||||
DEFAULT_ICONS = CONFIG_PATH + "/icons"
|
DEFAULT_ICONS = f"{CONFIG_PATH}/icons"
|
||||||
DEFAULT_ICON = DEFAULT_ICONS + "/text.png"
|
DEFAULT_ICON = f"{DEFAULT_ICONS}/text.png"
|
||||||
FFMPG_THUMBNLR = CONFIG_PATH + "/ffmpegthumbnailer" # Thumbnail generator binary
|
FFMPG_THUMBNLR = f"{CONFIG_PATH}/ffmpegthumbnailer" # Thumbnail generator binary
|
||||||
REMUX_FOLDER = USER_HOME + "/.remuxs" # Remuxed files folder
|
REMUX_FOLDER = f"{USER_HOME}/.remuxs" # Remuxed files folder
|
||||||
|
|
||||||
STEAM_BASE_URL = "https://steamcdn-a.akamaihd.net/steam/apps/"
|
STEAM_BASE_URL = "https://steamcdn-a.akamaihd.net/steam/apps/"
|
||||||
ICON_DIRS = ["/usr/share/pixmaps", "/usr/share/icons", USER_HOME + "/.icons" ,]
|
ICON_DIRS = ["/usr/share/pixmaps", "/usr/share/icons", f"{USER_HOME}/.icons" ,]
|
||||||
BASE_THUMBS_PTH = USER_HOME + "/.thumbnails" # Used for thumbnail generation
|
BASE_THUMBS_PTH = f"{USER_HOME}/.thumbnails" # Used for thumbnail generation
|
||||||
ABS_THUMBS_PTH = BASE_THUMBS_PTH + "/normal" # Used for thumbnail generation
|
ABS_THUMBS_PTH = f"{BASE_THUMBS_PTH}/normal" # Used for thumbnail generation
|
||||||
STEAM_ICONS_PTH = BASE_THUMBS_PTH + "/steam_icons"
|
STEAM_ICONS_PTH = f"{BASE_THUMBS_PTH}/steam_icons"
|
||||||
CONTAINER_ICON_WH = [128, 128]
|
CONTAINER_ICON_WH = [128, 128]
|
||||||
VIDEO_ICON_WH = [128, 64]
|
VIDEO_ICON_WH = [128, 64]
|
||||||
SYS_ICON_WH = [56, 56]
|
SYS_ICON_WH = [56, 56]
|
||||||
@ -74,7 +74,7 @@ class Settings:
|
|||||||
# Filters
|
# Filters
|
||||||
fvideos = ('.mkv', '.avi', '.flv', '.mov', '.m4v', '.mpg', '.wmv', '.mpeg', '.mp4', '.webm')
|
fvideos = ('.mkv', '.avi', '.flv', '.mov', '.m4v', '.mpg', '.wmv', '.mpeg', '.mp4', '.webm')
|
||||||
foffice = ('.doc', '.docx', '.xls', '.xlsx', '.xlt', '.xltx', '.xlm', '.ppt', 'pptx', '.pps', '.ppsx', '.odt', '.rtf')
|
foffice = ('.doc', '.docx', '.xls', '.xlsx', '.xlt', '.xltx', '.xlm', '.ppt', 'pptx', '.pps', '.ppsx', '.odt', '.rtf')
|
||||||
fimages = ('.png', '.jpg', '.jpeg', '.gif', '.ico', '.tga')
|
fimages = ('.png', '.jpg', '.jpeg', '.gif', '.ico', '.tga', '.webp')
|
||||||
ftext = ('.txt', '.text', '.sh', '.cfg', '.conf')
|
ftext = ('.txt', '.text', '.sh', '.cfg', '.conf')
|
||||||
fmusic = ('.psf', '.mp3', '.ogg', '.flac', '.m4a')
|
fmusic = ('.psf', '.mp3', '.ogg', '.flac', '.m4a')
|
||||||
fpdf = ('.pdf')
|
fpdf = ('.pdf')
|
||||||
|
BIN
src/core/utils/shellfm/windows/view/utils/rtsp-simple-server
Executable file
BIN
src/core/utils/shellfm/windows/view/utils/rtsp-simple-server
Executable file
Binary file not shown.
281
src/core/utils/shellfm/windows/view/utils/rtsp-simple-server.yml
Normal file
281
src/core/utils/shellfm/windows/view/utils/rtsp-simple-server.yml
Normal file
@ -0,0 +1,281 @@
|
|||||||
|
|
||||||
|
###############################################
|
||||||
|
# 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 number allows a wider throughput, a lower number 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",
|
||||||
|
# "action": "read|publish"
|
||||||
|
# "query": "url's raw 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 handshake 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: no
|
||||||
|
# Address of the RTMP listener.
|
||||||
|
rtmpAddress: :1935
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
# HLS parameters
|
||||||
|
|
||||||
|
# Disable support for the HLS protocol.
|
||||||
|
hlsDisable: no
|
||||||
|
# Address of the HLS listener.
|
||||||
|
hlsAddress: :8888
|
||||||
|
# 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: '*'
|
||||||
|
# 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
|
||||||
|
|
||||||
|
###############################################
|
||||||
|
# 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
|
||||||
|
# * 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
|
||||||
|
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 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:
|
||||||
|
|
||||||
|
# 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
|
Loading…
Reference in New Issue
Block a user