Rework of MPV Commander into a new project

This commit is contained in:
Bradley Bickford 2025-09-28 10:34:16 -04:00
commit f5e52da613
5 changed files with 356 additions and 0 deletions

77
config.json Normal file
View File

@ -0,0 +1,77 @@
{
"instance_name": "Test",
"db_name": "videocommander.db",
"video_tool": "ffplay",
"buttons": [
{
"name": "Goofy",
"video_url": "videos/goofy.mp4",
"video_tool_arguments": [
"-fs",
"-loop",
"0"
],
"default": true
},
{
"name": "Neutron Stars",
"video_url": "videos/neutronstars.mp4",
"video_tool_arguments": [
"-fs",
"-loop",
"0"
],
"default": false
},
{
"name": "Gimme The Yeet Boys",
"video_url": "videos/yeet.mp4",
"video_tool_arguments": [
"-fs",
"-loop",
"0"
],
"default": false
},
{
"name": "Hey Bender",
"video_url": "videos/bender.mp4",
"video_tool_arguments": [
"-fs",
"-loop",
"0"
],
"default": false
},
{
"name": "Dog of Wisdom",
"video_url": "videos/dog.mp4",
"video_tool_arguments": [
"-fs",
"-loop",
"0"
],
"default": false
},
{
"name": "Nyan Cat",
"video_url": "videos/nyan.mp4",
"video_tool_arguments": [
"-fs",
"-loop",
"0"
],
"default": false
},
{
"name": "Smooth Jazz Nyan Cat",
"video_url": "videos/smoothjazznyan.mp4",
"video_tool_arguments": [
"-fs",
"-loop",
"0"
],
"default": false
}
]
}

51
index.html Normal file
View File

@ -0,0 +1,51 @@
<!DOCTYPE html>
<html>
<head>
<title>Video Commander</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
</head>
<script>
async function onclick_processor(name) {
try {
const response = await fetch(
window.location.origin + "/stream",
{
method: "POST",
"headers": {
"Content-Type": "application/json"
},
body: JSON.stringify({ "name": name })
}
)
if (!response.ok) {
throw new Error(`Response status: ${response.status}`)
}
const json = await response.json()
console.log(json)
} catch (error) {
console.log(error.message)
}
}
</script>
<body>
<div class="container">
<h1 class="text-center">Video Commander Instance {{ instance_name }}</h1>
<h3 class="text-center">Which video stream do you want to show?</h3>
{% for index in range(buttons | length) %}
{% if index % 3 == 0 %}
<div class="row">
{% endif %}
{% set current_name = buttons[index]["name"] %}
<button class="col-sm-4 btn-default p-3 my-1" onclick="onclick_processor('{{ current_name }}')">{{ current_name }}</button>
{% if index + 1 == buttons | length or (index + 1) % 3 == 0 %}
</div>
{% endif %}
{% endfor %}
</div>
</body>
</html>

73
video_dbbuilder.py Normal file
View File

@ -0,0 +1,73 @@
import sqlite3
import json
import sys
destroy_table = r'''
DROP TABLE IF EXISTS buttons
'''
make_table = r'''
CREATE TABLE IF NOT EXISTS buttons
(button_name TEXT PRIMARY KEY,
video_url TEXT NOT NULL,
video_arguments TEXT NOT NULL,
is_default BOOLEAN NOT NULL CHECK (is_default IN (0, 1)),
is_active BOOLEAN NOT NULL CHECK (is_active IN (0, 1)))
'''
insert_row = r'''
INSERT INTO buttons VALUES
(?, ?, ?, ?, ?)
'''
with open("config.json", "r") as config_file:
config_json = json.loads(config_file.read())
config_issues = ""
if not "instance_name" in config_json.keys():
config_issues = config_issues + "The config item instance_name is required\n"
for i in range(len(config_json["buttons"])):
if not "name" in config_json["buttons"][i].keys():
config_issues = config_issues + "Config index {index} is missing the required name parameter\n".format(index = (i + 1))
if not "video_url" in config_json["buttons"][i].keys():
config_issues = config_issues + "Config index {index} is missing the required video_url parameter\n".format(index = (i + 1))
if not "default" in config_json["buttons"][i].keys():
config_issues = config_issues + "Config index {index} is missing the required default parameter\n".format(index = (i + 1))
all_defaults = [element for element in config_json["buttons"] if element["default"]]
if len(all_defaults) > 1:
config_issues = config_issues + "More than one button config is set as default, only one can be the default, remove one of these: {button_list}\n".format(
button_list = ", ".join([element["name"] if "name" in element else "NAME MISSING" for element in all_defaults])
)
elif len(all_defaults) == 0:
config_issues = config_issues = "At least one button config must be set as the default\n"
if len(config_issues) != 0:
print("Config issues detected! Please correct these issues before trying again")
print(config_issues)
sys.exit(1)
db = sqlite3.connect(config_json["db_name"])
db.execute(destroy_table)
db.execute(make_table)
for button in config_json["buttons"]:
db.execute(insert_row, [
button["name"],
button["video_url"],
" ".join(button["video_tool_arguments"]) if len(button["video_tool_arguments"]) != 0 else "",
1 if button["default"] else 0,
1 if button["default"] else 0
])
db.commit()
db.close()

65
video_player.py Normal file
View File

@ -0,0 +1,65 @@
import os
import signal
import subprocess
import sqlite3
import time
import json
get_active_video = r'''
SELECT video_url, video_arguments FROM buttons
WHERE is_active = 1
LIMIT 1
'''
def play_video(previous_process, video_player_path, player_arguments, video_url):
if not previous_process is None:
os.killpg(os.getpgid(previous_process.pid), signal.SIGTERM)
return subprocess.Popen(
"{video_player} {arguments} {video_url}".format(
video_player = video_player_path,
arguments = player_arguments,
video_url = video_url
),
stdout = subprocess.PIPE,
stderr = subprocess.PIPE,
shell = True,
preexec_fn = os.setsid
)
def get_active_video_url(db):
cursor = db.execute(get_active_video)
return cursor.fetchall()[0]
with open("config.json", "r") as config_file:
config_json = json.loads(config_file.read())
current_process = None
current_video = None
db = sqlite3.connect(config_json["db_name"])
while True:
if current_process is None:
current_video = get_active_video_url(db)
current_process = play_video(
current_process,
config_json["video_tool"],
current_video[0],
current_video[1]
)
active_video = get_active_video_url(db)
if current_video[0] != active_video[0]:
current_video = active_video
current_process = play_video(
current_process,
config_json["video_tool"],
current_video[0],
current_video[1]
)
time.sleep(1)

90
video_webserver.py Normal file
View File

@ -0,0 +1,90 @@
from flask import Flask, request, jsonify, g
from jinja2 import Template
import json
import sys
import sqlite3
disable_all_active = r'''
UPDATE buttons SET is_active = 0
'''
set_active_by_name_sql = r'''
UPDATE buttons SET is_active = 1
WHERE button_name = ?
'''
with open("config.json", "r") as config_file:
config_json = json.loads(config_file.read())
app = Flask(__name__)
def get_db():
db = getattr(g, "_database", None)
if db is None:
db = g._database = sqlite3.connect(config_json["db_name"])
return db
def set_active_by_name(name):
db = get_db()
db.execute(disable_all_active)
db.execute(set_active_by_name, name)
db.commit()
@app.route("/")
def root_route():
with open("index.html", "r") as html_file:
template = Template(html_file.read())
return template.render(config_json), 200
@app.route("/stream", methods=["POST"])
def stream_route():
if request.content_type == 'application/json':
body = request.get_json()
if not "name" in body.keys():
error_response = {
'status': "ERROR",
"reason": "A name must be specified in the body of the request"
}
return jsonify(error_response), 400
resource_list = [element for element in config_json["buttons"] if element["name"] == body["name"]]
if(len(resource_list) == 0):
error_response = {
'status': "ERROR",
"reason": "The name {name} does not exist in config['buttons'], check your config".format(name = body["name"])
}
return jsonify(error_response), 400
db = get_db()
db.execute(disable_all_active)
db.execute(set_active_by_name_sql, [body["name"]])
db.commit()
return jsonify(resource_list[0]), 200
else:
error_response = {
'status': 'ERROR',
'reason': 'Posted body must be of content type application/json'
}
return jsonify(error_response)
@app.teardown_appcontext
def close_connection(exception):
db = getattr(g, '_database', None)
if db is not None:
db.close()
if __name__ == '__main__':
app.run(debug=True, host="0.0.0.0", port=1801)