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

          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 1.14