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