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