Commit 51ee691c authored by nextime's avatar nextime

Bidirectional communication and status persistance implemented

parent 9ca09ce5
......@@ -37,9 +37,13 @@ config.read('shmcamstudio.conf')
# Inter thread communication
qcore = queue.Queue() # IN -> core application (studio.py)
qobs = queue.Queue() # IN -> OBS module
qweb = queue.Queue() # IN -> Web Panel
qpnl = queue.Queue() # IN -> tkinter panel
builtins.qcore = qcore
builtins.qobs = qobs
builtins.qweb = qweb
builtins.qpnl = qpnl
builtins.config = config
# Setup logging
......
......@@ -46,23 +46,29 @@ status.tease.enable = 5,3
[OUTPUT:smstefy]
obs = slut
scene = live SFW
source.title = 5
source.closed = 4
source.tease = 3
status.open.disable = 5,4,3
status.close.disable = 3
status.close.enable = 5,4
status.tease.disable = 4
status.tease.enable = 5,3
[OUTPUT:shine]
obs = slut
scene = SHINE
source.title = 4
source.closed = 2
source.tease = 3
status.open.disable = 4,2,3
status.close.disable = 3
status.close.enable = 4,2
status.tease.disable = 2
status.tease.enable = 4,3
#[OUTPUT:livejasmin]
#obs = slut
#scene = LIVE JM
#source_title = 4,5
#source_closed = 3
#source_tease = 2
#status.open.disable = 2,3,4,5
##status.close.disable = 2
#status.close.enable = 3,4,5
#status.tease.disable = 3
#status.tease.enable = 2,4,5
# BUTTON:<rownum>:<button_name>
......@@ -145,13 +151,18 @@ action = open_all
color = teal
[BUTTON:3:scene_manual]
title = SCENE MANUAL
title = MANUAL
action = scene_manual
color = blue
color.manual = blue
color.automatic = red
title.manual = MANUAL
title.automatic = AUTO
feedback = status
[ACTION:scene_manual]
setstatus = manual
setstatus = change
[ACTION:private_shine]
......@@ -179,10 +190,10 @@ execute = /usr/local/bin/smblur_stefy
#execute = /usr/local/bin/smblur_jasmin
[ACTION:tease_all]
execute = /usr/local/bin/smblur_tease
execute = /usr/local/bin/smblur_teaseall
[ACTION:tease]
execute = /usr/local/bin/smblur_teaseall
execute = /usr/local/bin/smblur_tease
[ACTION:open_all]
execute = /usr/local/bin/smblur_clean
......
......@@ -71,10 +71,8 @@ class OBSOutput:
if hasattr(self, 'status.'+status+'.enable'):
self.enable(getattr(self, 'status.'+status+'.enable').split(','))
def updateStatus(self):
def _updateStatus(self):
if not self.obss.online:
self.status = False
logging.info('FOUND False')
return False
for inp in self.inputs.keys():
self.inputs[inp] = self.obss.getInputStatus(self.scene, inp)
......@@ -90,13 +88,17 @@ class OBSOutput:
if self.inputs[disabled]:
found = False
if found:
self.status = found
logging.info("FOUND "+found)
return found
logging.info('FOUND False')
self.status = found
return found
def updateStatus(self):
status = self._updateStatus()
if self.status != status:
self.status = status
logging.info('OUTPUT '+self.output+" FOUND "+str(self.status))
self.obss.queue.put({ 'event': 'OUTPUTCHANGE', 'data': {'server': self.obss.server, 'output': self.output, 'status': self.status }})
return self.status
def getStatus(self):
if not self.status:
self.status = self.updateStatus()
......@@ -221,6 +223,10 @@ def run_obs_controller():
for output in obs_outputs.values():
if output.obss.server == data['server'] and output.scene==data['scene'] and str(data['source']) in output.inputs.keys():
output.updateStatus()
if event == 'OUTPUTCHANGE':
#{ 'event': 'OUTPUTCHANGE', 'data': {'server': self.obss.server, 'output': self.output, 'status': self.status }}
logging.info("OUTPUTCHANGE: output "+data['output']+ " on obs "+data['server']+" is now "+str(data['status']))
qcore.put(task)
time.sleep(.01)
......
......@@ -71,6 +71,9 @@ def create_panel_gui():
helv36 = tkFont.Font(family='Helvetica', size=config.get("Tkinter", "font_size", fallback=12), weight='bold')
# Interval in ms to read the queue
qinterval = 200
# Frame for the left side
fleft = tk.Frame(window)
fleft.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
......@@ -86,11 +89,12 @@ def create_panel_gui():
# Buttons configuration
buttons, numrows = get_buttons()
bh = int(55/numrows)
if numrows > 0:
row = 1
bframes = {}
outputs = {}
feedbacks = {}
while row <= numrows:
# create the frame for this row
bframes[row] = tk.Frame(fright)
......@@ -103,15 +107,27 @@ def create_panel_gui():
col=0
logging.info(buttons[row])
for b in buttons[row].keys():
command=None
if config.has_section('ACTION:'+buttons[row][b]['action']):
command=config.get('ACTION:'+buttons[row][b]['action'], 'execute', fallback=None)
setstatus=config.get('ACTION:'+buttons[row][b]['action'], 'setstatus', fallback=None)
#command=None
#if config.has_section('ACTION:'+buttons[row][b]['action']):
# command=config.get('ACTION:'+buttons[row][b]['action'], 'execute', fallback=None)
# setstatus=config.get('ACTION:'+buttons[row][b]['action'], 'setstatus', fallback=None)
color=buttons[row][b]['color']
button = tk.Button(bframes[row], text=buttons[row][b]['title'], font=helv36, width=bw, height=bh, bg=color, fg="white",
command=lambda cmd=command,sts=setstatus: run_action(cmd,sts))
command=lambda action=buttons[row][b]['action']: run_action(action))
#command=lambda cmd=command,sts=setstatus: run_action(cmd,sts))
button.grid(row=0, column=col, sticky='nsew')
if 'output' in buttons[row][b].keys():
outputs[buttons[row][b]['output']] = {'btn': button, 'cfg': buttons[row][b]}
elif 'feedback' in buttons[row][b].keys():
if not buttons[row][b]['feedback'] in feedbacks.keys():
feedbacks[buttons[row][b]['feedback']] = [{'btn': button, 'cfg': buttons[row][b]}]
else:
feedbacks[buttons[row][b]['feedback']].append({'btn': button, 'cfg': buttons[row][b]})
col = col+1
# Configure the columns in the frame
......@@ -125,5 +141,37 @@ def create_panel_gui():
bg="purple", fg="white", font=helv36, height=2)
web_button.grid(row=1, column=1, sticky='nsew')
def read_queue():
if not qpnl.empty():
task=qpnl.get(block=True)
if task:
logging.info('TASK INCOMING FOR TK PANEL')
logging.info(task)
event = task['event']
data = task['data']
if event == 'OUTPUTCHANGE':
if data['output'] in outputs.keys():
logging.info('CHANGE THE COLOR OF THE BUTTON FOR '+str(data['output']))
btn = outputs[data['output']]['btn']
bcfg = outputs[data['output']]['cfg']
if 'color.'+str(data['status']) in bcfg.keys():
btn.config(bg=bcfg['color.'+str(data['status'])])
if 'title.'+str(data['status']) in bcfg.keys():
btn.config(text=bcfg['title.'+str(data['status'])])
elif event == 'STATUSCHANGE':
if 'status' in feedbacks.keys():
for b in feedbacks['status']:
btn = b['btn']
bcfg = b['cfg']
if 'color.'+str(data['status']) in bcfg.keys():
btn.config(bg=bcfg['color.'+str(data['status'])])
if 'title.'+str(data['status']) in bcfg.keys():
btn.config(text=bcfg['title.'+str(data['status'])])
window.after(qinterval, read_queue)
window.after(qinterval, read_queue) # ms
return window
......@@ -22,22 +22,45 @@ logging.getLogger(__name__)
STATUSES=[
'manual',
'open',
'close'
'automatic'
]
class TaskEngine():
status='init'
status='manual'
scene='open'
def set_manual(self, manual=True):
nst = False
if manual and self.status != 'manual':
nst = 'manual'
elif not manual and self.status == 'manual':
nst = 'automatic'
if nst:
logging.info('CHANGE STATUS TO '+nst)
self.status=nst
evt={'event': 'STATUSCHANGE', 'data':{'status': nst}}
qpnl.put(evt)
qweb.put(evt)
def process_task(self, task):
cmd, val = task.split(':', 1)
if cmd=='SETSTATUS' and val in STATUSES:
logging.info('SETSTATUS TO '+val)
if self.status != val:
self.status=val
event = task['event']
if 'data' in task.keys():
data = task['data']
if event=='SETSTATUS' and data['status'] in ['manual','automatic']:
if self.status != data['status']:
self.set_manual(data['status']=='manual')
elif event=='CHANGESTATUS':
self.set_manual(self.status!='manual')
#if self.status == 'manual':
# self.set_manual(False)
#else:
# self.set_manual(True)
elif event=='OUTPUTCHANGE':
qpnl.put(task)
qweb.put(task)
def camstudio():
......
......@@ -31,9 +31,20 @@ def run_command(command):
return f"Error: {e}"
def run_action(command, setstatus=None):
def run_action(action):
if not config.has_section('ACTION:'+action):
return False
command=config.get('ACTION:'+action, 'execute', fallback=None)
setstatus=config.get('ACTION:'+action, 'setstatus', fallback=None)
open_outputs=config.get('ACTION:'+action, 'open.outputs', fallback=None)
if setstatus:
qcore.put('SETSTATUS:'+str(setstatus), block=False)
if setstatus in ['manual','automatic']:
qcore.put({'event': 'SETSTATUS', 'data': {'status': setstatus}})
elif setstatus in ['switch','change','flip']:
qcore.put({'event': 'CHANGESTATUS'})
if command:
return run_command(command)
......
......@@ -13,17 +13,48 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from flask import Flask, render_template, request
from flask import Flask, render_template, request, session
from utils import check_port_available, run_command, create_daemon, run_action
import sys
import os
from guiutils import get_buttons
import flask_restful as restful
from flask_socketio import SocketIO, emit
import logging
logging.getLogger(__name__)
# Flask App Setup
flask_app = Flask('SHMCamStudio', template_folder=TEMPLATE_DIR)
flask_api = restful.Api(flask_app)
# XXX BugFix in eventlet used in socketio
os.environ['EVENTLET_NO_GREENDNS'] = 'yes'
socketio = SocketIO(flask_app)
buttons, numrows = get_buttons()
outputs = {}
feedbacks = {}
if numrows > 0:
row = 1
while row <= numrows:
for b in buttons[row].keys():
if 'output' in buttons[row][b].keys():
outputs[buttons[row][b]['output']] = {'cfg': buttons[row][b]}
elif 'feedback' in buttons[row][b].keys():
if not buttons[row][b]['feedback'] in feedbacks.keys():
feedbacks[buttons[row][b]['feedback']] = [{'cfg': buttons[row][b]}]
else:
feedbacks[buttons[row][b]['feedback']].append({'btn': button, 'cfg': buttons[row][b]})
row = row+1
class PollAPI(restful.Resource):
......@@ -65,7 +96,7 @@ class AppData(restful.Resource):
@flask_app.route('/')
def index():
buttons, numrows = get_buttons()
#buttons, numrows = get_buttons()
row = 1
style_rows=""
htmlbuttons=""
......@@ -82,6 +113,8 @@ def index():
pollclass=''
if 'output' in buttons[row][b].keys():
pollclass='output_'+buttons[row][b]['output']
elif 'feedback'in buttons[row][b].keys():
pollclass='feedback_'+buttons[row][b]['feedback']
htmlbuttons=htmlbuttons+"""<button style="color:white;background-color:"""+color+""";"
class="button private button_row"""+str(row)+" "+pollclass+""" " onclick="executeCommand('"""+command+"""')">
"""+buttons[row][b]['title']+"""
......@@ -98,12 +131,7 @@ def execute():
command_key = request.form.get('command')
if config.has_section('ACTION:'+command_key):
command = config.get('ACTION:'+command_key, 'execute', fallback=None)
setstatus=config.get('ACTION:'+command_key, 'setstatus', fallback=None)
if command or setstatus:
result = run_action(command, setstatus)
else:
return "No command available"
result = run_action(command_key)
return result or 'OK'
else:
return "Invalid command", 400
......@@ -115,6 +143,49 @@ def stream():
@socketio.event
def my_event(message):
session['receive_count'] = session.get('receive_count', 0) + 1
emit('my_response',
{'data': message['data'], 'count': session['receive_count']})
@socketio.event
def my_ping():
emit('my_pong')
@socketio.event
def get_queue():
count=5
while not qweb.empty() and count > 0:
count = count-1
task=qweb.get(block=True)
if task:
logging.info('TASK INCOMING FOR WEB')
logging.info(task)
event = task['event']
data = task['data']
if event == 'OUTPUTCHANGE':
if data['output'] in outputs.keys():
logging.info('CHANGE THE COLOR OF THE WEB BUTTON FOR '+str(data['output']))
bcfg = outputs[data['output']]['cfg']
if 'color.'+str(data['status']) in bcfg.keys():
emit('change_output', {'button': data['output'], 'color': bcfg['color.'+str(data['status'])] }, broadcast=True)
if 'title.'+str(data['status']) in bcfg.keys():
emit('change_output', {'button': data['output'], 'title': bcfg['title.'+str(data['status'])] }, broadcast=True)
elif event == 'STATUSCHANGE':
if 'status' in feedbacks.keys():
for b in feedbacks['status']:
bcfg = b['cfg']
if 'color.'+str(data['status']) in bcfg.keys():
emit('change_feedback', {'feedback': 'status', 'color': bcfg['color.'+str(data['status'])] }, broadcast=True)
#btn.config(bg=bcfg['color.'+str(data['status'])])
if 'title.'+str(data['status']) in bcfg.keys():
emit('change_feedback', {'feedback': 'status', 'title': bcfg['title.'+str(data['status'])] }, broadcast=True)
def run_flask_app(port=5000, daemon_mode=False):
"""Run Flask app with optional daemon mode"""
if not check_port_available(port):
......@@ -128,5 +199,9 @@ def run_flask_app(port=5000, daemon_mode=False):
flask_api.add_resource(PollAPI, '/update')
flask_api.add_resource(AppData, '/data')
flask_app.run(host='0.0.0.0', port=port, debug=False, use_reloader=False)
#flask_app.run(host='0.0.0.0', port=port, debug=False, use_reloader=False)
# for socketio
socketio.run(flask_app, host='0.0.0.0', port=port, debug=True, use_reloader=False)
......@@ -157,6 +157,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
}
}
</style>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.5.1/jquery.min.js" integrity="sha512-bLT0Qm9VnAYZDflyKcBaQ2gg0hSYNQrJ8RilYldYQ1FxQYoCLtUjuuRuZo+fjqhx/qtq/1itJ0C2ejDxltZVFg==" crossorigin="anonymous"></script>
<script src="https://cdn.socket.io/4.7.5/socket.io.min.js" integrity="sha384-2huaZvOR9iDzHqslqwpR87isEmrfxqyWOF7hr7BY6KG0+hVKLoEXMPUJw3ynWuhO" crossorigin="anonymous"></script>
</head>
<body>
<div class="main-container">
......@@ -200,13 +202,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
.then(result => {
console.log(result);
// Visual feedback
event.target.style.backgroundColor = '#45a049';
/*event.target.style.backgroundColor = '#45a049';
setTimeout(() => {
event.target.style.backgroundColor =
event.target.classList.contains('private') ? '#4CAF50' :
event.target.classList.contains('toggle') ? '#2196F3' :
'#FF9800';
}, 300);
}, 300); */
})
.catch(error => {
console.error('Error:', error);
......@@ -248,54 +250,75 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
document.addEventListener('touchmove', function(event) {
if (event.scale !== 1) { event.preventDefault(); }
}, { passive: false });
</script>
</body>
</html>
<script>
const sidebar = document.querySelector('.sidebar');
const resizeHandle = document.getElementById('resize-handle');
const buttonsContainer = document.querySelector('.buttons-container');
const containerWidth = document.querySelector('.main-container').clientWidth;
$(document).ready(function() {
// Connect to the Socket.IO server.
// The connection URL has the following format, relative to the current page:
// http[s]://<domain>:<port>[/<namespace>]
var socket = io();
// Event handler for new connections.
// The callback function is invoked when a connection with the
// server is established.
socket.on('connect', function() {
socket.emit('my_event', {data: 'I\'m connected!'});
});
function adjustSidebarWidth(newWidth) {
sidebar.style.width = `${newWidth}px`;
buttonsContainer.style.width = `${containerWidth - newWidth}px`;
// Notify the iframe about the resize
var iframe = document.querySelector(".video-container iframe");
if (iframe) {
iframe.contentWindow.postMessage("resize", "*");
}
}
// Event handler for server sent data.
// The callback function is invoked whenever the server emits data
// to the client. The data is then displayed in the "Received"
// section of the page.
socket.on('my_response', function(msg, cb) {
$('#log').append('<br>' + $('<div/>').text('Received #' + msg.count + ': ' + msg.data).html());
if (cb)
cb();
});
let isResizing = false;
// Interval function that tests message latency by sending a "ping"
// message. The server then responds with a "pong" message and the
// round trip time is measured.
var ping_pong_times = [];
var start_time;
window.setInterval(function() {
start_time = (new Date).getTime();
$('#transport').text(socket.io.engine.transport.name);
socket.emit('my_ping');
}, 1000);
// Handler for the "pong" message. When the pong is received, the
// time from the ping is stored, and the average of the last 30
// samples is average and displayed.
socket.on('my_pong', function() {
var latency = (new Date).getTime() - start_time;
ping_pong_times.push(latency);
ping_pong_times = ping_pong_times.slice(-30); // keep last 30 samples
var sum = 0;
for (var i = 0; i < ping_pong_times.length; i++)
sum += ping_pong_times[i];
$('#ping-pong').text(Math.round(10 * sum / ping_pong_times.length) / 10);
});
resizeHandle.addEventListener('mousedown', (e) => {
isResizing = true;
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', stopResize);
});
window.setInterval(function() {
socket.emit('get_queue');
}, 200);
function handleMouseMove(e) {
if (!isResizing) return;
let newWidth = e.clientX;
// Limit sidebar width between 50px and 50% of screen
newWidth = Math.max(50, Math.min(newWidth, containerWidth / 2));
adjustSidebarWidth(newWidth);
}
socket.on('change_output', function(msg, cb) {
//console.log('CHANGE OUTPUT');
//console.log(msg);
$('.output_'+msg.button).css('background-color', msg.color);
});
function stopResize() {
isResizing = false;
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', stopResize);
}
socket.on('change_feedback', function(msg, cb) {
if(msg.hasOwnProperty("color"))
$('.feedback_'+msg.feedback).css('background-color', msg.color);
if(msg.hasOwnProperty("title"))
$('.feedback_'+msg.feedback).text(msg.title);
});
// Prevent zoom on double-tap for mobile
document.addEventListener('touchmove', function(event) {
if (event.scale !== 1) { event.preventDefault(); }
}, { passive: false });
});
</script>
</body>
<div id="log"></div>
<div id="ping-pong"></div>ms</b>
<div id="transport"></div>
</body>
</html>
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment