Ring Daemon
Loading...
Searching...
No Matches
sinkclient.cpp
Go to the documentation of this file.
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"
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
53namespace jami {
54namespace video {
55
56const constexpr char FILTER_INPUT_NAME[] = "in";
57
58#ifdef ENABLE_SHM
59// RAII class helper on sem_wait/sem_post sempahore operations
60class SemGuardLock
61{
62public:
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
74private:
75 sem_t& m_;
76};
77
78class ShmHolder
79{
80public:
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
88private:
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
105ShmHolder::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
144ShmHolder::~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
163bool
164ShmHolder::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
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
210void
211ShmHolder::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 {
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
244std::string
246{
247 if (shm_)
248 return shm_->name();
249 return {};
250}
251
252bool
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
270bool
272{
273 setFrameSize(0, 0);
274 setCrop(0, 0, 0, 0);
275 shm_.reset();
276 return true;
277}
278
279#else // ENABLE_SHM
280
281std::string
283{
284 return {};
285}
286
287bool
289{
290 return true;
291}
292
293bool
295{
296 setFrameSize(0, 0);
297 setCrop(0, 0, 0, 0);
298 return true;
299}
300
301#endif // !ENABLE_SHM
302
303SinkClient::SinkClient(const std::string& id, bool mixer)
304 : id_ {id}
305 , mixer_(mixer)
306 , scaler_(new VideoScaler())
308 , frameCount_(0u)
310#endif
311{
312 JAMI_DBG("[Sink:%p] Sink [%s] created", this, getId().c_str());
313}
314
316SinkClient::configureFrameDirect(const std::shared_ptr<jami::MediaFrame>& frame_p)
317{
319 av_frame_ref(outFrame.get(), std::static_pointer_cast<VideoFrame>(frame_p)->pointer());
320
321 if (crop_.w || crop_.h) {
322#ifdef ENABLE_HWACCEL
323 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 if (desc && (desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) {
329 std::shared_ptr<VideoFrame> frame = std::make_shared<VideoFrame>();
330 try {
331 frame = HardwareAccel::transferToMainMemory(*std::static_pointer_cast<VideoFrame>(frame_p),
333 } catch (const std::runtime_error& e) {
334 JAMI_ERR("[Sink:%p] Transfer to hardware acceleration memory failed: %s", this, e.what());
335 return {};
336 }
337 if (not frame)
338 return {};
340 av_frame_ref(outFrame.get(), frame->pointer());
341 }
342#endif
343 outFrame->crop_top = crop_.y;
344 outFrame->crop_bottom = (size_t) outFrame->height - crop_.y - crop_.h;
345 outFrame->crop_left = crop_.x;
346 outFrame->crop_right = (size_t) outFrame->width - crop_.x - crop_.w;
348 }
349 return outFrame;
350}
351
352void
353SinkClient::sendFrameTransformed(AVFrame* frame)
354{
355 if (frame->width > 0 and frame->height > 0) {
356 if (auto buffer_ptr = target_.pull()) {
357 scaler_->scale(frame, buffer_ptr.get());
358 target_.push(std::move(buffer_ptr));
359 }
360 }
361}
362
363std::shared_ptr<VideoFrame>
364SinkClient::applyTransform(VideoFrame& frame_p)
365{
366 std::shared_ptr<VideoFrame> frame = std::make_shared<VideoFrame>();
367#ifdef ENABLE_HWACCEL
368 const auto* desc = av_pix_fmt_desc_get((AVPixelFormat) frame_p.format());
369 if (desc && (desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) {
370 try {
372 } catch (const std::runtime_error& e) {
373 JAMI_ERR("[Sink:%p] Transfer to hardware acceleration memory failed: %s", this, e.what());
374 return {};
375 }
376 } else
377#endif
378 frame->copyFrom(frame_p);
379
380 int angle = frame->getOrientation();
381 if (angle != rotation_) {
382 filter_ = getTransposeFilter(angle, FILTER_INPUT_NAME, frame->width(), frame->height(), frame->format(), false);
383 rotation_ = angle;
384 }
385 if (filter_) {
386 filter_->feedInput(frame->pointer(), FILTER_INPUT_NAME);
387 frame = std::static_pointer_cast<VideoFrame>(std::shared_ptr<MediaFrame>(filter_->readOutput()));
388 }
389 if (crop_.w || crop_.h) {
390 frame->pointer()->crop_top = crop_.y;
391 frame->pointer()->crop_bottom = (size_t) frame->height() - crop_.y - crop_.h;
392 frame->pointer()->crop_left = crop_.x;
393 frame->pointer()->crop_right = (size_t) frame->width() - crop_.x - crop_.w;
395 }
396 return frame;
397}
398
399void
400SinkClient::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();
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;
411 }
412#endif
413
414 std::unique_lock lock(mtx_);
415 bool hasObservers = getObserversCount() != 0;
416 bool hasDirectListener = target_.push and not target_.pull;
417 bool hasTransformedListener = target_.push and target_.pull;
418
419 if (hasDirectListener) {
421 auto outFrame = configureFrameDirect(frame_p);
422 if (!outFrame) {
423 return;
424 }
425 if (outFrame->height != height_ || outFrame->width != width_) {
426 lock.unlock();
427 setFrameSize(outFrame->width, outFrame->height);
428 return;
429 }
430 target_.push(std::move(outFrame));
431 return;
432 }
433
435#ifdef ENABLE_SHM
437#endif
438
439 if (doTransfer) {
440 auto frame = applyTransform(*std::static_pointer_cast<VideoFrame>(frame_p));
441 if (not frame)
442 return;
443
444 notify(std::static_pointer_cast<MediaFrame>(frame));
445
446 if (frame->height() != height_ || frame->width() != width_) {
447 lock.unlock();
448 setFrameSize(frame->width(), frame->height());
449 return;
450 }
451#ifdef ENABLE_SHM
452 if (shm_ && doShmTransfer_)
453 shm_->renderFrame(*frame);
454#endif
456 sendFrameTransformed(frame->pointer());
457 }
458}
459
460void
461SinkClient::setFrameSize(int width, int height)
462{
463 width_ = width;
464 height_ = height;
465 if (width > 0 and height > 0) {
466 JAMI_DBG("[Sink:%p] Started - size=%dx%d, mixer=%s", this, width, height, mixer_ ? "Yes" : "No");
468 started_ = true;
469 } else if (started_) {
470 JAMI_DBG("[Sink:%p] Stopped - size=%dx%d, mixer=%s", this, width, height, mixer_ ? "Yes" : "No");
472 started_ = false;
473 }
474}
475
476void
477SinkClient::setCrop(int x, int y, int w, int h)
478{
479 if (x != crop_.x || y != crop_.y || w != crop_.w || h != crop_.h) {
480 JAMI_DBG("[Sink:%p] Change crop to [%dx%d at (%d, %d)]", this, w, h, x, y);
481 }
482 crop_.x = x;
483 crop_.y = y;
484 crop_.w = w;
485 crop_.h = h;
486}
487
488} // namespace video
489} // namespace jami
static std::unique_ptr< VideoFrame > transferToMainMemory(const VideoFrame &frame, AVPixelFormat desiredFormat)
Transfers hardware frame to main memory.
Definition accel.cpp:367
void setCrop(int x, int y, int w, int h)
const std::string & getId() const noexcept
Definition sinkclient.h:49
SinkClient(const std::string &id="", bool mixer=false)
bool start() noexcept
void update(Observable< std::shared_ptr< jami::MediaFrame > > *, const std::shared_ptr< jami::MediaFrame > &) override
std::string openedName() const noexcept
void setFrameSize(int width, int height)
void setFromMemory(uint8_t *data, int format, int width, int height) noexcept
#define JAMI_ERR(...)
Definition logger.h:230
#define JAMI_DBG(...)
Definition logger.h:228
#define JAMI_WARNING(formatstr,...)
Definition logger.h:242
std::unique_ptr< MediaFilter > getTransposeFilter(int rotation, std::string inputName, int width, int height, int format, bool rescale)
const constexpr char FILTER_INPUT_NAME[]
void emitSignal(Args... args)
Definition jami_signal.h:64
libjami::VideoFrame VideoFrame
Definition video_base.h:49
std::unique_ptr< AVFrame, AVFrame_deleter > FrameBuffer
std::function< FrameBuffer()> pull
std::function< void(FrameBuffer)> push
#define S_IRUSR
Definition windirent.h:105
#define S_IWUSR
Definition windirent.h:110