Hi @chunsiong.zoom,
Thanks for the response. My reproduction already implements exactly what you describe, and the failure is one step earlier — I never receive a shareSourceID to subscribe with:
- IMeetingShareCtrlEvent is registered before Join(), and it does receive callbacks (onShareSettingTypeChangedNotification fires), but onSharingStatus never fires during an active share, so no ZoomSDKSharingSourceInfo.shareSourceID ever arrives.
- As a fallback I poll GetViewableSharingUserList() → GetSharingSourceInfoList() every second — the list stays empty for the entire meeting while the host is actively sharing.
- When a shareSourceID does appear via either path, my code subscribes with subscribe(shareSourceID, RAW_DATA_TYPE_SHARE), it just never gets the chance
My question is: why share discovery itself (onSharingStatus + GetViewableSharingUserList()) produces nothing on Linux SDK 7.0.5.3529, arm64, headless (Ubuntu 22.04, Docker, Xvfb), while raw audio and camera video work fine with the same join flow. This type of flow has successfully worked on past versions of the SDK.
#include "h/rawdata/zoom_rawdata_api.h"
#include "h/rawdata/rawdata_audio_helper_interface.h"
#include "h/zoom_sdk_raw_data_def.h"
#include "h/meeting_service_components/meeting_recording_interface.h"
#include "h/meeting_service_components/meeting_audio_interface.h"
#include "h/meeting_service_components/meeting_sharing_interface.h"
#include "h/meeting_service_components/meeting_participants_ctrl_interface.h"
#include "h/rawdata/rawdata_renderer_interface.h"
#include "h/meeting_service_interface.h"
#include "h/zoom_sdk.h"
#include "h/zoom_sdk_def.h"
#include "h/auth_service_interface.h"
#include <glib.h>
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <map>
#include <mutex>
#include <cstdint>
#include <ctime>
#include <vector>
#include <set>
bool g_sdkAuth = false;
bool g_inMeeting = false; // set by MeetingListener on INMEETING
bool g_canRecord = false; // set by RecordingListener on privilege grant
class AuthListener: public ZOOMSDK::IAuthServiceEvent {
public:
explicit AuthListener(GMainLoop* loop) : loop_(loop) {}
void onAuthenticationReturn(ZOOMSDK::AuthResult ret) override {
if (ret == ZOOMSDK::AUTHRET_SUCCESS) {
std::cout << "[AuthListener::onAuthenticationReturn] authenticated successfully\n";
g_sdkAuth = true;
} else {
std::cout << "[AuthListener::onAuthenticationReturn] auth FAILED, AuthResult = " << ret << "\n";
}
g_main_loop_quit(loop_); // stop blocking once we have a result
}
// required pure-virtual stubs (no-ops for now)
void onLoginReturnWithReason(ZOOMSDK::LOGINSTATUS ret, ZOOMSDK::IAccountInfo* pAccountInfo,
ZOOMSDK::LoginFailReason reason) override {
// login callbacks don't fire during SDK auth; just log the codes
std::cout << "[AuthListener::onLoginReturnWithReason] login status " << ret
<< ", fail reason " << reason << "\n";
(void)pAccountInfo; // IAccountInfo has no operator<< and may be null
}
void onLogout() override {}
void onZoomIdentityExpired() override {}
void onZoomAuthIdentityExpired() override {}
private:
GMainLoop* loop_;
};
class MeetingListener: public ZOOMSDK::IMeetingServiceEvent {
public:
explicit MeetingListener(GMainLoop* loop) : loop_(loop) {};
void onMeetingStatusChanged(ZOOMSDK::MeetingStatus status, int iResult = 0) override {
std::cout << "[MeetingListener::onMeetingStatusChanged] status=" << status << " result=" << iResult << "\n";
switch (status) {
case ZOOMSDK::MEETING_STATUS_CONNECTING:
std::cout << "[MeetingListener] connecting...\n";
break;
case ZOOMSDK::MEETING_STATUS_INMEETING:
std::cout << "[MeetingListener] JOINED the meeting\n";
g_inMeeting = true;
g_main_loop_quit(loop_); // let join() return so main proceeds
break;
case ZOOMSDK::MEETING_STATUS_FAILED:
case ZOOMSDK::MEETING_STATUS_ENDED:
std::cout << "[MeetingListener] meeting ended/failed\n";
g_inMeeting = false;
g_main_loop_quit(loop_);
break;
default:
break; // WAITINGFORHOST, IN_WAITING_ROOM, etc. - keep waiting
}
}
// required stubs
void onMeetingStatisticsWarningNotification(ZOOMSDK::StatisticsWarningType) override {}
void onMeetingParameterNotification(const ZOOMSDK::MeetingParameter*) override {}
void onSuspendParticipantsActivities() override {}
void onAICompanionActiveChangeNotice(bool) override {}
void onMeetingTopicChanged(const zchar_t*) override {}
void onMeetingFullToWatchLiveStream(const zchar_t*) override {}
void onUserNetworkStatusChanged(ZOOMSDK::MeetingComponentType, ZOOMSDK::ConnectionQuality, unsigned int, bool) override {}
// note: onAppSignalPanelUpdated is WIN32-only in the SDK header, so it is NOT overridden on Linux
private:
GMainLoop* loop_;
};
class AudioRawListener : public ZOOMSDK::IZoomSDKAudioRawDataDelegate {
public:
// NOTE: AudioRawData is a GLOBAL type (declared outside ZOOM_SDK_NAMESPACE), not ZOOMSDK::AudioRawData
void onOneWayAudioRawDataReceived(AudioRawData* d, uint32_t user_id) override {
std::lock_guard<std::mutex> lk(mu_);
micBytes_[user_id] += d->GetBufferLen();
}
void onMixedAudioRawDataReceived(AudioRawData*) override {} // single mixed stream, no source
void onShareAudioRawDataReceived(AudioRawData* d, uint32_t user_id) override {
std::lock_guard<std::mutex> lk(mu_);
shareBytes_[user_id] += d->GetBufferLen(); // shared computer sound (separate stream, same user id)
}
void onOneWayInterpreterAudioRawDataReceived(AudioRawData*, const zchar_t*) override {}
std::map<uint32_t, uint64_t> micSnapshot() {
std::lock_guard<std::mutex> lk(mu_);
return micBytes_;
}
std::map<uint32_t, uint64_t> shareSnapshot() {
std::lock_guard<std::mutex> lk(mu_);
return shareBytes_;
}
private:
std::mutex mu_;
std::map<uint32_t, uint64_t> micBytes_;
std::map<uint32_t, uint64_t> shareBytes_;
};
class VideoRawListener : public ZOOMSDK::IZoomSDKRendererDelegate {
public:
// one delegate shared by all renderers; GetSourceID() distinguishes camera vs share source
// NOTE: YUVRawDataI420 is a GLOBAL type (declared outside ZOOM_SDK_NAMESPACE)
void onRawDataFrameReceived(YUVRawDataI420* d) override {
std::lock_guard<std::mutex> lk(mu_);
videoBytes_[d->GetSourceID()] += d->GetBufferLen();
}
void onRendererBeDestroyed() override {
std::cout << "[Video] onRendererBeDestroyed\n";
}
void onRawDataStatusChanged(RawDataStatus status) override {
std::cout << "[Video] onRawDataStatusChanged: "
<< (status == RawData_On ? "RawData_On" : "RawData_Off") << "\n";
}
std::map<uint32_t, uint64_t> snapshot() {
std::lock_guard<std::mutex> lk(mu_);
return videoBytes_;
}
private:
std::mutex mu_;
std::map<uint32_t, uint64_t> videoBytes_;
};
// Diagnostic: logs every sharing event so we can see if/when the SDK reports a share,
// and the share source id it assigns.
class ShareListener : public ZOOMSDK::IMeetingShareCtrlEvent {
public:
void onSharingStatus(ZOOMSDK::ZoomSDKSharingSourceInfo info) override {
std::cout << "[Share] onSharingStatus userid=" << info.userid
<< " shareSourceID=" << info.shareSourceID
<< " status=" << info.status
<< " contentType=" << info.contentType << "\n";
}
void onShareContentNotification(ZOOMSDK::ZoomSDKSharingSourceInfo info) override {
std::cout << "[Share] onShareContentNotification userid=" << info.userid
<< " shareSourceID=" << info.shareSourceID
<< " status=" << info.status << "\n";
}
void onFailedToStartShare() override { std::cout << "[Share] onFailedToStartShare\n"; }
void onLockShareStatus(bool bLocked) override { std::cout << "[Share] onLockShareStatus locked=" << bLocked << "\n"; }
void onMultiShareSwitchToSingleShareNeedConfirm(ZOOMSDK::IShareSwitchMultiToSingleConfirmHandler*) override {
std::cout << "[Share] onMultiShareSwitchToSingleShareNeedConfirm\n";
}
void onShareSettingTypeChangedNotification(ZOOMSDK::ShareSettingType type) override {
std::cout << "[Share] onShareSettingTypeChangedNotification type=" << type << "\n";
}
void onSharedVideoEnded() override { std::cout << "[Share] onSharedVideoEnded\n"; }
void onVideoFileSharePlayError(ZOOMSDK::ZoomSDKVideoFileSharePlayError err) override {
std::cout << "[Share] onVideoFileSharePlayError err=" << err << "\n";
}
void onOptimizingShareForVideoClipStatusChanged(ZOOMSDK::ZoomSDKSharingSourceInfo info) override {
std::cout << "[Share] onOptimizingShareForVideoClipStatusChanged userid=" << info.userid
<< " status=" << info.status << "\n";
}
};
class RecordingListener : public ZOOMSDK::IMeetingRecordingCtrlEvent {
public:
explicit RecordingListener(GMainLoop* loop) : loop_(loop) {}
void onRecordPrivilegeChanged(bool bCanRec) override {
std::cout << "[Recording] privilege changed: " << bCanRec << "\n";
g_canRecord = bCanRec;
g_main_loop_quit(loop_);
}
// required stubs
void onRecordingStatus(ZOOMSDK::RecordingStatus status) override {
std::cout << "[Recording] onRecordingStatus status=" << status << "\n";
}
void onCloudRecordingStatus(ZOOMSDK::RecordingStatus) override {}
void onLocalRecordingPrivilegeRequestStatus(ZOOMSDK::RequestLocalRecordingStatus) override {}
void onRequestCloudRecordingResponse(ZOOMSDK::RequestStartCloudRecordingStatus) override {}
void onLocalRecordingPrivilegeRequested(ZOOMSDK::IRequestLocalRecordingPrivilegeHandler*) override {}
void onStartCloudRecordingRequested(ZOOMSDK::IRequestStartCloudRecordingHandler*) override {}
// onRecording2MP4Done / onRecording2MP4Processing / onCustomizedLocalRecordingSourceNotification are WIN32-only
void onCloudRecordingStorageFull(time_t) override {}
void onEnableAndStartSmartRecordingRequested(ZOOMSDK::IRequestEnableAndStartSmartRecordingHandler*) override {}
void onSmartRecordingEnableActionCallback(ZOOMSDK::ISmartRecordingEnableActionHandler*) override {}
void onTranscodingStatusChanged(ZOOMSDK::TranscodingStatus, const zchar_t*) override {}
private:
GMainLoop* loop_;
};
class DemoBot {
private:
long long meetingId_;
std::string meetingPassword_;
std::string meetingToken_;
ZOOMSDK::IAuthService* authService_ = nullptr;
ZOOMSDK::IMeetingService* meetingService_ = nullptr;
GMainLoop* loop_ = nullptr;
AuthListener* authListener_ = nullptr;
MeetingListener* meetingListener_ = nullptr;
ZOOMSDK::IMeetingRecordingController* recordingController_ = nullptr;
RecordingListener* recordingListener_ = nullptr;
AudioRawListener audioListener_; // by value, lives with the bot
VideoRawListener videoListener_; // shared by all renderers
ShareListener* shareListener_ = nullptr;
std::vector<ZOOMSDK::IZoomSDKRenderer*> renderers_;
std::set<uint32_t> subscribedVideo_; // user ids we've made a video renderer for
std::set<uint32_t> subscribedShare_; // share source ids we've subscribed
guint reportTimerId_ = 0;
public:
DemoBot(long long meetingId,
std::string meetingPassword,
std::string meetingToken)
: meetingId_(meetingId),
meetingPassword_(std::move(meetingPassword)),
meetingToken_(std::move(meetingToken))
{
// log the default information
std::cout << "[DemoBot::DemoBot] constructed a bot with meetingId: "
<< meetingId_
<< ", meetingPassword: " << meetingPassword_
<< ", meetingToken: " << meetingToken_ << "\n";
// initialize the zoom sdk
ZOOMSDK::InitParam initParam;
initParam.strWebDomain = "https://zoom.us";
initParam.strSupportUrl = "https://zoom.us";
initParam.emLanguageID = ZOOMSDK::LANGUAGE_English;
initParam.enableLogByDefault = true;
initParam.enableGenerateDump = true;
ZOOMSDK::SDKError initResult = ZOOMSDK::InitSDK(initParam);
std::cout << "[DemoBot::DemoBot] initialize the sdk result: " << initResult << "\n";
std::cout << "[DemoBot::DemoBot] SDK version: " << ZOOMSDK::GetSDKVersion() << "\n";
}
~DemoBot() {
if (reportTimerId_) g_source_remove(reportTimerId_);
for (auto* r : renderers_) ZOOMSDK::destroyRenderer(r);
if (meetingListener_) delete meetingListener_;
if (recordingListener_) delete recordingListener_;
if (shareListener_) delete shareListener_;
if (authListener_) delete authListener_;
if (loop_) g_main_loop_unref(loop_); // GMainLoop is refcounted, not delete-able
// recordingController_ is owned by the SDK - do not delete
}
bool authenticate(const std::string& jwt) {
loop_ = g_main_loop_new(nullptr, false);
authListener_ = new AuthListener(loop_);
ZOOMSDK::SDKError err = ZOOMSDK::CreateAuthService(&authService_);
std::cout << "[DemoBot::authenticate] CreateAuthService exit code: " << err << "\n";
if (err != ZOOMSDK::SDKERR_SUCCESS) {
return false;
}
authService_->SetEvent(authListener_);
ZOOMSDK::AuthContext ctx;
ctx.jwt_token = jwt.c_str();
err = authService_->SDKAuth(ctx);
std::cout << "[DemoBot::authenticate] sdk auth exit code: " << err << "\n";
if (err != ZOOMSDK::SDKERR_SUCCESS) {
return false;
}
std::cout << "[DemoBot:authenticate] SDKAuth requested, waiting for callback...\n";
g_main_loop_run(loop_);
return true;
}
bool join() {
std::cout << "[DemoBot::join] joining the meeting...\n";
ZOOMSDK::SDKError err = ZOOMSDK::CreateMeetingService(&meetingService_);
std::cout << "[DemoBot::join] CreateMeetingService outcome: " << err << "\n";
if (err != ZOOMSDK::SDKERR_SUCCESS || !meetingService_) {
return false;
}
meetingListener_ = new MeetingListener(loop_);
meetingService_->SetEvent(meetingListener_);
if (auto* sc = meetingService_->GetMeetingShareController()) {
shareListener_ = new ShareListener();
ZOOMSDK::SDKError se = sc->SetEvent(shareListener_);
std::cout << "[DemoBot::join] share controller SetEvent: " << se << "\n";
} else {
std::cout << "[DemoBot::join] NO share controller\n";
}
ZOOMSDK::JoinParam joinParam;
joinParam.userType = ZOOMSDK::SDK_UT_WITHOUT_LOGIN;
ZOOMSDK::JoinParam4WithoutLogin& p = joinParam.param.withoutloginuserJoin;
p.meetingNumber = static_cast<UINT64>(meetingId_);
p.userName = "Test Screenshare Bot";
p.psw = meetingPassword_.c_str();
p.isVideoOff = true; // bot has no camera
p.isAudioOff = false; // keep audio for recording later
if (!meetingToken_.empty())
p.app_privilege_token = meetingToken_.c_str(); // grants local recording at join
err = meetingService_->Join(joinParam);
std::cout << "[DemoBot::join] Join() returned: " << err << "\n";
if (err != ZOOMSDK::SDKERR_SUCCESS) {
return false;
}
// Join is async - re-run the loop to process onMeetingStatusChanged
// callbacks. MeetingListener quits the loop on ENDED/FAILED.
std::cout << "[DemoBot::join] waiting for meeting status callbacks...\n";
g_main_loop_run(loop_); // quits on INMEETING (g_inMeeting=true) or FAILED
return g_inMeeting;
}
bool record() {
recordingController_ = meetingService_->GetMeetingRecordingController();
if (!recordingController_) {
std::cout << "[DemoBot::record] no recording controller\n";
return false;
}
recordingListener_ = new RecordingListener(loop_);
recordingController_->SetEvent(recordingListener_);
// (share event listener is registered early in join(), before Join)
// one-shot privilege gate (g_sdkAuth style)
if (recordingController_->CanStartRawRecording() == ZOOMSDK::SDKERR_SUCCESS) {
g_canRecord = true;
} else {
std::cout << "[DemoBot::record] requesting local recording privilege (host must approve)\n";
recordingController_->RequestLocalRecordingPrivilege();
g_main_loop_run(loop_); // quits when onRecordPrivilegeChanged sets g_canRecord
}
if (!g_canRecord) {
std::cout << "[DemoBot::record] recording not permitted\n";
return false;
}
ZOOMSDK::SDKError err = recordingController_->StartRawRecording();
std::cout << "[DemoBot::record] StartRawRecording: " << err << "\n";
if (err != ZOOMSDK::SDKERR_SUCCESS) {
return false;
}
if (auto* mi = meetingService_->GetMeetingInfo())
std::cout << "[meeting] number=" << mi->GetMeetingNumber() << "\n";
// raw audio requires the bot to be in the audio session, otherwise
// subscribe returns SDKERR_NOT_JOIN_AUDIO (32).
if (auto* audioCtrl = meetingService_->GetMeetingAudioController()) {
ZOOMSDK::SDKError ja = audioCtrl->JoinVoip();
std::cout << "[DemoBot::record] JoinVoip: " << ja << "\n";
}
if (auto* helper = ZOOMSDK::GetAudioRawdataHelper()) {
err = helper->subscribe(&audioListener_);
std::cout << "[DemoBot::record] audio subscribe: " << err << "\n";
}
reportTimerId_ = g_timeout_add_seconds(1, &DemoBot::reportTick, this);
// capture loop: runs until the meeting ends (MeetingListener quits on ENDED/FAILED)
std::cout << "[DemoBot::record] capturing; loop runs until meeting ends...\n";
g_main_loop_run(loop_);
return true;
}
ZOOMSDK::SDKError addRenderer(uint32_t id, ZOOMSDK::ZoomSDKRawDataType type, const char* label) {
ZOOMSDK::IZoomSDKRenderer* r = nullptr;
ZOOMSDK::SDKError err = ZOOMSDK::createRenderer(&r, &videoListener_);
if (err != ZOOMSDK::SDKERR_SUCCESS || !r) {
std::cout << "[DemoBot] createRenderer " << label << " src=" << id << " failed: " << err << "\n";
return err;
}
r->setRawDataResolution(ZOOMSDK::ZoomSDKResolution_720P);
err = r->subscribe(id, type);
std::cout << "[DemoBot] subscribe " << label << " src=" << id << " -> " << err << "\n";
if (err != ZOOMSDK::SDKERR_SUCCESS) {
ZOOMSDK::destroyRenderer(r); // don't keep a renderer that failed to subscribe
return err;
}
renderers_.push_back(r);
return err;
}
// poll current participants + shares and subscribe any new source (no event callbacks)
void subscribeNewVideoSources() {
if (auto* pc = meetingService_->GetMeetingParticipantsController()) {
if (auto* users = pc->GetParticipantsList()) {
for (int i = 0; i < users->GetCount(); ++i) {
uint32_t uid = users->GetItem(i);
if (subscribedVideo_.insert(uid).second) {
if (auto* u = pc->GetUserByUserID(uid)) {
std::cout << "[participant] id=" << uid
<< " name='" << (u->GetUserName() ? u->GetUserName() : "") << "'"
<< " host=" << u->IsHost()
<< " self=" << u->IsMySelf()
<< " bot=" << u->IsBotUser()
<< " videoOn=" << u->IsVideoOn() << "\n";
}
addRenderer(uid, ZOOMSDK::RAW_DATA_TYPE_VIDEO, "video");
}
// SHARE subscribe returns SDKERR_NO_SHARE_DATA(29) until the user is
// actually sharing; RETRY each tick until it succeeds so we capture
// what subscribe(userId, RAW_DATA_TYPE_SHARE) does during an active share.
if (subscribedShare_.find(uid) == subscribedShare_.end()) {
if (addRenderer(uid, ZOOMSDK::RAW_DATA_TYPE_SHARE, "share") == ZOOMSDK::SDKERR_SUCCESS)
subscribedShare_.insert(uid);
}
}
}
}
if (auto* sc = meetingService_->GetMeetingShareController()) {
auto* sharers = sc->GetViewableSharingUserList();
std::cout << "[poll] viewable sharers=" << (sharers ? sharers->GetCount() : -1) << "\n";
if (sharers) {
for (int i = 0; i < sharers->GetCount(); ++i) {
uint32_t suid = sharers->GetItem(i);
auto* infos = sc->GetSharingSourceInfoList(suid);
std::cout << "[poll] sharer uid=" << suid
<< " sources=" << (infos ? infos->GetCount() : -1) << "\n";
if (infos) {
for (int j = 0; j < infos->GetCount(); ++j) {
ZOOMSDK::ZoomSDKSharingSourceInfo info = infos->GetItem(j);
std::cout << "[poll] share source id=" << info.shareSourceID
<< " status=" << info.status
<< " type=" << info.contentType << "\n";
if (subscribedShare_.insert(info.shareSourceID).second)
addRenderer(info.shareSourceID, ZOOMSDK::RAW_DATA_TYPE_SHARE, "share");
}
}
}
}
}
}
static gboolean reportTick(gpointer self) {
auto* bot = static_cast<DemoBot*>(self);
bot->subscribeNewVideoSources(); // pick up new cameras / screenshares
for (const auto& kv : bot->audioListener_.micSnapshot())
std::cout << "[data] mic-audio src=" << kv.first << " total_bytes=" << kv.second << "\n";
for (const auto& kv : bot->audioListener_.shareSnapshot())
std::cout << "[data] share-audio src=" << kv.first << " total_bytes=" << kv.second << "\n";
for (const auto& kv : bot->videoListener_.snapshot())
std::cout << "[data] video src=" << kv.first << " total_bytes=" << kv.second << "\n";
return TRUE; // reschedule every second
}
};
int main(int argc, char *argv[]) {
setvbuf(stdout, nullptr, _IONBF, 0); // unbuffered stdout so logs flush even while blocked in the loop
std::cout << "meeting screenshare reproduction!\n";
// step 1: read meeting + auth config from the environment
const char* jwt = std::getenv("ZOOM_SDK_JWT");
const char* meetingIdEnv = std::getenv("ZOOM_MEETING_ID");
const char* meetingPwEnv = std::getenv("ZOOM_MEETING_PASSWORD");
if (!jwt || !meetingIdEnv) {
std::cout << "[main] missing required env: ZOOM_SDK_JWT and/or ZOOM_MEETING_ID\n";
return 1;
}
const char* recTokenEnv = std::getenv("ZOOM_RECORDING_TOKEN");
long long meetingId = std::stoll(meetingIdEnv); // meeting number
std::string meetingPassword = meetingPwEnv ? meetingPwEnv : "";
std::string meetingToken = recTokenEnv ? recTokenEnv : ""; // local-recording token (app_privilege_token)
DemoBot bot(meetingId, meetingPassword, meetingToken); // InitSDK happens here
// step 2: authenticate (blocks until onAuthenticationReturn fires)
bot.authenticate(jwt);
if (!g_sdkAuth) {
std::cout << "[main] auth did not succeed; aborting\n";
ZOOMSDK::CleanUPSDK();
return 1;
}
// step 3: join the meeting (blocks until INMEETING or FAILED)
// NOTE: requires a real, active meeting; a fake id yields MEETING_STATUS_FAILED
if (bot.join()) {
// step 4: start raw recording + capture (blocks in the loop until the meeting ends)
bot.record();
}
ZOOMSDK::CleanUPSDK();
return 0;
}