| /* |
| * Copyright (C) 2004-2021 Savoir-faire Linux Inc. |
| * |
| * Author: Guillaume Roguez <Guillaume.Roguez@savoirfairelinux.com> |
| * Author: Philippe Gorley <philippe.gorley@savoirfairelinux.com> |
| * |
| * This program is free software; you can redistribute it and/or modify |
| * it under the terms of the GNU General Public License as published by |
| * the Free Software Foundation; either version 3 of the License, or |
| * (at your option) any later version. |
| * |
| * This program is distributed in the hope that it will be useful, |
| * but WITHOUT ANY WARRANTY; without even the implied warranty of |
| * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| * GNU General Public License for more details. |
| * |
| * You should have received a copy of the GNU General Public License |
| * along with this program; if not, write to the Free Software |
| * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| */ |
| |
| #include "libav_deps.h" // MUST BE INCLUDED FIRST |
| |
| #include "video_mixer.h" |
| #include "media_buffer.h" |
| #include "client/videomanager.h" |
| #include "manager.h" |
| #include "media_filter.h" |
| #include "sinkclient.h" |
| #include "logger.h" |
| #include "filter_transpose.h" |
| #ifdef RING_ACCEL |
| #include "accel.h" |
| #endif |
| |
| #include <cmath> |
| #include <unistd.h> |
| |
| #include <opendht/thread_pool.h> |
| |
| static constexpr auto MIN_LINE_ZOOM |
| = 6; // Used by the ONE_BIG_WITH_SMALL layout for the small previews |
| |
| namespace jami { |
| namespace video { |
| |
| struct VideoMixer::VideoMixerSource |
| { |
| Observable<std::shared_ptr<MediaFrame>>* source {nullptr}; |
| int rotation {0}; |
| std::unique_ptr<MediaFilter> rotationFilter {nullptr}; |
| std::shared_ptr<VideoFrame> render_frame; |
| void atomic_copy(const VideoFrame& other) |
| { |
| std::lock_guard<std::mutex> lock(mutex_); |
| auto newFrame = std::make_shared<VideoFrame>(); |
| newFrame->copyFrom(other); |
| render_frame = newFrame; |
| } |
| |
| std::shared_ptr<VideoFrame> getRenderFrame() |
| { |
| std::lock_guard<std::mutex> lock(mutex_); |
| return render_frame; |
| } |
| |
| // Current render informations |
| int x {}; |
| int y {}; |
| int w {}; |
| int h {}; |
| bool hasVideo {true}; |
| |
| private: |
| std::mutex mutex_; |
| }; |
| |
| static constexpr const auto MIXER_FRAMERATE = 30; |
| static constexpr const auto FRAME_DURATION = std::chrono::duration<double>(1. / MIXER_FRAMERATE); |
| |
| VideoMixer::VideoMixer(const std::string& id, const std::string& localInput) |
| : VideoGenerator::VideoGenerator() |
| , id_(id) |
| , sink_(Manager::instance().createSinkClient(id, true)) |
| , loop_([] { return true; }, std::bind(&VideoMixer::process, this), [] {}) |
| { |
| // Local video camera is the main participant |
| if (not localInput.empty()) |
| videoLocal_ = getVideoInput(localInput); |
| if (videoLocal_) |
| videoLocal_->attach(this); |
| loop_.start(); |
| nextProcess_ = std::chrono::steady_clock::now(); |
| |
| JAMI_DBG("[mixer:%s] New instance created", id_.c_str()); |
| } |
| |
| VideoMixer::~VideoMixer() |
| { |
| stop_sink(); |
| |
| if (videoLocal_) { |
| videoLocal_->detach(this); |
| // prefer to release it now than after the next join |
| videoLocal_.reset(); |
| } |
| if (videoLocalSecondary_) { |
| videoLocalSecondary_->detach(this); |
| // prefer to release it now than after the next join |
| videoLocalSecondary_.reset(); |
| } |
| |
| loop_.join(); |
| |
| JAMI_DBG("[mixer:%s] Instance destroyed", id_.c_str()); |
| } |
| |
| void |
| VideoMixer::switchInput(const std::string& input) |
| { |
| JAMI_DBG("Set new input %s", input.c_str()); |
| |
| if (auto local = videoLocal_) { |
| // Detach videoInput from mixer |
| local->detach(this); |
| #if !VIDEO_CLIENT_INPUT |
| if (auto localInput = std::dynamic_pointer_cast<VideoInput>(local)) { |
| // Stop old VideoInput |
| localInput->stopInput(); |
| } |
| #endif |
| } |
| |
| if (input.empty()) { |
| JAMI_DBG("[mixer:%s] Input is empty, don't add it to the mixer", id_.c_str()); |
| return; |
| } |
| |
| // Re-attach videoInput to mixer |
| videoLocal_ = getVideoInput(input); |
| if (videoLocal_) |
| videoLocal_->attach(this); |
| } |
| |
| void |
| VideoMixer::switchSecondaryInput(const std::string& input) |
| { |
| if (auto local = videoLocalSecondary_) { |
| // Detach videoInput from mixer |
| local->detach(this); |
| #if !VIDEO_CLIENT_INPUT |
| if (auto localInput = std::dynamic_pointer_cast<VideoInput>(local)) { |
| // Stop old VideoInput |
| localInput->stopInput(); |
| } |
| #endif |
| } |
| |
| if (input.empty()) { |
| JAMI_DBG("[mixer:%s] Input is empty, don't add it in the mixer", id_.c_str()); |
| return; |
| } |
| |
| // Re-attach videoInput to mixer |
| videoLocalSecondary_ = getVideoInput(input); |
| if (videoLocalSecondary_) { |
| videoLocalSecondary_->attach(this); |
| } |
| } |
| |
| void |
| VideoMixer::stopInput() |
| { |
| if (auto local = std::move(videoLocal_)) { |
| local->detach(this); |
| } |
| } |
| |
| void |
| VideoMixer::setActiveHost() |
| { |
| activeSource_ = videoLocalSecondary_ ? videoLocalSecondary_.get() : videoLocal_.get(); |
| updateLayout(); |
| } |
| |
| void |
| VideoMixer::setActiveParticipant(Observable<std::shared_ptr<MediaFrame>>* ob) |
| { |
| activeSource_ = ob; |
| updateLayout(); |
| } |
| |
| void |
| VideoMixer::updateLayout() |
| { |
| layoutUpdated_ += 1; |
| } |
| |
| void |
| VideoMixer::attached(Observable<std::shared_ptr<MediaFrame>>* ob) |
| { |
| auto lock(rwMutex_.write()); |
| |
| auto src = std::unique_ptr<VideoMixerSource>(new VideoMixerSource); |
| src->render_frame = std::make_shared<VideoFrame>(); |
| src->source = ob; |
| sources_.emplace_back(std::move(src)); |
| updateLayout(); |
| } |
| |
| void |
| VideoMixer::detached(Observable<std::shared_ptr<MediaFrame>>* ob) |
| { |
| auto lock(rwMutex_.write()); |
| |
| for (const auto& x : sources_) { |
| if (x->source == ob) { |
| // Handle the case where the current shown source leave the conference |
| if (activeSource_ == ob) { |
| currentLayout_ = Layout::GRID; |
| activeSource_ = videoLocalSecondary_ ? videoLocalSecondary_.get() |
| : videoLocal_.get(); |
| } |
| sources_.remove(x); |
| updateLayout(); |
| break; |
| } |
| } |
| } |
| |
| void |
| VideoMixer::update(Observable<std::shared_ptr<MediaFrame>>* ob, |
| const std::shared_ptr<MediaFrame>& frame_p) |
| { |
| auto lock(rwMutex_.read()); |
| |
| for (const auto& x : sources_) { |
| if (x->source == ob) { |
| #ifdef RING_ACCEL |
| std::shared_ptr<VideoFrame> frame; |
| try { |
| frame = HardwareAccel::transferToMainMemory(*std::static_pointer_cast<VideoFrame>( |
| frame_p), |
| AV_PIX_FMT_NV12); |
| x->atomic_copy(*std::static_pointer_cast<VideoFrame>(frame)); |
| } catch (const std::runtime_error& e) { |
| JAMI_ERR("[mixer:%s] Accel failure: %s", id_.c_str(), e.what()); |
| return; |
| } |
| #else |
| x->atomic_copy(*std::static_pointer_cast<VideoFrame>(frame_p)); |
| #endif |
| return; |
| } |
| } |
| } |
| |
| void |
| VideoMixer::process() |
| { |
| nextProcess_ += std::chrono::duration_cast<std::chrono::microseconds>(FRAME_DURATION); |
| const auto delay = nextProcess_ - std::chrono::steady_clock::now(); |
| if (delay.count() > 0) |
| std::this_thread::sleep_for(delay); |
| |
| // Nothing to do. |
| if (width_ == 0 or height_ == 0) { |
| return; |
| } |
| |
| VideoFrame& output = getNewFrame(); |
| try { |
| output.reserve(format_, width_, height_); |
| } catch (const std::bad_alloc& e) { |
| JAMI_ERR("[mixer:%s] VideoFrame::allocBuffer() failed", id_.c_str()); |
| return; |
| } |
| |
| libav_utils::fillWithBlack(output.pointer()); |
| |
| { |
| auto lock(rwMutex_.read()); |
| |
| int i = 0; |
| bool activeFound = false; |
| bool needsUpdate = layoutUpdated_ > 0; |
| bool successfullyRendered = false; |
| for (auto& x : sources_) { |
| /* thread stop pending? */ |
| if (!loop_.isRunning()) |
| return; |
| |
| if (currentLayout_ != Layout::ONE_BIG or activeSource_ == x->source) { |
| // make rendered frame temporarily unavailable for update() |
| // to avoid concurrent access. |
| std::shared_ptr<VideoFrame> input = x->getRenderFrame(); |
| std::shared_ptr<VideoFrame> fooInput = std::make_shared<VideoFrame>(); |
| |
| auto wantedIndex = i; |
| if (currentLayout_ == Layout::ONE_BIG) { |
| wantedIndex = 0; |
| activeFound = true; |
| } else if (currentLayout_ == Layout::ONE_BIG_WITH_SMALL) { |
| if (activeSource_ == x->source) { |
| wantedIndex = 0; |
| activeFound = true; |
| } else if (not activeFound) { |
| wantedIndex += 1; |
| } |
| } |
| |
| auto hasVideo = x->hasVideo; |
| bool blackFrame = false; |
| |
| if (!input->height() or !input->width()) { |
| successfullyRendered = true; |
| fooInput->reserve(format_, width_, height_); |
| blackFrame = true; |
| } else { |
| fooInput.swap(input); |
| } |
| |
| // If orientation changed or if the first valid frame for source |
| // is received -> trigger layout calculation and confInfo update |
| if (x->rotation != fooInput->getOrientation() or !x->w or !x->h) { |
| updateLayout(); |
| needsUpdate = true; |
| } |
| |
| if (needsUpdate) |
| calc_position(x, fooInput, wantedIndex); |
| |
| if (!blackFrame) { |
| if (fooInput) |
| successfullyRendered |= render_frame(output, fooInput, x); |
| else |
| JAMI_WARN("[mixer:%s] Nothing to render for %p", id_.c_str(), x->source); |
| } |
| |
| x->hasVideo = !blackFrame && successfullyRendered; |
| if (hasVideo != x->hasVideo) { |
| updateLayout(); |
| needsUpdate = true; |
| } |
| } else if (needsUpdate) { |
| x->x = 0; |
| x->y = 0; |
| x->w = 0; |
| x->h = 0; |
| x->hasVideo = false; |
| } |
| |
| ++i; |
| } |
| if (needsUpdate and successfullyRendered) { |
| layoutUpdated_ -= 1; |
| if (layoutUpdated_ == 0) { |
| std::vector<SourceInfo> sourcesInfo; |
| sourcesInfo.reserve(sources_.size()); |
| for (auto& x : sources_) { |
| sourcesInfo.emplace_back( |
| SourceInfo {x->source, x->x, x->y, x->w, x->h, x->hasVideo}); |
| } |
| if (onSourcesUpdated_) |
| onSourcesUpdated_(std::move(sourcesInfo)); |
| } |
| } |
| } |
| |
| output.pointer()->pts = av_rescale_q_rnd(av_gettime() - startTime_, |
| {1, AV_TIME_BASE}, |
| {1, MIXER_FRAMERATE}, |
| static_cast<AVRounding>(AV_ROUND_NEAR_INF |
| | AV_ROUND_PASS_MINMAX)); |
| lastTimestamp_ = output.pointer()->pts; |
| publishFrame(); |
| } |
| |
| bool |
| VideoMixer::render_frame(VideoFrame& output, |
| const std::shared_ptr<VideoFrame>& input, |
| std::unique_ptr<VideoMixerSource>& source) |
| { |
| if (!width_ or !height_ or !input->pointer() or input->pointer()->format == -1) |
| return false; |
| |
| int cell_width = source->w; |
| int cell_height = source->h; |
| int xoff = source->x; |
| int yoff = source->y; |
| |
| int angle = input->getOrientation(); |
| const constexpr char filterIn[] = "mixin"; |
| if (angle != source->rotation) { |
| source->rotationFilter = video::getTransposeFilter(angle, |
| filterIn, |
| input->width(), |
| input->height(), |
| input->format(), |
| false); |
| source->rotation = angle; |
| } |
| std::shared_ptr<VideoFrame> frame; |
| if (source->rotationFilter) { |
| source->rotationFilter->feedInput(input->pointer(), filterIn); |
| frame = std::static_pointer_cast<VideoFrame>( |
| std::shared_ptr<MediaFrame>(source->rotationFilter->readOutput())); |
| } else { |
| frame = input; |
| } |
| |
| scaler_.scale_and_pad(*frame, output, xoff, yoff, cell_width, cell_height, true); |
| return true; |
| } |
| |
| void |
| VideoMixer::calc_position(std::unique_ptr<VideoMixerSource>& source, |
| const std::shared_ptr<VideoFrame>& input, |
| int index) |
| { |
| if (!width_ or !height_) |
| return; |
| |
| // Compute cell size/position |
| int cell_width, cell_height, cellW_off, cellH_off; |
| const int n = currentLayout_ == Layout::ONE_BIG ? 1 : sources_.size(); |
| const int zoom = currentLayout_ == Layout::ONE_BIG_WITH_SMALL ? std::max(MIN_LINE_ZOOM, n) |
| : ceil(sqrt(n)); |
| if (currentLayout_ == Layout::ONE_BIG_WITH_SMALL && index == 0) { |
| // In ONE_BIG_WITH_SMALL, the first line at the top is the previews |
| // The rest is the active source |
| cell_width = width_; |
| cell_height = height_ - height_ / zoom; |
| } else { |
| cell_width = width_ / zoom; |
| cell_height = height_ / zoom; |
| } |
| if (currentLayout_ == Layout::ONE_BIG_WITH_SMALL) { |
| if (index == 0) { |
| cellW_off = 0; |
| cellH_off = height_ / zoom; // First line height |
| } else { |
| cellW_off = (index - 1) * cell_width; |
| // Show sources in center |
| cellW_off += (width_ - (n - 1) * cell_width) / 2; |
| cellH_off = 0; |
| } |
| } else { |
| cellW_off = (index % zoom) * cell_width; |
| if (currentLayout_ == Layout::GRID && n % zoom != 0 && index >= (zoom * ((n - 1) / zoom))) { |
| // Last line, center participants if not full |
| cellW_off += (width_ - (n % zoom) * cell_width) / 2; |
| } |
| cellH_off = (index / zoom) * cell_height; |
| } |
| |
| // Compute frame size/position |
| float zoomW, zoomH; |
| int frameW, frameH, frameW_off, frameH_off; |
| |
| if (input->getOrientation() % 180) { |
| // Rotated frame |
| zoomW = (float) input->height() / cell_width; |
| zoomH = (float) input->width() / cell_height; |
| frameH = std::round(input->width() / std::max(zoomW, zoomH)); |
| frameW = std::round(input->height() / std::max(zoomW, zoomH)); |
| } else { |
| zoomW = (float) input->width() / cell_width; |
| zoomH = (float) input->height() / cell_height; |
| frameW = std::round(input->width() / std::max(zoomW, zoomH)); |
| frameH = std::round(input->height() / std::max(zoomW, zoomH)); |
| } |
| |
| // Center the frame in the cell |
| frameW_off = cellW_off + (cell_width - frameW) / 2; |
| frameH_off = cellH_off + (cell_height - frameH) / 2; |
| |
| // Update source's cache |
| source->w = frameW; |
| source->h = frameH; |
| source->x = frameW_off; |
| source->y = frameH_off; |
| } |
| |
| void |
| VideoMixer::setParameters(int width, int height, AVPixelFormat format) |
| { |
| auto lock(rwMutex_.write()); |
| |
| width_ = width; |
| height_ = height; |
| format_ = format; |
| |
| // cleanup the previous frame to have a nice copy in rendering method |
| std::shared_ptr<VideoFrame> previous_p(obtainLastFrame()); |
| if (previous_p) |
| libav_utils::fillWithBlack(previous_p->pointer()); |
| |
| start_sink(); |
| updateLayout(); |
| startTime_ = av_gettime(); |
| } |
| |
| void |
| VideoMixer::start_sink() |
| { |
| stop_sink(); |
| |
| if (width_ == 0 or height_ == 0) { |
| JAMI_WARN("[mixer:%s] MX: unable to start with zero-sized output", id_.c_str()); |
| return; |
| } |
| |
| if (not sink_->start()) { |
| JAMI_ERR("[mixer:%s] MX: sink startup failed", id_.c_str()); |
| return; |
| } |
| |
| if (this->attach(sink_.get())) |
| sink_->setFrameSize(width_, height_); |
| } |
| |
| void |
| VideoMixer::stop_sink() |
| { |
| this->detach(sink_.get()); |
| sink_->stop(); |
| } |
| |
| int |
| VideoMixer::getWidth() const |
| { |
| return width_; |
| } |
| |
| int |
| VideoMixer::getHeight() const |
| { |
| return height_; |
| } |
| |
| AVPixelFormat |
| VideoMixer::getPixelFormat() const |
| { |
| return format_; |
| } |
| |
| MediaStream |
| VideoMixer::getStream(const std::string& name) const |
| { |
| MediaStream ms; |
| ms.name = name; |
| ms.format = format_; |
| ms.isVideo = true; |
| ms.height = height_; |
| ms.width = width_; |
| ms.frameRate = {MIXER_FRAMERATE, 1}; |
| ms.timeBase = {1, MIXER_FRAMERATE}; |
| ms.firstTimestamp = lastTimestamp_; |
| |
| return ms; |
| } |
| |
| } // namespace video |
| } // namespace jami |