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