LCOV - code coverage report
Current view: top level - src/media/audio/pulseaudio - pulseloopbackcapture.cpp (source / functions) Coverage Total Hit
Test: jami-coverage-filtered.info Lines: 5.2 % 194 10
Test Date: 2026-06-13 09:18:46 Functions: 7.5 % 40 3

            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 : {}
        

Generated by: LCOV version 2.0-1