Full code that you need to save and run
import tkinter as tk
from tkinter import ttk, messagebox
import asyncio
import websockets
import requests
import json
import os
import threading
CONFIG_FILE = "openlp_vmix_config.json"
class OpenLPConnection:
def init(self, config):
self.config = config
self.running = False
async def monitor_slides(self):
uri = f"ws://{self.config['openlp_ip']}:{self.config['openlp_ws_port']}/ws"
while self.running:
try:
async with websockets.connect(uri) as websocket:
channels = ["live", "slide", "display", "alert", "controller"]
for ch in channels:
await websocket.send(json.dumps({"action": "subscribe", "channel": ch}))
self.update_vmix_title(self.get_current_slide())
while self.running:
msg = await websocket.recv()
data = json.loads(msg)
if "results" in data and isinstance(data["results"], dict):
r = data["results"]
if r.get("blank") or r.get("theme") or r.get("display"):
self.update_vmix_title("")
else:
self.update_vmix_title(self.get_current_slide())
except Exception as e:
print(f"WS error ({self.config['openlp_ip']}):", e)
await asyncio.sleep(5)
def get_current_slide(self):
try:
url = f"http://{self.config['openlp_ip']}:{self.config['openlp_http_port']}/api/v2/controller/live-items"
res = requests.get(url)
res.raise_for_status()
data = res.json()
for slide in data.get("slides", []):
if slide.get("selected", False):
return slide.get("text", "").strip()
except Exception as e:
print("Slide error:", e)
return ""
def update_vmix_title(self, text):
for input_name, text_field in self.config['vmix_inputs'].items():
try:
requests.get(self.config["vmix_server"], params={
"Function": "SetText",
"Input": input_name,
"SelectedName": text_field,
"Value": text
})
except Exception as e:
print("vMix error:", e)
def start(self):
self.running = True
threading.Thread(target=lambda: asyncio.run(self.monitor_slides())).start()
def stop(self):
self.running = False
class App(tk.Tk):
def init(self):
super().init()
self.title("OpenLP → vMix Slide Sync")
self.geometry("600x500")
self.resizable(False, False)
self.config_data = self.load_config()
self.connections = []
self.create_widgets()
def load_config(self):
if os.path.exists(CONFIG_FILE):
with open(CONFIG_FILE, "r") as f:
data = json.load(f)
if isinstance(data, list) and all(isinstance(item, str) for item in data):
# Convert strings to dicts if saved improperly
return [json.loads(item) for item in data]
return data
return []
def save_config(self):
with open(CONFIG_FILE, "w") as f:
json.dump(self.config_data, f, indent=2)
def create_widgets(self):
self.frames = []
self.frm = ttk.Frame(self)
self.frm.pack(padx=10, pady=10, fill="both", expand=True)
self.canvas = tk.Canvas(self.frm)
self.scrollbar = ttk.Scrollbar(self.frm, orient="vertical", command=self.canvas.yview)
self.scrollable_frame = ttk.Frame(self.canvas)
self.scrollable_frame.bind(
"<Configure>",
lambda e: self.canvas.configure(
scrollregion=self.canvas.bbox("all")
)
)
self.canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw")
self.canvas.configure(yscrollcommand=self.scrollbar.set)
self.canvas.pack(side="left", fill="both", expand=True)
self.scrollbar.pack(side="right", fill="y")
for conf in self.config_data:
self.add_connection_form(conf)
ttk.Button(self, text="+ Add Connection", command=lambda: self.add_connection_form()).pack(pady=5)
self.btn_start = ttk.Button(self, text="Start Monitoring", command=self.toggle_monitoring)
self.btn_start.pack(pady=10)
self.status_label = ttk.Label(self, text="Status: Disconnected", foreground="red")
self.status_label.pack()
def add_connection_form(self, preset=None):
container = ttk.LabelFrame(self.scrollable_frame, text=f"Connection {len(self.frames)+1}")
container.pack(padx=5, pady=5, fill="x")
entries = {
'openlp_ip': self._add_labeled_entry(container, "OpenLP IP:", preset.get("openlp_ip", "") if preset else ""),
'openlp_http_port': self._add_labeled_entry(container, "HTTP Port:", preset.get("openlp_http_port", "4316") if preset else "4316"),
'openlp_ws_port': self._add_labeled_entry(container, "WebSocket Port:", preset.get("openlp_ws_port", "4317") if preset else "4317"),
'vmix_server': self._add_labeled_entry(container, "vMix API URL:", preset.get("vmix_server", "http://localhost:8088/api") if preset else ""),
'vmix_inputs': self._add_labeled_entry(container, "vMix Inputs & Fields (input:textfield,...)", json.dumps(preset.get("vmix_inputs", {"OpenLPSlides": "Message.Text"})) if preset else "")
}
self.frames.append(entries)
def _add_labeled_entry(self, parent, label, default):
row = ttk.Frame(parent)
row.pack(fill="x", pady=2)
ttk.Label(row, text=label, width=30).pack(side="left")
entry = ttk.Entry(row)
entry.insert(0, default)
entry.pack(side="right", expand=True, fill="x")
return entry
def toggle_monitoring(self):
if not self.connections:
self.btn_start.config(text="Stop Monitoring")
self.status_label.config(text="Status: Running", foreground="green")
self.connections.clear()
self.config_data = []
for entry_set in self.frames:
conf = {
"openlp_ip": entry_set['openlp_ip'].get(),
"openlp_http_port": entry_set['openlp_http_port'].get(),
"openlp_ws_port": entry_set['openlp_ws_port'].get(),
"vmix_server": entry_set['vmix_server'].get(),
"vmix_inputs": json.loads(entry_set['vmix_inputs'].get())
}
conn = OpenLPConnection(conf)
self.connections.append(conn)
conn.start()
self.config_data.append(conf)
self.save_config()
else:
self.btn_start.config(text="Start Monitoring")
self.status_label.config(text="Status: Stopped", foreground="red")
for conn in self.connections:
conn.stop()
self.connections.clear()
if name == "main":
app = App()
app.mainloop()