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 122 34.4 %
Date: 2025-12-18 10:07:43 Functions: 7 10 70.0 %

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

Generated by: LCOV version 1.14