Line data Source code
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 :
18 : #include "pulseloopbackcapture.h"
19 : #include "logger.h"
20 :
21 : namespace {
22 :
23 : PulseLoopbackCapture*
24 0 : get_instance(void* userdata)
25 : {
26 0 : return static_cast<PulseLoopbackCapture*>(userdata);
27 : }
28 :
29 : } // namespace
30 :
31 3 : PulseLoopbackCapture::PulseLoopbackCapture()
32 : {
33 3 : myPid_ = getpid();
34 3 : uniqueSinkName_ = "audiocapture_null_sink_" + std::to_string(myPid_);
35 3 : }
36 :
37 3 : PulseLoopbackCapture::~PulseLoopbackCapture()
38 : {
39 3 : stopCapture();
40 3 : }
41 :
42 : bool
43 0 : PulseLoopbackCapture::startCaptureAsync(AudioFrameCallback callback)
44 : {
45 0 : if (running_) {
46 0 : JAMI_WARNING("[pulseloopbackcapture] Already running");
47 0 : return false;
48 : }
49 :
50 0 : if (mainloop_ || context_ || recordStream_) {
51 0 : JAMI_WARNING("[pulseloopbackcapture] Previous state not fully cleaned, forcing cleanup");
52 0 : stopCapture();
53 : }
54 :
55 0 : dataCallback_ = std::move(callback);
56 :
57 0 : mainloop_ = pa_threaded_mainloop_new();
58 0 : if (!mainloop_) {
59 0 : JAMI_ERROR("[pulseloopbackcapture] Failed to create mainloop");
60 0 : return false;
61 : }
62 :
63 0 : mainloopApi_ = pa_threaded_mainloop_get_api(mainloop_);
64 :
65 0 : context_ = pa_context_new(mainloopApi_, "AudioCaptureLib");
66 0 : if (!context_) {
67 0 : JAMI_ERROR("[pulseloopbackcapture] Failed to create context");
68 0 : pa_threaded_mainloop_free(mainloop_);
69 0 : mainloop_ = nullptr;
70 0 : return false;
71 : }
72 0 : pa_context_set_state_callback(context_, contextStateCallback, this);
73 :
74 0 : if (pa_context_connect(context_, nullptr, PA_CONTEXT_NOFLAGS, nullptr) < 0) {
75 0 : JAMI_ERROR("[pulseloopbackcapture] Failed to connect context: {}", pa_strerror(pa_context_errno(context_)));
76 0 : pa_context_unref(context_);
77 0 : context_ = nullptr;
78 0 : pa_threaded_mainloop_free(mainloop_);
79 0 : mainloop_ = nullptr;
80 0 : return false;
81 : }
82 :
83 0 : if (pa_threaded_mainloop_start(mainloop_) < 0) {
84 0 : JAMI_ERROR("[pulseloopbackcapture] Failed to start mainloop");
85 0 : pa_context_disconnect(context_);
86 0 : pa_context_unref(context_);
87 0 : context_ = nullptr;
88 0 : pa_threaded_mainloop_free(mainloop_);
89 0 : mainloop_ = nullptr;
90 0 : return false;
91 : }
92 :
93 0 : running_ = true;
94 0 : return true;
95 : }
96 :
97 : void
98 3 : PulseLoopbackCapture::stopCapture()
99 : {
100 3 : if (!running_ || !mainloop_)
101 3 : return;
102 :
103 0 : pa_threaded_mainloop_lock(mainloop_);
104 :
105 0 : auto wait_for_op = [this](pa_operation* op) {
106 0 : if (!op)
107 0 : return;
108 0 : while (pa_operation_get_state(op) == PA_OPERATION_RUNNING) {
109 0 : pa_threaded_mainloop_wait(mainloop_);
110 : }
111 0 : pa_operation_unref(op);
112 0 : };
113 :
114 0 : auto completion_cb = [](pa_context* c, int success, void* userdata) {
115 : (void) c;
116 : (void) success;
117 0 : pa_threaded_mainloop* m = (pa_threaded_mainloop*) userdata;
118 0 : pa_threaded_mainloop_signal(m, 0);
119 0 : };
120 :
121 0 : if (loopbackModuleIdx_ != PA_INVALID_INDEX) {
122 0 : pa_operation* op = pa_context_unload_module(context_, loopbackModuleIdx_, completion_cb, mainloop_);
123 0 : wait_for_op(op);
124 0 : loopbackModuleIdx_ = PA_INVALID_INDEX;
125 : }
126 0 : if (nullSinkModuleIdx_ != PA_INVALID_INDEX) {
127 0 : pa_operation* op = pa_context_unload_module(context_, nullSinkModuleIdx_, completion_cb, mainloop_);
128 0 : wait_for_op(op);
129 0 : nullSinkModuleIdx_ = PA_INVALID_INDEX;
130 : }
131 :
132 0 : if (recordStream_) {
133 0 : pa_stream_disconnect(recordStream_);
134 0 : pa_stream_unref(recordStream_);
135 0 : recordStream_ = nullptr;
136 : }
137 :
138 0 : pa_context_disconnect(context_);
139 0 : pa_context_unref(context_);
140 0 : context_ = nullptr;
141 :
142 0 : pa_threaded_mainloop_unlock(mainloop_);
143 :
144 0 : pa_threaded_mainloop_stop(mainloop_);
145 0 : pa_threaded_mainloop_free(mainloop_);
146 0 : mainloop_ = nullptr;
147 :
148 0 : running_ = false;
149 : }
150 :
151 : // -------------------------------------------------------------------------
152 : // Callbacks (Trampolines)
153 : // -------------------------------------------------------------------------
154 :
155 : void
156 0 : PulseLoopbackCapture::contextStateCallback(pa_context* c, void* userdata)
157 : {
158 0 : auto* self = get_instance(userdata);
159 0 : if (!self || !self->context_ || self->context_ != c)
160 0 : return;
161 :
162 0 : switch (pa_context_get_state(c)) {
163 0 : case PA_CONTEXT_READY: {
164 0 : pa_operation* o = pa_context_get_server_info(c, serverInfoCallback, self);
165 0 : if (o)
166 0 : pa_operation_unref(o);
167 0 : break;
168 : }
169 0 : case PA_CONTEXT_FAILED:
170 : case PA_CONTEXT_TERMINATED:
171 0 : JAMI_ERROR("[pulseloopbackcapture] Context failed/terminated: {}", pa_strerror(pa_context_errno(c)));
172 0 : break;
173 0 : default:
174 0 : break;
175 : }
176 : }
177 :
178 : void
179 0 : PulseLoopbackCapture::serverInfoCallback(pa_context* c, const pa_server_info* i, void* userdata)
180 : {
181 0 : if (!i)
182 0 : return;
183 0 : auto* self = get_instance(userdata);
184 0 : if (!self || !self->context_ || self->context_ != c)
185 0 : return;
186 :
187 0 : 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 0 : self->uniqueSinkName_,
192 0 : self->myPid_);
193 :
194 0 : pa_operation* o = pa_context_load_module(c, "module-null-sink", null_args.c_str(), moduleLoadedCallback, self);
195 0 : if (o)
196 0 : pa_operation_unref(o);
197 0 : }
198 :
199 : void
200 0 : PulseLoopbackCapture::moduleLoadedCallback(pa_context* c, uint32_t idx, void* userdata)
201 : {
202 0 : auto* self = get_instance(userdata);
203 0 : if (!self || !self->context_ || self->context_ != c)
204 0 : return;
205 :
206 0 : if (self->nullSinkModuleIdx_ == PA_INVALID_INDEX) {
207 0 : self->nullSinkModuleIdx_ = idx;
208 0 : JAMI_DEBUG("[pulseloopbackcapture] Loaded null-sink module: {}", idx);
209 :
210 : std::string loop_args = fmt::format("source={}.monitor sink={} latency_msec=50",
211 0 : self->uniqueSinkName_,
212 0 : self->defaultSinkName_);
213 0 : pa_operation* o = pa_context_load_module(c, "module-loopback", loop_args.c_str(), moduleLoadedCallback, self);
214 0 : if (o)
215 0 : pa_operation_unref(o);
216 0 : } else if (self->loopbackModuleIdx_ == PA_INVALID_INDEX) {
217 0 : self->loopbackModuleIdx_ = idx;
218 0 : JAMI_DEBUG("[pulseloopbackcapture] Loaded loopback module: {}", idx);
219 :
220 0 : pa_context_set_subscribe_callback(c, subscribeCallback, self);
221 0 : pa_operation* o = pa_context_subscribe(c,
222 : (pa_subscription_mask_t) PA_SUBSCRIPTION_MASK_SINK_INPUT,
223 : nullptr,
224 : nullptr);
225 0 : if (o)
226 0 : pa_operation_unref(o);
227 :
228 0 : o = pa_context_get_sink_input_info_list(c, sinkInputInfoCallback, self);
229 0 : if (o)
230 0 : pa_operation_unref(o);
231 :
232 0 : self->startRecordingStream();
233 : }
234 : }
235 :
236 : void
237 0 : PulseLoopbackCapture::startRecordingStream()
238 : {
239 0 : recordStream_ = pa_stream_new(context_, "DesktopCapture", &SAMPLE_SPEC, nullptr);
240 0 : if (!recordStream_) {
241 0 : JAMI_ERROR("[pulseloopbackcapture] Failed to create record stream");
242 0 : return;
243 : }
244 :
245 0 : pa_stream_set_read_callback(recordStream_, streamReadCallback, this);
246 :
247 0 : std::string monitor = uniqueSinkName_ + ".monitor";
248 :
249 : pa_buffer_attr attr;
250 0 : const uint32_t latency_ms = 10;
251 0 : const uint32_t sample_rate = SAMPLE_SPEC.rate;
252 0 : const uint32_t channels = SAMPLE_SPEC.channels;
253 0 : const uint32_t frame_size = sizeof(int16_t) * channels;
254 :
255 0 : attr.maxlength = sample_rate * frame_size * latency_ms / 1000;
256 0 : attr.tlength = (uint32_t) -1;
257 0 : attr.prebuf = (uint32_t) -1;
258 0 : attr.minreq = (uint32_t) -1;
259 0 : attr.fragsize = sample_rate * frame_size * latency_ms / 2000;
260 :
261 0 : JAMI_DEBUG("[pulseloopbackcapture] Buffer config: maxlength={}B fragsize={}B ({}ms latency)",
262 : attr.maxlength,
263 : attr.fragsize,
264 : latency_ms);
265 :
266 0 : pa_stream_connect_record(recordStream_, monitor.c_str(), &attr, PA_STREAM_ADJUST_LATENCY);
267 0 : JAMI_DEBUG("[pulseloopbackcapture] Recording stream connected to {}", monitor);
268 0 : }
269 :
270 : void
271 0 : PulseLoopbackCapture::streamReadCallback(pa_stream* s, size_t length, void* userdata)
272 : {
273 0 : auto* self = get_instance(userdata);
274 0 : if (!self || !self->context_ || !self->recordStream_ || self->recordStream_ != s)
275 0 : return;
276 :
277 : const void* data;
278 0 : if (pa_stream_peek(s, &data, &length) < 0)
279 0 : return;
280 :
281 0 : if (data && length > 0 && self->dataCallback_) {
282 0 : self->dataCallback_(data, length);
283 : }
284 :
285 0 : pa_stream_drop(s);
286 : }
287 :
288 : void
289 0 : PulseLoopbackCapture::subscribeCallback(pa_context* c, pa_subscription_event_type_t t, uint32_t idx, void* userdata)
290 : {
291 0 : auto* self = get_instance(userdata);
292 0 : if (!self || !self->context_ || self->context_ != c)
293 0 : return;
294 :
295 0 : if ((t & PA_SUBSCRIPTION_EVENT_FACILITY_MASK) == PA_SUBSCRIPTION_EVENT_SINK_INPUT) {
296 0 : if ((t & PA_SUBSCRIPTION_EVENT_TYPE_MASK) == PA_SUBSCRIPTION_EVENT_NEW) {
297 0 : pa_operation* o = pa_context_get_sink_input_info(c, idx, sinkInputInfoCallback, self);
298 0 : if (o)
299 0 : pa_operation_unref(o);
300 : }
301 : }
302 : }
303 :
304 : void
305 0 : PulseLoopbackCapture::sinkInputInfoCallback(pa_context* c, const pa_sink_input_info* i, int eol, void* userdata)
306 : {
307 0 : if (eol < 0 || !i)
308 0 : return;
309 0 : auto* self = get_instance(userdata);
310 0 : if (!self || !self->context_ || self->context_ != c)
311 0 : return;
312 :
313 0 : pid_t pid = 0;
314 0 : if (i->proplist) {
315 0 : const char* pid_str = pa_proplist_gets(i->proplist, PA_PROP_APPLICATION_PROCESS_ID);
316 0 : if (pid_str)
317 0 : pid = (pid_t) atoi(pid_str);
318 : }
319 :
320 0 : self->moveStreamIfNeeded(i->index, pid, i->owner_module, i->name);
321 : }
322 :
323 : void
324 0 : PulseLoopbackCapture::moveStreamIfNeeded(uint32_t streamIdx,
325 : pid_t streamPid,
326 : uint32_t ownerModuleIdx,
327 : const char* streamName)
328 : {
329 : (void) streamName;
330 0 : if (!context_)
331 0 : return;
332 0 : if (streamPid == myPid_)
333 0 : return;
334 0 : if (ownerModuleIdx != PA_INVALID_INDEX && ownerModuleIdx == loopbackModuleIdx_)
335 0 : return;
336 :
337 0 : pa_operation* o = pa_context_move_sink_input_by_name(context_, streamIdx, uniqueSinkName_.c_str(), nullptr, nullptr);
338 0 : if (o)
339 0 : pa_operation_unref(o);
340 : }
341 :
342 : void
343 0 : PulseLoopbackCapture::runMainLoop()
344 0 : {}
|