/* * Copyright (C) 2018-2023 Savoir-faire Linux Inc. * Author: Hugo Lefeuvre * Author: Sébastien Blin * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 2.1 of the License, or (at your option) any later version. * * This library 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 * Lesser General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #include "api/avmodel.h" #include "api/video.h" #include "api/lrc.h" #ifdef ENABLE_LIBWRAP #include "directrenderer.h" #else #include "shmrenderer.h" #endif #include "callbackshandler.h" #include "dbus/callmanager.h" #include "dbus/configurationmanager.h" #include "dbus/videomanager.h" #include "authority/storagehelper.h" #include #include #include #include #include #include // std::sort #include #include #include // for std::put_time #include #include #include #include #include #if defined(Q_OS_UNIX) && !defined(__APPLE__) #include #endif #ifdef WIN32 #include "Windows.h" #include #include #endif namespace lrc { using namespace api; using namespace api::video; using namespace lrc::video; class AVModelPimpl : public QObject { Q_OBJECT public: AVModelPimpl(AVModel& linked, const CallbacksHandler& callbacksHandler); const CallbacksHandler& callbacksHandler; QString getRecordingPath() const; static const QString recorderSavesSubdir; AVModel& linked_; std::mutex renderers_mtx_; std::map> renderers_; QString currentVideoCaptureDevice_ {}; #ifndef ENABLE_LIBWRAP // TODO: Init Video Renderers from daemon (see: // https://git.jami.net/savoirfairelinux/ring-daemon/issues/59) static void stopCameraAndQuit(int); static uint32_t SIZE_RENDERER; #endif /** * Get device via its type * @param type * @return the device name */ QString getDevice(int type) const; /** * Add video::Renderer to renderers_ and start it * @param id * @param size * @param shmPath */ void addRenderer(const QString& id, const QSize& res, const QString& shmPath = {}); /** * Remove renderer from renderers_ * @param id */ void removeRenderer(const QString& id); bool hasRenderer(const QString& id); QSize getRendererSize(const QString& id); Frame getRendererFrame(const QString& id); public Q_SLOTS: /** * Listen from CallbacksHandler when a renderer starts * @param id * @param shmPath * @param width * @param height */ void onDecodingStarted(const QString& id, const QString& shmPath, int width, int height); /** * Listen from CallbacksHandler when a renderer stops * @param id * @param shmPath */ void onDecodingStopped(const QString& id, const QString& shmPath); /** * Detect when a video device is plugged or unplugged */ void slotDeviceEvent(); /** * Detect when an audio device is plugged or unplugged */ void slotAudioDeviceEvent(); /** * Audio volume level * @param id Ringbuffer id * @param level Volume in range [0, 1] */ void slotAudioMeter(const QString& id, float level); /** * Listen from CallbacksHandler when a recorder stopped notice is incoming * @param filePath */ void slotRecordPlaybackStopped(const QString& filePath); }; const QString AVModelPimpl::recorderSavesSubdir = "sent_data"; #ifndef ENABLE_LIBWRAP uint32_t AVModelPimpl::SIZE_RENDERER = 0; #endif AVModel::AVModel(const CallbacksHandler& callbacksHandler) : QObject(nullptr) , pimpl_(std::make_unique(*this, callbacksHandler)) { #ifndef ENABLE_LIBWRAP // Because the client uses DBUS, if a crash occurs, the daemon will not // be able to know it. So, stop the camera if the user was just previewing. std::signal(SIGSEGV, AVModelPimpl::stopCameraAndQuit); std::signal(SIGINT, AVModelPimpl::stopCameraAndQuit); #endif } AVModel::~AVModel() { std::lock_guard lk(pimpl_->renderers_mtx_); for (auto r = pimpl_->renderers_.begin(); r != pimpl_->renderers_.end(); ++r) { (*r).second.reset(); } } QList AVModel::getRenderersInfo(QString id) { QList infoList; std::lock_guard lk(pimpl_->renderers_mtx_); for (auto r = pimpl_->renderers_.begin(); r != pimpl_->renderers_.end(); r++) { MapStringString qmap; auto& rend = r->second; MapStringString mapInfo = rend->getInfos(); if (id.isEmpty() || mapInfo["RENDERER_ID"] == id) { qmap.insert(rend->RES, mapInfo["RES"]); qmap.insert(rend->RENDERER_ID, mapInfo["RENDERER_ID"]); qmap.insert(rend->FPS, mapInfo["FPS"]); infoList.append(qmap); } } return infoList; return {}; } void AVModel::updateRenderersFPSInfo(QString rendererId) { std::unique_lock lk(pimpl_->renderers_mtx_); auto it = std::find_if(pimpl_->renderers_.begin(), pimpl_->renderers_.end(), [&rendererId](const auto& c) { return rendererId == c.second->getInfos()["RENDERER_ID"]; }); if (it != pimpl_->renderers_.end()) Q_EMIT onRendererFpsChange(qMakePair(rendererId, it->second->getInfos()["FPS"])); } bool AVModel::getDecodingAccelerated() const { bool result = VideoManager::instance().getDecodingAccelerated(); return result; } void AVModel::setDecodingAccelerated(bool accelerate) { VideoManager::instance().setDecodingAccelerated(accelerate); } bool AVModel::getEncodingAccelerated() const { bool result = VideoManager::instance().getEncodingAccelerated(); return result; } void AVModel::setEncodingAccelerated(bool accelerate) { VideoManager::instance().setEncodingAccelerated(accelerate); } bool AVModel::getHardwareAcceleration() const { bool result = getDecodingAccelerated() && getEncodingAccelerated(); return result; } void AVModel::setHardwareAcceleration(bool accelerate) { setDecodingAccelerated(accelerate); setEncodingAccelerated(accelerate); } QVector AVModel::getDevices() const { QStringList devices = VideoManager::instance().getDeviceList(); VectorString result; for (const auto& device : devices) { result.push_back(device); } return (QVector) result; } QString AVModel::getDefaultDevice() const { return VideoManager::instance().getDefaultDevice(); } void AVModel::setDefaultDevice(const QString& deviceId) { VideoManager::instance().setDefaultDevice(deviceId); } Settings AVModel::getDeviceSettings(const QString& deviceId) const { if (deviceId.isEmpty()) { return video::Settings(); } MapStringString settings = VideoManager::instance().getSettings(deviceId); if (settings["id"] != deviceId) { throw std::out_of_range("Device '" + deviceId.toStdString() + "' not found"); } video::Settings result; result.name = settings["name"]; result.id = settings["id"]; result.channel = settings["channel"]; result.size = settings["size"]; result.rate = settings["rate"].toFloat(); return result; } Capabilities AVModel::getDeviceCapabilities(const QString& deviceId) const { // Channel x Resolution x Framerate QMap>> capabilites = VideoManager::instance() .getCapabilities(deviceId); video::Capabilities result; for (auto& channel : capabilites.toStdMap()) { video::ResRateList channelCapabilities; for (auto& resToRates : channel.second.toStdMap()) { video::FrameratesList rates; QVectorIterator itRates(resToRates.second); while (itRates.hasNext()) { rates.push_back(itRates.next().toFloat()); } std::sort(rates.begin(), rates.end(), std::greater()); channelCapabilities.push_back(qMakePair(resToRates.first, rates)); } // sort by resolution widths std::sort(channelCapabilities.begin(), channelCapabilities.end(), [](const QPair& lhs, const QPair& rhs) { auto lhsWidth = lhs.first.left(lhs.first.indexOf("x")).toLongLong(); auto rhsWidth = rhs.first.left(rhs.first.indexOf("x")).toLongLong(); return lhsWidth > rhsWidth; }); result.insert(channel.first, channelCapabilities); } return result; } void AVModel::setDeviceSettings(video::Settings& settings) { MapStringString newSettings; auto rate = QString::number(settings.rate, 'f', 7); rate = rate.left(rate.length() - 1); newSettings["channel"] = settings.channel; newSettings["name"] = settings.name; newSettings["id"] = settings.id; newSettings["rate"] = rate; newSettings["size"] = settings.size; VideoManager::instance().applySettings(settings.id, newSettings); // If the preview is running, reload it // doing this during a call will cause re-invite, this is unwanted std::unique_lock lk(pimpl_->renderers_mtx_); auto it = pimpl_->renderers_.find(video::PREVIEW_RENDERER_ID); if (it != pimpl_->renderers_.end() && it->second && pimpl_->renderers_.size() == 1) { lk.unlock(); stopPreview(video::PREVIEW_RENDERER_ID); startPreview(video::PREVIEW_RENDERER_ID); } } QString AVModel::getDeviceIdFromName(const QString& deviceName) const { auto devices = getDevices(); auto iter = std::find_if(devices.begin(), devices.end(), [this, deviceName](const QString& d) { auto settings = getDeviceSettings(d); return settings.name == deviceName; }); if (iter == devices.end()) { qWarning() << "Couldn't find device: " << deviceName; return {}; } return *iter; } VectorString AVModel::getSupportedAudioManagers() const { QStringList managers = ConfigurationManager::instance().getSupportedAudioManagers(); VectorString result; for (const auto& manager : managers) { result.push_back(manager); } return result; } QString AVModel::getAudioManager() const { return ConfigurationManager::instance().getAudioManager(); } QVector AVModel::getAudioOutputDevices() const { QStringList devices = ConfigurationManager::instance().getAudioOutputDeviceList(); // A fix for ring-daemon#43 if (ConfigurationManager::instance().getAudioManager() == QStringLiteral("pulseaudio")) { if (devices.at(0) == QStringLiteral("default")) { devices[0] = QObject::tr("default"); } } VectorString result; for (const auto& device : devices) { result.push_back(device); } return (QVector) result; } QVector AVModel::getAudioInputDevices() const { QStringList devices = ConfigurationManager::instance().getAudioInputDeviceList(); // A fix for ring-daemon#43 if (ConfigurationManager::instance().getAudioManager() == QStringLiteral("pulseaudio")) { if (devices.at(0) == QStringLiteral("default")) { devices[0] = QObject::tr("default"); } } VectorString result; for (const auto& device : devices) { result.push_back(device); } return (QVector) result; } QString AVModel::getRingtoneDevice() const { const int RINGTONE_IDX = 2; return pimpl_->getDevice(RINGTONE_IDX); } QString AVModel::getOutputDevice() const { const int OUTPUT_IDX = 0; return pimpl_->getDevice(OUTPUT_IDX); } QString AVModel::getInputDevice() const { const int INPUT_IDX = 1; return pimpl_->getDevice(INPUT_IDX); } bool AVModel::isAudioMeterActive(const QString& id) const { return ConfigurationManager::instance().isAudioMeterActive(id); } void AVModel::setAudioMeterState(bool active, const QString& id) const { ConfigurationManager::instance().setAudioMeterState(id, active); } void AVModel::startAudioDevice() const { VideoManager::instance().startAudioDevice(); } void AVModel::stopAudioDevice() const { VideoManager::instance().stopAudioDevice(); } bool AVModel::setAudioManager(const QString& name) { return ConfigurationManager::instance().setAudioManager(name); } void AVModel::setRingtoneDevice(int idx) { ConfigurationManager::instance().setAudioRingtoneDevice(idx); } void AVModel::setOutputDevice(int idx) { ConfigurationManager::instance().setAudioOutputDevice(idx); } void AVModel::setInputDevice(int idx) { ConfigurationManager::instance().setAudioInputDevice(idx); } void AVModel::stopLocalRecorder(const QString& path) const { if (path.isEmpty()) { qWarning("stopLocalRecorder: can't stop non existing recording"); return; } VideoManager::instance().stopLocalRecorder(path); } QString AVModel::startLocalMediaRecorder(const QString& videoInputId) const { const QString path = pimpl_->getRecordingPath(); const QString finalPath = VideoManager::instance().startLocalMediaRecorder(videoInputId, path); return finalPath; } QString AVModel::getRecordPath() const { return ConfigurationManager::instance().getRecordPath(); } void AVModel::setRecordPath(const QString& path) const { ConfigurationManager::instance().setRecordPath(path.toUtf8()); } bool AVModel::getAlwaysRecord() const { return ConfigurationManager::instance().getIsAlwaysRecording(); } void AVModel::setAlwaysRecord(const bool& rec) const { ConfigurationManager::instance().setIsAlwaysRecording(rec); } bool AVModel::getRecordPreview() const { return ConfigurationManager::instance().getRecordPreview(); } void AVModel::setRecordPreview(const bool& rec) const { ConfigurationManager::instance().setRecordPreview(rec); } int AVModel::getRecordQuality() const { return ConfigurationManager::instance().getRecordQuality(); } void AVModel::setRecordQuality(const int& rec) const { ConfigurationManager::instance().setRecordQuality(rec); } QString AVModel::startPreview(const QString& resource) { return VideoManager::instance().openVideoInput(resource); } void AVModel::stopPreview(const QString& resource) { VideoManager::instance().closeVideoInput(resource); } #ifdef WIN32 BOOL IsAltTabWindow(HWND hwnd) { auto styles = (DWORD) GetWindowLongPtr(hwnd, GWL_STYLE); auto ex_styles = (DWORD) GetWindowLongPtr(hwnd, GWL_EXSTYLE); if (ex_styles & WS_EX_TOOLWINDOW) return false; if (styles & WS_CHILD) return false; DWORD cloaked = FALSE; HRESULT hrTemp = DwmGetWindowAttribute(hwnd, DWMWA_CLOAKED, &cloaked, sizeof(cloaked)); if (SUCCEEDED(hrTemp) && cloaked == DWM_CLOAKED_SHELL) { return false; } // Start at the root owner HWND hwndWalk = GetAncestor(hwnd, GA_ROOTOWNER); // See if we are the last active visible popup HWND hwndTry; while ((hwndTry = GetLastActivePopup(hwndWalk)) != hwndTry) { if (IsWindowVisible(hwndTry)) break; hwndWalk = hwndTry; } return hwndWalk == hwnd; } BOOL CALLBACK CbEnumAltTab(HWND hwnd, LPARAM lParam) { const size_t MAX_WINDOW_NAME = 256; TCHAR windowName[MAX_WINDOW_NAME]; GetWindowText(hwnd, windowName, MAX_WINDOW_NAME); // Do not show windows that has no caption if (0 == windowName[0]) return TRUE; std::wstring msg = std::wstring(windowName); auto name = QString::fromStdWString(msg); // Do not show invisible windows if (!IsWindowVisible(hwnd)) return TRUE; // Alt-tab test as described by Raymond Chen if (!IsAltTabWindow(hwnd)) return TRUE; auto isShellWindow = hwnd == GetShellWindow(); if (isShellWindow) return TRUE; QMap* windowList = reinterpret_cast*>(lParam); auto keys = windowList->keys(); if (keys.indexOf(name) > 0) { return FALSE; } else { std::stringstream ss; ss << hwnd; windowList->insert(name, QString::fromStdString(ss.str())); } return TRUE; } #endif #if defined(Q_OS_UNIX) && !defined(__APPLE__) static xcb_atom_t getAtom(xcb_connection_t* c, const std::string& atomName) { xcb_intern_atom_cookie_t atom_cookie = xcb_intern_atom(c, 0, atomName.size(), atomName.c_str()); if (auto* rep = xcb_intern_atom_reply(c, atom_cookie, nullptr)) { xcb_atom_t atom = rep->atom; free(rep); return atom; } return {}; } #endif const QVariantMap AVModel::getListWindows() const { QMap ret {}; #if defined(Q_OS_UNIX) && !defined(__APPLE__) std::unique_ptr c(xcb_connect(nullptr, nullptr), [](xcb_connection_t* ptr) { xcb_disconnect(ptr); }); if (xcb_connection_has_error(c.get())) { qDebug() << "xcb connection has error"; return ret; } auto atomNetClient = getAtom(c.get(), "_NET_CLIENT_LIST"); auto atomWMVisibleName = getAtom(c.get(), "_NET_WM_NAME"); if (!atomNetClient || !atomWMVisibleName) return ret; auto* screen = xcb_setup_roots_iterator(xcb_get_setup(c.get())).data; xcb_get_property_cookie_t propCookieList = xcb_get_property(c.get(), 0, screen->root, atomNetClient, XCB_GET_PROPERTY_TYPE_ANY, 0, 100); using propertyPtr = std::unique_ptr; xcb_generic_error_t* e; propertyPtr replyPropList(xcb_get_property_reply(c.get(), propCookieList, &e), [](auto* ptr) { free(ptr); }); if (e) { qDebug() << "Error: " << e->error_code; free(e); } if (replyPropList.get()) { int valueLegth = xcb_get_property_value_length(replyPropList.get()); if (valueLegth) { auto* win = static_cast(xcb_get_property_value(replyPropList.get())); for (int i = 0; i < valueLegth / 4; i++) { xcb_get_property_cookie_t prop_cookie = xcb_get_property(c.get(), 0, win[i], atomWMVisibleName, XCB_GET_PROPERTY_TYPE_ANY, 0, 1000); propertyPtr replyProp {xcb_get_property_reply(c.get(), prop_cookie, &e), [](auto* ptr) { free(ptr); }}; if (e) { qDebug() << "Error: " << e->error_code; free(e); } if (replyProp.get()) { int v_size = xcb_get_property_value_length(replyProp.get()); if (v_size) { auto v = std::string(reinterpret_cast( xcb_get_property_value(replyProp.get())), v_size); auto name = QString::fromUtf8(v.c_str()); if (ret.find(name) != ret.end()) name += QString(" - 0x%1").arg(win[i], 0, 16); ret.insert(name, QVariant(QString("0x%1").arg(win[i], 0, 16))); } } } } } #endif #ifdef WIN32 try { auto newWindow = true; LPARAM lParam = reinterpret_cast(&ret); while (newWindow) { newWindow = EnumWindows(CbEnumAltTab, lParam); } auto finishedloop = true; } catch (...) { } #endif return ret; } void AVModel::setCurrentVideoCaptureDevice(const QString& currentVideoCaptureDevice) { pimpl_->currentVideoCaptureDevice_ = currentVideoCaptureDevice; } QString AVModel::getCurrentVideoCaptureDevice() const { return pimpl_->currentVideoCaptureDevice_; } void AVModel::clearCurrentVideoCaptureDevice() { pimpl_->currentVideoCaptureDevice_.clear(); } void AVModel::addRenderer(const QString& id, const QSize& res, const QString& shmPath) { pimpl_->addRenderer(id, res, shmPath); } bool AVModel::hasRenderer(const QString& id) { return pimpl_->hasRenderer(id); } QSize AVModel::getRendererSize(const QString& id) { return pimpl_->getRendererSize(id); } Frame AVModel::getRendererFrame(const QString& id) { return pimpl_->getRendererFrame(id); } bool AVModel::useDirectRenderer() const { #ifdef ENABLE_LIBWRAP return true; #else return false; #endif } AVModelPimpl::AVModelPimpl(AVModel& linked, const CallbacksHandler& callbacksHandler) : callbacksHandler(callbacksHandler) , linked_(linked) { std::srand(std::time(nullptr)); #ifndef ENABLE_LIBWRAP SIZE_RENDERER = renderers_.size(); #endif connect(&callbacksHandler, &CallbacksHandler::deviceEvent, this, &AVModelPimpl::slotDeviceEvent); connect(&callbacksHandler, &CallbacksHandler::audioDeviceEvent, this, &AVModelPimpl::slotAudioDeviceEvent); connect(&callbacksHandler, &CallbacksHandler::audioMeter, this, &AVModelPimpl::slotAudioMeter); connect(&callbacksHandler, &CallbacksHandler::recordPlaybackStopped, this, &AVModelPimpl::slotRecordPlaybackStopped); // render connections connect(&callbacksHandler, &CallbacksHandler::decodingStarted, this, &AVModelPimpl::onDecodingStarted, Qt::DirectConnection); connect(&callbacksHandler, &CallbacksHandler::decodingStopped, this, &AVModelPimpl::onDecodingStopped, Qt::DirectConnection); auto startedPreview = false; auto restartRenderers = [&](const QStringList& callList) { for (const auto& callId : callList) { MapStringString rendererInfos = VideoManager::instance().getRenderer(callId); auto shmPath = rendererInfos[libjami::Media::Details::SHM_PATH]; auto width = rendererInfos[libjami::Media::Details::WIDTH].toInt(); auto height = rendererInfos[libjami::Media::Details::HEIGHT].toInt(); if (width > 0 && height > 0) { startedPreview = true; onDecodingStarted(callId, shmPath, width, height); } } }; restartRenderers(CallManager::instance().getCallList("")); auto confIds = lrc::api::Lrc::getConferences(); QStringList list; Q_FOREACH (QString confId, confIds) { list << confId; } restartRenderers(list); if (startedPreview) restartRenderers({"local"}); currentVideoCaptureDevice_ = VideoManager::instance().getDefaultDevice(); } QString AVModelPimpl::getRecordingPath() const { const QDir dir = authority::storage::getPath() + "/" + recorderSavesSubdir; dir.mkpath("."); std::chrono::time_point time_now = std::chrono::system_clock::now(); std::time_t time_now_t = std::chrono::system_clock::to_time_t(time_now); std::tm now_tm = *std::localtime(&time_now_t); std::stringstream ss; ss << dir.path().toStdString(); ss << "/"; ss << std::put_time(&now_tm, "%Y%m%d-%H%M%S"); ss << "-"; ss << std::rand(); QDir file_path(ss.str().c_str()); return file_path.path(); } void AVModelPimpl::onDecodingStarted(const QString& id, const QString& shmPath, int width, int height) { addRenderer(id, QSize(width, height), shmPath); } void AVModelPimpl::onDecodingStopped(const QString& id, const QString& shmPath) { Q_UNUSED(shmPath) removeRenderer(id); } #ifndef ENABLE_LIBWRAP void AVModelPimpl::stopCameraAndQuit(int) { if (SIZE_RENDERER == 1) { // This will stop the preview if needed (not in a call). VideoManager::instance().closeVideoInput(PREVIEW_RENDERER_ID); // HACK: this sleep is just here to let the camera stop and // avoid immediate raise std::this_thread::sleep_for(std::chrono::milliseconds(50)); } std::raise(SIGTERM); } #endif QString AVModelPimpl::getDevice(int type) const { if (type < 0 || type > 2) return {}; // No device QString result; VectorString devices; switch (type) { case 1: // INPUT devices = linked_.getAudioInputDevices(); break; case 0: // OUTPUT case 2: // RINGTONE devices = linked_.getAudioOutputDevices(); break; default: break; } QStringList currentDevicesIdx = ConfigurationManager::instance().getCurrentAudioDevicesIndex(); try { // Should not happen, but cannot retrieve current ringtone device if (currentDevicesIdx.size() < 3) return ""; auto deviceIdx = currentDevicesIdx[type].toInt(); if (deviceIdx < devices.size()) result = devices.at(deviceIdx); } catch (std::bad_alloc& ba) { qWarning() << "bad_alloc caught: " << ba.what(); return ""; } return result; } static std::unique_ptr createRenderer(const QString& id, const QSize& res, const QString& shmPath = {}) { #ifdef ENABLE_LIBWRAP Q_UNUSED(shmPath) return std::make_unique(id, res); #else return std::make_unique(id, res, shmPath); #endif } void AVModelPimpl::addRenderer(const QString& id, const QSize& res, const QString& shmPath) { // First remove the existing renderer. renderers_.erase(id); // Create a new one and add it. auto renderer = createRenderer(id, res, shmPath); std::lock_guard lk(renderers_mtx_); auto& r = renderers_[id]; r = std::move(renderer); renderers_mtx_.unlock(); // Listen and forward id-bound signals upwards. connect( r.get(), &Renderer::fpsChanged, this, [this, id](void) { linked_.updateRenderersFPSInfo(id); }, Qt::QueuedConnection); connect( r.get(), &Renderer::started, this, [this, id](const QSize& size) { Q_EMIT linked_.rendererStarted(id, size); }, Qt::DirectConnection); connect( r.get(), &Renderer::frameBufferRequested, this, [this, id](AVFrame* frame) { Q_EMIT linked_.frameBufferRequested(id, frame); }, Qt::DirectConnection); connect( r.get(), &Renderer::frameUpdated, this, [this, id] { Q_EMIT linked_.frameUpdated(id); }, Qt::DirectConnection); connect( r.get(), &Renderer::stopped, this, [this, id] { Q_EMIT linked_.rendererStopped(id); }, Qt::DirectConnection); r->startRendering(); } void AVModelPimpl::removeRenderer(const QString& id) { std::lock_guard lk(renderers_mtx_); auto it = renderers_.find(id); if (it == renderers_.end()) { qWarning() << "Cannot remove renderer. " << id << "not found"; return; } renderers_.erase(id); } bool AVModelPimpl::hasRenderer(const QString& id) { std::lock_guard lk(renderers_mtx_); return renderers_.find(id) != renderers_.end(); } QSize AVModelPimpl::getRendererSize(const QString& id) { std::lock_guard lk(renderers_mtx_); auto it = renderers_.find(id); if (it != renderers_.end()) { return it->second->size(); } return {}; } Frame AVModelPimpl::getRendererFrame(const QString& id) { std::lock_guard lk(renderers_mtx_); auto it = renderers_.find(id); if (it != renderers_.end()) { return it->second->currentFrame(); } return {}; } void AVModelPimpl::slotDeviceEvent() { Q_EMIT linked_.deviceEvent(); } void AVModelPimpl::slotAudioDeviceEvent() { Q_EMIT linked_.audioDeviceEvent(); } void AVModelPimpl::slotAudioMeter(const QString& id, float level) { Q_EMIT linked_.audioMeter(id, level); } void AVModelPimpl::slotRecordPlaybackStopped(const QString& filePath) { Q_EMIT linked_.recordPlaybackStopped(filePath); } } // namespace lrc #include "api/moc_avmodel.cpp" #include "avmodel.moc"