Ring Daemon 16.0.0
Loading...
Searching...
No Matches
sinkclient.cpp
Go to the documentation of this file.
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"
33#include "libav_utils.h"
34#include "video_scaler.h"
35#include "media_filter.h"
36#include "filter_transpose.h"
37
38#ifdef RING_ACCEL
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
55namespace jami {
56namespace video {
57
58const constexpr char FILTER_INPUT_NAME[] = "in";
59
60#ifdef ENABLE_SHM
61// RAII class helper on sem_wait/sem_post sempahore operations
62class SemGuardLock
63{
64public:
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
76private:
77 sem_t& m_;
78};
79
80class ShmHolder
81{
82public:
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
90private:
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",
98 openedName_.c_str(),
99 areaSize_,
100 errno);
101 }
102 }
103
104 SHMHeader* area_ {static_cast<SHMHeader*>(MAP_FAILED)};
105 std::size_t areaSize_ {0};
106 std::string openedName_;
107 int fd_ {-1};
108};
109
110ShmHolder::ShmHolder(const std::string& name)
111{
112 static constexpr int flags = O_RDWR | O_CREAT | O_TRUNC | O_EXCL;
113 static constexpr int perms = S_IRUSR | S_IWUSR;
114
115 static auto shmFailedWithErrno = [this](const std::string& what) {
116 throw std::runtime_error {fmt::format("ShmHolder[{}]: {} failed, errno={}", openedName_, what, errno)};
117 };
118
119 if (not name.empty()) {
120 openedName_ = name;
121 fd_ = ::shm_open(openedName_.c_str(), flags, perms);
122 if (fd_ < 0)
123 shmFailedWithErrno("shm_open");
124 } else {
125 for (int i = 0; fd_ < 0; ++i) {
126 openedName_ = fmt::format(PACKAGE_NAME "_shm_{}_{}", getpid(), i);
127 fd_ = ::shm_open(openedName_.c_str(), flags, perms);
128 if (fd_ < 0 and errno != EEXIST)
129 shmFailedWithErrno("shm_open");
130 }
131 }
132
133 // Set size enough for header only (no frame data)
134 if (!resizeArea(0))
135 shmFailedWithErrno("resizeArea");
136
137 // Header fields initialization
138 std::memset(area_, 0, areaSize_);
139
140 if (::sem_init(&area_->mutex, 1, 1) < 0)
141 shmFailedWithErrno("sem_init(mutex)");
142
143 if (::sem_init(&area_->frameGenMutex, 1, 0) < 0)
144 shmFailedWithErrno("sem_init(frameGenMutex)");
145
146 JAMI_DBG("[ShmHolder:%s] New holder created", openedName_.c_str());
147}
148
149ShmHolder::~ShmHolder()
150{
151 if (fd_ < 0)
152 return;
153
154 ::close(fd_);
155 ::shm_unlink(openedName_.c_str());
156
157 if (area_ == MAP_FAILED)
158 return;
159
160 ::sem_wait(&area_->mutex);
161 area_->frameSize = 0;
162 ::sem_post(&area_->mutex);
163
164 ::sem_post(&area_->frameGenMutex); // unlock waiting client before leaving
165 unMapShmArea();
166}
167
168bool
169ShmHolder::resizeArea(std::size_t frameSize) noexcept
170{
171 // aligned on 16-byte boundary frameSize
172 frameSize = (frameSize + 15) & ~15;
173
174 if (area_ != MAP_FAILED and frameSize == area_->frameSize)
175 return true;
176
177 // full area size: +15 to take care of maximum padding size
178 const auto areaSize = sizeof(SHMHeader) + 2 * frameSize + 15;
179 JAMI_DBG("[ShmHolder:%s] New size: f=%zu, a=%zu", openedName_.c_str(), frameSize, areaSize);
180
181 unMapShmArea();
182
183 if (::ftruncate(fd_, areaSize) < 0) {
184 JAMI_ERR("[ShmHolder:%s] ftruncate(%zu) failed with errno %d",
185 openedName_.c_str(),
186 areaSize,
187 errno);
188 return false;
189 }
190
191 area_ = static_cast<SHMHeader*>(
193
194 if (area_ == MAP_FAILED) {
195 areaSize_ = 0;
196 JAMI_ERR("[ShmHolder:%s] mmap(%zu) failed with errno %d",
197 openedName_.c_str(),
198 areaSize,
199 errno);
200 return false;
201 }
202
204
205 if (frameSize) {
206 SemGuardLock lk {area_->mutex};
207
208 area_->frameSize = frameSize;
209 area_->mapSize = areaSize;
210
211 // Compute aligned IO pointers
212 // Note: we not using std::align as not implemented in 4.9
213 // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=57350
214 auto p = reinterpret_cast<std::uintptr_t>(area_->data);
215 area_->writeOffset = ((p + 15) & ~15) - p;
216 area_->readOffset = area_->writeOffset + frameSize;
217 }
218
219 return true;
220}
221
222void
223ShmHolder::renderFrame(const VideoFrame& src) noexcept
224{
225 const auto width = src.width();
226 const auto height = src.height();
227 const auto format = AV_PIX_FMT_BGRA;
228 const auto frameSize = videoFrameSize(format, width, height);
229
230 if (!resizeArea(frameSize)) {
231 JAMI_ERR("[ShmHolder:%s] Unable to resize area size: %dx%d, format: %d",
232 openedName_.c_str(),
233 width,
234 height,
235 format);
236 return;
237 }
238
239 {
241 VideoScaler scaler;
242
243 dst.setFromMemory(area_->data + area_->writeOffset, format, width, height);
244 scaler.scale(src, dst);
245 }
246
247 {
248 SemGuardLock lk {area_->mutex};
249
250 ++area_->frameGen;
251 std::swap(area_->readOffset, area_->writeOffset);
252 ::sem_post(&area_->frameGenMutex);
253 }
254}
255
256std::string
258{
259 if (shm_)
260 return shm_->name();
261 return {};
262}
263
264bool
266{
267 if (not shm_) {
268 try {
269 char* envvar = getenv("JAMI_DISABLE_SHM");
270 if (envvar) // Do not use SHM if set
271 return true;
272 shm_ = std::make_shared<ShmHolder>();
273 JAMI_DBG("[Sink:%p] Shared memory [%s] created", this, openedName().c_str());
274 } catch (const std::runtime_error& e) {
275 JAMI_ERR("[Sink:%p] Failed to create shared memory: %s", this, e.what());
276 }
277 }
278
279 return static_cast<bool>(shm_);
280}
281
282bool
284{
285 setFrameSize(0, 0);
286 setCrop(0, 0, 0, 0);
287 shm_.reset();
288 return true;
289}
290
291#else // ENABLE_SHM
292
293std::string
295{
296 return {};
297}
298
299bool
301{
302 return true;
303}
304
305bool
307{
308 setFrameSize(0, 0);
309 setCrop(0, 0, 0, 0);
310 return true;
311}
312
313#endif // !ENABLE_SHM
314
315SinkClient::SinkClient(const std::string& id, bool mixer)
316 : id_ {id}
317 , mixer_(mixer)
318 , scaler_(new VideoScaler())
320 , frameCount_(0u)
322#endif
323{
324 JAMI_DBG("[Sink:%p] Sink [%s] created", this, getId().c_str());
325}
326
327void
328SinkClient::sendFrameDirect(const std::shared_ptr<jami::MediaFrame>& frame_p)
329{
331
333 av_frame_ref(outFrame.get(), std::static_pointer_cast<VideoFrame>(frame_p)->pointer());
334
335 if (crop_.w || crop_.h) {
336#ifdef RING_ACCEL
338 (AVPixelFormat) std::static_pointer_cast<VideoFrame>(frame_p)->format());
339 /*
340 Cropping does not work for hardware-decoded frames.
341 They need to be transferred to main memory.
342 */
343 if (desc && (desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) {
344 std::shared_ptr<VideoFrame> frame = std::make_shared<VideoFrame>();
345 try {
346 frame = HardwareAccel::transferToMainMemory(*std::static_pointer_cast<VideoFrame>(
347 frame_p),
349 } catch (const std::runtime_error& e) {
350 JAMI_ERR("[Sink:%p] Transfert to hardware acceleration memory failed: %s",
351 this,
352 e.what());
353 return;
354 }
355 if (not frame)
356 return;
358 av_frame_ref(outFrame.get(), frame->pointer());
359 }
360#endif
361 outFrame->crop_top = crop_.y;
362 outFrame->crop_bottom = (size_t) outFrame->height - crop_.y - crop_.h;
363 outFrame->crop_left = crop_.x;
364 outFrame->crop_right = (size_t) outFrame->width - crop_.x - crop_.w;
366 }
367 if (outFrame->height != height_ || outFrame->width != width_) {
368 setFrameSize(outFrame->width, outFrame->height);
369 return;
370 }
371 target_.push(std::move(outFrame));
372}
373
374void
375SinkClient::sendFrameTransformed(AVFrame* frame)
376{
377 if (frame->width > 0 and frame->height > 0) {
378 if (auto buffer_ptr = target_.pull()) {
379 scaler_->scale(frame, buffer_ptr.get());
380 target_.push(std::move(buffer_ptr));
381 }
382 }
383}
384
385std::shared_ptr<VideoFrame>
386SinkClient::applyTransform(VideoFrame& frame_p)
387{
388 std::shared_ptr<VideoFrame> frame = std::make_shared<VideoFrame>();
389#ifdef RING_ACCEL
391 if (desc && (desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) {
392 try {
394 } catch (const std::runtime_error& e) {
395 JAMI_ERR("[Sink:%p] Transfert to hardware acceleration memory failed: %s",
396 this,
397 e.what());
398 return {};
399 }
400 } else
401#endif
402 frame->copyFrom(frame_p);
403
404 int angle = frame->getOrientation();
405 if (angle != rotation_) {
406 filter_ = getTransposeFilter(angle,
408 frame->width(),
409 frame->height(),
410 frame->format(),
411 false);
412 rotation_ = angle;
413 }
414 if (filter_) {
415 filter_->feedInput(frame->pointer(), FILTER_INPUT_NAME);
416 frame = std::static_pointer_cast<VideoFrame>(
417 std::shared_ptr<MediaFrame>(filter_->readOutput()));
418 }
419 if (crop_.w || crop_.h) {
420 frame->pointer()->crop_top = crop_.y;
421 frame->pointer()->crop_bottom = (size_t) frame->height() - crop_.y - crop_.h;
422 frame->pointer()->crop_left = crop_.x;
423 frame->pointer()->crop_right = (size_t) frame->width() - crop_.x - crop_.w;
425 }
426 return frame;
427}
428
429void
430SinkClient::update(Observable<std::shared_ptr<MediaFrame>>* /*obs*/,
431 const std::shared_ptr<MediaFrame>& frame_p)
432{
433#ifdef DEBUG_FPS
434 auto currentTime = std::chrono::steady_clock::now();
436 ++frameCount_;
437 if (seconds > std::chrono::seconds(1)) {
438 auto fps = frameCount_ / std::chrono::duration<double>(seconds).count();
439 JAMI_WARNING("Sink {}, {} FPS", id_, fps);
440 frameCount_ = 0;
442 }
443#endif
444
445 std::unique_lock lock(mtx_);
446 bool hasObservers = getObserversCount() != 0;
447 bool hasDirectListener = target_.push and not target_.pull;
448 bool hasTransformedListener = target_.push and target_.pull;
449
450 if (hasDirectListener) {
451 sendFrameDirect(frame_p);
452 return;
453 }
454
456#ifdef ENABLE_SHM
458#endif
459
460 if (doTransfer) {
461 auto frame = applyTransform(*std::static_pointer_cast<VideoFrame>(frame_p));
462 if (not frame)
463 return;
464
465 notify(std::static_pointer_cast<MediaFrame>(frame));
466
467 if (frame->height() != height_ || frame->width() != width_) {
468 lock.unlock();
469 setFrameSize(frame->width(), frame->height());
470 return;
471 }
472#ifdef ENABLE_SHM
473 if (shm_ && doShmTransfer_)
474 shm_->renderFrame(*frame);
475#endif
477 sendFrameTransformed(frame->pointer());
478 }
479}
480
481void
482SinkClient::setFrameSize(int width, int height)
483{
484 width_ = width;
485 height_ = height;
486 if (width > 0 and height > 0) {
487 JAMI_DBG("[Sink:%p] Started - size=%dx%d, mixer=%s",
488 this,
489 width,
490 height,
491 mixer_ ? "Yes" : "No");
493 openedName(),
494 width,
495 height,
496 mixer_);
497 started_ = true;
498 } else if (started_) {
499 JAMI_DBG("[Sink:%p] Stopped - size=%dx%d, mixer=%s",
500 this,
501 width,
502 height,
503 mixer_ ? "Yes" : "No");
505 started_ = false;
506 }
507}
508
509void
510SinkClient::setCrop(int x, int y, int w, int h)
511{
512 JAMI_DBG("[Sink:%p] Change crop to [%dx%d at (%d, %d)]", this, w, h, x, y);
513 crop_.x = x;
514 crop_.y = y;
515 crop_.w = w;
516 crop_.h = h;
517}
518
519} // namespace video
520} // namespace jami
static std::unique_ptr< VideoFrame > transferToMainMemory(const VideoFrame &frame, AVPixelFormat desiredFormat)
Transfers hardware frame to main memory.
Definition accel.cpp:372
void setCrop(int x, int y, int w, int h)
const std::string & getId() const noexcept
Definition sinkclient.h:50
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:218
#define JAMI_DBG(...)
Definition logger.h:216
#define JAMI_WARNING(formatstr,...)
Definition logger.h:227
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 ring_signal.h:64
libjami::VideoFrame VideoFrame
Definition video_base.h:53
std::unique_ptr< AVFrame, AVFrame_deleter > FrameBuffer
Simple macro to hide class' copy constructor and assignment operator.
std::function< FrameBuffer()> pull
std::function< void(FrameBuffer)> push
#define S_IRUSR
Definition windirent.h:105
#define S_IWUSR
Definition windirent.h:110