Skip to content

Final Release v1.1.0 — ZCC, proxy, transcript params, individual video, Python asyncio/executor#109

Merged
MaxMansfield merged 69 commits intomainfrom
dev
Apr 16, 2026
Merged

Final Release v1.1.0 — ZCC, proxy, transcript params, individual video, Python asyncio/executor#109
MaxMansfield merged 69 commits intomainfrom
dev

Conversation

@MaxMansfield
Copy link
Copy Markdown
Collaborator

@MaxMansfield MaxMansfield commented Apr 15, 2026

Description

v1.1.0 adds Zoom Contact Center (ZCC) support, HTTP proxy configuration, transcript language
hints, per-participant video subscriptions, and a full Python concurrency upgrade (asyncio,
executor dispatch, EventLoop/EventLoopPool). Also includes several stability fixes — notably
suppressing spurious configure warnings on leave and a SIGSEGV race condition in the Python
poll/release path.

Related Issues

N/A

Type of Change

  • Bug fix (non-breaking change that fixes an issue)
  • New feature (non-breaking change that adds functionality)
  • Documentation update
  • Tests (adding or improving tests)
  • Build changes

Affected Components

  • Core C++ implementation
  • Node.js bindings
  • Python bindings
  • Build system
  • Documentation
  • Examples

Testing Performed

  • C++ unit tests: 70 tests via mock SDK — Client lifecycle, session/user events, media callbacks, proxy, transcript params, individual video, metadata
  • Python tests: 122 tests — asyncio, executor, context manager, GIL release, ZCC, individual video, constants, bidirectional param aliases
  • Node.js tests: 98 wrapper integration tests — Client methods, callbacks, event subscriptions, constants
  • Manual: All new features validated end-to-end against live Zoom meetings: proxy routing, transcript language hint, per-participant video, run_async, executor dispatch

Checklist

  • My code follows the style guidelines of this project
  • I have performed a self-review of my code
  • I have commented my code, particularly in hard-to-understand areas
  • I have made corresponding changes to the documentation
  • My changes generate no new warnings
  • I have added tests that prove my fix is effective or that my feature works
  • New and existing unit tests pass locally with my changes
  • Any dependent changes have been merged and published

Additional Context

Key fixes worth calling out:

  • Spurious configure warnings: updateMediaConfiguration was firing during callback teardown after session close — fixed by tracking sdk_opened_ state
  • DataOption: Unified stream delivery enum replacing the artificial split between AudioDataOption and VideoDataOption; both kept as backward-compat aliases

Advances both Node.js (package.json, package-lock.json) and Python
(pyproject.toml) to 1.1.0 in preparation for the v1.1 release.
Bump brace-expansion 2.0.2→2.0.3, markdown-it 14.1.0→14.1.1,
picomatch 2.3.1→2.3.2 and 4.0.3→4.0.4, minimatch 9.0.5→9.0.9,
tar 7.5.7→7.5.13, yaml 2.8.2→2.8.3.
- Pygments: 2.19.2 → 2.20.0 (fixes CVE-2026-4539)
- Add requirements-dev.txt pinning build/test/publish deps
  with Pygments>=2.20.0 to prevent regression
Keep the internal v1.1 spec out of the public repository.
Replace rtms_csdk.h with the new rtms_sdk.h C++ SDK. Client now
inherits rtms_sdk_sink and implements callbacks as virtual overrides,
eliminating the static sdk_registry_, registry_mutex_, and all nine
static handleXxx functions. Provider factory (rtms_sdk_provider)
replaces rtms_alloc/rtms_init/rtms_uninit/rtms_release. join() now
calls sdk_->open(this) instead of rtms_set_callbacks(). CMakeLists
updated to search for rtms_sdk.h instead of rtms_csdk.h.
Add RTMS_BUILD_TESTS=ON cmake option that wires up a Catch2 test target
(via FetchContent) linked against tests/mock_sdk.cpp instead of the real
Zoom SDK binary. The library requirement is bypassed for test-only builds.
Also bump cmake project version to 1.1.0 and add task test:cpp to
Taskfile.yml.
Add a stub implementation of rtms_sdk and rtms_sdk_provider that replaces
the Zoom SDK binary for unit testing. MockSdkState provides per-test
configurable return values and call recording; mock_trigger_* helpers
simulate SDK callbacks firing on the Client sink.

47 Catch2 tests cover: AudioParams/VideoParams/DeskshareParams validation,
Session null-pointer safety, MediaParams composition and toNative(),
Client lifecycle (create/join/poll/release), callback dispatch with guard
conditions (null buf, zero size, empty string), event subscription
deferral and on-confirm flush, and media type auto-enable on callback
registration. Runs in CI with no credentials or SDK binary required via
task test:cpp.
ZCC (Zoom Contact Center) uses engagement_id as its session identifier
instead of meeting_uuid. The Python test class TestZccEngagementId adds
four failing tests that verify:
  - join(engagement_id=...) returns True (not False from swallowed error)
  - _do_join() does not raise the missing-identifier ValueError
  - engagement_id is forwarded as the first positional arg to native join()
  - meeting_uuid takes priority when both identifiers are supplied

The JS test documents the expected API contract (the fully-mocked test
suite can't exercise real routing logic, so the Python tests carry the
behavioral coverage).
ZCC (Zoom Contact Center) identifies sessions with an engagement_id
rather than a meeting_uuid. This adds engagement_id to the JoinParams
interface and join() routing logic in all binding layers so ZCC callers
can pass engagement_id without falling back to an error.

Priority order: meeting_uuid > webinar_uuid > session_id > engagement_id.
The C++ core join() signature is unchanged — engagement_id is forwarded
as the meeting_uuid positional arg to sdk_->join().
Adds tests across all three layers that fail before implementation:
- C++: TranscriptParams default values, setters, toNative() mapping,
  MediaParams::setTranscriptParams/hasTranscriptParams, and
  Client::setTranscriptParams() triggering reconfigure
- Python: TranscriptParams class existence, defaults, setters,
  LANGUAGE_ID constants, set_transcript_params() on Client, __all__ exports
- JS: mock wired for setTranscriptParams and TranscriptParams;
  LANGUAGE_ID_ENGLISH/NONE constant values documented
Adds transcript parameter configuration to the C++ core, Node.js, and
Python bindings. TranscriptParams extends BaseMediaParams with
src_language (default -1/auto-detect) and enable_lid (default true).
TranscriptLanguage exposes all 38 language IDs as a named dict/object,
following the same pattern as AudioCodec and VideoCodec.
Adds 16 C++ enums (175 members) to rtms.h as the single source of
truth for all protocol constants, sourced from the Zoom RTMS data
types docs. Updates node.cpp and python.cpp to reference enum values
instead of raw integers throughout.

Key fixes:
- MediaType::ALL corrected from 31 to 32 (SDK_ALL = 0x1<<5)
- StopReason expanded from 19 to 27 values; fixes STOP_BC_HOST_DISABLED_APP (was APP_DISABLED_BY_HOST) and STOP_BC_INSTANCE_CONNECTION_INTERRUPTED (was MEETING_*)
- MessageType expanded from 19 to 30 values
- StreamState gains PAUSED=5 and RESUMED=6
- EventType gains PARTICIPANT_VIDEO_ON/OFF; ZCC voice events use separate ZccVoiceEventType enum
- Adds TranscriptLanguage dict and setTranscriptParams to node.cpp (parity with python.cpp)
- All pybind11 dict assignments use explicit (int) casts to avoid unregistered enum type errors
NVM and pyenv were unconditionally installed in every image build
regardless of TARGET, adding unnecessary time to py-only and js-only
builds. Moved both installs inside TARGET conditionals.

Consolidates Python dev dependencies into requirements-dev.txt
(adds auditwheel) so the Dockerfile uses a single pip install -r
instead of a hardcoded package list.

Updates test:py (Taskfile) to use .venv on darwin, and the test-py
Docker service to use system pip/pytest (no venv needed in container).
Enables proper TypeScript type checking and syntax in test files
(rtms.test.ts, rtms.wrapper.test.ts) via ts-jest transformer,
configured in jest.config.cjs.
Adds rtms.wrapper.test.ts which spawns real node processes against the
built ESM module to verify the actual compiled output — bypassing the
jest.mock() approach in rtms.test.ts that prevents genuine red/green
TDD cycles for new methods.

Adds jest.config.cjs (required because package.json has "type":"module")
to configure ts-jest for .ts test files and babel-jest for .js files.

Updates Taskfile test:js to run both test files.
Groups test files by language for clarity as the test suite grows.
Updates all path references in CMakeLists.txt, Taskfile.yml, and
compose.yaml to match the new layout.
Adds setProxy(proxy_type, proxy_url) to the C++ Client class,
delegating to the SDK's set_proxy() and throwing rtms::Exception
on failure. This is the shared implementation that all language
bindings delegate to.
Adds NodeClient::setProxy() in node.cpp with argument validation,
a passthrough in index.ts, and full JSDoc + type declaration in
rtms.d.ts.
Adds setProxy() to PyClient in python.cpp, set_proxy() as the
primary snake_case API in __init__.py (with setProxy as a legacy
camelCase alias), and type stubs in __init__.pyi.
Adds 3 Catch2 unit tests covering proxy forwarding, https support,
and SDK failure handling. Adds 1 pack-install integration test
verifying setProxy is present on the installed Client instance.
Also removes stray console.log from pack-install beforeAll.
Registers snake_case names as primary on the native pybind11 layer
(e.g. set_proxy, on_audio_data, subscribe_event) alongside the
existing camelCase names retained as legacy aliases. Updates the
Python wrapper to call super() via snake_case and renames all
wrapper-defined methods to snake_case with camelCase aliases.
Updates type stubs throughout.

This makes the Python SDK idiomatic (PEP 8) while preserving full
backward compatibility for callers using the camelCase API.
…o callbacks

Red phase for feat/individual-video (branch 5 of v1.1). Covers all four
test layers — C++ (subscribeVideo, setOnParticipantVideo, setOnVideoSubscribed),
TypeScript mock, TypeScript wrapper (real built module), and Python — with 15 Python,
5 JS wrapper, and 12 C++ compile-error failures confirming the feature is unimplemented.
Also adds mock_trigger_participant_video and mock_trigger_video_subscript_resp
trigger helpers to the C++ mock so behavioral tests can fire SDK sink callbacks
once the implementation exists.
Move detailed API examples (webhook validation, asyncio, executor
dispatch, scaling strategy) from README into dedicated language guides
(examples/node.md, examples/python.md). Add Zoom Contact Center guide
(examples/zcc.md). README is now a concise entry point with links to
in-depth per-language references.
The C SDK's rtms_metadata struct carries startTs, endTs, and an
ai_interpreter sub-struct (source language, sample rate, and up to 100
target language entries with voice/engine details) that were previously
silently dropped at the C++ wrapper boundary.

Adds AiTargetLanguage and AiInterpreter C++ classes, extends Metadata
to surface all four new fields, and propagates them through both the
Node.js (buildMetadataObj helper) and Python (new pybind11 class
registrations) bindings, including TypeScript interfaces and Python
type stubs.
When the C SDK fires audio/transcript callbacks without populating the
ai_interpreter struct, target_size is uninitialized and often negative.
The previous check (< 100) passed negative values through, which then
converted to a huge size_t in reserve(), throwing std::length_error and
crashing both Node.js and Python bindings.

Require target_size > 0 before trusting it; default to 0 targets.
…efaults

The C++ SDK's config() hangs when called before open() for VIDEO streams.
Introduced sdk_opened_ flag so configure() stores params but defers the
actual sdk_->config() call until join() has called sdk_->open().
join() now calls configure() unconditionally after open (not gated on
!media_params_updated_) to ensure pre-join setOn*/setVideoParams calls
are applied.

VideoParams() and DeskshareParams() default constructors now use
RAW_VIDEO / H264 / HD / 30fps / VIDEO_SINGLE_ACTIVE_STREAM instead of
all-zeros, enabling partial setVideoParams() calls (e.g. dataOpt only)
without triggering SDK errors or validation failures.

Tests updated to reflect deferred-config behavior (config_calls == 0
before join()) and corrected VideoParams default field values.
ParticipantInfo exposes id and name, not userId and userName.
The individual video subscribe example was using the wrong field names.
All RTMS_-prefixed enum class names inside the rtms namespace had a
redundant prefix (e.g. rtms::RTMS_EVENT_TYPE). Renamed to match the
ALL_CAPS convention of non-prefixed enums already in the file:

  RTMS_SESSION_STATE       → SESSION_STATE
  RTMS_STREAM_STATE        → STREAM_STATE
  RTMS_EVENT_TYPE          → EVENT_TYPE
  RTMS_ZCC_VOICE_EVENT_TYPE → ZCC_VOICE_EVENT_TYPE
  RTMS_MESSAGE_TYPE        → MESSAGE_TYPE
  RTMS_STOP_REASON         → STOP_REASON
  RTMS_TRANSCRIPT_LANGUAGE → TRANSCRIPT_LANGUAGE

Updated all references in src/rtms.{h,cpp}, src/node.cpp, src/python.cpp,
and the Python wrapper comments.
…pantVideo

Without calling subscribeEvent(), the SDK never delivered PARTICIPANT_VIDEO_ON
or PARTICIPANT_VIDEO_OFF events, so onParticipantVideo callbacks never fired.
Mirrors the same pattern used by setOnUserUpdate for JOIN/LEAVE/ACTIVE_SPEAKER.
…ode and Python refs

Both docs were missing setVideoParams/setAudioParams/setDeskshareParams coverage.
The individual video streams section in node.md also omitted the required
setVideoParams({ dataOpt: VIDEO_SINGLE_INDIVIDUAL_STREAM }) prerequisite call.

Node.js additions:
- New "Media Configuration" section with Video/Audio/Desktop Share subsections
- Updated "Individual Video Streams" to show setVideoParams as a required first step

Python additions:
- New "Media Callbacks" section showing all four callbacks and on_active_speaker_event
- New "Media Configuration" section with Video/Audio/Desktop Share/Transcript subsections
- New "Individual Video Streams" section mirroring the Node.js equivalent
…utes

Expose EVENT_PARTICIPANT_VIDEO_ON and EVENT_PARTICIPANT_VIDEO_OFF as
top-level module attributes in both Node.js and Python bindings, matching
the pattern already used for EVENT_CONSUMER_ANSWERED and similar constants.

Also fix a NameError in Python's _wrap_callback: _run_executor was
referenced but never defined; initialise it to None at module level and
wire the executor kwarg through run() / run_async() so the global fallback
executor actually works.
C++ tests (70/70):
- Fix compile error: Client::EVENT_PARTICIPANT_JOIN was never valid;
  replace with (int)EVENT_TYPE::PARTICIPANT_JOIN
- Fix setTranscriptParams test: configure() is deferred until after
  join/open, so call join() before checking config_calls
- Add setOnParticipantVideo auto-subscribe test
- Add Metadata start_ts/end_ts construction tests
- Add AiInterpreter target_size bounds-guarding tests

Python tests (122/122):
- Fix codec membership checks: use __members__ for enum name lookup
- Update test_client_has_polling_control: check EventLoop attributes
  (_do_alloc_and_join, _pending_join_params) instead of removed methods
- Update ZCC engagement_id tests: use _do_alloc_and_join() + pending
  params dict instead of the removed _do_join() / _start_polling API
- Add EVENT_PARTICIPANT_VIDEO_ON/OFF constant assertions
- Add individual video method and callback tests

Node.js wrapper tests (98/98):
- Add onUserUpdate to the callback method list
Covers all dev branch changes: ZCC engagement_id, TranscriptParams,
setProxy, individual video streams, Python EventLoop/EventLoopPool,
run_async, executor dispatch, async callbacks, GIL release, lazy alloc,
AiInterpreter metadata fields, RTMS_ enum prefix removal, and test suite
expansion (70 C++ / 122 Python / 98 Node.js tests).
stopCallbacks() replaces each media callback with an empty lambda, which
triggered setOnAudioData/Video/Transcript/Deskshare → updateMediaConfiguration
→ configure() on a session that had already been torn down. This printed four
"Warning: Failed to update media configuration: configure failed" lines every
time a meeting ended.

Two guards added:
1. updateMediaConfiguration now checks sdk_opened_ before calling configure(),
   matching the guard already present in configure() itself.
2. release() resets sdk_opened_ = false before release_sdk() so any configure
   path entered after leave() is silently short-circuited.
fix(core): suppress spurious configure warnings on leave
Three changes:
- Node.js tests: tests/rtms.test.ts → tests/ts/rtms.test.ts + rtms.wrapper.test.ts
  (tests were reorganised into ts/ subdirectory; wrapper tests were never run in CI)
- Node.js integration: tests/pack-install.test.js → tests/ts/pack-install.test.js
- Python tests: tests/test_rtms.py → tests/py/test_rtms.py

Add test-cpp-linux and test-cpp-macos jobs that build the mock-SDK C++ test
suite (RTMS_BUILD_TESTS=ON) and run it via ctest. No SDK binary required —
Catch2 is fetched via FetchContent and mock_sdk.cpp replaces the real Zoom SDK.

C++ jobs are added to the needs lists for check-version-change and build-docs
so CI only proceeds when all 3 languages pass.
ci: fix test paths and add C++ unit test jobs
…aram structs

TranscriptParams was snake_case-only (src_language, content_type, enable_lid)
while Audio/Video/DeskshareParams were camelCase-only (dataOpt, sampleRate, etc.)
causing AttributeError when using srcLanguage in Python code.

Add the missing direction in each struct so both forms always work:
- TranscriptParams: add srcLanguage, contentType, enableLid camelCase aliases
- AudioParams:      add data_opt, sample_rate, content_type, frame_size snake_case aliases
- VideoParams:      add data_opt, content_type snake_case aliases
- DeskshareParams:  add data_opt, content_type snake_case aliases

Update python.md docs to consistently use snake_case (data_opt, src_language)
as the canonical form while noting camelCase still works.
The previous fix set sdk_opened_=false inside Client::release(), but
PyClient::release() calls stopCallbacks() BEFORE client_->release(), so the
guard was never reached in time.

Real fix: add Client::markClosed() which sets sdk_opened_=false, then call it
from PyClient::release() before stopCallbacks(). This makes the four
setOnAudioData/Video/Deskshare/Transcript calls in stopCallbacks() hit the
sdk_opened_ guard in updateMediaConfiguration and short-circuit cleanly.

Client::release() retains its own sdk_opened_=false assignment for any path
that calls it directly without going through PyClient.
EventLoop thread calls poll() while the webhook thread calls leave()
→ release() → sdk_ = nullptr concurrently, causing a use-after-free.

Fix: add poll_mutex_ to PyClient.
- poll() releases the GIL *before* acquiring the mutex so the webhook
  thread (which holds the GIL while waiting for the mutex) never
  deadlocks the EventLoop thread (which holds the mutex without the GIL)
- release() holds the mutex for its entire sequence so it waits for any
  in-flight poll() to finish before tearing down the C SDK handle
- client_.reset() at the end of release() ensures subsequent poll()
  calls see nullptr and return early

Result: meeting end no longer crashes the process (exit 139 / SIGSEGV)
The split between AudioDataOption and VideoDataOption was artificial —
the underlying C++ enum (MEDIA_DATA_OPTION) is already unified. Having
two separate namespaces forced users to know which one to use for a
given param struct.

DataOption is now the canonical name with all audio and video stream
delivery modes. AudioDataOption and VideoDataOption are kept as aliases
so existing code continues to work without changes.

Also fixes VIDEO_SINGLE_INDIVIDUAL_STREAM which was missing from the
Python binding (only VIDEO_MIXED_SPEAKER_VIEW, the legacy name, was
exposed).
Add entries for the fixes landed after the initial changelog write:
- DataOption unified enum (replaces split AudioDataOption/VideoDataOption)
- Bidirectional snake_case/camelCase param aliases
- Spurious configure warnings on leave
- SIGSEGV race condition on meeting end (Python)
- CI test path fixes and C++ unit test jobs
C++ tests: add Node.js setup and run node scripts/check-deps.js before
cmake to download SDK headers from GitHub releases. RTMS_BUILD_TESTS=ON
mocks the SDK binary but still needs the headers for type definitions.

Node.js wrapper tests: add npx tsc step after npm install to produce
build/Release/index.js. The rtms.wrapper.test.ts integration test loads
the real compiled module and requires this file to exist; npm install
extracts the native prebuild but does not compile TypeScript.
@MaxMansfield MaxMansfield changed the title feat: v1.1.0 — ZCC, proxy, transcript params, individual video, Python asyncio/executor Final Release v1.1.0 — ZCC, proxy, transcript params, individual video, Python asyncio/executor Apr 15, 2026
install.js exits early when .git exists (dev mode detection), so
npm install never extracts the native prebuild in CI. Add an explicit
prebuild-install step to place rtms.node in build/Release/, then compile
TypeScript with tsc to produce build/Release/index.js. With both files
in place the wrapper integration tests can load the real built module.
install.js exits early when .git exists (dev mode detection). Add a
--force flag to bypass this check so CI can run the full install flow
(prebuild extraction + macOS framework unpacking) against the prebuilds
downloaded from the build artifact, without changing behavior for
regular npm install.
Node.js 20.x reached EOL in April 2026. Per the project's EOL policy,
update the minimum supported version to 22.0.0 (current LTS) and add
Node.js 24.x to the CI test matrix alongside 22.x. Update engines
field in package.json, CI matrix, and all docs accordingly.
execSync('prebuild-install') fails in CI because the binary is only in
node_modules/.bin/, not in system PATH. Resolve the full path explicitly
so the install script works in both CI and end-user environments.
execSync('prebuild-install') fails in CI when called directly outside
npm install because node_modules/.bin/ isn't in PATH. Check for the
local binary first; fall back to the bare command which npm's PATH
augmentation covers during lifecycle scripts.

Also switch the macOS CI test step to use install.js --force instead
of npx prebuild-install directly, so the macOS framework archive
extraction (extractFrameworks) runs and .framework.tar.gz files are
unpacked before the tests load the native module.
@MaxMansfield MaxMansfield merged commit a01c9ae into main Apr 16, 2026
42 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant