Ring Daemon
Loading...
Searching...
No Matches
pulseloopbackcapture.cpp
Go to the documentation of this file.
1/*
2 * Copyright (C) 2004-2026 Savoir-faire Linux Inc.
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 3 of the License, or
7 * (at your option) any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17
19#include "logger.h"
20
21namespace {
22
24get_instance(void* userdata)
25{
26 return static_cast<PulseLoopbackCapture*>(userdata);
27}
28
29} // namespace
30
32{
33 myPid_ = getpid();
34 uniqueSinkName_ = "audiocapture_null_sink_" + std::to_string(myPid_);
35}
36
41
42bool
44{
45 if (running_) {
46 JAMI_WARNING("[pulseloopbackcapture] Already running");
47 return false;
48 }
49
50 if (mainloop_ || context_ || recordStream_) {
51 JAMI_WARNING("[pulseloopbackcapture] Previous state not fully cleaned, forcing cleanup");
53 }
54
55 dataCallback_ = std::move(callback);
56
57 mainloop_ = pa_threaded_mainloop_new();
58 if (!mainloop_) {
59 JAMI_ERROR("[pulseloopbackcapture] Failed to create mainloop");
60 return false;
61 }
62
63 mainloopApi_ = pa_threaded_mainloop_get_api(mainloop_);
64
65 context_ = pa_context_new(mainloopApi_, "AudioCaptureLib");
66 if (!context_) {
67 JAMI_ERROR("[pulseloopbackcapture] Failed to create context");
68 pa_threaded_mainloop_free(mainloop_);
69 mainloop_ = nullptr;
70 return false;
71 }
72 pa_context_set_state_callback(context_, contextStateCallback, this);
73
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_);
77 context_ = nullptr;
78 pa_threaded_mainloop_free(mainloop_);
79 mainloop_ = nullptr;
80 return false;
81 }
82
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_);
87 context_ = nullptr;
88 pa_threaded_mainloop_free(mainloop_);
89 mainloop_ = nullptr;
90 return false;
91 }
92
93 running_ = true;
94 return true;
95}
96
97void
99{
100 if (!running_ || !mainloop_)
101 return;
102
103 pa_threaded_mainloop_lock(mainloop_);
104
105 auto wait_for_op = [this](pa_operation* op) {
106 if (!op)
107 return;
108 while (pa_operation_get_state(op) == PA_OPERATION_RUNNING) {
109 pa_threaded_mainloop_wait(mainloop_);
110 }
111 pa_operation_unref(op);
112 };
113
114 auto completion_cb = [](pa_context* c, int success, void* userdata) {
115 (void) c;
116 (void) success;
117 pa_threaded_mainloop* m = (pa_threaded_mainloop*) userdata;
118 pa_threaded_mainloop_signal(m, 0);
119 };
120
121 if (loopbackModuleIdx_ != PA_INVALID_INDEX) {
122 pa_operation* op = pa_context_unload_module(context_, loopbackModuleIdx_, completion_cb, mainloop_);
123 wait_for_op(op);
124 loopbackModuleIdx_ = PA_INVALID_INDEX;
125 }
126 if (nullSinkModuleIdx_ != PA_INVALID_INDEX) {
127 pa_operation* op = pa_context_unload_module(context_, nullSinkModuleIdx_, completion_cb, mainloop_);
128 wait_for_op(op);
129 nullSinkModuleIdx_ = PA_INVALID_INDEX;
130 }
131
132 if (recordStream_) {
133 pa_stream_disconnect(recordStream_);
134 pa_stream_unref(recordStream_);
135 recordStream_ = nullptr;
136 }
137
138 pa_context_disconnect(context_);
139 pa_context_unref(context_);
140 context_ = nullptr;
141
142 pa_threaded_mainloop_unlock(mainloop_);
143
144 pa_threaded_mainloop_stop(mainloop_);
145 pa_threaded_mainloop_free(mainloop_);
146 mainloop_ = nullptr;
147
148 running_ = false;
149}
150
151// -------------------------------------------------------------------------
152// Callbacks (Trampolines)
153// -------------------------------------------------------------------------
154
155void
156PulseLoopbackCapture::contextStateCallback(pa_context* c, void* userdata)
157{
158 auto* self = get_instance(userdata);
159 if (!self || !self->context_ || self->context_ != c)
160 return;
161
162 switch (pa_context_get_state(c)) {
163 case PA_CONTEXT_READY: {
164 pa_operation* o = pa_context_get_server_info(c, serverInfoCallback, self);
165 if (o)
166 pa_operation_unref(o);
167 break;
168 }
169 case PA_CONTEXT_FAILED:
170 case PA_CONTEXT_TERMINATED:
171 JAMI_ERROR("[pulseloopbackcapture] Context failed/terminated: {}", pa_strerror(pa_context_errno(c)));
172 break;
173 default:
174 break;
175 }
176}
177
178void
179PulseLoopbackCapture::serverInfoCallback(pa_context* c, const pa_server_info* i, void* userdata)
180{
181 if (!i)
182 return;
183 auto* self = get_instance(userdata);
184 if (!self || !self->context_ || self->context_ != c)
185 return;
186
187 self->defaultSinkName_ = i->default_sink_name;
188
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_,
192 self->myPid_);
193
194 pa_operation* o = pa_context_load_module(c, "module-null-sink", null_args.c_str(), moduleLoadedCallback, self);
195 if (o)
196 pa_operation_unref(o);
197}
198
199void
200PulseLoopbackCapture::moduleLoadedCallback(pa_context* c, uint32_t idx, void* userdata)
201{
202 auto* self = get_instance(userdata);
203 if (!self || !self->context_ || self->context_ != c)
204 return;
205
206 if (self->nullSinkModuleIdx_ == PA_INVALID_INDEX) {
207 self->nullSinkModuleIdx_ = idx;
208 JAMI_DEBUG("[pulseloopbackcapture] Loaded null-sink module: {}", idx);
209
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);
214 if (o)
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);
219
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,
223 nullptr,
224 nullptr);
225 if (o)
226 pa_operation_unref(o);
227
228 o = pa_context_get_sink_input_info_list(c, sinkInputInfoCallback, self);
229 if (o)
230 pa_operation_unref(o);
231
232 self->startRecordingStream();
233 }
234}
235
236void
237PulseLoopbackCapture::startRecordingStream()
238{
239 recordStream_ = pa_stream_new(context_, "DesktopCapture", &SAMPLE_SPEC, nullptr);
240 if (!recordStream_) {
241 JAMI_ERROR("[pulseloopbackcapture] Failed to create record stream");
242 return;
243 }
244
245 pa_stream_set_read_callback(recordStream_, streamReadCallback, this);
246
247 std::string monitor = uniqueSinkName_ + ".monitor";
248
249 pa_buffer_attr attr;
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;
254
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;
260
261 JAMI_DEBUG("[pulseloopbackcapture] Buffer config: maxlength={}B fragsize={}B ({}ms latency)",
262 attr.maxlength,
263 attr.fragsize,
264 latency_ms);
265
266 pa_stream_connect_record(recordStream_, monitor.c_str(), &attr, PA_STREAM_ADJUST_LATENCY);
267 JAMI_DEBUG("[pulseloopbackcapture] Recording stream connected to {}", monitor);
268}
269
270void
271PulseLoopbackCapture::streamReadCallback(pa_stream* s, size_t length, void* userdata)
272{
273 auto* self = get_instance(userdata);
274 if (!self || !self->context_ || !self->recordStream_ || self->recordStream_ != s)
275 return;
276
277 const void* data;
278 if (pa_stream_peek(s, &data, &length) < 0)
279 return;
280
281 if (data && length > 0 && self->dataCallback_) {
282 self->dataCallback_(data, length);
283 }
284
285 pa_stream_drop(s);
286}
287
288void
289PulseLoopbackCapture::subscribeCallback(pa_context* c, pa_subscription_event_type_t t, uint32_t idx, void* userdata)
290{
291 auto* self = get_instance(userdata);
292 if (!self || !self->context_ || self->context_ != c)
293 return;
294
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);
298 if (o)
299 pa_operation_unref(o);
300 }
301 }
302}
303
304void
305PulseLoopbackCapture::sinkInputInfoCallback(pa_context* c, const pa_sink_input_info* i, int eol, void* userdata)
306{
307 if (eol < 0 || !i)
308 return;
309 auto* self = get_instance(userdata);
310 if (!self || !self->context_ || self->context_ != c)
311 return;
312
313 pid_t pid = 0;
314 if (i->proplist) {
315 const char* pid_str = pa_proplist_gets(i->proplist, PA_PROP_APPLICATION_PROCESS_ID);
316 if (pid_str)
317 pid = (pid_t) atoi(pid_str);
318 }
319
320 self->moveStreamIfNeeded(i->index, pid, i->owner_module, i->name);
321}
322
323void
324PulseLoopbackCapture::moveStreamIfNeeded(uint32_t streamIdx,
325 pid_t streamPid,
326 uint32_t ownerModuleIdx,
327 const char* streamName)
328{
329 (void) streamName;
330 if (!context_)
331 return;
332 if (streamPid == myPid_)
333 return;
334 if (ownerModuleIdx != PA_INVALID_INDEX && ownerModuleIdx == loopbackModuleIdx_)
335 return;
336
337 pa_operation* o = pa_context_move_sink_input_by_name(context_, streamIdx, uniqueSinkName_.c_str(), nullptr, nullptr);
338 if (o)
339 pa_operation_unref(o);
340}
341
342void
343PulseLoopbackCapture::runMainLoop()
344{}
std::function< void(const void *data, size_t length)> AudioFrameCallback
bool startCaptureAsync(AudioFrameCallback callback)
#define JAMI_ERROR(formatstr,...)
Definition logger.h:243
#define JAMI_DEBUG(formatstr,...)
Definition logger.h:238
#define JAMI_WARNING(formatstr,...)
Definition logger.h:242
void monitor(bool continuous)