Ring Daemon 16.0.0
Loading...
Searching...
No Matches
video_mixer.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#include "libav_deps.h" // MUST BE INCLUDED FIRST
19
20#include "video_mixer.h"
21#include "media_buffer.h"
22#include "client/videomanager.h"
23#include "manager.h"
24#include "media_filter.h"
25#include "sinkclient.h"
26#include "logger.h"
27#include "filter_transpose.h"
28#ifdef RING_ACCEL
29#include "accel.h"
30#endif
32
33#include <cmath>
34#include <unistd.h>
35#include <mutex>
36
38#include <opendht/thread_pool.h>
39
40static constexpr auto MIN_LINE_ZOOM
41 = 6; // Used by the ONE_BIG_WITH_SMALL layout for the small previews
42
43namespace jami {
44namespace video {
45
47{
49 int rotation {0};
50 std::unique_ptr<MediaFilter> rotationFilter {nullptr};
51 std::shared_ptr<VideoFrame> render_frame;
53 {
54 std::lock_guard lock(mutex_);
55 auto newFrame = std::make_shared<VideoFrame>();
56 newFrame->copyFrom(other);
58 }
59
60 std::shared_ptr<VideoFrame> getRenderFrame()
61 {
62 std::lock_guard lock(mutex_);
63 return render_frame;
64 }
65
66 // Current render information
67 int x {};
68 int y {};
69 int w {};
70 int h {};
71 bool hasVideo {true};
72
73private:
74 std::mutex mutex_;
75};
76
77static constexpr const auto MIXER_FRAMERATE = 30;
78static constexpr const auto FRAME_DURATION = std::chrono::duration<double>(1. / MIXER_FRAMERATE);
79
80VideoMixer::VideoMixer(const std::string& id, const std::string& localInput, bool attachHost)
82 , id_(id)
83 , sink_(Manager::instance().createSinkClient(id, true))
84 , loop_([] { return true; }, std::bind(&VideoMixer::process, this), [] {})
85{
86 // Local video camera is the main participant
87 if (not localInput.empty() && attachHost) {
89 localInputs_.emplace_back(videoInput);
90 attachVideo(videoInput.get(),
91 "",
93 }
94 loop_.start();
95 nextProcess_ = std::chrono::steady_clock::now();
96
97 JAMI_DBG("[mixer:%s] New instance created", id_.c_str());
98}
99
101{
102 stopSink();
103 stopInputs();
104
105 loop_.join();
106
107 JAMI_DBG("[mixer:%s] Instance destroyed", id_.c_str());
108}
109
110void
111VideoMixer::switchInputs(const std::vector<std::string>& inputs)
112{
113 // Do not stop video inputs that are already there
114 // But only detach it to get new index
115 std::lock_guard lk(localInputsMtx_);
116 decltype(localInputs_) newInputs;
117 newInputs.reserve(inputs.size());
118 for (const auto& input : inputs) {
119 auto videoInput = getVideoInput(input);
120 // Note, video can be a previously stopped device (eg. restart a screen sharing)
121 // in this case, the videoInput will be found and must be restarted
122 videoInput->restart();
123 auto it = std::find(localInputs_.cbegin(), localInputs_.cend(), videoInput);
124 auto onlyDetach = it != localInputs_.cend();
125 if (onlyDetach) {
126 videoInput->detach(this);
127 localInputs_.erase(it);
128 }
129 newInputs.emplace_back(std::move(videoInput));
130 }
131 // Stop other video inputs
132 stopInputs();
133 localInputs_ = std::move(newInputs);
134
135 // Re-attach videoInput to mixer
136 for (size_t i = 0; i < localInputs_.size(); ++i) {
137 auto& input = localInputs_[i];
138 attachVideo(input.get(), "", sip_utils::streamId("", fmt::format("video_{}", i)));
139 }
140}
141
142void
143VideoMixer::stopInput(const std::shared_ptr<VideoFrameActiveWriter>& input)
144{
145 // Detach videoInputs from mixer
146 input->detach(this);
147}
148
149void
151{
152 for (auto& input : localInputs_)
153 stopInput(input);
154 localInputs_.clear();
155}
156
157void
158VideoMixer::setActiveStream(const std::string& id)
159{
160 activeStream_ = id;
161 updateLayout();
162}
163
164void
166{
167 if (activeStream_ == "")
168 currentLayout_ = Layout::GRID;
169 layoutUpdated_ += 1;
170}
171
172void
173VideoMixer::attachVideo(Observable<std::shared_ptr<MediaFrame>>* frame,
174 const std::string& callId,
175 const std::string& streamId)
176{
177 if (!frame)
178 return;
179 JAMI_DBG("Attaching video with streamId %s", streamId.c_str());
180 {
181 std::lock_guard lk(videoToStreamInfoMtx_);
182 videoToStreamInfo_[frame] = StreamInfo {callId, streamId};
183 }
184 frame->attach(this);
185}
186
187void
188VideoMixer::detachVideo(Observable<std::shared_ptr<MediaFrame>>* frame)
189{
190 if (!frame)
191 return;
192 bool detach = false;
193 std::unique_lock lk(videoToStreamInfoMtx_);
194 auto it = videoToStreamInfo_.find(frame);
195 if (it != videoToStreamInfo_.end()) {
196 JAMI_DBG("Detaching video of call %s", it->second.callId.c_str());
197 detach = true;
198 // Handle the case where the current shown source leave the conference
199 // Note, do not call resetActiveStream() to avoid multiple updates
200 if (verifyActive(it->second.streamId))
201 activeStream_ = {};
202 videoToStreamInfo_.erase(it);
203 }
204 lk.unlock();
205 if (detach)
206 frame->detach(this);
207}
208
209void
210VideoMixer::attached(Observable<std::shared_ptr<MediaFrame>>* ob)
211{
212 std::unique_lock lock(rwMutex_);
213
214 auto src = std::unique_ptr<VideoMixerSource>(new VideoMixerSource);
215 src->render_frame = std::make_shared<VideoFrame>();
216 src->source = ob;
217 JAMI_DBG("Add new source [%p]", src.get());
218 sources_.emplace_back(std::move(src));
219 JAMI_DEBUG("Total sources: {:d}", sources_.size());
220 updateLayout();
221}
222
223void
224VideoMixer::detached(Observable<std::shared_ptr<MediaFrame>>* ob)
225{
226 std::unique_lock lock(rwMutex_);
227
228 for (const auto& x : sources_) {
229 if (x->source == ob) {
230 JAMI_DBG("Remove source [%p]", x.get());
231 sources_.remove(x);
232 JAMI_DEBUG("Total sources: {:d}", sources_.size());
233 updateLayout();
234 break;
235 }
236 }
237}
238
239void
240VideoMixer::update(Observable<std::shared_ptr<MediaFrame>>* ob,
241 const std::shared_ptr<MediaFrame>& frame_p)
242{
243 std::shared_lock lock(rwMutex_);
244
245 for (const auto& x : sources_) {
246 if (x->source == ob) {
247#ifdef RING_ACCEL
248 std::shared_ptr<VideoFrame> frame;
249 try {
250 frame = HardwareAccel::transferToMainMemory(*std::static_pointer_cast<VideoFrame>(
251 frame_p),
253 x->atomic_copy(*std::static_pointer_cast<VideoFrame>(frame));
254 } catch (const std::runtime_error& e) {
255 JAMI_ERR("[mixer:%s] Accel failure: %s", id_.c_str(), e.what());
256 return;
257 }
258#else
259 x->atomic_copy(*std::static_pointer_cast<VideoFrame>(frame_p));
260#endif
261 return;
262 }
263 }
264}
265
266void
267VideoMixer::process()
268{
269 nextProcess_ += std::chrono::duration_cast<std::chrono::microseconds>(FRAME_DURATION);
270 const auto delay = nextProcess_ - std::chrono::steady_clock::now();
271 if (delay.count() > 0)
272 std::this_thread::sleep_for(delay);
273
274 // Nothing to do.
275 if (width_ == 0 or height_ == 0) {
276 return;
277 }
278
280 try {
281 output.reserve(format_, width_, height_);
282 } catch (const std::bad_alloc& e) {
283 JAMI_ERR("[mixer:%s] VideoFrame::allocBuffer() failed", id_.c_str());
284 return;
285 }
286
288
289 {
290 std::lock_guard lk(audioOnlySourcesMtx_);
291 std::shared_lock lock(rwMutex_);
292
293 int i = 0;
294 bool activeFound = false;
295 bool needsUpdate = layoutUpdated_ > 0;
296 bool successfullyRendered = audioOnlySources_.size() != 0 && sources_.size() == 0;
297 std::vector<SourceInfo> sourcesInfo;
298 sourcesInfo.reserve(sources_.size() + audioOnlySources_.size());
299 // add all audioonlysources
300 for (auto& [callId, streamId] : audioOnlySources_) {
301 auto active = verifyActive(streamId);
302 if (currentLayout_ != Layout::ONE_BIG or active) {
303 sourcesInfo.emplace_back(SourceInfo {{}, 0, 0, 10, 10, false, callId, streamId});
304 }
305 if (currentLayout_ == Layout::ONE_BIG) {
306 if (active)
308 else
309 sourcesInfo.emplace_back(SourceInfo {{}, 0, 0, 0, 0, false, callId, streamId});
310 // Add all participants info even in ONE_BIG layout.
311 // The width and height set to 0 here will led the peer to filter them out.
312 }
313 }
314 // add video sources
315 for (auto& x : sources_) {
316 /* thread stop pending? */
317 if (!loop_.isRunning())
318 return;
319
320 auto sinfo = streamInfo(x->source);
321 auto activeSource = verifyActive(sinfo.streamId);
322 if (currentLayout_ != Layout::ONE_BIG or activeSource) {
323 // make rendered frame temporarily unavailable for update()
324 // to avoid concurrent access.
325 std::shared_ptr<VideoFrame> input = x->getRenderFrame();
326 std::shared_ptr<VideoFrame> fooInput = std::make_shared<VideoFrame>();
327
328 auto wantedIndex = i;
329 if (currentLayout_ == Layout::ONE_BIG) {
330 wantedIndex = 0;
331 activeFound = true;
332 } else if (currentLayout_ == Layout::ONE_BIG_WITH_SMALL) {
333 if (activeSource) {
334 wantedIndex = 0;
335 activeFound = true;
336 } else if (not activeFound) {
337 wantedIndex += 1;
338 }
339 }
340
341 auto hasVideo = x->hasVideo;
342 bool blackFrame = false;
343
344 if (!input->height() or !input->width()) {
346 fooInput->reserve(format_, width_, height_);
347 blackFrame = true;
348 } else {
349 fooInput.swap(input);
350 }
351
352 // If orientation changed or if the first valid frame for source
353 // is received -> trigger layout calculation and confInfo update
354 if (x->rotation != fooInput->getOrientation() or !x->w or !x->h) {
355 updateLayout();
356 needsUpdate = true;
357 }
358
359 if (needsUpdate)
360 calc_position(x, fooInput, wantedIndex);
361
362 if (!blackFrame) {
363 if (fooInput)
364 successfullyRendered |= render_frame(output, fooInput, x);
365 else
366 JAMI_WARN("[mixer:%s] Nothing to render for %p", id_.c_str(), x->source);
367 }
368
369 x->hasVideo = !blackFrame && successfullyRendered;
370 if (hasVideo != x->hasVideo) {
371 updateLayout();
372 needsUpdate = true;
373 }
374 } else if (needsUpdate) {
375 x->x = 0;
376 x->y = 0;
377 x->w = 0;
378 x->h = 0;
379 x->hasVideo = false;
380 }
381
382 ++i;
383 }
385 layoutUpdated_ -= 1;
386 if (layoutUpdated_ == 0) {
387 for (auto& x : sources_) {
388 auto sinfo = streamInfo(x->source);
389 sourcesInfo.emplace_back(SourceInfo {x->source,
390 x->x,
391 x->y,
392 x->w,
393 x->h,
394 x->hasVideo,
395 sinfo.callId,
396 sinfo.streamId});
397 }
398 if (onSourcesUpdated_)
399 onSourcesUpdated_(std::move(sourcesInfo));
400 }
401 }
402 }
403
404 output.pointer()->pts = av_rescale_q_rnd(av_gettime() - startTime_,
405 {1, AV_TIME_BASE},
406 {1, MIXER_FRAMERATE},
407 static_cast<AVRounding>(AV_ROUND_NEAR_INF
409 lastTimestamp_ = output.pointer()->pts;
410 publishFrame();
411}
412
413bool
414VideoMixer::render_frame(VideoFrame& output,
415 const std::shared_ptr<VideoFrame>& input,
416 std::unique_ptr<VideoMixerSource>& source)
417{
418 if (!width_ or !height_ or !input->pointer() or input->pointer()->format == -1)
419 return false;
420
421 int cell_width = source->w;
422 int cell_height = source->h;
423 int xoff = source->x;
424 int yoff = source->y;
425
426 int angle = input->getOrientation();
427 const constexpr char filterIn[] = "mixin";
428 if (angle != source->rotation) {
429 source->rotationFilter = video::getTransposeFilter(angle,
430 filterIn,
431 input->width(),
432 input->height(),
433 input->format(),
434 false);
435 source->rotation = angle;
436 }
437 std::shared_ptr<VideoFrame> frame;
438 if (source->rotationFilter) {
439 source->rotationFilter->feedInput(input->pointer(), filterIn);
440 frame = std::static_pointer_cast<VideoFrame>(
441 std::shared_ptr<MediaFrame>(source->rotationFilter->readOutput()));
442 } else {
443 frame = input;
444 }
445
447 return true;
448}
449
450void
451VideoMixer::calc_position(std::unique_ptr<VideoMixerSource>& source,
452 const std::shared_ptr<VideoFrame>& input,
453 int index)
454{
455 if (!width_ or !height_)
456 return;
457
458 // Compute cell size/position
460 const int n = currentLayout_ == Layout::ONE_BIG ? 1 : sources_.size();
461 const int zoom = currentLayout_ == Layout::ONE_BIG_WITH_SMALL ? std::max(MIN_LINE_ZOOM, n)
462 : ceil(sqrt(n));
463 if (currentLayout_ == Layout::ONE_BIG_WITH_SMALL && index == 0) {
464 // In ONE_BIG_WITH_SMALL, the first line at the top is the previews
465 // The rest is the active source
466 cell_width = width_;
467 cell_height = height_ - height_ / zoom;
468 } else {
469 cell_width = width_ / zoom;
470 cell_height = height_ / zoom;
471
472 if (n == 1) {
473 // On some platforms (at least macOS/android) - Having one frame at the same
474 // size of the mixer cause it to be grey.
475 // Removing some pixels solve this. We use 16 because it's a multiple of 8
476 // (value that we prefer for video management)
477 cell_width -= 16;
478 cell_height -= 16;
479 }
480 }
481 if (currentLayout_ == Layout::ONE_BIG_WITH_SMALL) {
482 if (index == 0) {
483 cellW_off = 0;
484 cellH_off = height_ / zoom; // First line height
485 } else {
486 cellW_off = (index - 1) * cell_width;
487 // Show sources in center
488 cellW_off += (width_ - (n - 1) * cell_width) / 2;
489 cellH_off = 0;
490 }
491 } else {
492 cellW_off = (index % zoom) * cell_width;
493 if (currentLayout_ == Layout::GRID && n % zoom != 0 && index >= (zoom * ((n - 1) / zoom))) {
494 // Last line, center participants if not full
495 cellW_off += (width_ - (n % zoom) * cell_width) / 2;
496 }
497 cellH_off = (index / zoom) * cell_height;
498 if (n == 1) {
499 // Centerize (cellwidth = width_ - 16)
500 cellW_off += 8;
501 cellH_off += 8;
502 }
503 }
504
505 // Compute frame size/position
506 float zoomW, zoomH;
508
509 if (input->getOrientation() % 180) {
510 // Rotated frame
511 zoomW = (float) input->height() / cell_width;
512 zoomH = (float) input->width() / cell_height;
513 frameH = std::round(input->width() / std::max(zoomW, zoomH));
514 frameW = std::round(input->height() / std::max(zoomW, zoomH));
515 } else {
516 zoomW = (float) input->width() / cell_width;
517 zoomH = (float) input->height() / cell_height;
518 frameW = std::round(input->width() / std::max(zoomW, zoomH));
519 frameH = std::round(input->height() / std::max(zoomW, zoomH));
520 }
521
522 // Center the frame in the cell
525
526 // Update source's cache
527 source->w = frameW;
528 source->h = frameH;
529 source->x = frameW_off;
530 source->y = frameH_off;
531}
532
533void
534VideoMixer::setParameters(int width, int height, AVPixelFormat format)
535{
536 std::unique_lock lock(rwMutex_);
537
538 width_ = width;
539 height_ = height;
540 format_ = format;
541
542 // cleanup the previous frame to have a nice copy in rendering method
543 std::shared_ptr<VideoFrame> previous_p(obtainLastFrame());
544 if (previous_p)
546
547 startSink();
548 updateLayout();
549 startTime_ = av_gettime();
550}
551
552void
553VideoMixer::startSink()
554{
555 stopSink();
556
557 if (width_ == 0 or height_ == 0) {
558 JAMI_WARN("[mixer:%s] MX: unable to start with zero-sized output", id_.c_str());
559 return;
560 }
561
562 if (not sink_->start()) {
563 JAMI_ERR("[mixer:%s] MX: sink startup failed", id_.c_str());
564 return;
565 }
566
567 if (this->attach(sink_.get()))
568 sink_->setFrameSize(width_, height_);
569}
570
571void
572VideoMixer::stopSink()
573{
574 this->detach(sink_.get());
575 sink_->stop();
576}
577
578int
580{
581 return width_;
582}
583
584int
586{
587 return height_;
588}
589
592{
593 return format_;
594}
595
597VideoMixer::getStream(const std::string& name) const
598{
599 MediaStream ms;
600 ms.name = name;
601 ms.format = format_;
602 ms.isVideo = true;
603 ms.height = height_;
604 ms.width = width_;
605 ms.frameRate = {MIXER_FRAMERATE, 1};
606 ms.timeBase = {1, MIXER_FRAMERATE};
607 ms.firstTimestamp = lastTimestamp_;
608
609 return ms;
610}
611
612} // namespace video
613} // namespace jami
Manager (controller) of daemon.
Definition manager.h:67
bool attach(Observer< std::shared_ptr< MediaFrame > > *o)
Definition observer.h:70
bool detach(Observer< std::shared_ptr< MediaFrame > > *o)
Definition observer.h:101
bool isRunning() const noexcept
static std::unique_ptr< VideoFrame > transferToMainMemory(const VideoFrame &frame, AVPixelFormat desiredFormat)
Transfers hardware frame to main memory.
Definition accel.cpp:372
std::shared_ptr< VideoFrame > obtainLastFrame()
StreamInfo streamInfo(Observable< std::shared_ptr< MediaFrame > > *frame) const
void attached(Observable< std::shared_ptr< MediaFrame > > *ob) override
void update(Observable< std::shared_ptr< MediaFrame > > *ob, const std::shared_ptr< MediaFrame > &v) override
void switchInputs(const std::vector< std::string > &inputs)
Set all inputs at once.
AVPixelFormat getPixelFormat() const override
void detachVideo(Observable< std::shared_ptr< MediaFrame > > *frame)
VideoMixer(const std::string &id, const std::string &localInput={}, bool attachHost=false)
void setParameters(int width, int height, AVPixelFormat format=AV_PIX_FMT_YUV422P)
void detached(Observable< std::shared_ptr< MediaFrame > > *ob) override
void setActiveStream(const std::string &id)
void stopInputs()
Stop all inputs.
MediaStream getStream(const std::string &name) const
bool verifyActive(const std::string &id)
Definition video_mixer.h:92
int getHeight() const override
int getWidth() const override
void attachVideo(Observable< std::shared_ptr< MediaFrame > > *frame, const std::string &callId, const std::string &streamId)
void scale_and_pad(const VideoFrame &input, VideoFrame &output, unsigned xoff, unsigned yoff, unsigned dest_width, unsigned dest_height, bool keep_aspect)
#define JAMI_ERR(...)
Definition logger.h:218
#define JAMI_DBG(...)
Definition logger.h:216
#define JAMI_DEBUG(formatstr,...)
Definition logger.h:226
#define JAMI_WARN(...)
Definition logger.h:217
void fillWithBlack(AVFrame *frame)
std::string streamId(const std::string &callId, std::string_view label)
constexpr std::string_view DEFAULT_VIDEO_STREAMID
Definition sip_utils.h:146
static constexpr const auto MIXER_FRAMERATE
std::unique_ptr< MediaFilter > getTransposeFilter(int rotation, std::string inputName, int width, int height, int format, bool rescale)
static constexpr const auto FRAME_DURATION
void emitSignal(Args... args)
Definition ring_signal.h:64
libjami::VideoFrame VideoFrame
Definition video_base.h:53
rational< int > frameRate
std::string name
rational< int > timeBase
std::shared_ptr< VideoFrame > getRenderFrame()
std::shared_ptr< VideoFrame > render_frame
void atomic_copy(const VideoFrame &other)
std::unique_ptr< MediaFilter > rotationFilter
Observable< std::shared_ptr< MediaFrame > > * source
static constexpr auto MIN_LINE_ZOOM