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.
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
asks the broker for a short-lived Meeting SDK JWT; the broker validates the
signed-in Zoom access token before minting the SDK JWT.
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 slot 1โฆN โ 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.
Diagnostics
The dockable Zoom Diagnostics panel shows requested versus observed resolution, FPS, frame age, retry counters, and recent engine debug events.
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 JWTs are minted server-side after token validation. 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).
Auto ISO Recording
Record assigned participant video/audio tracks to separate files while optionally recording the main OBS program output.
Multi-Platform
Windows (x64/arm64), macOS (universal arm64+x86_64), Linux. SDK 5.17.x and 7.x header layouts supported.
Requirements
| Dependency | Version | Notes |
|---|---|---|
| OBS Studio | 30+ | libobs + obs-frontend-api |
| CMake | 3.16+ | Build system |
| Qt | 6.x | Core + Network + Widgets |
| Zoom Meeting SDK | 5.17.x or 7.x | Place at third_party/zoom-sdk/. CMake detects flat (5.x) and subfolder (7.x) header layouts automatically. |
| C++ Compiler | C++17 | MSVC 2022 / Clang 14+ / GCC 11+ |
| Zoom Developer Account | โ | Marketplace app with Public Client OAuth + PKCE, Meeting SDK / Embed enabled, and server-side broker secrets for Meeting SDK JWT minting. |
| Zoom bandwidth / app entitlements | โ | Raw data is available through Meeting SDK apps. Standard accounts commonly have a 30 Mbps incoming video envelope; Enhanced Media / HBM can raise that to roughly 100 Mbps. Developers may need an app-level flag to test more than a small number of concurrent raw streams. |
| FFmpeg | โ | Required at runtime for ISO recording. Also used when building with -DCOREVIDEO_HW_ACCEL=ON for hardware-accelerated I420โNV12 conversion. |
| Qt TLS backend (Windows) | โ | Required for OAuth HTTPS. Ship Schannel TLS plugins to obs-plugins/64bit/plugins/tls/. |
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.
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).
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.
Lifecycle
| Call | Effect |
|---|---|
ZoomEngineClient::instance().start(jwt) | Launches engine process, connects pipes/sockets (up to 300 retries ร 100 ms), sends init with JWT |
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.
| Platform | Plugin โ Engine | Engine โ Plugin |
|---|---|---|
| Windows | \\.\pipe\ZoomObsPlugin_P2E | \\.\pipe\ZoomObsPlugin_E2P |
| macOS / Linux | /tmp/ZoomObsPlugin_P2E.sock | /tmp/ZoomObsPlugin_E2P.sock |
Architecture: Video Pipeline
| Property | Options | Notes |
|---|---|---|
| Resolution | P360 / P720 / P1080 | Sent to engine in subscribe command; passed to SDK renderer |
| Frame format in SHM | I420 YUV planar | Written by engine directly; ZoomSource reads planes from ShmRegion |
| Output to OBS | I420 planar | VIDEO_FORMAT_I420 via obs_source_output_video() |
| Video loss โ LastFrame | Hold last decoded frame | Default OBS behaviour |
| Video loss โ Black | Push black I420 immediately | Useful for clean cuts |
| Preview callback | โค 5 fps raw I420 | Used 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.
| Property | Value |
|---|---|
| Sample rate | 48 000 Hz |
| Format | S16LE |
| Mono | Mixed feed downmixed to one channel |
| Stereo | Left/right separation via m_stereo_buf interleave in ZoomSource |
| Isolated audio | Configured per-source via isolate_audio flag in subscribe command |
Flow: Authentication
Published CoreVideo builds do not ship Meeting SDK secrets in the OBS plugin. After Zoom sign-in, the plugin asks the CoreVideo broker for a short-lived Meeting SDK JWT. The broker validates the signed-in Zoom access token and signs the SDK JWT server-side. The engine is not started on module load; it is launched the first time a join is requested.
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
Meeting SDK JWT Fetch (before each join)
Security Notes
| Property | Implementation |
|---|---|
| PKCE method | S256 - SHA-256 of a 32-byte random verifier, base64url-encoded by the broker |
| State parameter | 16-byte random base64url value; verified on callback to prevent CSRF |
| Client secret | No OAuth or Meeting SDK client secret is shipped in the OBS plugin. Meeting SDK secrets live only as Cloudflare Worker secrets used to mint short-lived SDK JWTs. |
| Token storage (Windows) | Access and refresh tokens encrypted with DPAPI before writing to OBS global config |
| No secret logging | Access tokens, refresh tokens, SDK JWTs, codes, broker tokens, and verifiers are never written to logs or IPC messages |
| Refresh rotation | Always persist the latest refresh token Zoom returns; stale tokens are discarded |
| Callback token bypass | The oauth_callback TCP command bypasses the control server token; the OAuth state and one-time verifier still guard against replay |
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.
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.
| Mode | Enum value | Behaviour |
|---|---|---|
| Participant | AssignmentMode::Participant | Subscribe 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 Speaker | AssignmentMode::ActiveSpeaker | Follow whoever is currently speaking. Uses the two-timer debounce (sensitivity + hold). See Active Speaker Mode section for details. |
| Spotlight Slot | AssignmentMode::SpotlightIndex | Subscribe to Spotlight slot N (1-based). The engine tracks which participant is in each spotlight position and sends subscribe_spotlight subscriptions automatically when the slot changes. |
| Screen Share | AssignmentMode::ScreenShare | Subscribe to the active screen-share feed. Uses subscribe_screenshare(uuid) on the engine. The source shows a placeholder when no share is active. |
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.
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 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:
- Sensitivity guard โ the candidate speaker must hold the
floor continuously for at least
speaker_sensitivity_ms(default 500 ms). A new candidate resets this clock. - Hold guard โ after any switch, no further switch may
occur for at least
speaker_hold_ms(default 2 000 ms), regardless of who speaks.
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.
Switch Sequence
Timing Parameters
| Parameter | Default | Range | Description |
|---|---|---|---|
speaker_sensitivity_ms | 500 ms | 0 โ 3 000 ms (step 50) | New speaker must hold the floor this long before the switch fires |
speaker_hold_ms | 2 000 ms | 0 โ 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
- Liveness flag โ
speaker_aliveis ashared_ptr<atomic<bool>>captured by value in every in-flight lambda. When the source is destroyed,speaker_alive โ falseis stored before deletion. The deferred UI task checks the flag before touchingZoomSource, preventing use-after-free. - Supersede logic โ if a new candidate arrives while one
is already pending,
pending_speaker_idandcandidate_sinceare updated to the newer speaker, restarting the sensitivity clock. Stale callbacks from the previous candidate seespk != pending_speaker_idand bail out silently. - Final verification โ immediately before calling
do_speaker_switch(), the code re-checksZoomParticipants::active_speaker_id() == spkso a switch never fires for a speaker who stopped talking during the hold period. - UI-thread commitment โ all state mutations (participant_id,
last_switch_time, video subscription) happen on the OBS UI thread via
obs_queue_task, avoiding data races with the properties panel.
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
| Command | Request | Use |
|---|---|---|
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 |
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
| Field | Default | Description |
|---|---|---|
enabled | true | Master switch for auto-reconnect |
max_attempts | 5 | Maximum retry attempts before giving up |
base_delay_ms | 2 000 ms | Initial delay before first retry |
max_delay_ms | 30 000 ms | Maximum delay between retries (caps the back-off) |
backoff_multiplier | 2.0 | Exponential multiplier: delay ร 2^attempt |
on_engine_crash | true | Trigger recovery on engine process exit |
on_disconnect | true | Trigger recovery on unexpected meeting disconnect |
on_auth_fail | false | Trigger recovery on auth failure (disabled by default) |
Recovery Triggers
| RecoveryReason | Cause |
|---|---|
EngineCrash | Engine process exited unexpectedly |
MeetingDisconnect | SDK reported unexpected meeting end |
NetworkDrop | Network connectivity lost mid-meeting |
AuthFailure | SDK authentication failed or expired without self-healing |
SdkError | Fatal SDK error returned to engine |
HostEndedMeeting | Host ended the meeting (re-join if meeting resumes) |
LicenseError | Raw-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() (via the dock or API) bumps the generation; the timer
thread sees the mismatch and discards the pending retry.
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.
Meeting Control Bar
| Control | Description |
|---|---|
CvStatusDot + label | Animated 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 field | Zoom meeting number; persisted from last session |
| Passcode field | Optional meeting passcode |
| Display Name field | Name shown inside the Zoom meeting; persisted from last session |
| Token type selector | Combo box to choose join auth method. Published builds use Auto Zoom sign-in, which fetches a broker-minted Meeting SDK JWT before joining. |
| Webinar checkbox | Use Zoom Webinar SDK join API instead of regular meeting API; persisted |
| Join button | Fetches a broker-minted Meeting SDK JWT when needed, starts ZoomObsEngine, then calls ZoomEngineClient::join() |
| Leave button | Calls ZoomEngineClient::leave() and clears reconnect session |
| Start Engine button | Starts raw media capture after the meeting is joined and sends Zoom video/audio to OBS outputs |
| Stop Engine button | Stops raw media capture while staying joined to the meeting |
| Participant filter | Filter participant list by display name |
| Participant list | Shows Zoom display name, user ID, video/audio state, talking state, spotlight, and screen-share tags; participants can be dragged onto output rows |
| Active speaker label | Shows 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:
- Recovery status message with attempt count
- Live countdown timer to next retry (refreshes every second)
- Cancel Recovery button โ calls
ZoomReconnectManager::cancel()
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, and isolated-audio 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.
| HwAccelMode | Backend | Platform |
|---|---|---|
None | CPU path (default) | All |
Auto | First available hardware backend | All |
Cuda | NVIDIA CUDA | Windows / Linux |
Vaapi | VAAPI (Intel / AMD) | Linux |
VideoToolbox | Apple VideoToolbox | macOS |
Qsv | Intel Quick Sync | Windows / 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.
-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.
echo '{"cmd":"status"}' | nc 127.0.0.1 19870
| Command | Request | Response 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
| State | Meaning |
|---|---|
idle | Not in a meeting |
joining | Join in progress |
in_meeting | Active meeting |
leaving | Leave in progress |
failed | Meeting 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
| Address | Type tags | Arguments | Action |
|---|---|---|---|
/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 flag | Retry stale video outputs and reply with recovered count |
/zoom/upgrade_low_quality_outputs | [,i] | optional force flag | Retry live video outputs below requested resolution and reply with upgraded count |
/zoom/list_participants | โ | โ | Reply one /zoom/participant packet per participant |
/zoom/join | ,sss | meeting_id, passcode, display_name | Join 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 | ,s | source | Switch source to active-speaker mode |
/zoom/isolate_audio | ,si | source, 0|1 | Toggle audio isolation for a source |
Reply Addresses (sent by plugin)
| Address | Type tags | Fields |
|---|---|---|
/zoom/status/meeting_state | ,s | state string |
/zoom/status/active_speaker | ,i | user_id |
/zoom/output | ,sisii | source_name, participant_id, display_name, active_speaker, isolate_audio |
/zoom/participant | ,isiii | user_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.
| Function | Description |
|---|---|
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 |
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
| Command | Payload | Description |
|---|---|---|
init | {"cmd":"init","jwt":"โฆ"} | Initialize SDK and authenticate with the broker-minted Meeting SDK JWT |
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 slot N (1-based) |
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
| Event | Payload | Description |
|---|---|---|
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 |
Installation
Get the Zoom SDK
Download from the Zoom Developer Portal
and place it at third_party/zoom-sdk/.
CMake auto-detects x64 / arm64 / x86 sub-layouts on Windows.
Configure CMake
cmake -B build \
-DCMAKE_BUILD_TYPE=Release \
-DZOOM_SDK_DIR=third_party/zoom-sdk \
-DCMAKE_PREFIX_PATH="/path/to/obs-studio;/path/to/Qt6"
Build
cmake --build build --config Release
Builds obs-zoom-plugin and ZoomObsEngine.
On Windows, sdk.dll is copied automatically if found.
Install into OBS
cmake --install build --prefix "/path/to/obs-studio"
Configure and launch
Open OBS โ Tools โ Zoom Plugin Settings, sign in with Zoom, and configure local control-server/reconnect settings. Published builds already know the CoreVideo broker URL; end users do not enter Zoom app credentials.
Configuration
Settings Dialog (Tools โ Zoom Plugin Settings)
SDK Credentials
| Field | Default | Description |
|---|---|---|
| SDK Key | Embedded/brokered | Developer-only override for local builds. Published builds use broker-minted Meeting SDK JWTs. |
| SDK Secret | Server-side only | Not entered by end users and not shipped in the plugin. The Cloudflare broker stores this as a secret. |
| JWT Token | โ | Optional developer override; leave blank for normal broker-backed joins. |
OAuth Settings
| Field | Default | Description |
|---|---|---|
| OAuth Client ID | Embedded/brokered | Developer-only field when the build has no embedded broker URL. Published builds use the broker public client configuration. |
| OAuth Client Secret | — | Not used in published builds. Public Client OAuth uses PKCE and no desktop secret. |
| Authorization URL | Embedded | Published builds use https://corevideo.iamfatness.us/oauth/start. |
| Redirect URI | corevideo://oauth/callback | Local return URI used by the callback helper after broker authorization. |
| Scopes | user:read:token user:read:user | OAuth 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
| Field | Default | Description |
|---|---|---|
| Control Server Port | 19870 | TCP JSON API port |
| Control Server Token | (empty) | Optional auth token; constant-time comparison |
| OSC Server Port | 19871 | UDP OSC API port |
Meeting Control Dock
| Field | Default | Description |
|---|---|---|
| Meeting ID or Zoom URL | last used | Numeric meeting ID or full Zoom join URL |
| Passcode | โ | Optional meeting password; auto-filled from join URLs when present |
| Display Name | OBS | Name shown inside the Zoom meeting |
| Join as Webinar / Zoom Events | off | Uses the webinar join path when selected |
Zoom Participant Source Properties
| Property | Default | Description |
|---|---|---|
| Output Label | โ | Label shown in the OBS Output Manager |
| Participant | โ | Select from live roster |
| Follow active speaker | off | Automatically follow whoever is speaking |
| Switch sensitivity (ms) | 300 | New speaker must hold the floor this long before switching |
| Minimum hold time (ms) | 2000 | Stay on current speaker at least this long after switching |
| Video Resolution | 1080p | 360p / 720p / 1080p |
| On video loss | Hold last frame | Hold last frame or show black |
| Audio Channels | Mono | None / Mono / Stereo |
| Isolate Audio | off | Use per-user feed instead of meeting mix |
Zoom Participant Audio Source
| Property | Description |
|---|---|
| Participant | Select from live roster |
| Audio Channels | Mono / Stereo |
Zoom Interpretation Audio Source
| Property | Description |
|---|---|
| Language | Language name as reported by Zoom (e.g. English, Spanish) |
Building from Source
Directory Layout
CoreVideo/
โโโ CMakeLists.txt
โโโ buildspec/
โ โโโ macos.cmake
โ โโโ windows.cmake
โโโ cmake/
โ โโโ CoreVideoOAuthCallback-Info.plist.in # macOS OAuth helper bundle plist
โโโ data/locale/en-US.ini
โโโ docs/ # GitHub Pages documentation
โ โโโ index.html
โ โโโ ZOOM_MARKETPLACE_OAUTH.md # OAuth PKCE setup guide
โ โโโ policies/ # Security & privacy policy documents
โโโ engine/src/ # ZoomObsEngine process (owns ALL SDK access)
โ โโโ main.cpp # IPC loop, SDK auth/join, roster/spotlight tracking
โ โโโ engine-video.cpp/h # IZoomSDKRenderer โ named shared memory (I420)
โ โโโ engine-audio.cpp/h # SDK audio โ named shared memory (PCM)
โโโ src/ # OBS plugin (no SDK linkage)
โโโ plugin-main.cpp # Module load/unload, dock, Tools menu, SIGPIPE
โโโ zoom-source.* # Participant source (ShmRegion, AssignmentMode,
โ # HwVideoPipeline, failover, hotkeys, placeholder)
โโโ zoom-engine-client.* # Singleton: IPC, engine launch, roster, callbacks,
โ # spotlight/screenshare subscribe, monitor thread
โโโ zoom-oauth.* # ZoomOAuthManager: broker PKCE, SDK JWT fetch, token storage
โโโ oauth-callback-helper.cpp # Windows: CoreVideoOAuthCallback.exe entry point
โโโ oauth-callback-helper-macos.mm # macOS: CoreVideoOAuthCallback.app entry point
โโโ zoom-dock.* # Qt dockable panel: CvStatusDot, CvBanner,
โ # token-type selector, recovery countdown
โโโ zoom-reconnect.* # Auto-reconnect manager (exponential back-off)
โโโ zoom-types.h # Shared enums + ZoomJoinAuthTokens struct
โโโ cv-style.h # CoreVideo Qt stylesheet (dark theme, button roles)
โโโ cv-widgets.* # CvStatusDot (animated dot), CvBanner (notice strip)
โโโ hw-video-pipeline.* # FFmpeg I420โNV12 (CUDA/VAAPI/VideoToolbox/QSV)
โโโ zoom-audio-delegate.* # Mixed / isolated SDK audio โ OBS
โโโ zoom-audio-router.* # Central SDK audio fan-out dispatcher
โโโ zoom-auth.* # SDK JWT auth + observable auth state
โโโ zoom-meeting.* # Meeting state machine
โโโ zoom-participants.* # Roster, active speaker, spotlight callbacks
โโโ zoom-participant-audio-source.* # Per-participant audio OBS source
โโโ zoom-interpretation-audio-source.* # Language interpretation channel OBS source
โโโ zoom-video-delegate.* # Video frames, resolution, loss mode, preview
โโโ zoom-share-delegate.* # Screen share frames โ OBS source
โโโ zoom-output-manager.* # Source registry + runtime reconfiguration
โโโ zoom-output-profile.* # Named JSON profile persistence
โโโ zoom-output-dialog.* # Qt Output Manager dock widget
โโโ zoom-control-server.* # TCP JSON control API (port 19870) + oauth_callback
โโโ zoom-osc-server.* # UDP OSC control API (port 19871)
โโโ zoom-settings.* # Broker URL, OAuth tokens, local ports + reconnect persistence
โโโ zoom-settings-dialog.* # Qt Settings dialog (Zoom sign-in + local settings)
โโโ zoom-credentials.h.in # Embedded SDK credentials (CMake-generated)
โโโ obs-zoom-version.h.in # Plugin version string (CMake-generated)
โโโ engine-ipc.h # IPC constants + cross-platform pipe/socket helpers
โโโ obs-utils.* # OBS helper functions
Platform Matrix
| Platform | Compiler | SDK Linkage | IPC Transport | Engine |
|---|---|---|---|---|
| Windows | MSVC 2022 | sdk.lib + sdk.dll | Named pipes | Yes |
| macOS | Clang 14+ | ZoomSDK.framework | Unix sockets | Yes |
| Linux | GCC 11+ | libsdk.so | Unix sockets | Yes |
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.
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.
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
| Address | Arguments | Action |
|---|---|---|
/zoom/iso/start | output_dir, optional record_program | Starts ISO recording |
/zoom/iso/stop | none | Stops ISO recording |
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 property | Appearance | Use |
|---|---|---|
role = "primary" | Zoom-blue accent fill | Affirmative actions: Join, Apply, Save |
role = "danger" | Red tint fill | Destructive or exit actions: Leave, Delete, Disconnect |
| (default) | Neutral dark fill | All 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.
| MeetingState | Colour | Animation |
|---|---|---|
| Idle | Grey #555 | Static |
| Joining | Amber #d29922 | Pulse glow |
| InMeeting | Green #3fb950 | Static |
| Leaving | Amber #d29922 | Pulse glow |
| Recovering | Orange #e36209 | Pulse glow |
| Failed | Red #f85149 | Static |
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.