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