CoreVideo

CoreVideo

An OBS Studio plugin that captures live Zoom meeting video, audio, screen share, and Zoom interpretation audio channel capture โ€” with a dockable control panel, auto-reconnect, ZoomISO-style assignment modes, hardware video acceleration, and TCP/OSC control APIs for full broadcast automation.

๐Ÿงช Beta status
CoreVideo is in public beta. Packaged builds are available for Windows x64 (installer or ZIP) and macOS Apple Silicon (signed beta installer). Linux requires a source build (details) โ€” see the Roadmap for known limitations and what's planned next. To report a problem, use GitHub Issues (the bug report template walks you through attaching a redacted support bundle). New releases are announced through a dismissible in-app update banner in the Zoom Control dock and on the GitHub Releases page.
Zoom Raw Data Bandwidth Planning
CoreVideo uses Zoom Meeting SDK raw data APIs. Raw data access does not require Enhanced Media by itself, but quality and stream count follow Zoom account and app entitlements. Standard accounts are commonly constrained to roughly 30 Mbps incoming video; Enhanced Media / HBM can raise that envelope to roughly 100 Mbps. Plan around 4-6 Mbps per standard 1080p feed.
Public beta Free OBS Studio 30+ Windows installer Sign in with Zoom OAuth 2.0 PKCE
Download for Windows  macOS beta installer  How to install

Introduction

CoreVideo integrates the Zoom Meeting SDK into OBS Studio with no screen capture or virtual camera required. It receives raw I420 video and 48 kHz PCM audio from any meeting participant, performs optional hardware-accelerated color conversion, and pushes frames into OBS as native sources.

The ZoomObsEngine child process hosts all SDK access in isolation, communicating with the plugin over JSON IPC (named pipes on Windows, Unix domain sockets on macOS/Linux) with frame data flowing through named shared memory. The plugin contains no Zoom SDK linkage.

Joining a meeting is driven by the built-in Zoom Control dock (or the TCP/OSC APIs). Once in a meeting, sources can follow a fixed participant, the active speaker, a ZoomISO-style spotlight slot, or the active screen share โ€” and switch automatically with a configurable failover participant if the primary leaves.

A ZoomReconnectManager automatically re-joins after engine crashes, network drops, or unexpected disconnects, with exponential back-off and a cancel-at-any-time UI in the dock.

For Zoom App Marketplace compliance and attributed joins, ZoomOAuthManager uses the CoreVideo HTTPS broker to run Zoom Public Client OAuth with PKCE and receive a short-lived broker token through the bundled corevideo://oauth/callback helper. Before each join, the plugin refreshes OAuth tokens through the broker when needed, fetches the signed-in user's ZAK, and starts the Meeting SDK helper with the embedded Marketplace Public Client ID as AuthContext.publicAppKey. The plugin also exposes TCP and OSC control APIs for external production automation, including source assignment, meeting control, and ISO recording.

Features

๐ŸŽฅ

Raw Video Capture

I420 YUV per participant. Selectable 360p / 720p / 1080p resolution. Hardware-accelerated color conversion (CUDA, VAAPI, VideoToolbox, QSV).

โฌ›

Video Loss & Placeholder

Hold last frame or show black on feed drop. Shows a color-bar placeholder when not yet subscribed.

๐ŸŽ™๏ธ

Raw Audio Capture

48 kHz PCM, mono or stereo, with per-participant audio isolation and mixer routing.

๐ŸŒ

Interpretation Audio Capture

Dedicated OBS source for existing Zoom interpretation audio channels in the meeting.

๐Ÿ–ฅ๏ธ

Screen Share Capture

Source type that follows active screen-share feed via ScreenShare assignment mode.

๐ŸŽฏ

Spotlight / ZoomISO Modes

Assign a source to Spotlight 1-8, exactly like ZoomISO. Engine resolves which participant owns each slot.

๐Ÿ”„

Active Speaker Mode

Sensitivity (ms) + hold-time (ms) debounce with liveness guard, supersede logic, and failover participant.

๐Ÿ”

Failover Participant

Configure a secondary participant ID that activates automatically when the primary leaves the meeting.

๐Ÿ”Œ

Auto-Reconnect

Automatic re-join after engine crash, network drop, or disconnection โ€” exponential back-off with cancel UI.

๐ŸŽฌ

Webinar Support

Join Zoom Webinars using the dedicated SDK entry point โ€” set the Webinar checkbox in the control dock.

๐Ÿ‘ฅ

Rich Participant Roster

Live list with video, mute, talking, host, co-host, raised hand, spotlight slot, and screen-sharing state.

๐Ÿ”Š

Per-Participant Audio

Standalone OBS audio source per meeting participant; audio mixed into OBS audio mixer.

๐ŸŽ›๏ธ

Control Dock

Dockable Qt panel with join/leave, state indicator, Active Speaker Director controls, recovery countdown, and Output Manager launch.

DIAG

Diagnostics

The dockable Zoom Diagnostics panel shows requested versus observed resolution, FPS, frame age, retry counters, recent engine debug events, ISO/FFmpeg recorder status, and redacted support bundle export.

OUT

Dockable Output Manager

Zoom Output Manager is now a persistent OBS dock for source assignments, live preview thumbnails, feed health, profile save/load, and manual recovery actions.

๐ŸŒ

TCP Control API

JSON server on port 19870 โ€” query, join, leave, assign outputs from scripts or dashboards.

๐ŸŽ›๏ธ

OSC Control API

UDP OSC server on port 19871 for lighting consoles and broadcast hardware.

๐Ÿ’พ

Output Profiles

Save and load named participant-to-source mappings as JSON files.

โŒจ๏ธ

OBS Hotkeys

Per-source hotkeys to enable/disable active speaker mode from OBS keyboard shortcuts.

๐Ÿ”’

Hardened Security

Constant-time token comparison, validated IPC input, sanitised participant IDs, SIGPIPE handling, DPAPI token storage (Windows).

๐Ÿ”‘

Zoom OAuth PKCE

Broker-backed OAuth 2.0 with S256 PKCE for attributed joins and Marketplace compliance. Meeting SDK auth uses the embedded Marketplace Public Client ID as publicAppKey. Custom URL scheme with platform callback helpers.

๐ŸŽจ

Modern UI

CoreVideo dark stylesheet with animated CvStatusDot, CvBanner first-run notices, and button role variants (primary / danger).

ISO

Auto ISO Recording

Record assigned participant video/audio tracks to separate files while optionally recording the main OBS program output.

๐Ÿ’ป

Multi-Platform

Windows has a release installer; macOS has a signed Apple Silicon beta installer. Linux requires a source build.

Adding the App

CoreVideo is distributed as a fully installable Windows application โ€” there is nothing to compile and no SDK to download. Installation takes a few minutes.

1. Download CoreVideo

Go to corevideo.io/download and get the latest Windows installer (CoreVideo-Setup.exe). Every release also publishes a matching .sha256 checksum you can verify.

2. Run the installer

Close OBS Studio if it is open, then double-click CoreVideo-Setup.exe and follow the prompts. The installer adds the CoreVideo plugin and its bundled Zoom runtime to your OBS Studio installation.

3. Open OBS Studio

Launch OBS Studio 30 or newer. CoreVideo loads automatically: you'll see the Zoom Control dock and a Tools โ†’ Zoom Plugin Settings menu item. If the dock is hidden, enable it from Docks โ†’ Zoom Control.

4. Sign in with Zoom

Click Sign in with Zoom and authorize CoreVideo in your browser (details in Sign in with Zoom below). That's it โ€” you're ready to use it.

Adding CoreVideo to your Zoom account

In Zoom's terms, an app is added to your Zoom account the first time you authorize it. There are two equivalent ways to do that:

Either way, CoreVideo then appears under Zoom App Marketplace โ†’ Manage โ†’ Added Apps, where you can review or remove it at any time.

โ„น๏ธ What you are granting
CoreVideo requests only two read-only scopes: user:read:user (your profile / display name) and user:read:token (a short-lived Zoom Access Key used to join meetings as you). It requests no write access, no recording access, and no access to other users' data. Tokens are stored encrypted on your machine โ€” see Sign in with Zoom.
โš ๏ธ Admin-managed accounts
If your organization restricts Marketplace apps, the authorization page may show Request pre-approval instead of Allow. Send the request (or ask your Zoom admin to approve CoreVideo under Admin โ†’ Advanced โ†’ App Marketplace), then sign in again once it's approved.
โ„น๏ธ Having trouble?
See Troubleshooting for installer, dock-not-showing, and sign-in fixes, or contact CoreVideo Support.

macOS: Apple Silicon beta

CoreVideo v0.1.45-beta.1 is available as a signed and notarized installer. Requires an Apple Silicon Mac (M1 or later). Use OBS Studio 32.2.1 or newer for this beta; the replacement package was built against OBS 32.2.1.

Download the macOS installer and SHA-256 checksum.

  1. Close OBS and open the downloaded .pkg.
  2. Install for your user account; no administrator password is needed.
  3. Reopen OBS, sign in to Zoom, and join a meeting. Allow the requested permissions. The meeting host must grant recording permission before raw video and audio can start.

The original macOS downloads were replaced on September 6, 2026 after signature errors. Download the replacement installer if you previously downloaded this beta. The old ZIP was removed.

ISO recording requires a working FFmpeg executable configured in Zoom ISO Recorder; the macOS installer does not bundle it.

Talkback remains Windows-only. For your own screen, use OBS's native macOS Screen Capture source. Intel Macs are not supported by this package.

Sign in with Zoom

Use case: Authorize CoreVideo to join meetings and receive raw participant media on your behalf. You sign in once; CoreVideo refreshes the session automatically.

Prerequisites: A Zoom account and CoreVideo installed. You do not need a Zoom developer account, an SDK key, or any client secret โ€” published builds use CoreVideo's hosted broker with OAuth 2.0 + PKCE.

1. Click "Sign in with Zoom"

In the Zoom Control dock, or via Tools โ†’ Zoom Plugin Settings.

Tools โ†’ Zoom Plugin Settings: Sign in with Zoom (no Client ID or secret for end users).
Tools โ†’ Zoom Plugin Settings: Sign in with Zoom (no Client ID or secret for end users).

2. Authorize in your browser

Your browser opens Zoom's consent screen. Approve access. CoreVideo registers its corevideo:// callback handler automatically the first time.

3. Done

The dock shows a connected status and token expiry. Tokens are stored locally on your machine (DPAPI-encrypted on Windows). CoreVideo never sees your Zoom password, and no client secret is stored in the app.

Requirements

RequirementNotes
Windows 10 / 11 (64-bit)Windows is the release channel (installer or ZIP). A signed macOS Apple Silicon beta installer is also available. Linux requires a source build.
OBS Studio 30+Install OBS first; CoreVideo loads as a plugin.
A Zoom accountAny account you can sign in with. No developer account, SDK key, or client secret required.
Network bandwidthPlan roughly 4-6 Mbps per 1080p participant feed. Your Zoom account and app entitlements affect maximum quality and stream count (standard accounts are commonly limited to ~30 Mbps incoming video; Enhanced Media / HBM can raise that to ~100 Mbps).

Using CoreVideo

Each capability below lists what it's for and what you need before using it. For deeper technical detail, see the Architecture and Reference sections further down.

Join a meeting from the Zoom Control dock

Use case: Connect OBS to a live Zoom meeting or webinar.

Prerequisites: Signed in with Zoom. The host should admit "OBS" (the default display name) from the waiting room, or allow guests.

Paste a Meeting ID or Zoom URL, an optional passcode, and a display name, then click Join. Toggle Join as Webinar for webinars. The status dot turns green when connected.

The Zoom Control dock โ€” Join Meeting and the Active Speaker Director.
The Zoom Control dock โ€” Join Meeting and the Active Speaker Director.

Add Zoom sources to a scene

Use case: Bring individual participants, the active screen share, a spotlight slot, or interpretation audio into OBS as native sources you can position, style, and mix.

Prerequisites: Signed in and joined to a meeting.

In OBS, click + under Sources and choose a CoreVideo source: Zoom Participant (video), Zoom Participant Audio, Zoom Screen Share, Zoom Spotlight, or Zoom Interpretation Audio. Pick a participant or slot in the source properties (see the Configuration Reference).

CoreVideo source types in the OBS Add-Source menu.
CoreVideo source types in the OBS Add-Source menu.

Active Speaker Director

Use case: Automatically switch a source to whoever is speaking, with broadcast-safe debounce so it doesn't cut on every cough.

Prerequisites: In a meeting with participants on camera.

Enable Follow active speaker on a Zoom Participant source, or use the dock's Director controls, and tune sensitivity and minimum hold time. Full algorithm: Active Speaker Mode.

Output Manager & profiles

Use case: See every source's assignment and feed health at a glance, and save/load named participant-to-source layouts between shows.

Prerequisites: CoreVideo sources added to your scene.

Open the Zoom Output Manager dock to assign participants, watch live thumbnails and health, and save/load profiles. See Output Profiles.

ISO recording

Use case: Record each assigned participant to a separate video/audio file (clean ISOs) for editing, while optionally recording the OBS program.

Prerequisites: In a meeting with assigned sources, and disk space for per-participant files.

Enable ISO recording from the dock. Details: Auto ISO Recording.

Auto-reconnect

Use case: Automatically re-join after a network drop or engine restart so a broadcast recovers on its own.

Prerequisites: None โ€” on by default; a cancel button appears during recovery. See Auto-Reconnect.

External control (TCP / OSC)

Use case: Drive CoreVideo from production tools, scripts, or lighting/broadcast hardware.

Prerequisites: Enable the control server (optionally set a token). See the TCP and OSC reference.

Removing the App

Removing CoreVideo has two parts: de-authorizing it from your Zoom account, and uninstalling the software.

1. De-authorize CoreVideo in Zoom

Sign in to Zoom App Marketplace โ†’ Manage โ†’ Installed Apps, find CoreVideo, and click Remove. You can also click Disconnect in Tools โ†’ Zoom Plugin Settings first to clear the local session.

โš ๏ธ What de-authorization does
Zoom immediately revokes CoreVideo's access and refresh tokens. CoreVideo can no longer join meetings or receive any media on your behalf until you sign in again, and any active capture stops at the next token refresh.

2. Uninstall the software

Close OBS, then remove CoreVideo from Windows Settings โ†’ Apps โ†’ Installed apps โ†’ CoreVideo โ†’ Uninstall (or run the bundled uninstaller). This removes the plugin and its Zoom runtime from OBS.

3. How your data is removed

CoreVideo stores your Zoom OAuth tokens locally on your machine (DPAPI-encrypted on Windows) inside the OBS plugin config directory. It does not upload or store your account data on any server โ€” CoreVideo's hosted broker only performs the OAuth token exchange in transit and retains nothing about you.

See our Privacy Policy for full details on data handling.

Troubleshooting

โš ๏ธ Public beta
CoreVideo is in public beta. The Windows installer is not yet code-signed, and the issues below are the ones we expect to see most from new installs. If something isn't covered here, see Filing a bug below.
SymptomFix
Zoom Control dock not visible after installEnable it from OBS Docks โ†’ Zoom Control (or Tools โ†’ Zoom Control, which creates and shows it even if you've never opened it before). Confirm OBS is version 30+ and was fully closed during install.
"Sign in with Zoom" doesn't completeFinish the browser consent and allow the corevideo:// link to open CoreVideo. Re-run sign-in from Tools โ†’ Zoom Plugin Settings. See Sign-in loop below for more.
Joined, but no videoWork through the No video frames checklist below.
Source shows color barsThe source isn't subscribed yet โ€” assign a participant or slot in the source properties or the Output Manager.
Choppy or low-resolution feedsReduce the number of concurrent 1080p feeds or the requested resolution, and check available bandwidth (~4-6 Mbps per 1080p feed).
Windows warns "Windows protected your PC" during installExpected for the unsigned beta installer โ€” see SmartScreen below.
Windows Firewall prompts for CoreVideo or ZoomObsEngineBoth control ports are loopback-only โ€” see Firewall prompts below.

Installer blocked by Windows SmartScreen

CoreVideo's beta installer (CoreVideo-Setup.exe) is not yet code-signed, so Windows SmartScreen will show "Windows protected your PC" the first time you run it. This is expected during the beta โ€” it does not mean the download is unsafe, but you should still confirm you got the file from corevideo.io/download or the official GitHub Releases page.

1. Click "More info"

On the SmartScreen dialog, click More info, then Run anyway. If you don't see a "More info" link, your organization's policy may be blocking unsigned installers entirely โ€” check with your IT administrator.

2. (Optional but recommended) Verify the checksum

Every release publishes a matching .sha256 file next to the installer and ZIP. Compare it against a local hash before running the installer:

certutil -hashfile CoreVideo-Setup.exe SHA256

or, in PowerShell:

Get-FileHash .\CoreVideo-Setup.exe -Algorithm SHA256

The result should match the contents of the corresponding .sha256 file from the same release.

"No video frames" checklist

Work through these in order โ€” each one rules out a different stage of the pipeline.

  1. Assignment mode is set. A newly added Zoom Participant / Spotlight / Screen Share source isn't subscribed to anything until you assign it โ€” in the source properties or the Zoom Output Manager dock. An unassigned source shows the color-bar placeholder, not black.
  2. The participant actually has their camera on and has been admitted from the waiting room (or the meeting allows guests). CoreVideo can only capture video a participant is sending.
  3. Check requested vs. observed resolution in the Zoom Diagnostics dock. Open Docks โ†’ Zoom Diagnostics (or Tools โ†’ Zoom Diagnostics) and look at the Outputs table. If Observed stays blank or 0ร—0 while Requested shows a resolution, the subscription never came up โ€” re-assign the source or click Refresh. If observed resolution is consistently lower than requested, that's usually a bandwidth or entitlement ceiling, not a bug.
  4. Confirm the engine is actually running. Every meeting session launches zoom-runtime\ZoomObsEngine.exe as a child process of OBS โ€” all Zoom SDK access happens there, not in the plugin DLL. If it isn't running (check Task Manager, or "Engine running" in a support bundle's summary.txt), the plugin never receives frames. Antivirus software occasionally quarantines or blocks a freshly-installed ZoomObsEngine.exe โ€” check your antivirus's quarantine/history if it's missing.
  5. Bandwidth and entitlements. Your Zoom account and app entitlements bound both quality and stream count: standard accounts are commonly limited to ~30 Mbps incoming video (roughly 4-6 Mbps per 1080p feed), while Enhanced Media / HBM can raise that to ~100 Mbps. If several 1080p feeds are requested at once on a standard account, expect some to negotiate down or fail โ€” see Requirements.

Sign-in loop / browser doesn't return to OBS

"Sign in with Zoom" opens your browser to Zoom's consent screen and expects the browser to hand control back to CoreVideo afterward through the corevideo:// URL scheme. A few different things can break that handoff:

Firewall prompts for TCP 19870 / UDP 19871

โ„น๏ธ Both ports are loopback-only
The TCP control API and OSC control API both bind to 127.0.0.1 only โ€” nothing outside your machine can reach either port, regardless of how you answer a Windows Firewall prompt. Windows may still prompt the first time one of these servers binds, because it can't tell in advance that a listener is loopback-restricted.

It's safe to Allow access on Private networks if you plan to drive CoreVideo from an external control surface (Companion, a lighting console, a custom script) running on the same machine. If you don't use the TCP/OSC APIs, it's equally safe to Cancel or deny the prompt โ€” Zoom capture, sources, and sign-in do not depend on either server; only external control does.

Zoom Diagnostics / Zoom Output Manager / other docks missing

Every CoreVideo dock โ€” Zoom Control, Zoom Diagnostics, Zoom Output Manager, and Zoom ISO Recorder โ€” can be re-shown from OBS's Docks menu once it has appeared at least once. The first time, or if you closed it permanently, use the matching item under the Tools menu instead (e.g. Tools โ†’ Zoom Diagnostics) โ€” the Tools menu items create and show the dock even if OBS has never registered it in the Docks list before.

Plugin not loading

If OBS starts normally but none of the Zoom Control / Diagnostics / Output Manager / ISO Recorder menu items or docks appear at all, the plugin itself likely failed to load:

Filing a bug

Still stuck? Open the Zoom Diagnostics dock (Docks โ†’ Zoom Diagnostics, or Tools โ†’ Zoom Diagnostics) and click Create Support Bundle. It writes a redacted bundle โ€” SDK keys, secrets, OAuth tokens, and similar credentials are stripped before anything is written to disk โ€” plus a matching .zip under %AppData%\obs-studio\plugin_config\obs-zoom-plugin\support-bundles\. Attach that .zip when you file a bug report, or contact CoreVideo Support for anything that isn't a clear bug.

Architecture: System Overview

All Zoom SDK access lives exclusively in ZoomObsEngine. The plugin communicates with it through ZoomEngineClient โ€” a singleton that launches the engine process, owns the IPC channels, and distributes roster events and frame notifications to registered sources. The plugin contains no Zoom SDK headers or linkage.

Meeting joining is handled by the Zoom Control dock (ZoomDock) or the TCP/OSC APIs. On unexpected disconnects the ZoomReconnectManager automatically re-joins with exponential back-off. Sources each hold an AssignmentMode that controls which feed they subscribe to: a fixed participant, the active speaker, a spotlight slot, or the active screen share.

CoreVideo system overview Zoom Cloud feeds the ZoomObsEngine child process over HTTPS/WSS. The engine owns all Zoom SDK access and writes I420 video and PCM audio into shared memory, which the obs-zoom-plugin reads and hands to OBS Studio sources. Plugin and engine exchange JSON IPC over a named pipe or Unix socket. External controllers reach the plugin over TCP 19870 and OSC 19871. ZOOM CLOUD RAW A/V SOURCE Live meeting ZOOMOBSENGINE ALL ZOOM SDK ACCESS · CHILD PROCESS Zoom SDK · auth / meeting / raw Participant + spotlight tracking EngineVideo — I420 → SHM EngineAudio — PCM → SHM ISO recorder — FFmpeg IPC loop OBS-ZOOM-PLUGIN OBS MODULE · NO SDK DEPENDENCY ZoomEngineClient · singleton ZoomSource ×N — SHM read ZoomDock — control UI OAuth manager — PKCE · ZAK Output manager · reconnect Control APIs — TCP · OSC OBS STUDIO Zoom sources Program out EXTERNAL CONTROL COMPANION · SCRIPTS · CONSOLES HTTPS / WSS SHARED MEMORY I420 · PCM JSON IPC pipe / socket FRAMES TCP :19870 · UDP OSC :19871
Fig 1 - All SDK access is in ZoomObsEngine. ZoomOAuthManager handles broker-backed OAuth PKCE, token storage, and ZAK fetches. TCP and OSC provide external control without linking the Zoom SDK into OBS.

Architecture: Plugin Components

All Zoom SDK access lives in ZoomObsEngine. The plugin-side hierarchy centers on ZoomEngineClient (IPC singleton) and ZoomSource (reads frames from ShmRegion). New additions: ZoomDock (join UI), ZoomReconnectManager (auto-reconnect), and HwVideoPipeline (hardware I420โ†’NV12).

classDiagram class ZoomEngineClient { <<Singleton>> +start() / stop() +join() / leave() +subscribe(uuid, pid) +subscribe_spotlight(uuid, slot) +subscribe_screenshare(uuid) +state() / roster() +register_source(uuid, callbacks) } class ZoomSource { +assignment : AssignmentMode +resolution : VideoResolution +video_loss_mode : VideoLossMode +configure_output_ex(...) +on_engine_frame(w, h) +on_engine_audio(bytes) } class ZoomOAuthManager { <<Singleton>> +begin_authorization(parent) bool +handle_redirect_url(url) bool +register_url_scheme() bool +refresh_access_token_blocking() bool +fetch_user_zak_blocking(zak) bool } class ZoomDock { +on_join_clicked() +on_leave_clicked() +on_cancel_recovery_clicked() +update_state_indicator() } class ZoomReconnectManager { <<Singleton>> +set_policy(policy) +store_session(...) +trigger(reason) / cancel() +on_join_success() / on_join_failed() +next_retry_ms() int } class HwVideoPipeline { +init(mode: HwAccelMode) bool +process(y,u,v, w,h, strides, frame) bool +active_mode() HwAccelMode +shutdown() } class ZoomOutputManager { <<Singleton>> +register_source / unregister_source +configure_output_ex(mode, โ€ฆ) +outputs() vector } class ZoomControlServer { <<Singleton>> +start(port=19870) +set_token(token) } class ZoomOscServer { <<Singleton>> +start(port=19871) } ZoomDock --> ZoomEngineClient : join / leave ZoomDock --> ZoomOAuthManager : begin_authorization ZoomOAuthManager ..> ZoomEngineClient : ZAK + publicAppKey โ†’ engine init ZoomReconnectManager --> ZoomEngineClient : stop_for_reconnect / start / join ZoomEngineClient ..> ZoomReconnectManager : trigger / on_join_success ZoomSource --> ZoomEngineClient : subscribe / unsubscribe ZoomSource ..> HwVideoPipeline : I420โ†’NV12 conversion ZoomSource --> ZoomOutputManager ZoomControlServer --> ZoomOutputManager ZoomControlServer --> ZoomEngineClient ZoomOscServer --> ZoomOutputManager
Fig 2 โ€” Plugin class diagram. ZoomEngineClient is the IPC singleton; ZoomDock owns join flow; ZoomReconnectManager handles recovery.

Architecture: ZoomEngineClient

ZoomEngineClient is the plugin-side singleton that manages the entire engine process lifecycle. It launches ZoomObsEngine as a child process, connects the two IPC channels, runs a dedicated reader thread for incoming events, and dispatches per-source frame/audio callbacks. All Zoom SDK state (auth, meeting, roster, active speaker) is owned by the engine; the client tracks it locally by processing the event stream.

graph TB subgraph Plugin["obs-zoom-plugin process"] ZEC["ZoomEngineClient\nโ˜… singleton"] Reader["reader_loop()\n(dedicated thread)"] S1["ZoomSource A"] S2["ZoomSource B"] ZEC --> Reader ZEC -->|"frame / audio callbacks"| S1 ZEC -->|"frame / audio callbacks"| S2 end subgraph Engine["ZoomObsEngine process"] SDK3["Zoom SDK\n(ALL SDK access)"] ETrack["Participant &\nSpeaker tracking"] SDK3 --- ETrack end ZEC -- "commands ยท JSON" --> SDK3 SDK3 -- "events ยท JSON" --> Reader SDK3 -- "frame write" --> SHM["Named shared memory"] S1 & S2 -- "mmap read" --> SHM
Fig 3 โ€” ZoomEngineClient launches the engine, owns IPC channels, and dispatches callbacks to each ZoomSource.

Lifecycle

CallEffect
ZoomEngineClient::instance().start(auth_token, public_app_key)Launches engine process, connects pipes/sockets (up to 300 retries ร— 100 ms), and sends init with publicAppKey for published builds
join(meeting_id, passcode, name)Sends join command; blocks until joined event or failure
subscribe(uuid, pid)Sends subscribe; engine begins writing frames to ZoomObsPlugin_<uuid> shared memory
unsubscribe(uuid)Stops frame delivery; ZoomSource releases its ShmRegion
leave() / stop()Sends leave / quit; reader thread joins

Roster, Active Speaker, and Spotlight

The engine sends a participants event whenever the roster changes and an active_speaker event when the speaking participant changes. ZoomEngineClient updates its internal roster and active-speaker ID from these events, then fires all registered RosterCallback functions. ZoomSource instances react based on their AssignmentMode: active-speaker sources run the debounce algorithm; spotlight sources send a subscribe_spotlight command when spotlight slots change.

Deferred Join (Pending Until Authenticated)

Calls to join() before the engine has authenticated store the meeting credentials in m_join_pending / m_pending_* fields. When the auth_ok event arrives, send_join_locked() is called automatically so operators can trigger join at any time.

Monitor Thread

A dedicated monitor_loop() thread watches for engine health issues (stalled reader, unexpected process exit) and sets the state to Failed, which triggers ZoomReconnectManager if recovery is configured.

Architecture: IPC Engine

ZoomObsEngine runs as a separate process on all platforms. Control messages use newline-delimited JSON over platform IPC channels. Frame data flows through named shared memory to avoid copying large buffers. ZoomEngineClient in the plugin abstracts all IPC details.

flowchart LR subgraph Plugin["obs-zoom-plugin"] ZEC2["ZoomEngineClient"] SHM_R["ZoomSource\nSHM read (mmap)"] end subgraph Engine["ZoomObsEngine"] ELoop["IPC loop"] EMed["EngineVideo ยท EngineAudio"] end ZEC2 -- "commands ยท JSON" --> ELoop ELoop -- "events ยท JSON" --> ZEC2 EMed -- "I420 + PCM" --> SHM["Named shared memory"] --> SHM_R
Fig 4 โ€” Cross-platform IPC: JSON over named pipes (Windows) or Unix sockets (macOS/Linux); frames through shared memory.
PlatformPlugin โ†’ EngineEngine โ†’ Plugin
Windows\\.\pipe\ZoomObsPlugin_P2E\\.\pipe\ZoomObsPlugin_E2P
macOS / Linux/tmp/ZoomObsPlugin_P2E.sock/tmp/ZoomObsPlugin_E2P.sock

Architecture: Video Pipeline

flowchart TB CAM["Participant\nCamera"] --> ZC["Zoom Cloud"] ZC -->|decoded I420| EV["Engine EngineVideo\n(IZoomSDKRenderer)"] EV -->|"write I420 to\nShmRegion"| SHM2["Shared Memory\nZoomObsPlugin_<uuid>"] SHM2 -->|"frame event\n{uuid, w, h}"| ZEC3["ZoomEngineClient\non_engine_frame cb"] ZEC3 --> ZS5["ZoomSource\non_engine_frame()"] ZS5 -->|"VideoLossMode:\nLastFrame / Black"| LOSS["Loss handling"] LOSS -->|"I420 planar\nPreviewCallback โ‰ค5fps"| OBS_V["OBS Video\nobs_source_output_video()"]
Fig 5 โ€” Video path: engine writes I420 to shared memory; ZoomSource reads via ShmRegion and forwards to OBS.
PropertyOptionsNotes
ResolutionP360 / P720 / P1080Sent to engine in subscribe command; passed to SDK renderer
Frame format in SHMI420 YUV planarWritten by engine directly; ZoomSource reads planes from ShmRegion
Output to OBSI420 planarVIDEO_FORMAT_I420 via obs_source_output_video()
Video loss โ€” LastFrameHold last decoded frameDefault OBS behaviour
Video loss โ€” BlackPush black I420 immediatelyUseful for clean cuts
Preview callbackโ‰ค 5 fps raw I420Used by OBS UI thumbnail / output dialog

Architecture: Audio Pipeline

Audio processing now lives entirely in the engine process. The engine writes PCM chunks to a per-source shared memory region and sends an audio event. ZoomSource reads the chunk from m_audio_shm and pushes it to OBS. Stereo sources use an internal m_stereo_buf to interleave channels before output.

flowchart TB SDK4["Zoom SDK\n(inside engine)"] EAud2["EngineAudio\n(mixed + per-user)"] SDK4 -->|raw PCM| EAud2 EAud2 -->|"write PCM to SHM\n+ send audio event"| SHM3["Shared Memory\nZoomObsPlugin_<uuid>"] SHM3 -->|"on_engine_audio(byte_len)"| ZS6["ZoomSource\n(m_audio_shm)"] ZS6 -->|"Mono: direct S16\nStereo: interleaved"| OBS_A["OBS Audio\nobs_source_output_audio()"]
Fig 6 โ€” Audio pipeline: engine mixes/isolates audio in-process and delivers chunks through shared memory.
PropertyValue
Sample rate48 000 Hz
FormatS16LE
MonoMixed feed downmixed to one channel
StereoLeft/right separation via m_stereo_buf interleave in ZoomSource
Isolated audioConfigured per-source via isolate_audio flag in subscribe command

Architecture: Screen Share Pipeline

Screen share handling now lives entirely inside ZoomObsEngine. The engine's IMeetingShareCtrlEvent handler detects share-start events, attaches an IZoomSDKRenderer to the share source, and writes I420 frames to a dedicated shared memory region. The plugin receives frame events on the share source UUID and forwards them to OBS exactly like participant video.

flowchart TB SC2["Screen Share\nIn Meeting"] --> ZC3["Zoom Cloud"] ZC3 -->|onSharingStatus| EShare["Engine\nShareDelegate\n(inside ZoomObsEngine)"] EShare -->|subscribe share renderer| Rnd["IZoomSDKRenderer"] Rnd -->|"I420 frames"| SHM4["Shared Memory\nZoomObsPlugin_share"] SHM4 -->|"frame event"| ZEC4["ZoomEngineClient\nโ†’ ZoomSource (share)"] ZEC4 -->|obs_source_output_video| OBS_SS["OBS Zoom\nShare Source"]
Fig 7 โ€” Screen share: engine-side delegate writes I420 to shared memory; plugin receives via ZoomEngineClient frame callback.

Flow: Authentication

Published CoreVideo builds do not ship Meeting SDK secrets in the OBS plugin. After Zoom sign-in, the plugin refreshes OAuth tokens through the CoreVideo broker when needed, fetches the signed-in user's ZAK, and starts the helper with AuthContext.publicAppKey. The engine is not started on module load; it is launched the first time a join is requested.

sequenceDiagram participant OBS as OBS Studio participant PM as plugin-main participant ZDock as ZoomDock participant ZEC5 as ZoomEngineClient participant ZOA2 as ZoomOAuthManager participant Broker as CoreVideo Broker participant Engine as ZoomObsEngine participant SDK as Zoom SDK OBS->>PM: obs_module_load() PM->>ZOA2: load OAuth tokens and broker URL PM->>ZoomControlServer: start(control_server_port) PM->>ZoomOscServer: start(osc_server_port) PM->>ZDock: ensure_zoom_dock() after frontend loaded Note over ZDock: User clicks Join or API sends join cmd ZDock->>ZOA2: fetch_user_zak_blocking() ZOA2->>Broker: refresh OAuth token if needed ZOA2->>Zoom Cloud: GET /v2/users/me/token?type=zak ZOA2-->>ZDock: user ZAK ZDock->>ZEC5: start(public_app_key) ZEC5->>Engine: launch_engine() [fork / CreateProcess] ZEC5->>ZEC5: connect_ipc() [300 x 100ms retries] Engine-->>ZEC5: {"cmd":"ready"} ZEC5->>Engine: {"cmd":"init","public_app_key":"โ€ฆ"} Engine->>SDK: InitSDK() + SDKAuth({publicAppKey}) SDK-->>Engine: onAuthenticationReturn(SUCCESS) Engine-->>ZEC5: {"cmd":"auth_ok"} Note over ZEC5: authenticated true, send pending join if queued opt Identity expires later SDK-->>Engine: onZoomIdentityExpired() Engine->>SDK: SDKAuth({publicAppKey}) Engine-->>ZEC5: {"cmd":"auth_ok"} end
Fig 8 - OAuth tokens and ZAK are prepared before join. Engine is launched on first join and authenticates the Meeting SDK with the embedded publicAppKey.

Flow: Zoom OAuth PKCE

ZoomOAuthManager implements broker-backed OAuth 2.0 with PKCE (Proof Key for Code Exchange, S256) for user-level attributed meeting joins and Zoom App Marketplace compliance. The desktop plugin opens the broker start URL, receives a short-lived broker token through corevideo://oauth/callback, and never stores a Zoom app secret.

Setup (one-time per machine)

Use the embedded broker

Published builds already contain the CoreVideo broker URL. Open Tools โ†’ Zoom Plugin Settings; there are no Client ID, Client Secret, or Authorization URL fields for end users to configure.

Register the URL scheme

Click Sign in with Zoom. CoreVideo registers the corevideo:// callback helper automatically on first use.

Authorize

CoreVideo opens the browser at https://corevideo.iamfatness.us/oauth/start. Approve the app in Zoom.

Callback forwarding

Zoom redirects to the broker callback, then the broker returns an encrypted broker token to corevideo://oauth/callback. The helper forwards that URL to the plugin via the TCP control server (oauth_callback command). Tokens are stored; on Windows they are DPAPI-protected before writing to OBS config.

Authorization Flow

sequenceDiagram participant User participant ZDock3 as ZoomDock participant ZOA as ZoomOAuthManager participant Browser participant Broker2 as CoreVideo Broker participant ZoomCloud2 as Zoom Cloud participant Helper as CoreVideoOAuthCallback participant ZCS2 as ZoomControlServer User->>ZDock3: click "Sign in with Zoom" ZDock3->>ZOA: begin_authorization() ZOA->>ZOA: random_base64url(16) โ†’ state ZOA->>Browser: QDesktopServices::openUrl(/oauth/start?state=โ€ฆ) Browser->>Broker2: GET /oauth/start Broker2->>Broker2: generate PKCE verifier/challenge Broker2->>ZoomCloud2: redirect โ†’ /v2/authorize (PKCE) ZoomCloud2-->>Browser: consent screen User->>Browser: approve ZoomCloud2-->>Broker2: redirect to /oauth/callback?code=โ€ฆ&state=โ€ฆ Broker2-->>Browser: redirect โ†’ corevideo://oauth/callback Browser->>Helper: launch CoreVideoOAuthCallback helper Helper->>ZCS2: POST {"cmd":"oauth_callback", url} ZCS2->>ZOA: handle_redirect_url(url) ZOA->>ZOA: verify state matches m_pending_state ZOA->>Broker2: POST /oauth/redeem broker_token=โ€ฆ Broker2->>ZoomCloud2: POST /oauth/token (PKCE, no secret) Broker2-->>ZOA: {access_token, refresh_token, expires_in} ZOA->>ZOA: store tokens (DPAPI on Windows) Note over ZOA: Ready to fetch ZAK on next join
Fig 8b - OAuth PKCE flow: browser opens broker start URL, broker performs Zoom PKCE exchange, helper binary forwards broker token, plugin stores returned tokens.

ZAK Fetch and publicAppKey SDK Auth (before each join)

sequenceDiagram participant ZDock4 as ZoomDock participant ZOA2 as ZoomOAuthManager participant Broker3 as CoreVideo Broker participant ZoomAPI as Zoom REST API participant ZEC8 as ZoomEngineClient ZDock4->>ZOA2: fetch_user_zak_blocking(zak) ZOA2->>ZOA2: refresh access token if expired ZOA2->>ZoomAPI: GET /users/me/token?type=zak ZoomAPI-->>ZOA2: signed-in user ZAK ZOA2-->>ZDock4: zak string ZDock4->>ZEC8: start(publicAppKey) โ†’ join(โ€ฆ, zak) ZEC8->>Engine: {"cmd":"init"} then {"cmd":"join"}
Fig 8c - A user ZAK is fetched before each join using the stored OAuth access token; token refresh is automatic when expired.

Security Notes

PropertyImplementation
PKCE methodS256 - SHA-256 of a 32-byte random verifier, base64url-encoded by the broker
State parameter16-byte random base64url value; verified on callback to prevent CSRF
Client secretNo OAuth or Meeting SDK client secret is shipped in the OBS plugin. Public Client OAuth and Meeting SDK publicAppKey are used for published builds.
Token storage (Windows)Access and refresh tokens encrypted with DPAPI before writing to OBS global config
No secret loggingAccess tokens, refresh tokens, ZAKs, codes, broker tokens, and verifiers are never written to logs or IPC messages
Refresh rotationAlways persist the latest refresh token Zoom returns; stale tokens are discarded
Callback token bypassThe oauth_callback TCP command bypasses the control server token; the OAuth state and one-time verifier still guard against replay
Full setup guide
See docs/ZOOM_MARKETPLACE_OAUTH.md for the complete Zoom Marketplace app configuration, local setup walkthrough, and Marketplace review checklist.

Flow: Meeting Join & Capture

Joining is centralized in ZoomDock (or the TCP/OSC control APIs) โ€” not in individual sources. Sources subscribe to the already-joined meeting.

sequenceDiagram participant U as User / Control API participant ZDock2 as ZoomDock participant ZRM2 as ZoomReconnectManager participant ZEC6 as ZoomEngineClient participant ZS3 as ZoomSource participant Engine as ZoomObsEngine U->>ZDock2: click Join ZDock2->>ZRM2: store_session(auth_token, id, pass, name) ZDock2->>ZEC6: start() โ€” launch engine if needed ZDock2->>ZEC6: join(id, pass, name, kind=Meeting|Webinar) ZEC6->>Engine: {"cmd":"join", kind} Engine-->>ZEC6: {"cmd":"joined"} ZEC6-->>ZRM2: on_join_success() Note over ZS3: ZoomSource.activate() when OBS activates source ZS3->>ZEC6: subscribe() / subscribe_spotlight() ZEC6->>Engine: {"cmd":"subscribe", uuid, pid} loop Live capture Engine-->>ZEC6: {"cmd":"frame","uuid":"โ€ฆ","w":W,"h":H} ZEC6-->>ZS3: on_engine_frame(w, h) ZS3->>ZS3: read I420 from SHM (+HW pipeline) ZS3->>OBS: obs_source_output_video() Engine-->>ZEC6: {"cmd":"audio","uuid":"โ€ฆ","byte_len":B} ZEC6-->>ZS3: on_engine_audio(byte_len) ZS3->>OBS: obs_source_output_audio() end opt Engine crash / unexpected disconnect ZEC6-->>ZRM2: trigger(EngineCrash | MeetingDisconnect) ZRM2->>ZRM2: schedule retry with exponential back-off ZRM2->>ZEC6: stop_for_reconnect() [on retry] ZRM2->>ZEC6: start(public_app_key) + join(โ€ฆ) end U->>ZDock2: click Leave ZDock2->>ZEC6: leave() ZEC6->>Engine: {"cmd":"leave"} ZRM2->>ZRM2: clear_session() [no recovery after explicit leave]
Fig 9 โ€” Join is driven by ZoomDock. Sources subscribe once in meeting. ZoomReconnectManager handles unexpected disconnects.

Assignment Modes

Each ZoomSource has an AssignmentMode property that controls which feed it subscribes to. This is the ZoomISO 3.0 model: sources are reusable roles rather than fixed participant bindings.

ModeEnum valueBehaviour
ParticipantAssignmentMode::ParticipantSubscribe to a fixed participant_id. If a failover_participant_id is set and the primary participant leaves, the source automatically re-subscribes to the failover.
Active SpeakerAssignmentMode::ActiveSpeakerFollow whoever is currently speaking. Uses the two-timer debounce (sensitivity + hold). See Active Speaker Mode section for details.
Spotlight SlotAssignmentMode::SpotlightIndexSubscribe to Spotlight 1-8. The engine tracks which participant is in each spotlight position and sends subscribe_spotlight subscriptions automatically when the slot changes.
Screen ShareAssignmentMode::ScreenShareSubscribe to the active screen-share feed. Uses subscribe_screenshare(uuid) on the engine. The source shows a placeholder when no share is active.

Screen Share Workflow

To capture slides, demos, or a shared desktop, add a CoreVideo Screen Share source or set any CoreVideo Participant source to Assignment > Active screen share. The source follows Zoom's active share feed and shows a placeholder when no participant is sharing.

The Zoom Output Manager assignment menu also includes Screen share. When a share is active, the menu label includes the sharing participant name. The TCP list_outputs response for screen-share outputs includes the current sharing participant ID and display name.

{
  "assignment_mode": "screen_share",
  "screen_share_available": true,
  "screen_share_participant_id": 123456,
  "screen_share_participant_name": "Alex Rivera",
  "observed_width": 1920,
  "observed_height": 1080,
  "observed_fps": 30.0
}

Failover Participant

In Participant mode, a failover_participant_id (0 = disabled) can be configured alongside the primary participant. When the engine sends a participants event showing the primary ID has left, ZoomSource automatically re-subscribes to the failover ID. When the primary rejoins, it switches back.

Spotlight Tracking

In Spotlight Slot mode, the roster callback checks ParticipantInfo::spotlight_index on every roster update. The source sends a subscribe_spotlight(uuid, slot) to the engine, which resolves the participant in that slot and sets up the SDK renderer. This mirrors the ZoomISO "Spotlight 1/2/3" output model.

flowchart LR subgraph Source["ZoomSource (AssignmentMode)"] P["Participant\nfixed pid + failover"] AS["ActiveSpeaker\ndebounce + liveness"] SP["SpotlightIndex\nslot 1-8"] SS["ScreenShare\nactive share feed"] end P -->|subscribe fixed participant| ZEC7["ZoomEngineClient"] AS -->|subscribe active speaker| ZEC7 SP -->|subscribe spotlight slot| ZEC7 SS -->|subscribe screen share| ZEC7 ZEC7 -->|JSON IPC| Engine2["ZoomObsEngine\n(resolves โ†’ SDK renderer)"]
Assignment modes map to different ZoomEngineClient subscribe calls; the engine resolves each to the correct SDK renderer.

Active Speaker Mode

When Follow active speaker is enabled on a Zoom Participant source, the plugin continuously monitors who is speaking and switches the video subscription (and optionally the isolated audio feed) to the new speaker. A two-timer debounce prevents rapid camera cuts caused by brief interruptions.

The current build also includes a central Active Speaker Director in the Zoom Control dock and a dedicated CoreVideo Active Speaker OBS source. The director tracks the raw Zoom speaker, candidate speaker, directed speaker, last directed speaker, and manual take/release state. The dock also shows a plain-language status line for waiting, holding, candidate evaluation, and manual supersede states. The dedicated source follows the directed speaker and uses a two-slot handoff: the current participant stays visible while the next participant warms on a hidden slot, then the source cuts only after a valid frame is available.

Debounce Algorithm

Every time ZoomEngineClient dispatches a roster callback (triggered by an active_speaker or participants IPC event), the source runs on_active_speaker_changed(). Two independent timers must both be satisfied before a switch is committed:

The actual delay is max(hold_remain, sense_remain). If the delay is zero the switch fires immediately on the calling thread; otherwise a detached background thread sleeps for the delay and then posts a UI task via obs_queue_task(OBS_TASK_UI, โ€ฆ) to re-evaluate on the OBS UI thread.

stateDiagram-v2 [*] --> Idle : active_speaker_mode = off Idle --> Idle : speaker change\n(mode is off, ignored) state "Active Speaker Mode ON" as ASM { [*] --> Watching Watching --> Evaluating : ZoomEngineClient fires roster cb\nnew active_speaker_id\n(โ‰  current participant) Evaluating --> Switching : delay = max(hold_remain, sense_remain) == 0\nAND speaker still active Evaluating --> Scheduled : delay > 0\nโ†’ schedule_speaker_check(spk, delay) Scheduled --> Evaluating : timer fires\nโ†’ try_commit_speaker() on UI thread Scheduled --> Watching : newer candidate\nsupersedes pending Switching --> Watching : do_speaker_switch()\nโ€ข unsubscribe old video\nโ€ข subscribe(new_pid, resolution)\nโ€ข update isolated audio user\nโ€ข record last_switch_time Evaluating --> Watching : speaker changed again\nbefore commit\n(pending_speaker_id reset) } Idle --> ASM : active_speaker_mode = on ASM --> Idle : active_speaker_mode = off\nor source destroyed
Fig 10 โ€” Active speaker state machine: debounce evaluation, deferred commit, and supersede handling.

Switch Sequence

sequenceDiagram participant ZP as ZoomEngineClient\n(roster_callback) participant ZS4 as ZoomSource participant Thread as Background Thread participant UI as OBS UI Thread participant VD as VideoDelegate ZP-->>ZS4: roster callback: active_speaker = X ZS4->>ZS4: pending = X, start hold timer alt delay == 0 ZS4->>ZS4: do_speaker_switch(X) ZS4->>VD: unsubscribe() ZS4->>VD: subscribe(X, resolution) Note over ZS4: isolate_audio โ†’ set_isolated_user(X) Note over ZS4: last_switch_time = now else delay > 0 ZS4->>Thread: schedule_speaker_check(X, delay_ms) Thread->>Thread: sleep(delay_ms) Thread->>UI: obs_queue_task(try_commit_speaker, X) UI->>ZS4: try_commit_speaker(X) alt X == pending_speaker_id AND still active ZS4->>ZS4: do_speaker_switch(X) ZS4->>VD: unsubscribe() + subscribe(X, resolution) else superseded or speaker moved on ZS4->>ZS4: pending_speaker_id = 0\n(or re-schedule) end end
Fig 11 โ€” Active speaker switch sequence: immediate vs. deferred commit with UI-thread safety.

Timing Parameters

ParameterDefaultRangeDescription
speaker_sensitivity_ms500 ms0 โ€“ 3 000 ms (step 50)New speaker must hold the floor this long before the switch fires
speaker_hold_ms2 000 ms0 โ€“ 10 000 ms (step 100)Minimum time to stay on current speaker after any switch

The Zoom Control dock exposes these values through the Active Speaker Director controls. Operators can also manually take a participant to air and release that supersede when automatic direction should resume.

Safety Mechanisms

Audio Isolation Interaction

When Isolate Audio is also enabled, every speaker switch triggers a new subscribe command to the engine with the updated participant ID and the isolate_audio flag, so the engine adjusts the audio feed to follow the same participant as the video.

UI Behaviour

Enabling active speaker mode in the Properties panel automatically disables the Participant dropdown (not relevant while following the speaker) and enables the Sensitivity and Hold sliders. The participant list still shows live roster entries with talking (โ—) and video ([video]) indicators, and updates when the Refresh button is pressed.

Director TCP Controls

CommandRequestUse
speaker_director_status{"cmd":"speaker_director_status"}Returns directed, raw, candidate, last, manual, sensitivity, and hold state
speaker_director_configure{"cmd":"speaker_director_configure","sensitivity_ms":650,"hold_ms":2500}Updates director timing
speaker_director_take{"cmd":"speaker_director_take","participant_id":123}Manually holds a participant as the directed speaker
speaker_director_release{"cmd":"speaker_director_release"}Returns to automatic speaker direction

The status payload includes numeric IDs and resolved participant objects for directed_speaker, raw_speaker, candidate_speaker, last_speaker, manual_speaker, and excluded_participants, plus a status value such as holding, candidate_pending, manual_supersede, or waiting_for_speaker. TCP subscribers receive speaker_director_changed when the directed, candidate, or manual speaker changes.

Auto-Reconnect

ZoomReconnectManager automatically re-joins the meeting after engine crashes, network drops, SDK errors, or host-ended meetings. It is configurable per-session and shows a live countdown in the Zoom Control dock.

Reconnect Policy

FieldDefaultDescription
enabledtrueMaster switch for auto-reconnect
max_attempts5Maximum retry attempts before giving up
base_delay_ms2 000 msInitial delay before first retry
max_delay_ms30 000 msMaximum delay between retries (caps the back-off)
backoff_multiplier2.0Exponential multiplier: delay ร— 2^attempt
on_engine_crashtrueTrigger recovery on engine process exit
on_disconnecttrueTrigger recovery on unexpected meeting disconnect
on_auth_failfalseTrigger recovery on auth failure (disabled by default)

Recovery Triggers

RecoveryReasonCause
EngineCrashEngine process exited unexpectedly
MeetingDisconnectSDK reported unexpected meeting end
NetworkDropNetwork connectivity lost mid-meeting
AuthFailureSDK authentication failed or expired without self-healing
SdkErrorFatal SDK error returned to engine
HostEndedMeetingHost ended the meeting (re-join if meeting resumes)
LicenseErrorRaw-data permission, app approval, or developer entitlement issue

Cancellation Safety

A monotonically increasing generation counter ensures stale timer wakeups never fire a retry that was already cancelled or superseded. Calling cancel() bumps the generation; the timer thread sees the mismatch and discards the pending retry. The dock, TCP recovery_cancel, and OSC /zoom/recovery/cancel entry points also stop the engine and clear the stored join session so a canceled retry loop cannot resurrect itself.

Explicit leave() calls set the m_user_leaving flag and call clear_session(), preventing any recovery attempt after a deliberate disconnect.

Zoom Control Dock

ZoomDock is a dockable Qt widget added to OBS on plugin load. Access it via Tools โ†’ Zoom Control or drag it to any dock position in OBS. It is the primary UI for meeting management.

CoreVideo OBS workspace with Zoom Control dock
CoreVideo runs as normal OBS UI: the Zoom Control dock manages meeting state and raw media while Zoom Participant sources render inside OBS scenes.

Meeting Control Bar

ControlDescription
CvStatusDot + labelAnimated QPainter status indicator โ€” pulses during transitional states (Joining / Leaving / Recovering); static dot for Idle / InMeeting / Failed
CvBanner (first-run)Dismissable notice strip shown until SDK credentials are configured
Meeting ID fieldZoom meeting number; persisted from last session
Passcode fieldOptional meeting passcode
Display Name fieldName shown inside the Zoom meeting; persisted from last session
Token type selectorCombo box to choose join auth method. Published builds use Auto Zoom sign-in, which fetches a user ZAK and authenticates the SDK with publicAppKey before joining.
Webinar checkboxUse Zoom Webinar SDK join API instead of regular meeting API; persisted
Join buttonFetches a user ZAK when needed, starts ZoomObsEngine with publicAppKey auth, then calls ZoomEngineClient::join()
Leave buttonCalls ZoomEngineClient::leave() and clears reconnect session
Start Engine buttonStarts raw media capture after the meeting is joined and sends Zoom video/audio to OBS outputs
Stop Engine buttonStops raw media capture while staying joined to the meeting
Participant filterFilter participant list by display name
Participant listShows Zoom display name, user ID, video/audio state, talking state, spotlight, and screen-share tags; participants can be dragged onto output rows
Active speaker labelShows current active speaker's display name

Session Persistence

The dock saves last_meeting_id, last_display_name, and last_was_webinar to the plugin settings file on each successful join. These values are restored and pre-filled in the dock on next OBS launch.

Recovery Panel

Shown automatically when MeetingState::Recovering. Displays:

Routing Section

The dock no longer embeds the full output table. Its routing section opens the dedicated Zoom Output Manager dock, which is the primary assignment surface for saving, loading, deleting profiles, and reviewing requested resolution, observed signal, frame rate, audio mode, audio role, screen-share state, and spotlight state.

Hardware Video Acceleration

When built with -DCOREVIDEO_HW_ACCEL=ON, each ZoomSource owns a HwVideoPipeline that converts incoming I420 frames to NV12 using FFmpeg hardware acceleration. This offloads color-space conversion from the CPU to the GPU/media engine.

HwAccelModeBackendPlatform
NoneCPU path (default)All
AutoFirst available hardware backendAll
CudaNVIDIA CUDAWindows / Linux
VaapiVAAPI (Intel / AMD)Linux
VideoToolboxApple VideoToolboxmacOS
QsvIntel Quick SyncWindows / Linux

A per-source hw_accel_override property (โˆ’1 = use global setting) allows different acceleration modes per source. HwVideoPipeline builds a dynamic FFmpeg filter graph on the first frame and rebuilds it automatically on resolution change. On any error it falls back to the CPU path and sets a m_broken flag to avoid repeated failures.

Build requirement
Hardware acceleration requires FFmpeg (libavfilter, libavhwaccel). Without -DCOREVIDEO_HW_ACCEL=ON, HwVideoPipeline is compiled to an empty stub and init() always returns false.

TCP Control API

Listens on 127.0.0.1:19870 (configurable). Each request and response is a single-line compact JSON terminated by \n. If a token is configured, every request must include "token":"<value>". Token comparison uses constant-time equality to prevent timing attacks.

Quick test
echo '{"cmd":"status"}' | nc 127.0.0.1 19870
CommandRequestResponse fields
help{"cmd":"help"}commands โ€” array of names
status{"cmd":"status"}meeting_state, active_speaker_id
list_participants{"cmd":"list_participants"}participants array
list_outputs{"cmd":"list_outputs"}outputs array with requested signal, observed signal, subscribed age, stale state, recovery attempts, quality-upgrade attempts, and retry cooldowns
recover_stale_outputs{"cmd":"recover_stale_outputs","force":true}recovered count after retrying stale feeds
upgrade_low_quality_outputs{"cmd":"upgrade_low_quality_outputs","force":true}upgraded count after retrying live feeds below requested resolution; skips feeds already at 1080p
assign_output{"cmd":"assign_output","source":"โ€ฆ","participant_id":N,"active_speaker":false,"isolate_audio":true,"audio_channels":"stereo"}ok, error
speaker_director_status{"cmd":"speaker_director_status"}speaker_director object with directed/raw/candidate/manual/timing state
speaker_director_configure{"cmd":"speaker_director_configure","sensitivity_ms":650,"hold_ms":2500}speaker_director object after applying timing changes
speaker_director_take{"cmd":"speaker_director_take","participant_id":N}speaker_director object with manual supersede enabled
speaker_director_release{"cmd":"speaker_director_release"}speaker_director object with automatic direction restored
join{"cmd":"join","meeting_id":"โ€ฆ","passcode":"โ€ฆ","display_name":"OBS"}ok
leave{"cmd":"leave"}ok
oauth_callback{"cmd":"oauth_callback","url":"corevideo://oauth/callback?broker_token=โ€ฆ&state=โ€ฆ"}ok or error - forwards the OS URL-scheme callback to ZoomOAuthManager::handle_redirect_url(). Token check is bypassed for this command; OAuth state and broker token validation still guard against replay.

Participant object

{ "id": 123, "name": "Alice", "has_video": true, "is_talking": false, "is_muted": false }

Output object

{ "source": "Zoom Participant 1", "participant_id": 123, "active_speaker": false,
  "assignment_mode": "participant", "video_resolution": "1080p",
  "observed_width": 1920, "observed_height": 1080, "observed_fps": 29.97,
  "isolate_audio": true, "audio_channels": "stereo" }

Meeting states

StateMeaning
idleNot in a meeting
joiningJoin in progress
in_meetingActive meeting
leavingLeave in progress
failedMeeting connection failed

OSC Control API

Listens on 127.0.0.1:19871 UDP (configurable). Accepts standard OSC 1.0 datagrams. Replies are sent back to the originating host/port. Supported argument types: i (int32), f (float32), s (string), T / F (true/false booleans).

Incoming Addresses

AddressType tagsArgumentsAction
/zoom/statusโ€”โ€”Reply with meeting state + active speaker
/zoom/list_outputsโ€”โ€”Reply one /zoom/output packet per output
/zoom/recover_stale_outputs[,i]optional force flagRetry stale video outputs and reply with recovered count
/zoom/upgrade_low_quality_outputs[,i]optional force flagRetry live video outputs below requested resolution and reply with upgraded count
/zoom/list_participantsโ€”โ€”Reply one /zoom/participant packet per participant
/zoom/join,sssmeeting_id, passcode, display_nameJoin meeting
/zoom/leaveโ€”โ€”Leave meeting
/zoom/assign_output,si[i]source, participant_id, [active_speaker 0/1]Assign source to participant
/zoom/assign_output/active_speaker,ssourceSwitch source to active-speaker mode
/zoom/speaker_director/status--Reply with directed/raw/candidate/manual speaker and timing state
/zoom/speaker_director/configure[,iiiii]sensitivity_ms, hold_ms, [require_video], [exclude1], [exclude2]Configure Active Speaker Director timing and exclusions
/zoom/speaker_director/take,iparticipant_idManually hold a participant as the directed speaker
/zoom/speaker_director/release--Return to automatic speaker switching
/zoom/isolate_audio,sisource, 0|1Toggle audio isolation for a source

Reply Addresses (sent by plugin)

AddressType tagsFields
/zoom/status/meeting_state,sstate string
/zoom/status/active_speaker,iuser_id
/zoom/output,sisiisource_name, participant_id, display_name, active_speaker, isolate_audio
/zoom/speaker_director/status,iiiiiiiidirected, raw, candidate, last, manual, sensitivity_ms, hold_ms, require_video
/zoom/speaker_director/configured,iiisensitivity_ms, hold_ms, require_video
/zoom/speaker_director/take/result,i1 if manual take succeeded, otherwise 0
/zoom/speaker_director/release/result,i1 if manual mode changed, otherwise 0
/zoom/participant,isiiiuser_id, display_name, has_video, is_talking, is_muted

Output Profiles

The ZoomOutputProfile namespace persists named output configurations as JSON files under the OBS plugin config directory:

obs-studio/plugin_config/obs-zoom-plugin/profiles/<name>.json

Each profile stores all current output assignments (source name, participant ID, active speaker flag, audio isolation, audio mode). Use the Zoom Output Manager dock (or OBS โ†’ Tools to focus it) to save, load, and delete profiles interactively, or call the ZoomOutputProfile API directly from code.

FunctionDescription
ZoomOutputProfile::list()Returns names of all saved profiles
ZoomOutputProfile::save(name, outputs)Writes profile JSON file
ZoomOutputProfile::load(name)Reads profile and returns output configs
ZoomOutputProfile::remove(name)Deletes profile JSON file
CoreVideo Output Manager with participant and output mapping
The Output Manager mirrors the dock assignment controls and remains useful for named profile save/load workflows.

Profile JSON format

[
  {
    "source": "Zoom Participant 1",
    "display_name": "Camera A",
    "participant_id": 123,
    "active_speaker": false,
    "isolate_audio": true,
    "audio_channels": "stereo"
  }
]

IPC Protocol Reference

All messages are UTF-8 JSON terminated by \n. Frame data is transferred through named shared memory (prefix ZoomObsPlugin_).

Plugin โ†’ Engine

CommandPayloadDescription
init{"cmd":"init","public_app_key":"โ€ฆ"}Initialize SDK and authenticate with the embedded Marketplace Public Client ID
join{"cmd":"join","meeting_id":"โ€ฆ","passcode":"โ€ฆ","display_name":"OBS","kind":"meeting|webinar","user_zak":"โ€ฆ","on_behalf_token":"โ€ฆ"}Join meeting or webinar. Published broker-backed joins authenticate the SDK during init; user_zak and on_behalf_token remain developer/manual fallback fields.
leave{"cmd":"leave"}Leave meeting
subscribe{"cmd":"subscribe","source_uuid":"โ€ฆ","participant_id":N,"isolate_audio":bool}Subscribe to fixed participant frames
subscribe_spotlight{"cmd":"subscribe_spotlight","source_uuid":"โ€ฆ","slot":N}Subscribe to Spotlight 1-8
subscribe_screenshare{"cmd":"subscribe_screenshare","source_uuid":"โ€ฆ"}Subscribe to active screen-share feed
unsubscribe{"cmd":"unsubscribe","source_uuid":"โ€ฆ"}Stop frame delivery for this source
quit{"cmd":"quit"}Shut down engine process

Engine โ†’ Plugin

EventPayloadDescription
ready{"cmd":"ready"}Engine started and IPC connected
auth_ok{"cmd":"auth_ok"}SDK authenticated; pending join fires now
auth_fail{"cmd":"auth_fail","code":N}Authentication failed
joined{"cmd":"joined"}Now in meeting
left{"cmd":"left"}Left meeting (normal or host-ended)
frame{"cmd":"frame","uuid":"โ€ฆ","shm":"โ€ฆ","w":W,"h":H}Video frame written to named shared memory
audio{"cmd":"audio","uuid":"โ€ฆ","shm":"โ€ฆ","byte_len":B}PCM audio chunk written to named shared memory
participants{"cmd":"participants","active_speaker_id":N,"participants":[{"user_id":N,"display_name":"โ€ฆ","has_video":bool,"is_talking":bool,"is_muted":bool,"is_host":bool,"is_co_host":bool,"raised_hand":bool,"spotlight_index":N,"is_sharing_screen":bool},โ€ฆ]}Full roster snapshot on any roster change
active_speaker{"cmd":"active_speaker","participant_id":N}Active speaking participant changed
error{"cmd":"error","msg":"โ€ฆ","code":N}SDK or meeting error

Configuration

Settings Dialog (Tools โ†’ Zoom Plugin Settings)

Published builds use the embedded CoreVideo broker — end users do not enter SDK keys, secrets, or JWT tokens.

OAuth Settings

FieldDefaultDescription
OAuth Client IDEmbedded/brokeredDeveloper-only field when the build has no embedded broker URL. Published builds use the broker public client configuration.
OAuth Client SecretNot used in published builds. Public Client OAuth uses PKCE and no desktop secret.
Authorization URLEmbeddedPublished builds use https://corevideo.iamfatness.us/oauth/start.
Redirect URIcorevideo://oauth/callbackLocal return URI used by the callback helper after broker authorization.
Scopesuser:read:token user:read:userOAuth scopes requested during authorization.
OAuth Status labelโ€”Shows connected/disconnected status and token expiry
Register corevideo:// URL Schemeโ€”Registers the OS URL scheme handler for the callback helper; published builds do this automatically on sign-in.
Sign in with Zoomโ€”Opens browser to begin the broker-backed PKCE authorization flow.
Refresh Tokenโ€”Manually trigger token refresh
Disconnectโ€”Clears stored access and refresh tokens

Control & Network

FieldDefaultDescription
Control Server Port19870TCP JSON API port
Control Server Token(empty)Optional auth token; constant-time comparison
OSC Server Port19871UDP OSC API port

Meeting Control Dock

FieldDefaultDescription
Meeting ID or Zoom URLlast usedNumeric meeting ID or full Zoom join URL
Passcodeโ€”Optional meeting password; auto-filled from join URLs when present
Display NameOBSName shown inside the Zoom meeting
Join as Webinar / Zoom EventsoffUses the webinar join path when selected

Zoom Participant Source Properties

CoreVideo Zoom Participant source properties
Each Zoom Participant source keeps its own assignment mode, audio routing, requested resolution, and failover behavior.
PropertyDefaultDescription
Output Labelโ€”Label shown in the OBS Output Manager
Participantโ€”Select from live roster
Follow active speakeroffAutomatically follow whoever is speaking
Switch sensitivity (ms)300New speaker must hold the floor this long before switching
Minimum hold time (ms)2000Stay on current speaker at least this long after switching
Video Resolution1080p360p / 720p / 1080p
On video lossHold last frameHold last frame or show black
Audio ChannelsMonoNone / Mono / Stereo
Isolate AudiooffUse per-user feed instead of meeting mix

Zoom Participant Audio Source

PropertyDescription
ParticipantSelect from live roster
Audio ChannelsMono / Stereo

Zoom Interpretation Audio Source

PropertyDescription
LanguageLanguage name as reported by Zoom (e.g. English, Spanish)
โš ๏ธ Display Name
The plugin joins meetings as the configured display name (default "OBS"). Ensure the host allows guests or pre-admits the OBS participant from the waiting room.

Auto ISO Recording

CoreVideo can record assigned participant media as separate ISO files while optionally starting the main OBS program recording. ISO recording is owned by the OBS plugin process: the engine remains minimal and only publishes raw I420 video and PCM audio through the existing IPC and shared-memory path.

OBS ISO Recorder Panel

Open Tools โ†’ Zoom ISO Recorder to manage ISO recording from a separate OBS dock. The panel lets operators choose an output folder, set or test the FFmpeg executable, choose whether to also control OBS program recording, start/stop ISO recording, and monitor active ISO sessions with source, participant, resolution, frame counts, audio chunk counts, and output file paths. Recently completed sessions remain visible after stop so operators can confirm completed MP4/WAV outputs before opening the folder.

The panel uses the same ZoomIsoRecorder backend as the TCP and OSC APIs and persists its folder, FFmpeg path, and program-recording toggle in OBS global settings. If a selected hardware encoder is unavailable in the FFmpeg build, CoreVideo falls back to libx264 when available and reports the requested encoder, actual encoder, and fallback state in the panel and TCP status payload.

CoreVideo ISO recording flow
Assignments resolve the participant IDs. Each assigned Zoom source can feed an FFmpeg writer for video/audio ISO files while OBS continues to own the program output.

Start Recording

{
  "cmd": "iso_recording_start",
  "output_dir": "C:/Recordings/CoreVideo",
  "ffmpeg_path": "ffmpeg",
  "record_program": true
}

Check Status

{ "cmd": "iso_recording_status" }

Stop Recording

{ "cmd": "iso_recording_stop" }

OSC Controls

AddressArgumentsAction
/zoom/iso/startoutput_dir, optional record_programStarts ISO recording
/zoom/iso/stopnoneStops ISO recording
Source assignment triggers the ISOs
ISO files are created for participants that are assigned through fixed, active-speaker, spotlight, or screen-share workflows. If the active speaker changes, the recorder follows the resolved assignment and keeps participant file naming stable by participant ID and display name.

See the Core Plugin Functionality guide for complete TCP examples, OSC examples, troubleshooting notes, and the plugin media pipeline diagrams.

Modern UI (Phase 1)

CoreVideo applies its own scoped Qt stylesheet and custom widgets to the plugin UI, giving it a consistent dark-theme appearance without interfering with OBS's global QApplication stylesheet.

CoreVideo Stylesheet (cv-style.h)

Applied widget-level via cv_stylesheet(). Covers group boxes, buttons (with role variants), line edits, combo boxes, tables, and scroll bars.

Button role propertyAppearanceUse
role = "primary"Zoom-blue accent fillAffirmative actions: Join, Apply, Save
role = "danger"Red tint fillDestructive or exit actions: Leave, Delete, Disconnect
(default)Neutral dark fillAll other buttons

CvStatusDot

A QWidget subclass that draws a coloured status indicator using QPainter rather than a Unicode bullet character. In transitional states (Joining, Leaving, Recovering) a QTimer drives a sinusoidal pulse glow animation.

MeetingStateColourAnimation
IdleGrey #555Static
JoiningAmber #d29922Pulse glow
InMeetingGreen #3fb950Static
LeavingAmber #d29922Pulse glow
RecoveringOrange #e36209Pulse glow
FailedRed #f85149Static

CvBanner

A compact QFrame-derived notice strip. Supports three kinds: Info (blue), Warning (amber), Error (red). An optional underlined action button on the right emits actionClicked(). Used in ZoomDock as a first-run prompt to configure SDK credentials.