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 8811 : std::shared_ptr<VideoFrame> getRenderFrame()
59 : {
60 8811 : std::lock_guard lock(mutex_);
61 17622 : return render_frame;
62 8811 : }
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 37 : VideoMixer::VideoMixer(const std::string& id, const std::string& localInput, bool attachHost)
79 : : VideoGenerator::VideoGenerator()
80 37 : , id_(id)
81 37 : , sink_(Manager::instance().createSinkClient(id, true))
82 147 : , loop_([] { return true; }, std::bind(&VideoMixer::process, this), [] {})
83 : {
84 : // Local video camera is the main participant
85 37 : if (not localInput.empty() && attachHost) {
86 0 : auto videoInput = getVideoInput(localInput);
87 0 : localInputs_.emplace_back(videoInput);
88 0 : attachVideo(videoInput.get(), "", sip_utils::streamId("", sip_utils::DEFAULT_VIDEO_STREAMID));
89 0 : }
90 37 : loop_.start();
91 37 : nextProcess_ = std::chrono::steady_clock::now();
92 :
93 37 : JAMI_DBG("[mixer:%s] New instance created", id_.c_str());
94 37 : }
95 :
96 74 : VideoMixer::~VideoMixer()
97 : {
98 37 : stopSink();
99 37 : stopInputs();
100 :
101 37 : loop_.join();
102 :
103 37 : JAMI_DBG("[mixer:%s] Instance destroyed", id_.c_str());
104 37 : }
105 :
106 : void
107 29 : VideoMixer::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 29 : std::lock_guard lk(localInputsMtx_);
112 29 : decltype(localInputs_) newInputs;
113 29 : newInputs.reserve(inputs.size());
114 59 : for (const auto& input : inputs) {
115 60 : 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 30 : videoInput->restart();
119 30 : auto it = std::find(localInputs_.cbegin(), localInputs_.cend(), videoInput);
120 30 : auto onlyDetach = it != localInputs_.cend();
121 30 : if (onlyDetach) {
122 1 : videoInput->detach(this);
123 1 : localInputs_.erase(it);
124 : }
125 30 : newInputs.emplace_back(std::move(videoInput));
126 30 : }
127 : // Stop other video inputs
128 29 : stopInputs();
129 29 : localInputs_ = std::move(newInputs);
130 :
131 : // Re-attach videoInput to mixer
132 59 : for (size_t i = 0; i < localInputs_.size(); ++i) {
133 30 : auto& input = localInputs_[i];
134 60 : attachVideo(input.get(), "", sip_utils::streamId("", fmt::format("video_{}", i)));
135 : }
136 29 : }
137 :
138 : void
139 29 : VideoMixer::stopInput(const std::shared_ptr<VideoFrameActiveWriter>& input)
140 : {
141 : // Detach videoInputs from mixer
142 29 : input->detach(this);
143 29 : }
144 :
145 : void
146 92 : VideoMixer::stopInputs()
147 : {
148 121 : for (auto& input : localInputs_)
149 29 : stopInput(input);
150 92 : localInputs_.clear();
151 92 : }
152 :
153 : void
154 1 : VideoMixer::setActiveStream(const std::string& id)
155 : {
156 1 : activeStream_ = id;
157 1 : updateLayout();
158 1 : }
159 :
160 : void
161 392 : VideoMixer::updateLayout()
162 : {
163 392 : if (activeStream_ == "")
164 391 : currentLayout_ = Layout::GRID;
165 392 : layoutUpdated_ += 1;
166 392 : }
167 :
168 : void
169 80 : VideoMixer::attachVideo(Observable<std::shared_ptr<MediaFrame>>* frame,
170 : const std::string& callId,
171 : const std::string& streamId)
172 : {
173 80 : if (!frame)
174 0 : return;
175 80 : JAMI_DBG("Attaching video with streamId %s", streamId.c_str());
176 : {
177 80 : std::lock_guard lk(videoToStreamInfoMtx_);
178 80 : videoToStreamInfo_[frame] = StreamInfo {callId, streamId};
179 80 : }
180 80 : frame->attach(this);
181 : }
182 :
183 : void
184 49 : VideoMixer::detachVideo(Observable<std::shared_ptr<MediaFrame>>* frame)
185 : {
186 49 : if (!frame)
187 0 : return;
188 49 : bool detach = false;
189 49 : std::unique_lock lk(videoToStreamInfoMtx_);
190 49 : auto it = videoToStreamInfo_.find(frame);
191 49 : if (it != videoToStreamInfo_.end()) {
192 49 : JAMI_DBG("Detaching video of call %s", it->second.callId.c_str());
193 49 : 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 49 : if (verifyActive(it->second.streamId))
197 0 : activeStream_ = {};
198 49 : videoToStreamInfo_.erase(it);
199 : }
200 49 : lk.unlock();
201 49 : if (detach)
202 49 : frame->detach(this);
203 49 : }
204 :
205 : void
206 80 : VideoMixer::attached(Observable<std::shared_ptr<MediaFrame>>* ob)
207 : {
208 80 : std::unique_lock lock(rwMutex_);
209 :
210 80 : auto src = std::unique_ptr<VideoMixerSource>(new VideoMixerSource);
211 80 : src->render_frame = std::make_shared<VideoFrame>();
212 80 : src->source = ob;
213 80 : JAMI_DBG("Add new source [%p]", src.get());
214 80 : sources_.emplace_back(std::move(src));
215 320 : JAMI_DEBUG("Total sources: {:d}", sources_.size());
216 80 : updateLayout();
217 80 : }
218 :
219 : void
220 80 : VideoMixer::detached(Observable<std::shared_ptr<MediaFrame>>* ob)
221 : {
222 80 : std::unique_lock lock(rwMutex_);
223 :
224 124 : for (const auto& x : sources_) {
225 124 : if (x->source == ob) {
226 80 : JAMI_DBG("Remove source [%p]", x.get());
227 80 : sources_.remove(x);
228 320 : JAMI_DEBUG("Total sources: {:d}", sources_.size());
229 80 : updateLayout();
230 80 : break;
231 : }
232 : }
233 80 : }
234 :
235 : void
236 0 : VideoMixer::update(Observable<std::shared_ptr<MediaFrame>>* ob, const std::shared_ptr<MediaFrame>& frame_p)
237 : {
238 0 : std::shared_lock lock(rwMutex_);
239 :
240 0 : for (const auto& x : sources_) {
241 0 : if (x->source == ob) {
242 : #ifdef ENABLE_HWACCEL
243 0 : std::shared_ptr<VideoFrame> frame;
244 : try {
245 0 : frame = HardwareAccel::transferToMainMemory(*std::static_pointer_cast<VideoFrame>(frame_p),
246 0 : AV_PIX_FMT_NV12);
247 0 : x->atomic_copy(*std::static_pointer_cast<VideoFrame>(frame));
248 0 : } catch (const std::runtime_error& e) {
249 0 : JAMI_ERR("[mixer:%s] Accel failure: %s", id_.c_str(), e.what());
250 0 : return;
251 0 : }
252 : #else
253 : x->atomic_copy(*std::static_pointer_cast<VideoFrame>(frame_p));
254 : #endif
255 0 : return;
256 0 : }
257 : }
258 0 : }
259 :
260 : void
261 3576 : VideoMixer::process()
262 : {
263 3576 : nextProcess_ += std::chrono::duration_cast<std::chrono::microseconds>(FRAME_DURATION);
264 3576 : const auto delay = nextProcess_ - std::chrono::steady_clock::now();
265 3576 : if (delay.count() > 0)
266 3477 : std::this_thread::sleep_for(delay);
267 :
268 : // Nothing to do.
269 3576 : if (width_ == 0 or height_ == 0) {
270 0 : return;
271 : }
272 :
273 3576 : VideoFrame& output = getNewFrame();
274 : try {
275 3576 : output.reserve(format_, width_, height_);
276 0 : } catch (const std::bad_alloc& e) {
277 0 : JAMI_ERR("[mixer:%s] VideoFrame::allocBuffer() failed", id_.c_str());
278 0 : return;
279 0 : }
280 :
281 3576 : libav_utils::fillWithBlack(output.pointer());
282 :
283 : {
284 3576 : std::lock_guard lk(audioOnlySourcesMtx_);
285 3576 : std::shared_lock lock(rwMutex_);
286 :
287 3576 : int i = 0;
288 3576 : bool activeFound = false;
289 3576 : bool needsUpdate = layoutUpdated_ > 0;
290 3576 : bool successfullyRendered = audioOnlySources_.size() != 0 && sources_.size() == 0;
291 3576 : std::vector<SourceInfo> sourcesInfo;
292 3576 : sourcesInfo.reserve(sources_.size() + audioOnlySources_.size());
293 : // add all audioonlysources
294 4195 : for (auto& [callId, streamId] : audioOnlySources_) {
295 619 : auto active = verifyActive(streamId);
296 619 : if (currentLayout_ != Layout::ONE_BIG or active) {
297 619 : sourcesInfo.emplace_back(SourceInfo {{}, 0, 0, 10, 10, false, callId, streamId});
298 : }
299 619 : if (currentLayout_ == Layout::ONE_BIG) {
300 0 : if (active)
301 0 : successfullyRendered = true;
302 : else
303 0 : 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 12387 : for (auto& x : sources_) {
310 : /* thread stop pending? */
311 8811 : if (!loop_.isRunning())
312 0 : return;
313 :
314 8811 : auto sinfo = streamInfo(x->source);
315 8811 : auto activeSource = verifyActive(sinfo.streamId);
316 8811 : if (currentLayout_ != Layout::ONE_BIG or activeSource) {
317 : // make rendered frame temporarily unavailable for update()
318 : // to avoid concurrent access.
319 8811 : std::shared_ptr<VideoFrame> input = x->getRenderFrame();
320 8811 : std::shared_ptr<VideoFrame> fooInput = std::make_shared<VideoFrame>();
321 :
322 8811 : auto wantedIndex = i;
323 8811 : if (currentLayout_ == Layout::ONE_BIG) {
324 0 : wantedIndex = 0;
325 0 : activeFound = true;
326 8811 : } else if (currentLayout_ == Layout::ONE_BIG_WITH_SMALL) {
327 0 : if (activeSource) {
328 0 : wantedIndex = 0;
329 0 : activeFound = true;
330 0 : } else if (not activeFound) {
331 0 : wantedIndex += 1;
332 : }
333 : }
334 :
335 8811 : auto hasVideo = x->hasVideo;
336 8811 : bool blackFrame = false;
337 :
338 8811 : if (!input->height() or !input->width()) {
339 8811 : successfullyRendered = true;
340 8811 : fooInput->reserve(format_, width_, height_);
341 8811 : blackFrame = true;
342 : } else {
343 0 : 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 8811 : if (x->rotation != fooInput->getOrientation() or !x->w or !x->h) {
349 78 : updateLayout();
350 78 : needsUpdate = true;
351 : }
352 :
353 8811 : if (needsUpdate)
354 635 : calc_position(x, fooInput, wantedIndex);
355 :
356 8811 : if (!blackFrame) {
357 0 : if (fooInput)
358 0 : successfullyRendered |= render_frame(output, fooInput, x);
359 : else
360 0 : JAMI_WARN("[mixer:%s] Nothing to render for %p", id_.c_str(), x->source);
361 : }
362 :
363 8811 : x->hasVideo = !blackFrame && successfullyRendered;
364 8811 : if (hasVideo != x->hasVideo) {
365 78 : updateLayout();
366 78 : needsUpdate = true;
367 : }
368 8811 : } else if (needsUpdate) {
369 0 : x->x = 0;
370 0 : x->y = 0;
371 0 : x->w = 0;
372 0 : x->h = 0;
373 0 : x->hasVideo = false;
374 : }
375 :
376 8811 : ++i;
377 8811 : }
378 3576 : if (needsUpdate and successfullyRendered) {
379 303 : layoutUpdated_ -= 1;
380 303 : if (layoutUpdated_ == 0) {
381 222 : for (auto& x : sources_) {
382 153 : auto sinfo = streamInfo(x->source);
383 153 : sourcesInfo.emplace_back(
384 306 : SourceInfo {x->source, x->x, x->y, x->w, x->h, x->hasVideo, sinfo.callId, sinfo.streamId});
385 153 : }
386 69 : if (onSourcesUpdated_)
387 69 : onSourcesUpdated_(std::move(sourcesInfo));
388 : }
389 : }
390 3578 : }
391 :
392 3575 : output.pointer()->pts = av_rescale_q_rnd(av_gettime() - startTime_,
393 : {1, AV_TIME_BASE},
394 : {1, MIXER_FRAMERATE},
395 : static_cast<AVRounding>(AV_ROUND_NEAR_INF | AV_ROUND_PASS_MINMAX));
396 3575 : lastTimestamp_ = output.pointer()->pts;
397 3575 : publishFrame();
398 : }
399 :
400 : bool
401 0 : VideoMixer::render_frame(VideoFrame& output,
402 : const std::shared_ptr<VideoFrame>& input,
403 : std::unique_ptr<VideoMixerSource>& source)
404 : {
405 0 : if (!width_ or !height_ or !input->pointer() or input->pointer()->format == -1)
406 0 : return false;
407 :
408 0 : int cell_width = source->w;
409 0 : int cell_height = source->h;
410 0 : int xoff = source->x;
411 0 : int yoff = source->y;
412 :
413 0 : int angle = input->getOrientation();
414 0 : const constexpr char filterIn[] = "mixin";
415 0 : if (angle != source->rotation) {
416 0 : source->rotationFilter
417 0 : = video::getTransposeFilter(angle, filterIn, input->width(), input->height(), input->format(), false);
418 0 : source->rotation = angle;
419 : }
420 0 : std::shared_ptr<VideoFrame> frame;
421 0 : if (source->rotationFilter) {
422 0 : source->rotationFilter->feedInput(input->pointer(), filterIn);
423 0 : frame = std::static_pointer_cast<VideoFrame>(std::shared_ptr<MediaFrame>(source->rotationFilter->readOutput()));
424 : } else {
425 0 : frame = input;
426 : }
427 :
428 0 : scaler_.scale_and_pad(*frame, output, xoff, yoff, cell_width, cell_height, true);
429 0 : return true;
430 0 : }
431 :
432 : void
433 635 : VideoMixer::calc_position(std::unique_ptr<VideoMixerSource>& source, const std::shared_ptr<VideoFrame>& input, int index)
434 : {
435 635 : if (!width_ or !height_)
436 0 : return;
437 :
438 : // Compute cell size/position
439 : int cell_width, cell_height, cellW_off, cellH_off;
440 635 : const int n = currentLayout_ == Layout::ONE_BIG ? 1 : static_cast<int>(sources_.size());
441 635 : const int zoom = currentLayout_ == Layout::ONE_BIG_WITH_SMALL ? std::max(MIN_LINE_ZOOM, n)
442 635 : : static_cast<int>(ceil(sqrt(n)));
443 635 : 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 0 : cell_width = width_;
447 0 : cell_height = height_ - height_ / zoom;
448 : } else {
449 635 : cell_width = width_ / zoom;
450 635 : cell_height = height_ / zoom;
451 :
452 635 : 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 42 : cell_width -= 16;
458 42 : cell_height -= 16;
459 : }
460 : }
461 635 : if (currentLayout_ == Layout::ONE_BIG_WITH_SMALL) {
462 0 : if (index == 0) {
463 0 : cellW_off = 0;
464 0 : cellH_off = height_ / zoom; // First line height
465 : } else {
466 0 : cellW_off = (index - 1) * cell_width;
467 : // Show sources in center
468 0 : cellW_off += (width_ - (n - 1) * cell_width) / 2;
469 0 : cellH_off = 0;
470 : }
471 : } else {
472 635 : cellW_off = (index % zoom) * cell_width;
473 635 : if (currentLayout_ == Layout::GRID && n % zoom != 0 && index >= (zoom * ((n - 1) / zoom))) {
474 : // Last line, center participants if not full
475 69 : cellW_off += (width_ - (n % zoom) * cell_width) / 2;
476 : }
477 635 : cellH_off = (index / zoom) * cell_height;
478 635 : if (n == 1) {
479 : // Centerize (cellwidth = width_ - 16)
480 42 : cellW_off += 8;
481 42 : cellH_off += 8;
482 : }
483 : }
484 :
485 : // Compute frame size/position
486 : int frameW, frameH, frameW_off, frameH_off;
487 : float zoomW, zoomH, denom;
488 :
489 635 : float inputW = static_cast<float>(input->width());
490 635 : float inputH = static_cast<float>(input->height());
491 :
492 635 : if (input->getOrientation() % 180) {
493 : // Rotated frame
494 0 : zoomW = inputH / static_cast<float>(cell_width);
495 0 : zoomH = inputW / static_cast<float>(cell_height);
496 0 : denom = std::max(zoomW, zoomH);
497 0 : frameH = static_cast<int>(std::lround(inputW / denom));
498 0 : frameW = static_cast<int>(std::lround(inputH / denom));
499 : } else {
500 635 : zoomW = inputW / static_cast<float>(cell_width);
501 635 : zoomH = inputH / static_cast<float>(cell_height);
502 635 : denom = std::max(zoomW, zoomH);
503 635 : frameW = static_cast<int>(std::lround(inputW / denom));
504 635 : frameH = static_cast<int>(std::lround(inputH / denom));
505 : }
506 :
507 : // Center the frame in the cell
508 635 : frameW_off = cellW_off + (cell_width - frameW) / 2;
509 635 : frameH_off = cellH_off + (cell_height - frameH) / 2;
510 :
511 : // Update source's cache
512 635 : source->w = frameW;
513 635 : source->h = frameH;
514 635 : source->x = frameW_off;
515 635 : source->y = frameH_off;
516 : }
517 :
518 : void
519 37 : VideoMixer::setParameters(int width, int height, AVPixelFormat format)
520 : {
521 37 : std::unique_lock lock(rwMutex_);
522 :
523 37 : width_ = width;
524 37 : height_ = height;
525 37 : format_ = format;
526 :
527 : // cleanup the previous frame to have a nice copy in rendering method
528 37 : std::shared_ptr<VideoFrame> previous_p(obtainLastFrame());
529 37 : if (previous_p)
530 0 : libav_utils::fillWithBlack(previous_p->pointer());
531 :
532 37 : startSink();
533 37 : updateLayout();
534 37 : startTime_ = av_gettime();
535 37 : }
536 :
537 : void
538 37 : VideoMixer::startSink()
539 : {
540 37 : stopSink();
541 :
542 37 : if (width_ == 0 or height_ == 0) {
543 0 : JAMI_WARN("[mixer:%s] MX: unable to start with zero-sized output", id_.c_str());
544 0 : return;
545 : }
546 :
547 37 : if (not sink_->start()) {
548 0 : JAMI_ERR("[mixer:%s] MX: sink startup failed", id_.c_str());
549 0 : return;
550 : }
551 :
552 37 : if (this->attach(sink_.get()))
553 37 : sink_->setFrameSize(width_, height_);
554 : }
555 :
556 : void
557 74 : VideoMixer::stopSink()
558 : {
559 74 : this->detach(sink_.get());
560 74 : sink_->stop();
561 74 : }
562 :
563 : int
564 68 : VideoMixer::getWidth() const
565 : {
566 68 : return width_;
567 : }
568 :
569 : int
570 68 : VideoMixer::getHeight() const
571 : {
572 68 : return height_;
573 : }
574 :
575 : AVPixelFormat
576 0 : VideoMixer::getPixelFormat() const
577 : {
578 0 : return format_;
579 : }
580 :
581 : MediaStream
582 50 : VideoMixer::getStream(const std::string& name) const
583 : {
584 50 : MediaStream ms;
585 50 : ms.name = name;
586 50 : ms.format = format_;
587 50 : ms.isVideo = true;
588 50 : ms.height = height_;
589 50 : ms.width = width_;
590 50 : ms.frameRate = {MIXER_FRAMERATE, 1};
591 50 : ms.timeBase = {1, MIXER_FRAMERATE};
592 50 : ms.firstTimestamp = lastTimestamp_;
593 :
594 50 : return ms;
595 0 : }
596 :
597 : } // namespace video
598 : } // namespace jami
|