Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,18 @@ are tagged `vMAJOR.MINOR.PATCH` and published as

## [Unreleased]

## [0.1.48] - 2026-09-15

### Fixed
- Windows startup no longer sets the system microphone volume to zero. The
Zoom engine uses a silent virtual microphone and disables automatic input
level adjustment; joining is blocked if the silent source cannot initialize.

### Validation
- Windows build and 75 regression tests pass, including silent microphone
lifecycle, failed registration, and independent receive-subscription checks.
- Live Zoom startup and talkback verification remains pending.

## [0.1.47] - 2026-09-15

### Added
Expand Down
18 changes: 7 additions & 11 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -425,17 +425,13 @@ Every one of these is documented at length where it lives; the list is the map.
none, under a comment describing the code above.
**The leak question, answered from the code rather than assumed**: `Join`
sets `isAudioOff = false` / `isMyVoiceInMix = true`
(`engine/src/main.cpp`) and **nothing in this repository calls
`setExternalAudioSource()`** — the only `IZoomSDKAudioRawDataHelper` use
anywhere is `engine-audio.cpp`'s `subscribe()`/`unSubscribe()`, the
*receive* path — so a bare unmute would open the OBS machine's **default
capture device** live into the meeting. The insurance runs once at auth,
in `main.cpp`'s existing `CreateSettingService` block (stage
`mic_insurance`): `SelectMic()` onto a device id that matches nothing plus
`SetMicVol(0)`. **Weaker than ZComms's** never-fed virtual mic, and
deliberately so — theirs installs a virtual mic into the same helper our
show-critical receive subscribe uses. If a live gate ever hears the room
through this, `setExternalAudioSource()` is the escalation.
(`engine/src/main.cpp`). Authentication installs `EngineSilentMic` with
`setExternalAudioSource()` before reporting auth_ok. It never sends PCM,
never selects a physical mic, and outlives SDK cleanup. Registration
failure blocks Join. Both automatic microphone level controls are disabled.
Never use `SetMicVol` or a fake device selection as a silence safeguard:
Zoom may fall back to the Windows default mic and change its system level.
EngineAudio's receive subscription and talkback channel PCM stay separate.
2. **The rate limit is per membership CALL, and invites count** — see the
next bullet, which this rewrote.
3. **A same-account host collision hangs the join forever unless answered.**
Expand Down
5 changes: 5 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1335,6 +1335,11 @@ if(BUILD_TESTING)
# tree does not, so the SDK-present check alone let this target register
# there and fail at #include (PR #231 CI, 2026-08-27).
if(WIN32 AND EXISTS "${ZOOM_SDK_INCLUDE_DIR}/meeting_service_components/meeting_talkback_ctrl_interface.h")
add_executable(CoreVideoEngineSilentMicTest tests/engine-silent-mic-test.cpp)
target_include_directories(CoreVideoEngineSilentMicTest PRIVATE
"${CMAKE_CURRENT_SOURCE_DIR}/engine/src" "${ZOOM_SDK_INCLUDE_DIR}")
target_compile_definitions(CoreVideoEngineSilentMicTest PRIVATE NOMINMAX WIN32_LEAN_AND_MEAN)
add_test(NAME CoreVideoEngineSilentMic COMMAND CoreVideoEngineSilentMicTest)
add_executable(CoreVideoEngineTalkbackSelectTest
tests/engine-talkback-select-test.cpp
engine/src/engine-talkback.cpp
Expand Down
35 changes: 35 additions & 0 deletions engine/src/engine-silent-mic.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
#pragma once

#include <atomic>
#include <cstdint>
#if defined(_WIN32)
#include <windows.h>
#endif
#if __has_include(<rawdata/rawdata_audio_helper_interface.h>)
#include <rawdata/rawdata_audio_helper_interface.h>
#else
#include <rawdata_audio_helper_interface.h>
#endif

// SDK capture source with no physical device and no outgoing PCM. Keep alive
// until after CleanUPSDK; raw audio RECEIVE subscriptions are independent.
class EngineSilentMic final : public ZOOMSDK::IZoomSDKVirtualAudioMicEvent {
public:
ZOOMSDK::SDKError install(ZOOMSDK::IZoomSDKAudioRawDataHelper *helper)
{
reset();
const auto result = helper ? helper->setExternalAudioSource(this)
: ZOOMSDK::SDKERR_UNINITIALIZE;
m_ready.store(result == ZOOMSDK::SDKERR_SUCCESS);
return result;
}
void reset() { m_ready.store(false); }
bool ready() const { return m_ready.load(); }
void onMicInitialize(ZOOMSDK::IZoomSDKAudioRawDataSender *) override {}
void onMicStartSend() override {}
void onMicStopSend() override {}
void onMicUninitialized() override {}

private:
std::atomic<bool> m_ready{false};
};
33 changes: 6 additions & 27 deletions engine/src/engine-talkback.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -3608,33 +3608,12 @@ void EngineTalkback::debug_expire_pending_create_for_test()
// client's mic put the ENGINE MACHINE'S microphone into the meeting? YES, it
// could, and the insurance is therefore real rather than theoretical:
// * engine/src/main.cpp's Join sets JoinParam4WithoutLogin::isAudioOff =
// false and isMyVoiceInMix = true, so the SDK connects VoIP audio on join
// with whatever capture device it picks.
// * NOTHING in this engine installs a virtual mic. The only
// IZoomSDKAudioRawDataHelper use in the whole repository is
// engine/src/engine-audio.cpp's subscribe()/unSubscribe(), which is the
// RECEIVE path; setExternalAudioSource() -- ZComms's never-fed virtual
// mic, and the airtight version of this insurance -- is called nowhere in
// engine/ or src/.
// * So a bare UnMuteAudio() would open the DEFAULT SYSTEM CAPTURE DEVICE of
// the machine running OBS, into a live meeting. That is a hot mic in a
// control room.
// The insurance is applied ONCE, at authentication, in main.cpp's existing
// CreateSettingService block (see "mic_insurance" there): the SDK's audio
// settings are pointed at a device that does not exist and the mic volume is
// set to zero, before any join. It lives there and not here for a hard
// reason as well as a tidy one -- CreateSettingService() is an SDK EXPORT, and
// engine-talkback.cpp is compiled into a test target that links no SDK library
// at all (tests/engine-talkback-select-test.cpp fakes pure-virtual interfaces
// only). Everything in THIS function goes through IMeetingService's virtual
// interfaces, which is exactly why the ordering below can be pinned.
// WEAKER THAN ZCOMMS'S, and stated rather than glossed: a never-fed virtual
// mic is silent by construction, while a dead device selection is silent
// because Zoom honours it. If a live gate ever hears the room, the escalation
// is setExternalAudioSource() with a never-fed source -- deliberately not
// taken here, because it would install a virtual mic into the same helper the
// engine's show-critical RECEIVE subscribe uses, and that interaction is
// untested.
// false and isMyVoiceInMix = true. Authentication installs EngineSilentMic
// as the SDK's external capture source before auth_ok. This never-fed
// virtual microphone cannot capture the physical input and never changes
// Windows endpoint volume. Join fails closed if registration fails.
// The microphone object outlives CleanUPSDK. Raw audio receive subscriptions
// remain owned by EngineAudio; talkback PCM uses its own channel sender.
bool EngineTalkback::ensure_mic_open(const char *when)
{
const std::string when_field = R"(,"when":")" + std::string(when) + "\"";
Expand Down
134 changes: 36 additions & 98 deletions engine/src/main.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@
#include "engine-audio.h"
#include "engine-json.h"
#include "engine-talkback.h"
#include "engine-silent-mic.h"
#if __has_include(<rawdata/zoom_rawdata_api.h>)
#include <rawdata/zoom_rawdata_api.h>
#else
#include <zoom_rawdata_api.h>
#endif
#include <zoom_sdk.h>
#include <auth_service_interface.h>
#include <setting_service_interface.h>
Expand Down Expand Up @@ -699,7 +705,10 @@ class EngineAuthEvent : public ZOOMSDK::IAuthServiceEvent {
public:
explicit EngineAuthEvent(IpcFd e2p) : m_e2p(e2p) {}

bool microphone_ready() const { return m_silent_mic.ready(); }
void reset_microphone() { m_silent_mic.reset(); }
void onAuthenticationReturn(ZOOMSDK::AuthResult ret) override {
reset_microphone();
if (ret == ZOOMSDK::AUTHRET_SUCCESS) {
// Opt into HD video so the meeting negotiates Group HD / 1080p
// streams when the account is entitled. Defaults to off on
Expand All @@ -717,111 +726,31 @@ class EngineAuthEvent : public ZOOMSDK::IAuthServiceEvent {
std::string(vs->IsHDVideoEnabled() ? "true" : "false") +
"}");
}
// TALKBACK DELIVERY LAW 1's INSURANCE (2026-08-29). Talkback
// only delivers while this client's meeting audio is OPEN
// (see EngineTalkback::ensure_mic_open()), so a key press now
// UNMUTES this client. Read the code before deciding that is
// safe: main.cpp's Join sets isAudioOff = false and
// isMyVoiceInMix = true, and nothing in this repository calls
// IZoomSDKAudioRawDataHelper::setExternalAudioSource() -- the
// only raw-audio-helper use anywhere is engine-audio.cpp's
// subscribe()/unSubscribe(), which is the RECEIVE path. So the
// SDK would open the DEFAULT SYSTEM CAPTURE DEVICE of the
// machine running OBS, live into the meeting. In a control
// room that is a hot mic on air.
//
// So the mic is made dead BEFORE any join, once, here: point
// the SDK at a device id that matches nothing (its own
// fallback is "the default mic if there is no mic selected via
// SelectMic()", which is precisely what must not happen) and
// set the mic volume to zero as the second, independent half
// -- SetMicVol() is documented to act on the selected mic and
// covers the case where SelectMic() is refused.
//
// Reported with both codes, never silently: this is the guard
// between a talkback key and the control room's own
// microphone, and a guard that fails quietly is worse than no
// guard, because the unmute happens either way.
//
// WEAKER THAN ZCOMMS'S, stated rather than glossed: theirs is
// a never-fed SDK virtual mic (setExternalAudioSource), silent
// by construction rather than by Zoom honouring a setting.
// Deliberately not taken here -- it installs a virtual mic
// into the same helper this engine's show-critical receive
// subscribe uses, an interaction nothing has tested. If a live
// gate ever hears the room through this, that is the
// escalation.
{
if (auto *as = settings->GetAudioSettings()) {
#if defined(WIN32)
const zchar_t *dead_id = L"corevideo-no-microphone";
const zchar_t *dead_name = L"CoreVideo (no microphone)";
#else
const zchar_t *dead_id = "corevideo-no-microphone";
const zchar_t *dead_name = "CoreVideo (no microphone)";
#endif
const ZOOMSDK::SDKError m_err =
as->SelectMic(dead_id, dead_name);
FLOAT silent = 0.0f;
const ZOOMSDK::SDKError v_err = as->SetMicVol(silent);
// REVIEW ROUND 1, m6: BOTH HALVES REFUSED IS A HOT MIC,
// and it used to be reported as a "debug" line with two
// numbers in it -- filtered by stage, read by nobody.
// These two calls are the only thing standing between a
// talkback key and the control room's own microphone
// going live into the meeting, so a failure has to be
// loud on the channel the operator actually sees, and
// has to say what to DO about it. Either half alone
// still silences the device, which is why this is an
// AND: SelectMic sends the SDK at a device that does not
// exist, SetMicVol zeroes whatever it settles on.
const bool insured = (m_err == ZOOMSDK::SDKERR_SUCCESS) ||
(v_err == ZOOMSDK::SDKERR_SUCCESS);
EngineIpc::write(
std::string(R"({"cmd":"debug","stage":"mic_insurance","ok":)") +
(insured ? "true" : "false") +
R"(,"select_code":)" +
std::to_string(static_cast<int>(m_err)) +
R"(,"volume_code":)" +
std::to_string(static_cast<int>(v_err)) + "}");
if (!insured) {
EngineIpc::write(
R"({"cmd":"error","msg":"mic_insurance_failed",)"
R"("reason":"hot_mic_risk","select_code":)" +
std::to_string(static_cast<int>(m_err)) +
R"(,"volume_code":)" +
std::to_string(static_cast<int>(v_err)) +
R"(,"action":"Zoom refused both attempts to )"
R"(silence this machine's microphone. A talkback )"
R"(key will unmute CoreVideo in the meeting, so )"
R"(the default capture device may be heard. Set )"
R"(Zoom's microphone to a disconnected device, )"
R"(or mute it at the OS, before keying."})");
}
} else {
// Same severity, one door earlier: with no audio
// settings there is no insurance at all, and the unmute
// still happens on the first key.
EngineIpc::write(
R"({"cmd":"debug","stage":"mic_insurance","ok":false,)"
R"("reason":"no_audio_settings"})");
EngineIpc::write(
R"({"cmd":"error","msg":"mic_insurance_failed",)"
R"("reason":"no_audio_settings",)"
R"("action":"Zoom exposed no audio settings, so )"
R"(CoreVideo could not silence this machine's )"
R"(microphone. A talkback key will unmute CoreVideo )"
R"(in the meeting. Set Zoom's microphone to a )"
R"(disconnected device, or mute it at the OS, before )"
R"(keying."})");
}
// The SDK must never adjust the operator's physical input level.
if (auto *as = settings->GetAudioSettings()) {
const auto err = as->EnableAutoAdjustMic(false);
EngineIpc::write(
R"({"cmd":"debug","stage":"disable_mic_auto_adjust","code":)" +
std::to_string(static_cast<int>(err)) + "}");
}
ZOOMSDK::DestroySettingService(settings);
} else {
EngineIpc::write(
R"({"cmd":"debug","stage":"create_setting_service_failed","code":)" +
std::to_string(static_cast<int>(s_err)) + "}");
}
// Supply a silent SDK microphone instead of selecting a fake device or
// setting Windows input volume to zero. Fail closed before any join.
const auto mic_error = m_silent_mic.install(ZOOMSDK::GetAudioRawdataHelper());
EngineIpc::write(
R"({"cmd":"debug","stage":"mic_insurance","mode":"virtual_silent","code":)" +
std::to_string(static_cast<int>(mic_error)) + "}");
if (!microphone_ready()) {
EngineIpc::write(
R"({"cmd":"auth_fail","stage":"silent_microphone","code":)" +
std::to_string(static_cast<int>(mic_error)) + "}");
return;
}
EngineIpc::write( R"({"cmd":"auth_ok"})");
} else
EngineIpc::write( R"({"cmd":"auth_fail","code":)" +
Expand All @@ -844,6 +773,7 @@ class EngineAuthEvent : public ZOOMSDK::IAuthServiceEvent {
#endif
private:
IpcFd m_e2p;
EngineSilentMic m_silent_mic;
};

// ── Meeting event handler ─────────────────────────────────────────────────────
Expand Down Expand Up @@ -1606,6 +1536,7 @@ int main()
std::to_string(static_cast<int>(err)) + "}");
continue;
}
auth_event.reset_microphone();
auth_svc->SetEvent(&auth_event);
ZOOMSDK::AuthContext ctx{};
if (!public_app_key.empty()) {
Expand Down Expand Up @@ -1640,6 +1571,12 @@ int main()
}

} else if (command == IpcCommand::Join) {
if (!auth_event.microphone_ready()) {
EngineIpc::write(
R"({"cmd":"error","msg":"silent_microphone_unavailable",)"
R"("action":"Reconnect CoreVideo before joining. The silent meeting microphone could not be initialized."})");
continue;
}
std::string meeting_id = json_str(line, "meeting_id");
std::string passcode = json_str(line, "passcode");
std::string display_name = json_str(line, "display_name");
Expand Down Expand Up @@ -1671,6 +1608,7 @@ int main()
if (meeting_svc) {
auto *cfg = meeting_svc->GetMeetingConfiguration();
if (cfg) {
cfg->EnableAutoAdjustMicVolumeWhenJoinAudio(false);
cfg->SetEvent(&meeting_event);
} else {
EngineIpc::write(
Expand Down
58 changes: 58 additions & 0 deletions tests/engine-silent-mic-test.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
#include "engine-silent-mic.h"
#include <cstdlib>
#include <iostream>

using namespace ZOOMSDK;
static void require(bool value)
{
if (!value) {
std::cerr << "Silent microphone regression failed\n";
std::exit(1);
}
}
struct Sender : IZoomSDKAudioRawDataSender {
int sent = 0;
SDKError send(char *, unsigned int, int, ZoomSDKAudioChannel) override
{
++sent;
return SDKERR_SUCCESS;
}
};
struct Helper : IZoomSDKAudioRawDataHelper {
IZoomSDKVirtualAudioMicEvent *source = nullptr;
int receive_changes = 0;
SDKError result = SDKERR_SUCCESS;
SDKError subscribe(IZoomSDKAudioRawDataDelegate *, bool) override
{
++receive_changes;
return SDKERR_SUCCESS;
}
SDKError unSubscribe() override { ++receive_changes; return SDKERR_SUCCESS; }
SDKError setExternalAudioSource(IZoomSDKVirtualAudioMicEvent *value) override
{
source = value;
return result;
}
};
int main()
{
EngineSilentMic mic;
Helper helper;
Sender sender;
require(!mic.ready());
require(mic.install(nullptr) != SDKERR_SUCCESS && !mic.ready());
require(mic.install(&helper) == SDKERR_SUCCESS && mic.ready());
require(helper.source == &mic);
for (int session = 0; session < 2; ++session) {
helper.source->onMicInitialize(&sender);
helper.source->onMicStartSend();
helper.source->onMicStopSend();
helper.source->onMicUninitialized();
}
require(sender.sent == 0 && helper.receive_changes == 0);
mic.reset();
require(!mic.ready());
require(mic.install(&helper) == SDKERR_SUCCESS && mic.ready());
helper.result = SDKERR_UNINITIALIZE;
require(mic.install(&helper) != SDKERR_SUCCESS && !mic.ready());
}
Loading