--- # ============================================================================ # life-church/playbooks/thinkcenter-obs.yml # Install OBS Studio on thinkcenter via the official OBS PPA (latest stable), # plus v4l2loopback (virtual camera) + ffmpeg. thinkcenter ONLY. Idempotent. # Native packages only (no flatpak/snap — matches the homelab audio rule). # become password from Infisical /lc-hosts at runtime. # # Mint gotcha handled: `add-apt-repository` mis-detects Mint's codename ("zena") # instead of the Ubuntu base ("noble"). We add the PPA source explicitly pinned # to the Ubuntu base codename so apt can resolve it. # # UNDO: # sudo apt remove --purge obs-studio v4l2loopback-dkms # sudo add-apt-repository --remove ppa:obsproject/obs-studio # (or delete /etc/apt/sources.list.d/obsproject-*.list + key) # ============================================================================ # --- Play 1 (localhost): fetch thinkcenter sudo pass from Infisical ---------- - name: "thinkcenter OBS — resolve become password from Infisical" hosts: localhost gather_facts: false tasks: - name: "[resolve] Infisical creds from env" ansible.builtin.set_fact: infisical_client_id: "{{ lookup('env','INFISICAL_CLIENT_ID') }}" infisical_client_secret: "{{ lookup('env','INFISICAL_CLIENT_SECRET') }}" infisical_project_id: "{{ lookup('env','INFISICAL_PROJECT_ID') }}" infisical_url: "{{ lookup('env','INFISICAL_API_URL') | default('http://192.168.122.41:8080', true) }}" infisical_env_slug: "{{ lookup('env','INFISICAL_ENV_SLUG') | default('prod', true) }}" - name: "[resolve] Assert Infisical creds present" ansible.builtin.assert: that: - infisical_client_id | length > 0 - infisical_client_secret | length > 0 - infisical_project_id | length > 0 fail_msg: "Infisical creds missing from Semaphore environment 'infisical-dev'." - name: "[infisical] Log in" infisical.vault.login: url: "{{ infisical_url }}" auth_method: universal_auth universal_auth_client_id: "{{ infisical_client_id }}" universal_auth_client_secret: "{{ infisical_client_secret }}" register: infisical_login no_log: true - name: "[infisical] Read /lc-hosts" infisical.vault.read_secrets: login_data: "{{ infisical_login.login_data }}" project_id: "{{ infisical_project_id }}" env_slug: "{{ infisical_env_slug }}" path: "/lc-hosts" as_dict: true register: lc_secrets no_log: true - name: "[set] Stash thinkcenter sudo pass" ansible.builtin.set_fact: thinkcenter_become_pass: "{{ lc_secrets.secrets['THINKCENTER_SUDO_PASS'] }}" no_log: true # --- Play 2: OBS on thinkcenter ONLY ----------------------------------------- - name: "Install OBS Studio on thinkcenter" hosts: thinkcenter gather_facts: true become: true vars: ansible_become_pass: "{{ hostvars['localhost'].thinkcenter_become_pass }}" obs_ppa_codename: noble # Ubuntu base for Mint 22.3 (NOT 'zena') v4l2_version: "0.15.4" # source build — distro dkms 0.12.7 fails on kernel 7.x tasks: - name: "[safety] Confirm we are on thinkcenter" ansible.builtin.assert: that: inventory_hostname == 'thinkcenter' fail_msg: "Refusing to run — this play is scoped to thinkcenter only." - name: "[safety] Confirm Ubuntu-family host (apt/PPA path)" ansible.builtin.assert: that: ansible_facts.os_family == 'Debian' fail_msg: "This playbook targets Debian/Ubuntu (Mint). Host is {{ ansible_facts.os_family }}." - name: "[repo] Ensure keyrings dir exists" ansible.builtin.file: path: /etc/apt/keyrings state: directory mode: "0755" - name: "[repo] Fetch OBS PPA signing key into keyring (modern signed-by method)" ansible.builtin.get_url: url: "https://keyserver.ubuntu.com/pks/lookup?op=get&search=0xBC7345F522079769F5BBE987EFC71127F425E228" dest: /etc/apt/keyrings/obsproject-obs-studio.asc mode: "0644" force: false register: obs_key until: obs_key is succeeded retries: 3 delay: 10 - name: "[repo] Add OBS PPA source pinned to Ubuntu base codename (avoids Mint 'zena' bug)" ansible.builtin.apt_repository: repo: "deb [signed-by=/etc/apt/keyrings/obsproject-obs-studio.asc] https://ppa.launchpadcontent.net/obsproject/obs-studio/ubuntu {{ obs_ppa_codename }} main" filename: obsproject-obs-studio state: present update_cache: false register: obs_repo - name: "[repo] Update apt cache (retry on transient failure)" ansible.builtin.apt: update_cache: true register: apt_cache until: apt_cache is succeeded retries: 3 delay: 15 - name: "[apt] Repair any half-configured dpkg state before installing" ansible.builtin.command: dpkg --configure -a register: dpkg_repair changed_when: "'Setting up' in (dpkg_repair.stdout | default(''))" failed_when: false - name: "[apt] Install OBS Studio + ffmpeg (core — must succeed; retry on lock)" ansible.builtin.apt: name: - obs-studio - ffmpeg state: present register: obs_install until: obs_install is succeeded retries: 3 delay: 15 # ----------------------------------------------------------------------- # Virtual camera (v4l2loopback) — OPTIONAL / best-effort. The Ubuntu-packaged # v4l2loopback-dkms (0.12.7) FAILS to build on kernel 7.x (upstream changed # the v4l2_fh_add/del signature). So we DO NOT install that package. Instead # we build v4l2loopback {{ v4l2_version }} from source, which supports the # new kernel API. If anything here fails, we log and continue — OBS itself # is already installed; only the "Start Virtual Camera" button is affected. # ----------------------------------------------------------------------- - name: "[vcam] Ensure the broken distro dkms package is not installed" ansible.builtin.apt: name: v4l2loopback-dkms state: absent purge: true register: vcam_dkms_removed failed_when: false - name: "[vcam] Install build prerequisites + v4l2loopback-utils (best-effort)" ansible.builtin.apt: name: - build-essential - "linux-headers-{{ ansible_kernel }}" - v4l2loopback-utils state: present register: vcam_deps failed_when: false until: vcam_deps is succeeded retries: 2 delay: 10 - name: "[vcam] Is a working v4l2loopback module already present for this kernel?" ansible.builtin.command: "modinfo -k {{ ansible_kernel }} v4l2loopback" register: vcam_modinfo changed_when: false failed_when: false - name: "[vcam] Build + install v4l2loopback {{ v4l2_version }} from source (only if missing)" when: vcam_modinfo.rc != 0 block: - name: "[vcam] Fetch v4l2loopback source" ansible.builtin.get_url: url: "https://github.com/v4l2loopback/v4l2loopback/archive/refs/tags/v{{ v4l2_version }}.tar.gz" dest: "/tmp/v4l2loopback-{{ v4l2_version }}.tar.gz" mode: "0644" register: v4l2_dl until: v4l2_dl is succeeded retries: 3 delay: 10 - name: "[vcam] Extract source" ansible.builtin.unarchive: src: "/tmp/v4l2loopback-{{ v4l2_version }}.tar.gz" dest: /tmp remote_src: true - name: "[vcam] Build the kernel module (make)" ansible.builtin.command: cmd: make chdir: "/tmp/v4l2loopback-{{ v4l2_version }}" register: v4l2_make changed_when: v4l2_make.rc == 0 - name: "[vcam] Install the module + utils (make install)" ansible.builtin.command: cmd: make install chdir: "/tmp/v4l2loopback-{{ v4l2_version }}" register: v4l2_makeinstall changed_when: v4l2_makeinstall.rc == 0 - name: "[vcam] depmod so the new module is discoverable" ansible.builtin.command: "depmod -a {{ ansible_kernel }}" changed_when: true rescue: - name: "[vcam] Source build failed — record, do NOT fail the play" ansible.builtin.debug: msg: "v4l2loopback source build failed; OBS is installed but the Virtual Camera won't work until this is resolved. See /tmp/v4l2loopback-{{ v4l2_version }}." - name: "[vcam] Configure v4l2loopback module options for the OBS virtual camera" ansible.builtin.copy: dest: /etc/modprobe.d/obs-v4l2loopback.conf mode: "0644" content: | options v4l2loopback devices=1 video_nr=10 card_label="OBS Virtual Camera" exclusive_caps=1 register: vcam_conf - name: "[vcam] Load v4l2loopback now (best-effort)" community.general.modprobe: name: v4l2loopback state: present register: vcam_load failed_when: false # --- Verify, with automatic repair + re-verify on failure --------------- - name: "[verify] Verify OBS (auto-repair + re-verify if it fails)" block: - name: "[verify] Query obs-studio package status (dpkg, not the GUI binary)" ansible.builtin.command: dpkg-query -W -f='${Status} ${Version}' obs-studio register: obs_pkg changed_when: false failed_when: false - name: "[verify] obs binary present on PATH" ansible.builtin.command: which obs register: obs_bin changed_when: false failed_when: false - name: "[verify] Assert OBS installed" ansible.builtin.assert: that: - "'install ok installed' in (obs_pkg.stdout | default(''))" - obs_bin.rc == 0 fail_msg: "OBS verify failed. pkg='{{ obs_pkg.stdout | default('') }}', bin rc={{ obs_bin.rc }}" success_msg: "OBS Studio installed." rescue: - name: "[reverify] Repair dpkg + reinstall OBS, then re-verify" block: - name: "[reverify] dpkg --configure -a" ansible.builtin.command: dpkg --configure -a failed_when: false changed_when: false - name: "[reverify] apt-get install -f (fix broken deps)" ansible.builtin.command: apt-get -y -f install failed_when: false changed_when: false - name: "[reverify] Reinstall OBS + ffmpeg" ansible.builtin.apt: name: [obs-studio, ffmpeg] state: present register: obs_reinstall until: obs_reinstall is succeeded retries: 3 delay: 15 - name: "[reverify] Re-query package + binary" ansible.builtin.shell: "dpkg-query -W -f='${Status}' obs-studio; echo '|'; which obs || true" register: obs_recheck changed_when: false - name: "[reverify] Assert OBS present after repair" ansible.builtin.assert: that: - "'install ok installed' in obs_recheck.stdout" - "'/obs' in obs_recheck.stdout" fail_msg: "OBS still not installed after repair attempt: {{ obs_recheck.stdout }}" success_msg: "OBS Studio installed (after auto-repair)." - name: "[report] OBS summary" ansible.builtin.debug: msg: - "===== thinkcenter OBS Studio =====" - "packages changed this run : {{ obs_install.changed }}" - "OBS package : {{ obs_pkg.stdout | default('n/a') | trim }}" - "v4l2loopback module loaded: {{ 'yes' if vcam_load.rc | default(1) == 0 else 'not yet (loads on Start Virtual Camera / reboot)' }}" - "NOTE: on Mint X11 use XSHM/XComposite capture sources; PipeWire capture is for Wayland."