Libre Office: How to Integrate live webcam image

Ubuntu Version:
Ubuntu 24.04.2 LTS

Desktop Environment (if applicable):
GNOME with X11 (Wayneland does not work with Zoom screensharing)

Problem Description:
After using Linux about 15 years ago and then switched to Windows I want to migrate back using Ubuntu. I’m very happy how easy it is now to install the system and I found a solution for every use case I had.

But this one causes me some headacheand I hope to find a better solution than the current one:

Microsoft Powerpoint offers the cameo function allowing to integrate a live image from any connected webcam in my presentations. I use this in my presenstations to show a live pictrue from my audience using a webcam. (This is a little suprice for them to see themself live in the screen and a very important effect when I host events.)

Libre Office Impress does not offer this function. So i need another solution.

I only need to display we current webcam picture in full screen mode replacing one of my slides.

Here are the solutions I was thinking about:

1st:
Just open VLC or some other webcam app and switch manually between presentation and webcam windows. But I need to stand directly at my laptop to do so (which is not that easy in many locations) and it brekas my natural presentation flow. So I’d like to avoid this setting.
For this the solution has to work properly with just my Logitech presenter sending page down/page up.

2nd:
Creating a green (or any other colored) “slide” and somehow use OBS studio to replace this slide with the current webcam picture.
But I didn’t find out how to conbfigure OBS studio and it comes with a problem:
The same color might appear on other slides as well and that might cause strange overlay effects.

3rd:
Using another office solution with offers a Cameo function. But I didn’t find one.

4th:
Using wine and Powerpoint. No thanks. I want to get rid of all Microsoft based solutions… :wink:

5th my current solution:
The most complex solution was a python script controlling everything:

  • It starts the webcam app in fullscreen mode and gets the window id.
  • It starts Libre office using with activated API, starts presentation mode and grabs that window id too.
  • From a parameter it knows which slide to replace.
  • Instead of switching slides in Libre Office the python skript now gets the input and sends next effect / last effect actions using the API to libre office. And it checks which slide is active.
  • For the slide I’d like to replace it sets the focus to the webcam window (brining it to the front) and after leaving that slide it brings back the presentation window.

After looking into focus issues (VLC steals the focus from the python skript) this works now.

I put the script in a comment her just in case someone can use it or parts of it.

But this solution is very complex and might run into issues whenever interfaces etc. change.

So I hope you can give me ideas for a more simple and robust solution.
Any ideas? Maybe the OBS thing or another office suite I missed?

Jan

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()

Hello and welcome!
Today I read about FreeShow and remembered your question. But I’ve never used FreeShow myself and I do not know if embedding camera live stream is possible.

Hello!

Thanks a lot!

From the feature list ist shows “camera” as a feature. So I’ll give it a try.

Jan