Here is the script I’m using as my current solution:
import subprocess
import time
import sys
import os
import uno
import threading
import traceback
import termios
import tty
import selectors
def get_window_id_by_class_or_title(match_term):
result = subprocess.run(['wmctrl', '-lx'], capture_output=True, text=True)
for line in result.stdout.splitlines():
if match_term.lower() in line.lower():
return line.split()[0]
return None
def activate_window_by_id(window_id, label=""):
if window_id:
print(f"→ Aktiviere Fenster: {label} ({window_id})")
subprocess.run(['xdotool', 'windowactivate', '--sync', window_id])
subprocess.run(['xdotool', 'windowfocus', '--sync', window_id])
def launch_libreoffice(file_path):
return subprocess.Popen([
"libreoffice", "--impress", "--norestore",
"--accept=socket,host=localhost,port=2002;urp;", file_path
])
def launch_vlc(device):
return subprocess.Popen([
"cvlc", f"v4l2://{device}",
"--no-video-title-show", "--quiet"
])
def wait_for_vlc_window(timeout=10):
for _ in range(timeout * 2):
video_id = get_window_id_by_class_or_title("vlc.Vlc")
if video_id:
return video_id
time.sleep(0.5)
return None
def connect_to_libreoffice():
local_context = uno.getComponentContext()
resolver = local_context.ServiceManager.createInstanceWithContext(
"com.sun.star.bridge.UnoUrlResolver", local_context)
context = resolver.resolve(
"uno:socket,host=localhost,port=2002;urp;StarOffice.ComponentContext")
return context, context.ServiceManager
def get_presentation_component(context):
desktop = context.ServiceManager.createInstanceWithContext("com.sun.star.frame.Desktop", context)
return desktop.getCurrentComponent(), desktop
def start_presentation(presentation):
presentation.getPresentation().start()
def wait_for_slideshow_controller(presentation, timeout=10):
for _ in range(timeout * 2):
try:
slideshow = presentation.getPresentation()
controller = slideshow.getController()
if controller and slideshow.isRunning():
return controller, slideshow
except Exception:
pass
time.sleep(0.5)
return None, None
def get_slide_index(slideshow):
return slideshow.getController().getCurrentSlideIndex()
def is_presentation_running(slideshow):
return slideshow.isRunning()
def input_listener(callback, stop_flag):
fd = sys.stdin.fileno()
old_settings = termios.tcgetattr(fd)
tty.setcbreak(fd)
sel = selectors.DefaultSelector()
sel.register(fd, selectors.EVENT_READ)
try:
while not stop_flag.is_set():
events = sel.select(timeout=0.5)
for key, _ in events:
ch = os.read(fd, 1).decode()
if ch == '\x1b':
if os.read(fd, 1).decode() == '[':
code = os.read(fd, 1).decode()
if code == '5' and os.read(fd, 1).decode() == '~':
callback("prev")
elif code == '6' and os.read(fd, 1).decode() == '~':
callback("next")
elif code == 'C':
callback("next")
elif code == 'D':
callback("prev")
elif ch in ('n', 'N'):
callback("next")
elif ch in ('p', 'P'):
callback("prev")
finally:
termios.tcsetattr(fd, termios.TCSADRAIN, old_settings)
def run():
file_path = None
video_slide = None
video_device = "/dev/video0"
for arg in sys.argv[1:]:
if arg.startswith("--presentation="):
file_path = os.path.expanduser(arg.split("=", 1)[1])
elif arg.startswith("--page="):
video_slide = int(arg.split("=", 1)[1])
elif arg.startswith("--videosource="):
video_device = arg.split("=", 1)[1]
if not file_path or not video_slide:
print("Nutzung: python3 present-with-webcam.py --presentation=DATEI --page=NUMMER [--videosource=/dev/videoX]")
sys.exit(1)
if not os.path.isfile(file_path):
print("Datei nicht gefunden.")
sys.exit(1)
vlc_process = launch_vlc(video_device)
print(f"VLC mit {video_device} gestartet...")
video_id = wait_for_vlc_window()
if not video_id:
print("Fehler: VLC-Fenster nicht gefunden.")
vlc_process.terminate()
sys.exit(1)
lo_process = launch_libreoffice(file_path)
time.sleep(5)
context, smgr = connect_to_libreoffice()
presentation, desktop = get_presentation_component(context)
if not presentation:
print("Konnte Präsentation nicht laden.")
vlc_process.terminate()
lo_process.terminate()
sys.exit(1)
try:
start_presentation(presentation)
print("Präsentation wurde gestartet.")
except Exception:
traceback.print_exc()
vlc_process.terminate()
lo_process.terminate()
sys.exit(1)
controller, slideshow = wait_for_slideshow_controller(presentation)
if not controller:
print("Fehler: Präsentation konnte nicht gestartet werden.")
vlc_process.terminate()
lo_process.terminate()
sys.exit(1)
impress_window_id = get_window_id_by_class_or_title("soffice.Soffice")
terminal_window_id = get_window_id_by_class_or_title("gnome-terminal-server")
last_index = get_slide_index(slideshow)
print("✅ Alles geladen, es kann losgehen!")
def switch_windows(from_index, to_index):
if from_index == to_index:
return
print(f"→ Wechsle von Folie {from_index + 1} zu Folie {to_index + 1}")
if from_index + 1 != video_slide and to_index + 1 == video_slide:
activate_window_by_id(video_id, "VLC")
if terminal_window_id:
activate_window_by_id(terminal_window_id, "Terminal")
elif from_index + 1 == video_slide and to_index + 1 != video_slide:
activate_window_by_id(impress_window_id, "Impress")
if terminal_window_id:
activate_window_by_id(terminal_window_id, "Terminal")
def on_key(direction):
nonlocal last_index
try:
print(f"→ Steuerung: {direction}")
if direction == "next":
ok = controller.gotoNextEffect()
elif direction == "prev":
ok = controller.gotoPreviousEffect()
else:
return
print(f" → Effekt ausgelöst? {ok}")
time.sleep(0.3)
new_index = get_slide_index(slideshow)
if new_index != last_index:
print(f"Folie gewechselt: {last_index + 1} → {new_index + 1}")
switch_windows(last_index, new_index)
last_index = new_index
else:
print("Folie hat sich nicht verändert.")
except Exception:
print("Fehler bei der Steuerung:")
traceback.print_exc()
stop_flag = threading.Event()
listener = threading.Thread(target=input_listener, args=(on_key, stop_flag), daemon=True)
listener.start()
try:
while is_presentation_running(slideshow):
time.sleep(1)
finally:
stop_flag.set()
print("VLC wird beendet...")
vlc_process.terminate()
print("Präsentationsmodus beendet. Skript wird geschlossen.")
lo_process.terminate()
run()