LCOV - code coverage report
Current view: top level - foo/src/media/video - sinkclient.cpp (source / functions) Hit Total Coverage
Test: jami-coverage-filtered.info Lines: 42 123 34.1 %
Date: 2026-02-28 10:41:24 Functions: 7 10 70.0 %

          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             : #ifdef HAVE_CONFIG_H
      19             : #include "config.h"
      20             : #endif
      21             : 
      22             : #include "sinkclient.h"
      23             : 
      24             : #ifdef ENABLE_SHM
      25             : #include "shm_header.h"
      26             : #endif // ENABLE_SHM
      27             : 
      28             : #include "media_buffer.h"
      29             : #include "logger.h"
      30             : #include "client/jami_signal.h"
      31             : #include "jami/videomanager_interface.h"
      32             : #include "libav_utils.h"
      33             : #include "video_scaler.h"
      34             : #include "media_filter.h"
      35             : #include "filter_transpose.h"
      36             : 
      37             : #ifdef ENABLE_HWACCEL
      38             : #include "accel.h"
      39             : #endif
      40             : 
      41             : #ifndef _WIN32
      42             : #include <sys/mman.h>
      43             : #endif
      44             : 
      45             : #include <fcntl.h>
      46             : #include <cstdio>
      47             : #include <unistd.h>
      48             : #include <cerrno>
      49             : #include <cstring>
      50             : #include <stdexcept>
      51             : #include <cmath>
      52             : 
      53             : namespace jami {
      54             : namespace video {
      55             : 
      56             : const constexpr char FILTER_INPUT_NAME[] = "in";
      57             : 
      58             : #ifdef ENABLE_SHM
      59             : // RAII class helper on sem_wait/sem_post sempahore operations
      60             : class SemGuardLock
      61             : {
      62             : public:
      63             :     explicit SemGuardLock(sem_t& mutex)
      64             :         : m_(mutex)
      65             :     {
      66             :         auto ret = ::sem_wait(&m_);
      67             :         if (ret < 0) {
      68             :             throw std::logic_error {fmt::format("SHM mutex@{} lock failed ({})", fmt::ptr(&m_), ret)};
      69             :         }
      70             :     }
      71             : 
      72             :     ~SemGuardLock() { ::sem_post(&m_); }
      73             : 
      74             : private:
      75             :     sem_t& m_;
      76             : };
      77             : 
      78             : class ShmHolder
      79             : {
      80             : public:
      81             :     ShmHolder(const std::string& name = {});
      82             :     ~ShmHolder();
      83             : 
      84             :     std::string name() const noexcept { return openedName_; }
      85             : 
      86             :     void renderFrame(const VideoFrame& src) noexcept;
      87             : 
      88             : private:
      89             :     bool resizeArea(std::size_t desired_length) noexcept;
      90             :     char* getShmAreaDataPtr() noexcept;
      91             : 
      92             :     void unMapShmArea() noexcept
      93             :     {
      94             :         if (area_ != MAP_FAILED and ::munmap(area_, areaSize_) < 0) {
      95             :             JAMI_ERR("[ShmHolder:%s] munmap(%zu) failed with errno %d", openedName_.c_str(), areaSize_, errno);
      96             :         }
      97             :     }
      98             : 
      99             :     SHMHeader* area_ {static_cast<SHMHeader*>(MAP_FAILED)};
     100             :     std::size_t areaSize_ {0};
     101             :     std::string openedName_;
     102             :     int fd_ {-1};
     103             : };
     104             : 
     105             : ShmHolder::ShmHolder(const std::string& name)
     106             : {
     107             :     static constexpr int flags = O_RDWR | O_CREAT | O_TRUNC | O_EXCL;
     108             :     static constexpr int perms = S_IRUSR | S_IWUSR;
     109             : 
     110             :     static auto shmFailedWithErrno = [this](const std::string& what) {
     111             :         throw std::runtime_error {fmt::format("ShmHolder[{}]: {} failed, errno={}", openedName_, what, errno)};
     112             :     };
     113             : 
     114             :     if (not name.empty()) {
     115             :         openedName_ = name;
     116             :         fd_ = ::shm_open(openedName_.c_str(), flags, perms);
     117             :         if (fd_ < 0)
     118             :             shmFailedWithErrno("shm_open");
     119             :     } else {
     120             :         for (int i = 0; fd_ < 0; ++i) {
     121             :             openedName_ = fmt::format(PACKAGE_NAME "_shm_{}_{}", getpid(), i);
     122             :             fd_ = ::shm_open(openedName_.c_str(), flags, perms);
     123             :             if (fd_ < 0 and errno != EEXIST)
     124             :                 shmFailedWithErrno("shm_open");
     125             :         }
     126             :     }
     127             : 
     128             :     // Set size enough for header only (no frame data)
     129             :     if (!resizeArea(0))
     130             :         shmFailedWithErrno("resizeArea");
     131             : 
     132             :     // Header fields initialization
     133             :     std::memset(area_, 0, areaSize_);
     134             : 
     135             :     if (::sem_init(&area_->mutex, 1, 1) < 0)
     136             :         shmFailedWithErrno("sem_init(mutex)");
     137             : 
     138             :     if (::sem_init(&area_->frameGenMutex, 1, 0) < 0)
     139             :         shmFailedWithErrno("sem_init(frameGenMutex)");
     140             : 
     141             :     JAMI_DBG("[ShmHolder:%s] New holder created", openedName_.c_str());
     142             : }
     143             : 
     144             : ShmHolder::~ShmHolder()
     145             : {
     146             :     if (fd_ < 0)
     147             :         return;
     148             : 
     149             :     ::close(fd_);
     150             :     ::shm_unlink(openedName_.c_str());
     151             : 
     152             :     if (area_ == MAP_FAILED)
     153             :         return;
     154             : 
     155             :     ::sem_wait(&area_->mutex);
     156             :     area_->frameSize = 0;
     157             :     ::sem_post(&area_->mutex);
     158             : 
     159             :     ::sem_post(&area_->frameGenMutex); // unlock waiting client before leaving
     160             :     unMapShmArea();
     161             : }
     162             : 
     163             : bool
     164             : ShmHolder::resizeArea(std::size_t frameSize) noexcept
     165             : {
     166             :     // aligned on 16-byte boundary frameSize
     167             :     frameSize = (frameSize + 15) & ~15;
     168             : 
     169             :     if (area_ != MAP_FAILED and frameSize == area_->frameSize)
     170             :         return true;
     171             : 
     172             :     // full area size: +15 to take care of maximum padding size
     173             :     const auto areaSize = sizeof(SHMHeader) + 2 * frameSize + 15;
     174             :     JAMI_DBG("[ShmHolder:%s] New size: f=%zu, a=%zu", openedName_.c_str(), frameSize, areaSize);
     175             : 
     176             :     unMapShmArea();
     177             : 
     178             :     if (::ftruncate(fd_, areaSize) < 0) {
     179             :         JAMI_ERR("[ShmHolder:%s] ftruncate(%zu) failed with errno %d", openedName_.c_str(), areaSize, errno);
     180             :         return false;
     181             :     }
     182             : 
     183             :     area_ = static_cast<SHMHeader*>(::mmap(nullptr, areaSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0));
     184             : 
     185             :     if (area_ == MAP_FAILED) {
     186             :         areaSize_ = 0;
     187             :         JAMI_ERR("[ShmHolder:%s] mmap(%zu) failed with errno %d", openedName_.c_str(), areaSize, errno);
     188             :         return false;
     189             :     }
     190             : 
     191             :     areaSize_ = areaSize;
     192             : 
     193             :     if (frameSize) {
     194             :         SemGuardLock lk {area_->mutex};
     195             : 
     196             :         area_->frameSize = frameSize;
     197             :         area_->mapSize = areaSize;
     198             : 
     199             :         // Compute aligned IO pointers
     200             :         // Note: we not using std::align as not implemented in 4.9
     201             :         // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=57350
     202             :         auto p = reinterpret_cast<std::uintptr_t>(area_->data);
     203             :         area_->writeOffset = ((p + 15) & ~15) - p;
     204             :         area_->readOffset = area_->writeOffset + frameSize;
     205             :     }
     206             : 
     207             :     return true;
     208             : }
     209             : 
     210             : void
     211             : ShmHolder::renderFrame(const VideoFrame& src) noexcept
     212             : {
     213             :     const auto width = src.width();
     214             :     const auto height = src.height();
     215             :     const auto format = AV_PIX_FMT_BGRA;
     216             :     const auto frameSize = videoFrameSize(format, width, height);
     217             : 
     218             :     if (!resizeArea(frameSize)) {
     219             :         JAMI_ERR("[ShmHolder:%s] Unable to resize area size: %dx%d, format: %d",
     220             :                  openedName_.c_str(),
     221             :                  width,
     222             :                  height,
     223             :                  format);
     224             :         return;
     225             :     }
     226             : 
     227             :     {
     228             :         VideoFrame dst;
     229             :         VideoScaler scaler;
     230             : 
     231             :         dst.setFromMemory(area_->data + area_->writeOffset, format, width, height);
     232             :         scaler.scale(src, dst);
     233             :     }
     234             : 
     235             :     {
     236             :         SemGuardLock lk {area_->mutex};
     237             : 
     238             :         ++area_->frameGen;
     239             :         std::swap(area_->readOffset, area_->writeOffset);
     240             :         ::sem_post(&area_->frameGenMutex);
     241             :     }
     242             : }
     243             : 
     244             : std::string
     245             : SinkClient::openedName() const noexcept
     246             : {
     247             :     if (shm_)
     248             :         return shm_->name();
     249             :     return {};
     250             : }
     251             : 
     252             : bool
     253             : SinkClient::start() noexcept
     254             : {
     255             :     if (not shm_) {
     256             :         try {
     257             :             char* envvar = getenv("JAMI_DISABLE_SHM");
     258             :             if (envvar) // Do not use SHM if set
     259             :                 return true;
     260             :             shm_ = std::make_shared<ShmHolder>();
     261             :             JAMI_DBG("[Sink:%p] Shared memory [%s] created", this, openedName().c_str());
     262             :         } catch (const std::runtime_error& e) {
     263             :             JAMI_ERR("[Sink:%p] Failed to create shared memory: %s", this, e.what());
     264             :         }
     265             :     }
     266             : 
     267             :     return static_cast<bool>(shm_);
     268             : }
     269             : 
     270             : bool
     271             : SinkClient::stop() noexcept
     272             : {
     273             :     setFrameSize(0, 0);
     274             :     setCrop(0, 0, 0, 0);
     275             :     shm_.reset();
     276             :     return true;
     277             : }
     278             : 
     279             : #else // ENABLE_SHM
     280             : 
     281             : std::string
     282         174 : SinkClient::openedName() const noexcept
     283             : {
     284         174 :     return {};
     285             : }
     286             : 
     287             : bool
     288         238 : SinkClient::start() noexcept
     289             : {
     290         238 :     return true;
     291             : }
     292             : 
     293             : bool
     294         270 : SinkClient::stop() noexcept
     295             : {
     296         270 :     setFrameSize(0, 0);
     297         270 :     setCrop(0, 0, 0, 0);
     298         270 :     return true;
     299             : }
     300             : 
     301             : #endif // !ENABLE_SHM
     302             : 
     303         214 : SinkClient::SinkClient(const std::string& id, bool mixer)
     304         214 :     : id_ {id}
     305         214 :     , mixer_(mixer)
     306         214 :     , scaler_(new VideoScaler())
     307             : #ifdef DEBUG_FPS
     308             :     , frameCount_(0u)
     309             :     , lastFrameDebug_(std::chrono::steady_clock::now())
     310             : #endif
     311             : {
     312         214 :     JAMI_DBG("[Sink:%p] Sink [%s] created", this, getId().c_str());
     313         214 : }
     314             : 
     315             : libjami::FrameBuffer
     316           0 : SinkClient::configureFrameDirect(const std::shared_ptr<jami::MediaFrame>& frame_p)
     317             : {
     318           0 :     libjami::FrameBuffer outFrame(av_frame_alloc());
     319           0 :     av_frame_ref(outFrame.get(), std::static_pointer_cast<VideoFrame>(frame_p)->pointer());
     320             : 
     321           0 :     if (crop_.w || crop_.h) {
     322             : #ifdef ENABLE_HWACCEL
     323           0 :         const auto* desc = av_pix_fmt_desc_get((AVPixelFormat) std::static_pointer_cast<VideoFrame>(frame_p)->format());
     324             :         /*
     325             :          Cropping does not work for hardware-decoded frames.
     326             :          They need to be transferred to main memory.
     327             :          */
     328           0 :         if (desc && (desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) {
     329           0 :             std::shared_ptr<VideoFrame> frame = std::make_shared<VideoFrame>();
     330             :             try {
     331           0 :                 frame = HardwareAccel::transferToMainMemory(*std::static_pointer_cast<VideoFrame>(frame_p),
     332           0 :                                                             AV_PIX_FMT_NV12);
     333           0 :             } catch (const std::runtime_error& e) {
     334           0 :                 JAMI_ERR("[Sink:%p] Transfer to hardware acceleration memory failed: %s", this, e.what());
     335           0 :                 return {};
     336           0 :             }
     337           0 :             if (not frame)
     338           0 :                 return {};
     339           0 :             av_frame_unref(outFrame.get());
     340           0 :             av_frame_ref(outFrame.get(), frame->pointer());
     341           0 :         }
     342             : #endif
     343           0 :         outFrame->crop_top = crop_.y;
     344           0 :         outFrame->crop_bottom = (size_t) outFrame->height - crop_.y - crop_.h;
     345           0 :         outFrame->crop_left = crop_.x;
     346           0 :         outFrame->crop_right = (size_t) outFrame->width - crop_.x - crop_.w;
     347           0 :         av_frame_apply_cropping(outFrame.get(), AV_FRAME_CROP_UNALIGNED);
     348             :     }
     349           0 :     return outFrame;
     350           0 : }
     351             : 
     352             : void
     353           0 : SinkClient::sendFrameTransformed(AVFrame* frame)
     354             : {
     355           0 :     if (frame->width > 0 and frame->height > 0) {
     356           0 :         if (auto buffer_ptr = target_.pull()) {
     357           0 :             scaler_->scale(frame, buffer_ptr.get());
     358           0 :             target_.push(std::move(buffer_ptr));
     359           0 :         }
     360             :     }
     361           0 : }
     362             : 
     363             : std::shared_ptr<VideoFrame>
     364           0 : SinkClient::applyTransform(VideoFrame& frame_p)
     365             : {
     366           0 :     std::shared_ptr<VideoFrame> frame = std::make_shared<VideoFrame>();
     367             : #ifdef ENABLE_HWACCEL
     368           0 :     const auto* desc = av_pix_fmt_desc_get((AVPixelFormat) frame_p.format());
     369           0 :     if (desc && (desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) {
     370             :         try {
     371           0 :             frame = HardwareAccel::transferToMainMemory(frame_p, AV_PIX_FMT_NV12);
     372           0 :         } catch (const std::runtime_error& e) {
     373           0 :             JAMI_ERR("[Sink:%p] Transfer to hardware acceleration memory failed: %s", this, e.what());
     374           0 :             return {};
     375           0 :         }
     376           0 :     } else
     377             : #endif
     378           0 :         frame->copyFrom(frame_p);
     379             : 
     380           0 :     int angle = frame->getOrientation();
     381           0 :     if (angle != rotation_) {
     382           0 :         filter_ = getTransposeFilter(angle, FILTER_INPUT_NAME, frame->width(), frame->height(), frame->format(), false);
     383           0 :         rotation_ = angle;
     384             :     }
     385           0 :     if (filter_) {
     386           0 :         filter_->feedInput(frame->pointer(), FILTER_INPUT_NAME);
     387           0 :         frame = std::static_pointer_cast<VideoFrame>(std::shared_ptr<MediaFrame>(filter_->readOutput()));
     388             :     }
     389           0 :     if (crop_.w || crop_.h) {
     390           0 :         frame->pointer()->crop_top = crop_.y;
     391           0 :         frame->pointer()->crop_bottom = (size_t) frame->height() - crop_.y - crop_.h;
     392           0 :         frame->pointer()->crop_left = crop_.x;
     393           0 :         frame->pointer()->crop_right = (size_t) frame->width() - crop_.x - crop_.w;
     394           0 :         av_frame_apply_cropping(frame->pointer(), AV_FRAME_CROP_UNALIGNED);
     395             :     }
     396           0 :     return frame;
     397           0 : }
     398             : 
     399             : void
     400        8847 : SinkClient::update(Observable<std::shared_ptr<MediaFrame>>* /*obs*/, const std::shared_ptr<MediaFrame>& frame_p)
     401             : {
     402             : #ifdef DEBUG_FPS
     403             :     auto currentTime = std::chrono::steady_clock::now();
     404             :     auto seconds = currentTime - lastFrameDebug_;
     405             :     ++frameCount_;
     406             :     if (seconds > std::chrono::seconds(1)) {
     407             :         auto fps = frameCount_ / std::chrono::duration<double>(seconds).count();
     408             :         JAMI_WARNING("Sink {}, {} FPS", id_, fps);
     409             :         frameCount_ = 0;
     410             :         lastFrameDebug_ = currentTime;
     411             :     }
     412             : #endif
     413             : 
     414        8847 :     std::unique_lock lock(mtx_);
     415        8847 :     bool hasObservers = getObserversCount() != 0;
     416        8847 :     bool hasDirectListener = target_.push and not target_.pull;
     417        8847 :     bool hasTransformedListener = target_.push and target_.pull;
     418             : 
     419        8847 :     if (hasDirectListener) {
     420           0 :         notify(frame_p);
     421           0 :         auto outFrame = configureFrameDirect(frame_p);
     422           0 :         if (!outFrame) {
     423           0 :             return;
     424             :         }
     425           0 :         if (outFrame->height != height_ || outFrame->width != width_) {
     426           0 :             lock.unlock();
     427           0 :             setFrameSize(outFrame->width, outFrame->height);
     428           0 :             return;
     429             :         }
     430           0 :         target_.push(std::move(outFrame));
     431           0 :         return;
     432           0 :     }
     433             : 
     434        8847 :     bool doTransfer = hasTransformedListener or hasObservers;
     435             : #ifdef ENABLE_SHM
     436             :     doTransfer |= (shm_ && doShmTransfer_);
     437             : #endif
     438             : 
     439        8847 :     if (doTransfer) {
     440           0 :         auto frame = applyTransform(*std::static_pointer_cast<VideoFrame>(frame_p));
     441           0 :         if (not frame)
     442           0 :             return;
     443             : 
     444           0 :         notify(std::static_pointer_cast<MediaFrame>(frame));
     445             : 
     446           0 :         if (frame->height() != height_ || frame->width() != width_) {
     447           0 :             lock.unlock();
     448           0 :             setFrameSize(frame->width(), frame->height());
     449           0 :             return;
     450             :         }
     451             : #ifdef ENABLE_SHM
     452             :         if (shm_ && doShmTransfer_)
     453             :             shm_->renderFrame(*frame);
     454             : #endif
     455           0 :         if (hasTransformedListener)
     456           0 :             sendFrameTransformed(frame->pointer());
     457           0 :     }
     458        8847 : }
     459             : 
     460             : void
     461         407 : SinkClient::setFrameSize(int width, int height)
     462             : {
     463         407 :     width_ = width;
     464         407 :     height_ = height;
     465         407 :     if (width > 0 and height > 0) {
     466          87 :         JAMI_DBG("[Sink:%p] Started - size=%dx%d, mixer=%s", this, width, height, mixer_ ? "Yes" : "No");
     467          87 :         emitSignal<libjami::VideoSignal::DecodingStarted>(getId(), openedName(), width, height, mixer_);
     468          87 :         started_ = true;
     469         320 :     } else if (started_) {
     470          87 :         JAMI_DBG("[Sink:%p] Stopped - size=%dx%d, mixer=%s", this, width, height, mixer_ ? "Yes" : "No");
     471          87 :         emitSignal<libjami::VideoSignal::DecodingStopped>(getId(), openedName(), mixer_);
     472          87 :         started_ = false;
     473             :     }
     474         407 : }
     475             : 
     476             : void
     477         270 : SinkClient::setCrop(int x, int y, int w, int h)
     478             : {
     479         270 :     if (x != crop_.x || y != crop_.y || w != crop_.w || h != crop_.h) {
     480           0 :         JAMI_DBG("[Sink:%p] Change crop to [%dx%d at (%d, %d)]", this, w, h, x, y);
     481             :     }
     482         270 :     crop_.x = x;
     483         270 :     crop_.y = y;
     484         270 :     crop_.w = w;
     485         270 :     crop_.h = h;
     486         270 : }
     487             : 
     488             : } // namespace video
     489             : } // namespace jami

Generated by: LCOV version 1.14