24get_instance(
void* userdata)
34 uniqueSinkName_ =
"audiocapture_null_sink_" + std::to_string(myPid_);
50 if (mainloop_ || context_ || recordStream_) {
51 JAMI_WARNING(
"[pulseloopbackcapture] Previous state not fully cleaned, forcing cleanup");
55 dataCallback_ = std::move(callback);
57 mainloop_ = pa_threaded_mainloop_new();
59 JAMI_ERROR(
"[pulseloopbackcapture] Failed to create mainloop");
63 mainloopApi_ = pa_threaded_mainloop_get_api(mainloop_);
65 context_ = pa_context_new(mainloopApi_,
"AudioCaptureLib");
67 JAMI_ERROR(
"[pulseloopbackcapture] Failed to create context");
68 pa_threaded_mainloop_free(mainloop_);
72 pa_context_set_state_callback(context_, contextStateCallback,
this);
74 if (pa_context_connect(context_,
nullptr, PA_CONTEXT_NOFLAGS,
nullptr) < 0) {
75 JAMI_ERROR(
"[pulseloopbackcapture] Failed to connect context: {}", pa_strerror(pa_context_errno(context_)));
76 pa_context_unref(context_);
78 pa_threaded_mainloop_free(mainloop_);
83 if (pa_threaded_mainloop_start(mainloop_) < 0) {
84 JAMI_ERROR(
"[pulseloopbackcapture] Failed to start mainloop");
85 pa_context_disconnect(context_);
86 pa_context_unref(context_);
88 pa_threaded_mainloop_free(mainloop_);
100 if (!running_ || !mainloop_)
103 pa_threaded_mainloop_lock(mainloop_);
105 auto wait_for_op = [
this](pa_operation* op) {
108 while (pa_operation_get_state(op) == PA_OPERATION_RUNNING) {
109 pa_threaded_mainloop_wait(mainloop_);
111 pa_operation_unref(op);
114 auto completion_cb = [](pa_context* c,
int success,
void* userdata) {
117 pa_threaded_mainloop* m = (pa_threaded_mainloop*) userdata;
118 pa_threaded_mainloop_signal(m, 0);
121 if (loopbackModuleIdx_ != PA_INVALID_INDEX) {
122 pa_operation* op = pa_context_unload_module(context_, loopbackModuleIdx_, completion_cb, mainloop_);
124 loopbackModuleIdx_ = PA_INVALID_INDEX;
126 if (nullSinkModuleIdx_ != PA_INVALID_INDEX) {
127 pa_operation* op = pa_context_unload_module(context_, nullSinkModuleIdx_, completion_cb, mainloop_);
129 nullSinkModuleIdx_ = PA_INVALID_INDEX;
133 pa_stream_disconnect(recordStream_);
134 pa_stream_unref(recordStream_);
135 recordStream_ =
nullptr;
138 pa_context_disconnect(context_);
139 pa_context_unref(context_);
142 pa_threaded_mainloop_unlock(mainloop_);
144 pa_threaded_mainloop_stop(mainloop_);
145 pa_threaded_mainloop_free(mainloop_);
156PulseLoopbackCapture::contextStateCallback(pa_context* c,
void* userdata)
158 auto* self = get_instance(userdata);
159 if (!self || !self->context_ || self->context_ != c)
162 switch (pa_context_get_state(c)) {
163 case PA_CONTEXT_READY: {
164 pa_operation* o = pa_context_get_server_info(c, serverInfoCallback, self);
166 pa_operation_unref(o);
169 case PA_CONTEXT_FAILED:
170 case PA_CONTEXT_TERMINATED:
171 JAMI_ERROR(
"[pulseloopbackcapture] Context failed/terminated: {}", pa_strerror(pa_context_errno(c)));
179PulseLoopbackCapture::serverInfoCallback(pa_context* c,
const pa_server_info* i,
void* userdata)
183 auto* self = get_instance(userdata);
184 if (!self || !self->context_ || self->context_ != c)
187 self->defaultSinkName_ = i->default_sink_name;
189 std::string null_args = fmt::format(
"sink_name={} sink_properties=device.description='Desktop_Capture_Mix_{}' "
190 "format=s16le rate=48000 channels=2",
191 self->uniqueSinkName_,
194 pa_operation* o = pa_context_load_module(c,
"module-null-sink", null_args.c_str(), moduleLoadedCallback, self);
196 pa_operation_unref(o);
200PulseLoopbackCapture::moduleLoadedCallback(pa_context* c, uint32_t idx,
void* userdata)
202 auto* self = get_instance(userdata);
203 if (!self || !self->context_ || self->context_ != c)
206 if (self->nullSinkModuleIdx_ == PA_INVALID_INDEX) {
207 self->nullSinkModuleIdx_ = idx;
208 JAMI_DEBUG(
"[pulseloopbackcapture] Loaded null-sink module: {}", idx);
210 std::string loop_args = fmt::format(
"source={}.monitor sink={} latency_msec=50",
211 self->uniqueSinkName_,
212 self->defaultSinkName_);
213 pa_operation* o = pa_context_load_module(c,
"module-loopback", loop_args.c_str(), moduleLoadedCallback, self);
215 pa_operation_unref(o);
216 }
else if (self->loopbackModuleIdx_ == PA_INVALID_INDEX) {
217 self->loopbackModuleIdx_ = idx;
218 JAMI_DEBUG(
"[pulseloopbackcapture] Loaded loopback module: {}", idx);
220 pa_context_set_subscribe_callback(c, subscribeCallback, self);
221 pa_operation* o = pa_context_subscribe(c,
222 (pa_subscription_mask_t) PA_SUBSCRIPTION_MASK_SINK_INPUT,
226 pa_operation_unref(o);
228 o = pa_context_get_sink_input_info_list(c, sinkInputInfoCallback, self);
230 pa_operation_unref(o);
232 self->startRecordingStream();
237PulseLoopbackCapture::startRecordingStream()
239 recordStream_ = pa_stream_new(context_,
"DesktopCapture", &SAMPLE_SPEC,
nullptr);
240 if (!recordStream_) {
241 JAMI_ERROR(
"[pulseloopbackcapture] Failed to create record stream");
245 pa_stream_set_read_callback(recordStream_, streamReadCallback,
this);
247 std::string
monitor = uniqueSinkName_ +
".monitor";
250 const uint32_t latency_ms = 10;
251 const uint32_t sample_rate = SAMPLE_SPEC.rate;
252 const uint32_t
channels = SAMPLE_SPEC.channels;
253 const uint32_t frame_size =
sizeof(int16_t) *
channels;
255 attr.maxlength = sample_rate * frame_size * latency_ms / 1000;
256 attr.tlength = (uint32_t) -1;
257 attr.prebuf = (uint32_t) -1;
258 attr.minreq = (uint32_t) -1;
259 attr.fragsize = sample_rate * frame_size * latency_ms / 2000;
261 JAMI_DEBUG(
"[pulseloopbackcapture] Buffer config: maxlength={}B fragsize={}B ({}ms latency)",
266 pa_stream_connect_record(recordStream_,
monitor.c_str(), &attr, PA_STREAM_ADJUST_LATENCY);
267 JAMI_DEBUG(
"[pulseloopbackcapture] Recording stream connected to {}", monitor);
271PulseLoopbackCapture::streamReadCallback(pa_stream* s,
size_t length,
void* userdata)
273 auto* self = get_instance(userdata);
274 if (!self || !self->context_ || !self->recordStream_ || self->recordStream_ != s)
278 if (pa_stream_peek(s, &data, &length) < 0)
281 if (data && length > 0 && self->dataCallback_) {
282 self->dataCallback_(data, length);
289PulseLoopbackCapture::subscribeCallback(pa_context* c, pa_subscription_event_type_t t, uint32_t idx,
void* userdata)
291 auto* self = get_instance(userdata);
292 if (!self || !self->context_ || self->context_ != c)
295 if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK_INPUT) {
296 if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_NEW) {
297 pa_operation* o = pa_context_get_sink_input_info(c, idx, sinkInputInfoCallback, self);
299 pa_operation_unref(o);
305PulseLoopbackCapture::sinkInputInfoCallback(pa_context* c,
const pa_sink_input_info* i,
int eol,
void* userdata)
309 auto* self = get_instance(userdata);
310 if (!self || !self->context_ || self->context_ != c)
315 const char* pid_str = pa_proplist_gets(i->proplist, PA_PROP_APPLICATION_PROCESS_ID);
317 pid = (pid_t) atoi(pid_str);
320 self->moveStreamIfNeeded(i->index, pid, i->owner_module, i->name);
324PulseLoopbackCapture::moveStreamIfNeeded(uint32_t streamIdx,
326 uint32_t ownerModuleIdx,
327 const char* streamName)
332 if (streamPid == myPid_)
334 if (ownerModuleIdx != PA_INVALID_INDEX && ownerModuleIdx == loopbackModuleIdx_)
337 pa_operation* o = pa_context_move_sink_input_by_name(context_, streamIdx, uniqueSinkName_.c_str(),
nullptr,
nullptr);
339 pa_operation_unref(o);
343PulseLoopbackCapture::runMainLoop()
std::function< void(const void *data, size_t length)> AudioFrameCallback
bool startCaptureAsync(AudioFrameCallback callback)
#define JAMI_ERROR(formatstr,...)
#define JAMI_DEBUG(formatstr,...)
#define JAMI_WARNING(formatstr,...)
void monitor(bool continuous)