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