| /* |
| * Copyright (C) 2004-2021 Savoir-faire Linux Inc. |
| * |
| * Author: Tristan Matthews <tristan.matthews@savoirfairelinux.com> |
| * Author: Vivien Didelot <vivien.didelot@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. |
| */ |
| |
| #ifdef HAVE_CONFIG_H |
| #include "config.h" |
| #endif |
| |
| #include "video_input.h" |
| |
| #include "media_decoder.h" |
| #include "media_const.h" |
| #include "manager.h" |
| #include "client/videomanager.h" |
| #include "client/ring_signal.h" |
| #include "sinkclient.h" |
| #include "logger.h" |
| #include "media/media_buffer.h" |
| |
| #include <libavformat/avio.h> |
| |
| #include <string> |
| #include <sstream> |
| #include <cassert> |
| #ifdef _MSC_VER |
| #include <io.h> // for access |
| #else |
| #include <unistd.h> |
| #endif |
| extern "C" { |
| #include <libavutil/display.h> |
| } |
| |
| namespace jami { |
| namespace video { |
| |
| static constexpr unsigned default_grab_width = 640; |
| static constexpr unsigned default_grab_height = 480; |
| |
| VideoInput::VideoInput(VideoInputMode inputMode, const std::string& id_) |
| : VideoGenerator::VideoGenerator() |
| , loop_(std::bind(&VideoInput::setup, this), |
| std::bind(&VideoInput::process, this), |
| std::bind(&VideoInput::cleanup, this)) |
| { |
| inputMode_ = inputMode; |
| if (inputMode_ == VideoInputMode::Undefined) { |
| #if (defined(__ANDROID__) || defined(RING_UWP) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS)) |
| inputMode_ = VideoInputMode::ManagedByClient; |
| #else |
| inputMode_ = VideoInputMode::ManagedByDaemon; |
| #endif |
| } |
| #ifdef __ANDROID__ |
| sink_ = Manager::instance().createSinkClient(id_); |
| #else |
| if (inputMode_ == VideoInputMode::ManagedByDaemon) { |
| sink_ = Manager::instance().createSinkClient(id_); |
| } |
| #endif |
| switchInput(id_); |
| } |
| |
| VideoInput::~VideoInput() |
| { |
| isStopped_ = true; |
| if (videoManagedByClient()) { |
| emitSignal<DRing::VideoSignal::StopCapture>(decOpts_.input); |
| capturing_ = false; |
| return; |
| } |
| loop_.join(); |
| } |
| |
| void |
| VideoInput::startLoop() |
| { |
| if (videoManagedByClient()) { |
| switchDevice(); |
| return; |
| } |
| if (!loop_.isRunning()) |
| loop_.start(); |
| } |
| |
| void |
| VideoInput::switchDevice() |
| { |
| if (switchPending_.exchange(false)) { |
| JAMI_DBG("Switching input to '%s'", decOpts_.input.c_str()); |
| if (decOpts_.input.empty()) { |
| capturing_ = false; |
| return; |
| } |
| |
| emitSignal<DRing::VideoSignal::StartCapture>(decOpts_.input); |
| capturing_ = true; |
| } |
| } |
| |
| int |
| VideoInput::getWidth() const |
| { |
| if (videoManagedByClient()) { |
| return decOpts_.width; |
| } |
| return decoder_->getWidth(); |
| } |
| |
| int |
| VideoInput::getHeight() const |
| { |
| if (videoManagedByClient()) { |
| return decOpts_.height; |
| } |
| return decoder_->getHeight(); |
| } |
| |
| AVPixelFormat |
| VideoInput::getPixelFormat() const |
| { |
| if (!videoManagedByClient()) { |
| return decoder_->getPixelFormat(); |
| } |
| return (AVPixelFormat) std::stoi(decOpts_.format); |
| } |
| |
| void |
| VideoInput::setRotation(int angle) |
| { |
| std::shared_ptr<AVBufferRef> displayMatrix {av_buffer_alloc(sizeof(int32_t) * 9), |
| [](AVBufferRef* buf) { |
| av_buffer_unref(&buf); |
| }}; |
| if (displayMatrix) { |
| av_display_rotation_set(reinterpret_cast<int32_t*>(displayMatrix->data), angle); |
| displayMatrix_ = std::move(displayMatrix); |
| } |
| } |
| |
| bool |
| VideoInput::setup() |
| { |
| if (not attach(sink_.get())) { |
| JAMI_ERR("attach sink failed"); |
| return false; |
| } |
| |
| if (!sink_->start()) |
| JAMI_ERR("start sink failed"); |
| |
| JAMI_DBG("VideoInput ready to capture"); |
| |
| return true; |
| } |
| |
| void |
| VideoInput::process() |
| { |
| if (playingFile_) { |
| if (paused_) { |
| std::this_thread::sleep_for(std::chrono::milliseconds(20)); |
| return; |
| } |
| decoder_->emitFrame(false); |
| return; |
| } |
| if (switchPending_) |
| createDecoder(); |
| |
| if (not captureFrame()) { |
| loop_.stop(); |
| return; |
| } |
| } |
| |
| void |
| VideoInput::setSeekTime(int64_t time) |
| { |
| if (decoder_) { |
| decoder_->setSeekTime(time); |
| } |
| } |
| |
| void |
| VideoInput::cleanup() |
| { |
| deleteDecoder(); // do it first to let a chance to last frame to be displayed |
| stopSink(); |
| JAMI_DBG("VideoInput closed"); |
| } |
| |
| bool |
| VideoInput::captureFrame() |
| { |
| // Return true if capture could continue, false if must be stop |
| if (not decoder_) |
| return false; |
| |
| switch (decoder_->decode()) { |
| case MediaDemuxer::Status::EndOfFile: |
| createDecoder(); |
| return static_cast<bool>(decoder_); |
| case MediaDemuxer::Status::ReadError: |
| JAMI_ERR() << "Failed to decode frame"; |
| return false; |
| default: |
| return true; |
| } |
| } |
| void |
| VideoInput::flushBuffers() |
| { |
| if (decoder_) { |
| decoder_->flushBuffers(); |
| } |
| } |
| |
| void |
| VideoInput::configureFilePlayback(const std::string&, |
| std::shared_ptr<MediaDemuxer>& demuxer, |
| int index) |
| { |
| deleteDecoder(); |
| clearOptions(); |
| |
| auto decoder = std::make_unique<MediaDecoder>(demuxer, |
| index, |
| [this](std::shared_ptr<MediaFrame>&& frame) { |
| publishFrame( |
| std::static_pointer_cast<VideoFrame>( |
| frame)); |
| }); |
| decoder->setInterruptCallback( |
| [](void* data) -> int { return not static_cast<VideoInput*>(data)->isCapturing(); }, this); |
| decoder->emulateRate(); |
| |
| decoder_ = std::move(decoder); |
| playingFile_ = true; |
| loop_.start(); |
| |
| /* Signal the client about readable sink */ |
| sink_->setFrameSize(decoder_->getWidth(), decoder_->getHeight()); |
| } |
| |
| void |
| VideoInput::createDecoder() |
| { |
| deleteDecoder(); |
| |
| switchPending_ = false; |
| |
| if (decOpts_.input.empty()) { |
| foundDecOpts(decOpts_); |
| return; |
| } |
| |
| auto decoder = std::make_unique<MediaDecoder>( |
| [this](const std::shared_ptr<MediaFrame>& frame) mutable { |
| publishFrame(std::static_pointer_cast<VideoFrame>(frame)); |
| }); |
| |
| if (emulateRate_) |
| decoder->emulateRate(); |
| |
| decoder->setInterruptCallback( |
| [](void* data) -> int { return not static_cast<VideoInput*>(data)->isCapturing(); }, this); |
| |
| bool ready = false, restartSink = false; |
| if (decOpts_.format == "x11grab" && !decOpts_.is_area) { |
| decOpts_.width = 0; |
| decOpts_.height = 0; |
| } |
| while (!ready && !isStopped_) { |
| // Retry to open the video till the input is opened |
| auto ret = decoder->openInput(decOpts_); |
| ready = ret >= 0; |
| if (ret < 0 && -ret != EBUSY) { |
| JAMI_ERR("Could not open input \"%s\" with status %i", decOpts_.input.c_str(), ret); |
| foundDecOpts(decOpts_); |
| return; |
| } else if (-ret == EBUSY) { |
| // If the device is busy, this means that it can be used by another call. |
| // If this is the case, cleanup() can occurs and this will erase shmPath_ |
| // So, be sure to regenerate a correct shmPath for clients. |
| restartSink = true; |
| } |
| std::this_thread::sleep_for(std::chrono::milliseconds(10)); |
| } |
| |
| if (isStopped_) |
| return; |
| |
| if (restartSink && !isStopped_) { |
| sink_->start(); |
| } |
| |
| /* Data available, finish the decoding */ |
| if (decoder->setupVideo() < 0) { |
| JAMI_ERR("decoder IO startup failed"); |
| foundDecOpts(decOpts_); |
| return; |
| } |
| |
| auto ret = decoder->decode(); // Populate AVCodecContext fields |
| if (ret == MediaDemuxer::Status::ReadError) { |
| JAMI_INFO() << "Decoder error"; |
| return; |
| } |
| |
| decOpts_.width = ((decoder->getWidth() >> 3) << 3); |
| decOpts_.height = ((decoder->getHeight() >> 3) << 3); |
| decOpts_.framerate = decoder->getFps(); |
| AVPixelFormat fmt = decoder->getPixelFormat(); |
| if (fmt != AV_PIX_FMT_NONE) { |
| decOpts_.pixel_format = av_get_pix_fmt_name(fmt); |
| } else { |
| JAMI_WARN("Could not determine pixel format, using default"); |
| decOpts_.pixel_format = av_get_pix_fmt_name(AV_PIX_FMT_YUV420P); |
| } |
| |
| JAMI_DBG("created decoder with video params : size=%dX%d, fps=%lf pix=%s", |
| decOpts_.width, |
| decOpts_.height, |
| decOpts_.framerate.real(), |
| decOpts_.pixel_format.c_str()); |
| if (onSuccessfulSetup_) |
| onSuccessfulSetup_(MEDIA_VIDEO, 0); |
| |
| decoder_ = std::move(decoder); |
| foundDecOpts(decOpts_); |
| |
| /* Signal the client about readable sink */ |
| sink_->setFrameSize(decoder_->getWidth(), decoder_->getHeight()); |
| } |
| |
| void |
| VideoInput::deleteDecoder() |
| { |
| if (not decoder_) |
| return; |
| flushFrames(); |
| decoder_.reset(); |
| } |
| |
| void |
| VideoInput::stopInput() |
| { |
| clearOptions(); |
| loop_.stop(); |
| } |
| |
| void |
| VideoInput::clearOptions() |
| { |
| decOpts_ = {}; |
| emulateRate_ = false; |
| } |
| |
| bool |
| VideoInput::isCapturing() const noexcept |
| { |
| if (videoManagedByClient()) { |
| return capturing_; |
| } |
| return loop_.isRunning(); |
| } |
| |
| bool |
| VideoInput::initCamera(const std::string& device) |
| { |
| decOpts_ = jami::getVideoDeviceMonitor().getDeviceParams(device); |
| return true; |
| } |
| |
| static constexpr unsigned |
| round2pow(unsigned i, unsigned n) |
| { |
| return (i >> n) << n; |
| } |
| |
| bool |
| VideoInput::initX11(const std::string& display) |
| { |
| // Patterns |
| // full screen sharing : :1+0,0 2560x1440 - SCREEN 1, POSITION 0X0, RESOLUTION 2560X1440 |
| // area sharing : :1+882,211 1532x779 - SCREEN 1, POSITION 882x211, RESOLUTION 1532x779 |
| // window sharing : :+1,0 0x0 window-id:0x0340021e - POSITION 0X0 |
| size_t space = display.find(' '); |
| std::string windowIdStr = "window-id:"; |
| size_t winIdPos = display.find(windowIdStr); |
| |
| DeviceParams p = jami::getVideoDeviceMonitor().getDeviceParams(DEVICE_DESKTOP); |
| if (winIdPos != std::string::npos) { |
| p.window_id = display.substr(winIdPos + windowIdStr.size()); // "0x0340021e"; |
| p.is_area = 0; |
| } |
| if (space != std::string::npos) { |
| p.input = display.substr(1, space); |
| if (p.window_id.empty()) { |
| p.input = display.substr(0, space); |
| JAMI_INFO() << "p.window_id.empty()"; |
| auto splits = jami::split_string_to_unsigned(display.substr(space + 1), 'x'); |
| // round to 8 pixel block |
| p.width = round2pow(splits[0], 3); |
| p.height = round2pow(splits[1], 3); |
| p.is_area = 1; |
| } |
| } else { |
| p.input = display; |
| p.width = default_grab_width; |
| p.height = default_grab_height; |
| p.is_area = 1; |
| } |
| |
| auto dec = std::make_unique<MediaDecoder>(); |
| if (dec->openInput(p) < 0 || dec->setupVideo() < 0) |
| return initCamera(jami::getVideoDeviceMonitor().getDefaultDevice()); |
| |
| clearOptions(); |
| decOpts_ = p; |
| decOpts_.width = round2pow(dec->getStream().width, 3); |
| decOpts_.height = round2pow(dec->getStream().height, 3); |
| |
| return true; |
| } |
| |
| bool |
| VideoInput::initAVFoundation(const std::string& display) |
| { |
| size_t space = display.find(' '); |
| |
| clearOptions(); |
| decOpts_.format = "avfoundation"; |
| decOpts_.pixel_format = "nv12"; |
| decOpts_.name = "Capture screen 0"; |
| decOpts_.input = "Capture screen 0"; |
| decOpts_.framerate = jami::getVideoDeviceMonitor().getDeviceParams(DEVICE_DESKTOP).framerate; |
| |
| if (space != std::string::npos) { |
| std::istringstream iss(display.substr(space + 1)); |
| char sep; |
| unsigned w, h; |
| iss >> w >> sep >> h; |
| decOpts_.width = round2pow(w, 3); |
| decOpts_.height = round2pow(h, 3); |
| } else { |
| decOpts_.width = default_grab_width; |
| decOpts_.height = default_grab_height; |
| } |
| return true; |
| } |
| |
| bool |
| VideoInput::initGdiGrab(const std::string& params) |
| { |
| size_t space = params.find(' '); |
| clearOptions(); |
| decOpts_ = jami::getVideoDeviceMonitor().getDeviceParams(DEVICE_DESKTOP); |
| |
| if (space != std::string::npos) { |
| std::istringstream iss(params.substr(space + 1)); |
| char sep; |
| unsigned w, h; |
| iss >> w >> sep >> h; |
| decOpts_.width = round2pow(w, 3); |
| decOpts_.height = round2pow(h, 3); |
| |
| size_t plus = params.find('+'); |
| std::istringstream dss(params.substr(plus + 1, space - plus)); |
| dss >> decOpts_.offset_x >> sep >> decOpts_.offset_y; |
| } else { |
| decOpts_.width = default_grab_width; |
| decOpts_.height = default_grab_height; |
| } |
| |
| return true; |
| } |
| |
| bool |
| VideoInput::initFile(std::string path) |
| { |
| size_t dot = path.find_last_of('.'); |
| std::string ext = dot == std::string::npos ? "" : path.substr(dot + 1); |
| |
| /* File exists? */ |
| if (access(path.c_str(), R_OK) != 0) { |
| JAMI_ERR("file '%s' unavailable\n", path.c_str()); |
| return false; |
| } |
| |
| // check if file has video, fall back to default device if none |
| // FIXME the way this is done is hackish, but it can't be done in createDecoder because that |
| // would break the promise returned in switchInput |
| DeviceParams p; |
| p.input = path; |
| p.name = path; |
| auto dec = std::make_unique<MediaDecoder>(); |
| if (dec->openInput(p) < 0 || dec->setupVideo() < 0) { |
| return initCamera(jami::getVideoDeviceMonitor().getDefaultDevice()); |
| } |
| |
| clearOptions(); |
| emulateRate_ = true; |
| decOpts_.input = path; |
| decOpts_.name = path; |
| decOpts_.loop = "1"; |
| |
| // Force 1fps for static image |
| if (ext == "jpeg" || ext == "jpg" || ext == "png") { |
| decOpts_.format = "image2"; |
| decOpts_.framerate = 1; |
| } else { |
| JAMI_WARN("Guessing file type for %s", path.c_str()); |
| } |
| |
| return false; |
| } |
| |
| std::shared_future<DeviceParams> |
| VideoInput::switchInput(const std::string& resource) |
| { |
| if (resource == currentResource_) |
| return futureDecOpts_; |
| |
| JAMI_DBG("MRL: '%s'", resource.c_str()); |
| |
| if (switchPending_.exchange(true)) { |
| JAMI_ERR("Video switch already requested"); |
| return {}; |
| } |
| |
| currentResource_ = resource; |
| decOptsFound_ = false; |
| |
| std::promise<DeviceParams> p; |
| foundDecOpts_.swap(p); |
| |
| // Switch off video input? |
| if (resource.empty()) { |
| clearOptions(); |
| futureDecOpts_ = foundDecOpts_.get_future(); |
| startLoop(); |
| return futureDecOpts_; |
| } |
| |
| // Supported MRL schemes |
| static const std::string sep = DRing::Media::VideoProtocolPrefix::SEPARATOR; |
| |
| const auto pos = resource.find(sep); |
| if (pos == std::string::npos) |
| return {}; |
| |
| const auto prefix = resource.substr(0, pos); |
| if ((pos + sep.size()) >= resource.size()) |
| return {}; |
| |
| const auto suffix = resource.substr(pos + sep.size()); |
| |
| bool ready = false; |
| |
| if (prefix == DRing::Media::VideoProtocolPrefix::CAMERA) { |
| /* Video4Linux2 */ |
| ready = initCamera(suffix); |
| } else if (prefix == DRing::Media::VideoProtocolPrefix::DISPLAY) { |
| /* X11 display name */ |
| #ifdef __APPLE__ |
| ready = initAVFoundation(suffix); |
| #elif defined(_WIN32) |
| ready = initGdiGrab(suffix); |
| #else |
| ready = initX11(suffix); |
| #endif |
| } else if (prefix == DRing::Media::VideoProtocolPrefix::FILE) { |
| /* Pathname */ |
| ready = initFile(suffix); |
| } |
| |
| if (ready) { |
| foundDecOpts(decOpts_); |
| } |
| futureDecOpts_ = foundDecOpts_.get_future().share(); |
| startLoop(); |
| return futureDecOpts_; |
| } |
| |
| MediaStream |
| VideoInput::getInfo() const |
| { |
| if (!videoManagedByClient()) { |
| if (decoder_) |
| return decoder_->getStream("v:local"); |
| } |
| auto opts = futureDecOpts_.get(); |
| rational<int> fr(opts.framerate.numerator(), opts.framerate.denominator()); |
| return MediaStream("v:local", |
| av_get_pix_fmt(opts.pixel_format.c_str()), |
| 1 / fr, |
| opts.width, |
| opts.height, |
| 0, |
| fr); |
| } |
| |
| void |
| VideoInput::foundDecOpts(const DeviceParams& params) |
| { |
| if (not decOptsFound_) { |
| decOptsFound_ = true; |
| foundDecOpts_.set_value(params); |
| } |
| } |
| |
| void |
| VideoInput::setSink(const std::string& sinkId) |
| { |
| sink_ = Manager::instance().createSinkClient(sinkId); |
| } |
| |
| void |
| VideoInput::setFrameSize(const int width, const int height) |
| { |
| /* Signal the client about readable sink */ |
| sink_->setFrameSize(width, height); |
| } |
| |
| void |
| VideoInput::setupSink() |
| { |
| setup(); |
| } |
| |
| void |
| VideoInput::stopSink() |
| { |
| detach(sink_.get()); |
| sink_->stop(); |
| } |
| |
| void |
| VideoInput::updateStartTime(int64_t startTime) |
| { |
| if (decoder_) { |
| decoder_->updateStartTime(startTime); |
| } |
| } |
| |
| } // namespace video |
| } // namespace jami |