LCOV - code coverage report
Current view: top level - source - GStreamerWebAudioPlayerClient.cpp (source / functions) Coverage Total Hit
Test: coverage.info Lines: 98.3 % 237 233
Test Date: 2025-06-24 14:11:58 Functions: 100.0 % 29 29

            Line data    Source code
       1              : /*
       2              :  * Copyright (C) 2023 Sky UK
       3              :  *
       4              :  * This library is free software; you can redistribute it and/or
       5              :  * modify it under the terms of the GNU Lesser General Public
       6              :  * License as published by the Free Software Foundation;
       7              :  * version 2.1 of the License.
       8              :  *
       9              :  * This library 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 GNU
      12              :  * Lesser General Public License for more details.
      13              :  *
      14              :  * You should have received a copy of the GNU Lesser General Public
      15              :  * License along with this library; if not, write to the Free Software
      16              :  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
      17              :  */
      18              : 
      19              : #include "GStreamerWebAudioPlayerClient.h"
      20              : #include "GstreamerCatLog.h"
      21              : 
      22              : #include <string.h>
      23              : 
      24              : #include <algorithm>
      25              : #include <chrono>
      26              : #include <cstdlib>
      27              : #include <thread>
      28              : 
      29              : #define GST_CAT_DEFAULT rialtoGStreamerCat
      30              : 
      31              : namespace
      32              : {
      33              : constexpr std::size_t kMaxQueueSize{40};
      34           51 : bool parseGstStructureFormat(const std::string &format, uint32_t &sampleSize, bool &isBigEndian, bool &isSigned,
      35              :                              bool &isFloat)
      36              : {
      37           51 :     if (format.size() != 5)
      38              :     {
      39            1 :         return false;
      40              :     }
      41           50 :     std::string sampleSizeStr = format.substr(1, 2);
      42           50 :     char *pEnd = NULL;
      43           50 :     errno = 0;
      44           50 :     sampleSize = strtoul(sampleSizeStr.c_str(), &pEnd, 10);
      45           50 :     if (errno == ERANGE)
      46              :     {
      47            0 :         return false;
      48              :     }
      49              : 
      50           50 :     isBigEndian = format.substr(3) == "BE";
      51              : 
      52           50 :     switch (format[0])
      53              :     {
      54           45 :     case 'S':
      55           45 :         isSigned = true;
      56           45 :         isFloat = false;
      57           45 :         break;
      58            3 :     case 'U':
      59            3 :         isSigned = false;
      60            3 :         isFloat = false;
      61            3 :         break;
      62            1 :     case 'F':
      63            1 :         isSigned = false;
      64            1 :         isFloat = true;
      65            1 :         break;
      66            1 :     default:
      67            1 :         return false;
      68              :         break;
      69              :     }
      70           49 :     return true;
      71           50 : }
      72              : 
      73            2 : bool operator!=(const firebolt::rialto::WebAudioPcmConfig &lac, const firebolt::rialto::WebAudioPcmConfig &rac)
      74              : {
      75            2 :     return lac.rate != rac.rate || lac.channels != rac.channels || lac.sampleSize != rac.sampleSize ||
      76            4 :            lac.isBigEndian != rac.isBigEndian || lac.isSigned != rac.isSigned || lac.isFloat != rac.isFloat;
      77              : }
      78              : } // namespace
      79              : 
      80           67 : GStreamerWebAudioPlayerClient::GStreamerWebAudioPlayerClient(
      81              :     std::unique_ptr<firebolt::rialto::client::WebAudioClientBackendInterface> &&webAudioClientBackend,
      82              :     std::unique_ptr<IMessageQueue> &&backendQueue, WebAudioSinkCallbacks callbacks,
      83           67 :     std::shared_ptr<ITimerFactory> timerFactory)
      84           67 :     : m_backendQueue{std::move(backendQueue)}, m_clientBackend{std::move(webAudioClientBackend)}, m_isOpen{false},
      85           67 :       m_dataBuffers{}, m_timerFactory{timerFactory}, m_pushSamplesTimer{nullptr}, m_preferredFrames{0},
      86           67 :       m_maximumFrames{0}, m_supportDeferredPlay{false}, m_isEos{false}, m_frameSize{0}, m_mimeType{}, m_config{{}},
      87          201 :       m_callbacks{callbacks}
      88              : {
      89           67 :     m_backendQueue->start();
      90              : }
      91              : 
      92           67 : GStreamerWebAudioPlayerClient::~GStreamerWebAudioPlayerClient()
      93              : {
      94           67 :     m_backendQueue->stop();
      95              : }
      96              : 
      97           56 : bool GStreamerWebAudioPlayerClient::open(GstCaps *caps)
      98              : {
      99           56 :     GST_DEBUG("entry:");
     100              : 
     101           56 :     bool result = false;
     102           56 :     GstStructure *structure = gst_caps_get_structure(caps, 0);
     103           56 :     std::string audioMimeType = gst_structure_get_name(structure);
     104           56 :     const auto spacePosition = audioMimeType.find(' ');
     105           56 :     if (spacePosition != std::string::npos)
     106              :     {
     107            0 :         audioMimeType.resize(spacePosition);
     108              :     }
     109           56 :     const gchar *formatCStr{gst_structure_get_string(structure, "format")};
     110          112 :     std::string format{formatCStr ? formatCStr : ""};
     111              :     firebolt::rialto::WebAudioPcmConfig pcm;
     112              :     gint tmp;
     113              : 
     114           56 :     if (format.empty())
     115              :     {
     116            3 :         GST_ERROR("Format not found in caps");
     117            3 :         return result;
     118              :     }
     119              : 
     120           53 :     if (!gst_structure_get_int(structure, "rate", &tmp))
     121              :     {
     122            1 :         GST_ERROR("Rate not found in caps");
     123            1 :         return result;
     124              :     }
     125           52 :     pcm.rate = tmp;
     126              : 
     127           52 :     if (!gst_structure_get_int(structure, "channels", &tmp))
     128              :     {
     129            1 :         GST_ERROR("Rate not found in caps");
     130            1 :         return result;
     131              :     }
     132           51 :     pcm.channels = tmp;
     133              : 
     134           51 :     if (!parseGstStructureFormat(format, pcm.sampleSize, pcm.isBigEndian, pcm.isSigned, pcm.isFloat))
     135              :     {
     136            2 :         GST_ERROR("Can't parse format or it is not supported: %s", format.c_str());
     137            2 :         return result;
     138              :     }
     139              : 
     140           98 :     m_backendQueue->callInEventLoop(
     141           49 :         [&]()
     142              :         {
     143              :             // "configWorkaround" is used because there doesn't seem to be an easy way to use
     144              :             // make_shared in conjunction with an initalizer list, and adding a constructor to
     145              :             // WebAudioConfig stops initalizer lists (used elsewhere in the code) from working...
     146           49 :             firebolt::rialto::WebAudioConfig configWorkaround{pcm};
     147              :             std::shared_ptr<firebolt::rialto::WebAudioConfig> config =
     148           49 :                 std::make_shared<firebolt::rialto::WebAudioConfig>(configWorkaround);
     149              : 
     150              :             // Only recreate player if the config has changed
     151           49 :             if (!m_isOpen || isNewConfig(audioMimeType, config))
     152              :             {
     153           48 :                 if (m_isOpen)
     154              :                 {
     155              :                     // Destroy the previously created player
     156            3 :                     m_clientBackend->destroyWebAudioBackend();
     157              :                 }
     158              : 
     159           48 :                 uint32_t priority = 1;
     160           48 :                 if (m_clientBackend->createWebAudioBackend(shared_from_this(), audioMimeType, priority, config))
     161              :                 {
     162           47 :                     if (!m_clientBackend->getDeviceInfo(m_preferredFrames, m_maximumFrames, m_supportDeferredPlay))
     163              :                     {
     164            1 :                         GST_ERROR("GetDeviceInfo failed, could not process samples");
     165              :                     }
     166           47 :                     m_frameSize = (pcm.sampleSize * pcm.channels) / CHAR_BIT;
     167           47 :                     m_isOpen = true;
     168              : 
     169              :                     // Store config
     170           47 :                     m_config.pcm = pcm;
     171           47 :                     m_mimeType = audioMimeType;
     172              :                 }
     173              :                 else
     174              :                 {
     175            1 :                     GST_ERROR("Could not create web audio backend");
     176            1 :                     m_isOpen = false;
     177              :                 }
     178           48 :                 result = m_isOpen;
     179              :             }
     180           49 :         });
     181              : 
     182           49 :     return result;
     183           56 : }
     184              : 
     185           20 : bool GStreamerWebAudioPlayerClient::close()
     186              : {
     187           20 :     GST_DEBUG("entry:");
     188              : 
     189           40 :     m_backendQueue->callInEventLoop(
     190           20 :         [&]()
     191              :         {
     192           20 :             m_clientBackend->destroyWebAudioBackend();
     193           20 :             m_pushSamplesTimer.reset();
     194           20 :             m_isOpen = false;
     195           20 :         });
     196              : 
     197           20 :     return true;
     198              : }
     199              : 
     200           12 : bool GStreamerWebAudioPlayerClient::play()
     201              : {
     202           12 :     GST_DEBUG("entry:");
     203              : 
     204           12 :     bool result = false;
     205           24 :     m_backendQueue->callInEventLoop(
     206           12 :         [&]()
     207              :         {
     208           12 :             if (m_isOpen)
     209              :             {
     210           11 :                 result = m_clientBackend->play();
     211              :             }
     212              :             else
     213              :             {
     214            1 :                 GST_ERROR("No web audio backend");
     215              :             }
     216           12 :         });
     217              : 
     218           12 :     return result;
     219              : }
     220              : 
     221           12 : bool GStreamerWebAudioPlayerClient::pause()
     222              : {
     223           12 :     GST_DEBUG("entry:");
     224              : 
     225           12 :     bool result = false;
     226           24 :     m_backendQueue->callInEventLoop(
     227           12 :         [&]()
     228              :         {
     229           12 :             if (m_isOpen)
     230              :             {
     231           11 :                 result = m_clientBackend->pause();
     232              :             }
     233              :             else
     234              :             {
     235            1 :                 GST_ERROR("No web audio backend");
     236              :             }
     237           12 :         });
     238              : 
     239           12 :     return result;
     240              : }
     241              : 
     242            7 : bool GStreamerWebAudioPlayerClient::setEos()
     243              : {
     244            7 :     GST_DEBUG("entry:");
     245              : 
     246            7 :     bool result = false;
     247           14 :     m_backendQueue->callInEventLoop(
     248            7 :         [&]()
     249              :         {
     250            7 :             if (m_isOpen && !m_isEos)
     251              :             {
     252            5 :                 m_isEos = true;
     253            5 :                 if (m_dataBuffers.empty())
     254              :                 {
     255            4 :                     result = m_clientBackend->setEos();
     256              :                 }
     257              :                 else
     258              :                 {
     259            1 :                     pushSamples();
     260            1 :                     result = true;
     261              :                 }
     262              :             }
     263              :             else
     264              :             {
     265            2 :                 GST_DEBUG("No web audio backend, valid scenario");
     266              :             }
     267            7 :         });
     268              : 
     269            7 :     return result;
     270              : }
     271              : 
     272           21 : bool GStreamerWebAudioPlayerClient::isOpen()
     273              : {
     274           21 :     GST_DEBUG("entry:");
     275              : 
     276           21 :     bool result = false;
     277           42 :     m_backendQueue->callInEventLoop([&]() { result = m_isOpen; });
     278              : 
     279           21 :     return result;
     280              : }
     281              : 
     282            2 : void GStreamerWebAudioPlayerClient::notifyPushSamplesTimerExpired()
     283              : {
     284            4 :     m_backendQueue->scheduleInEventLoop([&]() { pushSamples(); });
     285            2 : }
     286              : 
     287            9 : bool GStreamerWebAudioPlayerClient::notifyNewSample(GstBuffer *buf)
     288              : {
     289            9 :     GST_DEBUG("entry:");
     290              : 
     291            9 :     bool result = false;
     292              : 
     293              :     {
     294            9 :         std::unique_lock lock{m_queueSizeMutex};
     295           18 :         m_queueSizeCv.wait(lock, [&]() { return m_dataBuffers.size() < kMaxQueueSize; });
     296            9 :     }
     297              : 
     298           18 :     m_backendQueue->callInEventLoop(
     299            9 :         [&]()
     300              :         {
     301            9 :             if (buf)
     302              :             {
     303            9 :                 if (m_pushSamplesTimer)
     304              :                 {
     305            1 :                     m_pushSamplesTimer->cancel();
     306            1 :                     m_pushSamplesTimer.reset();
     307              :                 }
     308              :                 {
     309            9 :                     std::unique_lock lock{m_queueSizeMutex};
     310            9 :                     m_dataBuffers.push(buf);
     311              :                 }
     312            9 :                 pushSamples();
     313            9 :                 result = true;
     314              :             }
     315            9 :         });
     316              : 
     317            9 :     return result;
     318              : }
     319              : 
     320           12 : void GStreamerWebAudioPlayerClient::pushSamples()
     321              : {
     322           12 :     GST_DEBUG("entry:");
     323           12 :     if (!m_isOpen || m_dataBuffers.empty())
     324              :     {
     325            1 :         return;
     326              :     }
     327              : 
     328           11 :     uint32_t availableFrames = 0u;
     329              :     do
     330              :     {
     331           14 :         if (!m_clientBackend->getBufferAvailable(availableFrames))
     332              :         {
     333            4 :             GST_ERROR("getBufferAvailable failed, could not process the samples");
     334              :             // clear the queue if getBufferAvailable failed
     335            4 :             std::queue<GstBuffer *> empty;
     336            4 :             std::unique_lock lock{m_queueSizeMutex};
     337            4 :             std::swap(m_dataBuffers, empty);
     338            4 :             m_queueSizeCv.notify_one();
     339              :         }
     340           10 :         else if (0 != availableFrames)
     341              :         {
     342            5 :             bool writeFailure = false;
     343            5 :             GstBuffer *buffer = m_dataBuffers.front();
     344            5 :             gsize bufferSize = gst_buffer_get_size(buffer);
     345            5 :             auto framesToWrite = std::min(availableFrames, static_cast<uint32_t>(bufferSize / m_frameSize));
     346            5 :             if (framesToWrite > 0)
     347              :             {
     348              :                 GstMapInfo bufferMap;
     349            4 :                 if (!gst_buffer_map(buffer, &bufferMap, GST_MAP_READ))
     350              :                 {
     351            0 :                     GST_ERROR("Could not map audio buffer, discarding buffer!");
     352            0 :                     writeFailure = true;
     353              :                 }
     354              :                 else
     355              :                 {
     356            4 :                     if (!m_clientBackend->writeBuffer(framesToWrite, bufferMap.data))
     357              :                     {
     358            1 :                         GST_ERROR("Could not map audio buffer, discarding buffer!");
     359            1 :                         writeFailure = true;
     360              :                     }
     361            4 :                     gst_buffer_unmap(buffer, &bufferMap);
     362              :                 }
     363              :             }
     364              : 
     365            5 :             if ((!writeFailure) && (framesToWrite * m_frameSize < bufferSize))
     366              :             {
     367              :                 // Handle any leftover data
     368            3 :                 uint32_t leftoverData = bufferSize - (availableFrames * m_frameSize);
     369            3 :                 gst_buffer_resize(buffer, framesToWrite * m_frameSize, leftoverData);
     370            3 :                 if ((leftoverData / m_frameSize == 0) && (m_dataBuffers.size() > 1))
     371              :                 {
     372              :                     // If the leftover data is smaller than a frame, it must be processed with the next buffer
     373            1 :                     std::unique_lock lock{m_queueSizeMutex};
     374            1 :                     m_dataBuffers.pop();
     375            1 :                     m_dataBuffers.front() = gst_buffer_append(buffer, m_dataBuffers.front());
     376            1 :                     gst_buffer_unref(buffer);
     377            1 :                     m_queueSizeCv.notify_one();
     378              :                 }
     379            3 :             }
     380              :             else
     381              :             {
     382            2 :                 std::unique_lock lock{m_queueSizeMutex};
     383            2 :                 m_dataBuffers.pop();
     384            2 :                 gst_buffer_unref(buffer);
     385            2 :                 m_queueSizeCv.notify_one();
     386              :             }
     387              :         }
     388           14 :     } while (!m_dataBuffers.empty() && availableFrames != 0);
     389              : 
     390              :     // If we still have samples stored that could not be pushed
     391              :     // This avoids any stoppages in the pushing of samples to the server if the consumption of
     392              :     // samples is slow.
     393           11 :     if (m_dataBuffers.size())
     394              :     {
     395              :         m_pushSamplesTimer =
     396            6 :             m_timerFactory->createTimer(std::chrono::milliseconds(10), [this]() { notifyPushSamplesTimerExpired(); });
     397              :     }
     398            6 :     else if (m_isEos)
     399              :     {
     400            1 :         m_clientBackend->setEos();
     401              :     }
     402              : }
     403              : 
     404            4 : bool GStreamerWebAudioPlayerClient::isNewConfig(const std::string &audioMimeType,
     405              :                                                 std::weak_ptr<const firebolt::rialto::WebAudioConfig> webAudioConfig)
     406              : {
     407            4 :     if (audioMimeType != m_mimeType)
     408              :     {
     409            1 :         return true;
     410              :     }
     411              : 
     412            3 :     if (audioMimeType != "audio/x-raw")
     413              :     {
     414            1 :         GST_ERROR("Cannot compare none pcm config");
     415            1 :         return true;
     416              :     }
     417              : 
     418            2 :     std::shared_ptr<const firebolt::rialto::WebAudioConfig> config = webAudioConfig.lock();
     419            2 :     if (!config || config->pcm != m_config.pcm)
     420              :     {
     421            1 :         return true;
     422              :     }
     423              : 
     424            1 :     return false;
     425            2 : }
     426              : 
     427           16 : void GStreamerWebAudioPlayerClient::notifyState(firebolt::rialto::WebAudioPlayerState state)
     428              : {
     429           16 :     switch (state)
     430              :     {
     431            3 :     case firebolt::rialto::WebAudioPlayerState::END_OF_STREAM:
     432              :     {
     433            3 :         GST_INFO("Notify end of stream.");
     434            3 :         if (m_callbacks.eosCallback)
     435              :         {
     436            3 :             m_callbacks.eosCallback();
     437              :         }
     438            3 :         m_isEos = false;
     439            3 :         break;
     440              :     }
     441            2 :     case firebolt::rialto::WebAudioPlayerState::FAILURE:
     442              :     {
     443            2 :         std::string errMessage = "Rialto server webaudio playback failed";
     444            2 :         GST_ERROR("%s", errMessage.c_str());
     445            2 :         if (m_callbacks.errorCallback)
     446              :         {
     447            2 :             m_callbacks.errorCallback(errMessage.c_str());
     448              :         }
     449            2 :         break;
     450              :     }
     451           10 :     case firebolt::rialto::WebAudioPlayerState::IDLE:
     452              :     case firebolt::rialto::WebAudioPlayerState::PLAYING:
     453              :     case firebolt::rialto::WebAudioPlayerState::PAUSED:
     454              :     {
     455           10 :         if (m_callbacks.stateChangedCallback)
     456              :         {
     457           10 :             m_callbacks.stateChangedCallback(state);
     458              :         }
     459           10 :         break;
     460              :     }
     461            1 :     case firebolt::rialto::WebAudioPlayerState::UNKNOWN:
     462              :     default:
     463              :     {
     464            1 :         GST_WARNING("Web audio player sent unknown state");
     465            1 :         break;
     466              :     }
     467              :     }
     468           16 : }
     469              : 
     470            4 : bool GStreamerWebAudioPlayerClient::setVolume(double volume)
     471              : {
     472            4 :     bool status{false};
     473            8 :     m_backendQueue->callInEventLoop([&]() { status = m_clientBackend->setVolume(volume); });
     474            4 :     return status;
     475              : }
     476              : 
     477            3 : bool GStreamerWebAudioPlayerClient::getVolume(double &volume)
     478              : {
     479            3 :     bool status{false};
     480            6 :     m_backendQueue->callInEventLoop([&]() { status = m_clientBackend->getVolume(volume); });
     481            3 :     return status;
     482              : }
        

Generated by: LCOV version 2.0-1