| /* |
| * Copyright (C) 2004-2021 Savoir-faire Linux Inc. |
| * |
| * Author: Tristan Matthews <tristan.matthews@savoirfairelinux.com> |
| * Author: Guillaume Roguez <guillaume.roguez@savoirfairelinux.com> |
| * Author: Alexandre Lision <alexandre.lision@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. |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include "config.h" |
| #endif |
| |
| #include "sinkclient.h" |
| |
| #if HAVE_SHM |
| #include "shm_header.h" |
| #endif // HAVE_SHM |
| |
| #include "media_buffer.h" |
| #include "logger.h" |
| #include "noncopyable.h" |
| #include "client/ring_signal.h" |
| #include "jami/videomanager_interface.h" |
| #include "libav_utils.h" |
| #include "video_scaler.h" |
| #include "smartools.h" |
| #include "media_filter.h" |
| #include "filter_transpose.h" |
| |
| #ifdef RING_ACCEL |
| #include "accel.h" |
| #endif |
| |
| #ifndef _WIN32 |
| #include <sys/mman.h> |
| #endif |
| #include <ciso646> // fix windows compiler bug |
| #include <fcntl.h> |
| #include <cstdio> |
| #include <sstream> |
| #include <unistd.h> |
| #include <cerrno> |
| #include <cstring> |
| #include <stdexcept> |
| #include <cmath> |
| |
| namespace jami { |
| namespace video { |
| |
| const constexpr char FILTER_INPUT_NAME[] = "in"; |
| |
| #if HAVE_SHM |
| // RAII class helper on sem_wait/sem_post sempahore operations |
| class SemGuardLock |
| { |
| public: |
| explicit SemGuardLock(sem_t& mutex) |
| : m_(mutex) |
| { |
| auto ret = ::sem_wait(&m_); |
| if (ret < 0) { |
| std::ostringstream msg; |
| msg << "SHM mutex@" << &m_ << " lock failed (" << ret << ")"; |
| throw std::logic_error {msg.str()}; |
| } |
| } |
| |
| ~SemGuardLock() { ::sem_post(&m_); } |
| |
| private: |
| sem_t& m_; |
| }; |
| |
| class ShmHolder |
| { |
| public: |
| ShmHolder(const std::string& name = {}); |
| ~ShmHolder(); |
| |
| std::string name() const noexcept { return openedName_; } |
| |
| void renderFrame(const VideoFrame& src) noexcept; |
| |
| private: |
| bool resizeArea(std::size_t desired_length) noexcept; |
| char* getShmAreaDataPtr() noexcept; |
| |
| void unMapShmArea() noexcept |
| { |
| if (area_ != MAP_FAILED and ::munmap(area_, areaSize_) < 0) { |
| JAMI_ERR("[ShmHolder:%s] munmap(%zu) failed with errno %d", |
| openedName_.c_str(), |
| areaSize_, |
| errno); |
| } |
| } |
| |
| SHMHeader* area_ {static_cast<SHMHeader*>(MAP_FAILED)}; |
| std::size_t areaSize_ {0}; |
| std::string openedName_; |
| int fd_ {-1}; |
| }; |
| |
| ShmHolder::ShmHolder(const std::string& name) |
| { |
| static constexpr int flags = O_RDWR | O_CREAT | O_TRUNC | O_EXCL; |
| static constexpr int perms = S_IRUSR | S_IWUSR; |
| |
| static auto shmFailedWithErrno = [this](const std::string& what) { |
| std::ostringstream msg; |
| msg << "ShmHolder[" << openedName_ << "]: " << what << " failed, errno=" << errno; |
| throw std::runtime_error {msg.str()}; |
| }; |
| |
| if (not name.empty()) { |
| openedName_ = name; |
| fd_ = ::shm_open(openedName_.c_str(), flags, perms); |
| if (fd_ < 0) |
| shmFailedWithErrno("shm_open"); |
| } else { |
| for (int i = 0; fd_ < 0; ++i) { |
| std::ostringstream tmpName; |
| tmpName << PACKAGE_NAME << "_shm_" << getpid() << "_" << i; |
| openedName_ = tmpName.str(); |
| fd_ = ::shm_open(openedName_.c_str(), flags, perms); |
| if (fd_ < 0 and errno != EEXIST) |
| shmFailedWithErrno("shm_open"); |
| } |
| } |
| |
| // Set size enough for header only (no frame data) |
| if (!resizeArea(0)) |
| shmFailedWithErrno("resizeArea"); |
| |
| // Header fields initialization |
| std::memset(area_, 0, areaSize_); |
| |
| if (::sem_init(&area_->mutex, 1, 1) < 0) |
| shmFailedWithErrno("sem_init(mutex)"); |
| |
| if (::sem_init(&area_->frameGenMutex, 1, 0) < 0) |
| shmFailedWithErrno("sem_init(frameGenMutex)"); |
| |
| JAMI_DBG("[ShmHolder:%s] New holder created", openedName_.c_str()); |
| } |
| |
| ShmHolder::~ShmHolder() |
| { |
| if (fd_ < 0) |
| return; |
| |
| ::close(fd_); |
| ::shm_unlink(openedName_.c_str()); |
| |
| if (area_ == MAP_FAILED) |
| return; |
| |
| ::sem_wait(&area_->mutex); |
| area_->frameSize = 0; |
| ::sem_post(&area_->mutex); |
| |
| ::sem_post(&area_->frameGenMutex); // unlock waiting client before leaving |
| unMapShmArea(); |
| } |
| |
| bool |
| ShmHolder::resizeArea(std::size_t frameSize) noexcept |
| { |
| // aligned on 16-byte boundary frameSize |
| frameSize = (frameSize + 15) & ~15; |
| |
| if (area_ != MAP_FAILED and frameSize == area_->frameSize) |
| return true; |
| |
| // full area size: +15 to take care of maximum padding size |
| const auto areaSize = sizeof(SHMHeader) + 2 * frameSize + 15; |
| JAMI_DBG("[ShmHolder:%s] New size: f=%zu, a=%zu", openedName_.c_str(), frameSize, areaSize); |
| |
| unMapShmArea(); |
| |
| if (::ftruncate(fd_, areaSize) < 0) { |
| JAMI_ERR("[ShmHolder:%s] ftruncate(%zu) failed with errno %d", |
| openedName_.c_str(), |
| areaSize, |
| errno); |
| return false; |
| } |
| |
| area_ = static_cast<SHMHeader*>( |
| ::mmap(nullptr, areaSize, PROT_READ | PROT_WRITE, MAP_SHARED, fd_, 0)); |
| |
| if (area_ == MAP_FAILED) { |
| areaSize_ = 0; |
| JAMI_ERR("[ShmHolder:%s] mmap(%zu) failed with errno %d", |
| openedName_.c_str(), |
| areaSize, |
| errno); |
| return false; |
| } |
| |
| areaSize_ = areaSize; |
| |
| if (frameSize) { |
| SemGuardLock lk {area_->mutex}; |
| |
| area_->frameSize = frameSize; |
| area_->mapSize = areaSize; |
| |
| // Compute aligned IO pointers |
| // Note: we not using std::align as not implemented in 4.9 |
| // https://gcc.gnu.org/bugzilla/show_bug.cgi?id=57350 |
| auto p = reinterpret_cast<std::uintptr_t>(area_->data); |
| area_->writeOffset = ((p + 15) & ~15) - p; |
| area_->readOffset = area_->writeOffset + frameSize; |
| } |
| |
| return true; |
| } |
| |
| void |
| ShmHolder::renderFrame(const VideoFrame& src) noexcept |
| { |
| const auto width = src.width(); |
| const auto height = src.height(); |
| const auto format = AV_PIX_FMT_BGRA; |
| const auto frameSize = videoFrameSize(format, width, height); |
| |
| if (!resizeArea(frameSize)) { |
| JAMI_ERR("[ShmHolder:%s] Could not resize area size: %dx%d, format: %d", |
| openedName_.c_str(), |
| width, |
| height, |
| format); |
| return; |
| } |
| |
| { |
| VideoFrame dst; |
| VideoScaler scaler; |
| |
| dst.setFromMemory(area_->data + area_->writeOffset, format, width, height); |
| scaler.scale(src, dst); |
| } |
| |
| { |
| SemGuardLock lk {area_->mutex}; |
| |
| ++area_->frameGen; |
| std::swap(area_->readOffset, area_->writeOffset); |
| ::sem_post(&area_->frameGenMutex); |
| } |
| } |
| |
| std::string |
| SinkClient::openedName() const noexcept |
| { |
| if (shm_) |
| return shm_->name(); |
| return {}; |
| } |
| |
| bool |
| SinkClient::start() noexcept |
| { |
| if (not shm_) { |
| try { |
| char* envvar = getenv("JAMI_DISABLE_SHM"); |
| if (envvar) // Do not use SHM if set |
| return true; |
| shm_ = std::make_shared<ShmHolder>(); |
| JAMI_DBG("[Sink:%p] Shared memory [%s] created", this, openedName().c_str()); |
| } catch (const std::runtime_error& e) { |
| JAMI_ERR("[Sink:%p] Failed to create shared memory: %s", this, e.what()); |
| } |
| } |
| |
| return static_cast<bool>(shm_); |
| } |
| |
| bool |
| SinkClient::stop() noexcept |
| { |
| setFrameSize(0, 0); |
| setCrop(0, 0, 0, 0); |
| shm_.reset(); |
| return true; |
| } |
| |
| #else // HAVE_SHM |
| |
| std::string |
| SinkClient::openedName() const noexcept |
| { |
| return {}; |
| } |
| |
| bool |
| SinkClient::start() noexcept |
| { |
| return true; |
| } |
| |
| bool |
| SinkClient::stop() noexcept |
| { |
| setFrameSize(0, 0); |
| setCrop(0, 0, 0, 0); |
| return true; |
| } |
| |
| #endif // !HAVE_SHM |
| |
| SinkClient::SinkClient(const std::string& id, bool mixer) |
| : id_ {id} |
| , mixer_(mixer) |
| , scaler_(new VideoScaler()) |
| #ifdef DEBUG_FPS |
| , frameCount_(0u) |
| , lastFrameDebug_(std::chrono::system_clock::now()) |
| #endif |
| { |
| JAMI_DBG("[Sink:%p] Sink [%s] created", this, getId().c_str()); |
| } |
| |
| void |
| SinkClient::update(Observable<std::shared_ptr<MediaFrame>>* /*obs*/, |
| const std::shared_ptr<MediaFrame>& frame_p) |
| { |
| #ifdef DEBUG_FPS |
| auto currentTime = std::chrono::system_clock::now(); |
| const std::chrono::duration<double> seconds = currentTime - lastFrameDebug_; |
| ++frameCount_; |
| if (seconds.count() > 1) { |
| auto fps = frameCount_ / seconds.count(); |
| // Send the framerate in smartInfo |
| Smartools::getInstance().setFrameRate(id_, std::to_string(fps)); |
| frameCount_ = 0; |
| lastFrameDebug_ = currentTime; |
| } |
| #endif |
| |
| std::lock_guard<std::mutex> lock(mtx_); |
| if (avTarget_.push) { |
| auto outFrame = std::make_unique<VideoFrame>(); |
| outFrame->copyFrom(*std::static_pointer_cast<VideoFrame>(frame_p)); |
| if (crop_.w || crop_.h) { |
| outFrame->pointer()->crop_top = crop_.y; |
| outFrame->pointer()->crop_bottom = (size_t) outFrame->height() - crop_.y - crop_.h; |
| outFrame->pointer()->crop_left = crop_.x; |
| outFrame->pointer()->crop_right = (size_t) outFrame->width() - crop_.x - crop_.w; |
| av_frame_apply_cropping(outFrame->pointer(), AV_FRAME_CROP_UNALIGNED); |
| } |
| if (outFrame->height() != height_ || outFrame->width() != width_) { |
| setFrameSize(0, 0); |
| setFrameSize(outFrame->width(), outFrame->height()); |
| return; |
| } |
| avTarget_.push(std::move(outFrame)); |
| return; |
| } |
| |
| bool doTransfer = (target_.pull != nullptr); |
| #if HAVE_SHM |
| doTransfer |= (shm_ != nullptr); |
| #endif |
| |
| if (doTransfer) { |
| std::shared_ptr<VideoFrame> frame = std::make_shared<VideoFrame>(); |
| #ifdef RING_ACCEL |
| auto desc = av_pix_fmt_desc_get( |
| (AVPixelFormat) (std::static_pointer_cast<VideoFrame>(frame_p))->format()); |
| if (desc && (desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) { |
| try { |
| frame = HardwareAccel::transferToMainMemory(*std::static_pointer_cast<VideoFrame>( |
| frame_p), |
| AV_PIX_FMT_NV12); |
| } catch (const std::runtime_error& e) { |
| JAMI_ERR("[Sink:%p] Transfert to hardware acceleration memory failed: %s", |
| this, |
| e.what()); |
| return; |
| } |
| } else |
| #endif |
| frame->copyFrom(*std::static_pointer_cast<VideoFrame>(frame_p)); |
| |
| int angle = frame->getOrientation(); |
| |
| if (angle != rotation_) { |
| filter_ = getTransposeFilter(angle, |
| FILTER_INPUT_NAME, |
| frame->width(), |
| frame->height(), |
| frame->format(), |
| false); |
| rotation_ = angle; |
| } |
| if (filter_) { |
| filter_->feedInput(frame->pointer(), FILTER_INPUT_NAME); |
| frame = std::static_pointer_cast<VideoFrame>( |
| std::shared_ptr<MediaFrame>(filter_->readOutput())); |
| } |
| |
| if (crop_.w || crop_.h) { |
| frame->pointer()->crop_top = crop_.y; |
| frame->pointer()->crop_bottom = (size_t) frame->height() - crop_.y - crop_.h; |
| frame->pointer()->crop_left = crop_.x; |
| frame->pointer()->crop_right = (size_t) frame->width() - crop_.x - crop_.w; |
| av_frame_apply_cropping(frame->pointer(), AV_FRAME_CROP_UNALIGNED); |
| } |
| |
| if (frame->height() != height_ || frame->width() != width_) { |
| setFrameSize(0, 0); |
| setFrameSize(frame->width(), frame->height()); |
| return; |
| } |
| #if HAVE_SHM |
| if (shm_) |
| shm_->renderFrame(*frame); |
| #endif |
| if (target_.pull) { |
| int width = frame->width(); |
| int height = frame->height(); |
| #if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_IPHONE) |
| const int format = AV_PIX_FMT_RGBA; |
| #else |
| const int format = AV_PIX_FMT_BGRA; |
| #endif |
| const auto bytes = videoFrameSize(format, width, height); |
| if (bytes > 0) { |
| if (auto buffer_ptr = target_.pull(bytes)) { |
| if (buffer_ptr->avframe) { |
| scaler_->scale(*frame, buffer_ptr->avframe.get()); |
| } else { |
| buffer_ptr->format = format; |
| buffer_ptr->width = width; |
| buffer_ptr->height = height; |
| VideoFrame dst; |
| dst.setFromMemory(buffer_ptr->ptr, format, width, height); |
| scaler_->scale(*frame, dst); |
| } |
| target_.push(std::move(buffer_ptr)); |
| } |
| } |
| } |
| } |
| } |
| |
| void |
| SinkClient::setFrameSize(int width, int height) |
| { |
| width_ = width; |
| height_ = height; |
| if (width > 0 and height > 0) { |
| JAMI_DBG("[Sink:%p] Started - size=%dx%d, mixer=%s", |
| this, |
| width, |
| height, |
| mixer_ ? "Yes" : "No"); |
| emitSignal<DRing::VideoSignal::DecodingStarted>(getId(), openedName(), width, height, mixer_); |
| started_ = true; |
| } else if (started_) { |
| JAMI_DBG("[Sink:%p] Stopped - size=%dx%d, mixer=%s", |
| this, |
| width, |
| height, |
| mixer_ ? "Yes" : "No"); |
| emitSignal<DRing::VideoSignal::DecodingStopped>(getId(), openedName(), mixer_); |
| started_ = false; |
| } |
| } |
| |
| void |
| SinkClient::setCrop(int x, int y, int w, int h) |
| { |
| JAMI_DBG("[Sink:%p] Change crop to [%dx%d at (%d, %d)]", this, w, h, x, y); |
| crop_.x = x; |
| crop_.y = y; |
| crop_.w = w; |
| crop_.h = h; |
| } |
| |
| } // namespace video |
| } // namespace jami |