From cba34e82c0ce52969b4a3596287d72e8ddea757b Mon Sep 17 00:00:00 2001 From: Julien Wadel Date: Tue, 9 Apr 2024 16:36:22 +0200 Subject: [PATCH] Video management --- Linphone/core/call/CallCore.cpp | 6 +- Linphone/core/call/CallCore.hpp | 6 +- Linphone/core/camera/CameraGui.cpp | 2 +- Linphone/core/camera/PreviewManager.cpp | 14 ++++- .../data/assistant/create-app-sip-account.rc | 5 ++ .../data/assistant/use-app-sip-account.rc | 5 ++ .../data/assistant/use-other-sip-account.rc | 5 ++ Linphone/model/call/CallModel.cpp | 56 +++++++++++++++++-- Linphone/model/call/CallModel.hpp | 2 +- Linphone/model/tool/ToolModel.cpp | 9 ++- Linphone/model/tool/ToolModel.hpp | 2 +- Linphone/tool/Utils.cpp | 12 ++-- Linphone/tool/Utils.hpp | 8 +-- Linphone/view/App/CallsWindow.qml | 13 +---- Linphone/view/App/Layout/MainLayout.qml | 2 +- Linphone/view/Item/Call/WaitingRoom.qml | 2 +- Linphone/view/Item/Contact/ContactsList.qml | 2 +- Linphone/view/Item/Contact/Sticker.qml | 19 ++++--- .../view/Layout/Call/ActiveSpeakerLayout.qml | 8 ++- Linphone/view/Layout/Call/GridLayout.qml | 2 +- external/linphone-sdk | 2 +- 21 files changed, 129 insertions(+), 53 deletions(-) diff --git a/Linphone/core/call/CallCore.cpp b/Linphone/core/call/CallCore.cpp index 902858687..27d3a1df8 100644 --- a/Linphone/core/call/CallCore.cpp +++ b/Linphone/core/call/CallCore.cpp @@ -53,7 +53,10 @@ CallCore::CallCore(const std::shared_ptr &call) : QObject(nullpt mDuration = call->getDuration(); mMicrophoneMuted = call->getMicrophoneMuted(); mSpeakerMuted = call->getSpeakerMuted(); - mCameraEnabled = call->cameraEnabled(); + // mCameraEnabled = call->cameraEnabled(); + auto videoDirection = call->getCurrentParams()->getVideoDirection(); + mCameraEnabled = + videoDirection == linphone::MediaDirection::SendOnly || videoDirection == linphone::MediaDirection::SendRecv; mState = LinphoneEnums::fromLinphone(call->getState()); mPeerAddress = Utils::coreStringToAppString(call->getRemoteAddress()->asStringUriOnly()); mStatus = LinphoneEnums::fromLinphone(call->getCallLog()->getStatus()); @@ -355,6 +358,7 @@ bool CallCore::getCameraEnabled() const { void CallCore::setCameraEnabled(bool enabled) { if (mCameraEnabled != enabled) { mCameraEnabled = enabled; + qWarning() << "CameraEnabled: " << mCameraEnabled; emit cameraEnabledChanged(); } } diff --git a/Linphone/core/call/CallCore.hpp b/Linphone/core/call/CallCore.hpp index d7d8fd170..ff3cf7822 100644 --- a/Linphone/core/call/CallCore.hpp +++ b/Linphone/core/call/CallCore.hpp @@ -62,7 +62,7 @@ class CallCore : public QObject, public AbstractObject { Q_PROPERTY(LinphoneEnums::CallState transferState READ getTransferState NOTIFY transferStateChanged) Q_PROPERTY(ConferenceGui *conference READ getConferenceGui NOTIFY conferenceChanged) Q_PROPERTY(LinphoneEnums::ConferenceLayout conferenceVideoLayout READ getConferenceVideoLayout WRITE - lSetConferenceVideoLayout NOTIFY conferenceVideoLayoutChanged) + lSetConferenceVideoLayout NOTIFY conferenceVideoLayoutChanged) public: // Should be call from model Thread. Will be automatically in App thread after initialization @@ -180,6 +180,7 @@ signals: void lSetSpeakerMuted(bool muted); void lSetMicrophoneMuted(bool isMuted); void lSetCameraEnabled(bool enabled); + void lSetVideoEnabled(bool enabled); void lSetPaused(bool paused); void lTransferCall(QString &est); void lStartRecording(); @@ -226,7 +227,8 @@ private: int mDuration = 0; bool mSpeakerMuted; bool mMicrophoneMuted; - bool mCameraEnabled; + bool mCameraEnabled = false; + bool mVideoEnabled = false; bool mPaused = false; bool mRemoteVideoEnabled = false; bool mRecording = false; diff --git a/Linphone/core/camera/CameraGui.cpp b/Linphone/core/camera/CameraGui.cpp index b83974647..fdd1bf343 100644 --- a/Linphone/core/camera/CameraGui.cpp +++ b/Linphone/core/camera/CameraGui.cpp @@ -197,7 +197,7 @@ void CameraGui::setWindowIdLocation(const WindowIdLocation &location) { if (mWindowIdLocation != location) { lDebug() << log().arg("Update Window Id location from %2 to %3").arg(mWindowIdLocation).arg(location); if (mWindowIdLocation == CorePreview) PreviewManager::getInstance()->unsubscribe(this); - else resetWindowId(); // Location change: Reset old window ID. + else if (mWindowIdLocation != None) resetWindowId(); // Location change: Reset old window ID. mWindowIdLocation = location; if (mWindowIdLocation == CorePreview) PreviewManager::getInstance()->subscribe(this); update(); diff --git a/Linphone/core/camera/PreviewManager.cpp b/Linphone/core/camera/PreviewManager.cpp index 88c36bbf6..a98ebdac4 100644 --- a/Linphone/core/camera/PreviewManager.cpp +++ b/Linphone/core/camera/PreviewManager.cpp @@ -75,7 +75,7 @@ QQuickFramebufferObject::Renderer *PreviewManager::subscribe(const CameraGui *ca (QQuickFramebufferObject::Renderer *)CoreModel::getInstance()->getCore()->createNativePreviewWindowId(); } if (isFirst) { - lDebug() << "[PreviewManager] " << name << " Set Native Preview Id"; + lDebug() << "[PreviewManager] " << name << " Set Native Preview Id with " << renderer; CoreModel::getInstance()->getCore()->setNativePreviewWindowId(renderer); } }); @@ -118,9 +118,17 @@ void PreviewManager::unsubscribe(QObject *sender) { } void PreviewManager::activate() { - App::postModelBlock([]() { CoreModel::getInstance()->getCore()->enableVideoPreview(true); }); + App::postModelBlock([]() { + qDebug() << "[PreviewManager] Activation"; + CoreModel::getInstance()->getCore()->enableVideoPreview(true); + CoreModel::getInstance()->getCore()->iterate(); + }); } void PreviewManager::deactivate() { - App::postModelBlock([]() { CoreModel::getInstance()->getCore()->enableVideoPreview(false); }); + App::postModelBlock([]() { + qDebug() << "[PreviewManager] Deactivation"; + CoreModel::getInstance()->getCore()->enableVideoPreview(false); + CoreModel::getInstance()->getCore()->iterate(); + }); } diff --git a/Linphone/data/assistant/create-app-sip-account.rc b/Linphone/data/assistant/create-app-sip-account.rc index a23f4995a..f55f6927d 100644 --- a/Linphone/data/assistant/create-app-sip-account.rc +++ b/Linphone/data/assistant/create-app-sip-account.rc @@ -34,6 +34,11 @@
sips:rls@sip.linphone.org
+
+ 1 + 0 + 2 +
sip.linphone.org SHA-256 diff --git a/Linphone/data/assistant/use-app-sip-account.rc b/Linphone/data/assistant/use-app-sip-account.rc index 4e6917299..487baf469 100644 --- a/Linphone/data/assistant/use-app-sip-account.rc +++ b/Linphone/data/assistant/use-app-sip-account.rc @@ -34,6 +34,11 @@
sips:rls@sip.linphone.org
+
+ 1 + 0 + 2 +
sip.linphone.org SHA-256 diff --git a/Linphone/data/assistant/use-other-sip-account.rc b/Linphone/data/assistant/use-other-sip-account.rc index c5413129e..f8733de1f 100644 --- a/Linphone/data/assistant/use-other-sip-account.rc +++ b/Linphone/data/assistant/use-other-sip-account.rc @@ -30,6 +30,11 @@
+
+ 1 + 0 + 2 +
MD5 diff --git a/Linphone/model/call/CallModel.cpp b/Linphone/model/call/CallModel.cpp index 1b0876024..6ab8f2cad 100644 --- a/Linphone/model/call/CallModel.cpp +++ b/Linphone/model/call/CallModel.cpp @@ -124,11 +124,51 @@ void CallModel::setSpeakerMuted(bool isMuted) { void CallModel::setCameraEnabled(bool enabled) { mustBeInLinphoneThread(log().arg(Q_FUNC_INFO)); - mMonitor->enableCamera(enabled); - auto core = CoreModel::getInstance()->getCore(); - auto params = core->createCallParams(mMonitor); - params->enableVideo(enabled); - emit cameraEnabledChanged(enabled); + // mMonitor->enableCamera(enabled); + auto params = CoreModel::getInstance()->getCore()->createCallParams(mMonitor); + params->enableVideo(true); + auto direction = mMonitor->getCurrentParams()->getVideoDirection(); + auto videoDirection = linphone::MediaDirection::RecvOnly; + if (enabled) { // +Send + switch (direction) { + case linphone::MediaDirection::RecvOnly: + videoDirection = linphone::MediaDirection::SendRecv; + break; + case linphone::MediaDirection::SendOnly: + videoDirection = linphone::MediaDirection::SendOnly; + break; + case linphone::MediaDirection::SendRecv: + videoDirection = linphone::MediaDirection::SendRecv; + break; + default: + videoDirection = linphone::MediaDirection::SendOnly; + } + } else { // -Send + switch (direction) { + case linphone::MediaDirection::RecvOnly: + videoDirection = linphone::MediaDirection::RecvOnly; + break; + case linphone::MediaDirection::SendOnly: + videoDirection = linphone::MediaDirection::Inactive; + break; + case linphone::MediaDirection::SendRecv: + videoDirection = linphone::MediaDirection::RecvOnly; + break; + default: + videoDirection = linphone::MediaDirection::Inactive; + } + } + /* + auto videoDirection = + !enabled ? linphone::MediaDirection::RecvOnly + : direction == linphone::MediaDirection::RecvOnly // + ? linphone::MediaDirection::SendRecv + : direction == linphone::MediaDirection::SendRecv || direction == linphone::MediaDirection::SendOnly + ? linphone::MediaDirection::RecvOnly + : linphone::MediaDirection::SendOnly; + */ + params->setVideoDirection(videoDirection); + mMonitor->update(params); } void CallModel::startRecording() { @@ -319,7 +359,11 @@ void CallModel::onStateChanged(const std::shared_ptr &call, // After UpdatedByRemote, video direction could be changed. auto params = call->getRemoteParams(); emit remoteVideoEnabledChanged(params && params->videoEnabled()); - emit cameraEnabledChanged(call->cameraEnabled()); + qWarning() << "CallCameraEnabled:" << call->cameraEnabled(); + auto videoDirection = call->getCurrentParams()->getVideoDirection(); + emit cameraEnabledChanged(videoDirection == linphone::MediaDirection::SendOnly || + videoDirection == linphone::MediaDirection::SendRecv); + // emit cameraEnabledChanged(call->cameraEnabled()); setConference(call->getConference()); updateConferenceVideoLayout(); } diff --git a/Linphone/model/call/CallModel.hpp b/Linphone/model/call/CallModel.hpp index 08e309ba7..feb11fed8 100644 --- a/Linphone/model/call/CallModel.hpp +++ b/Linphone/model/call/CallModel.hpp @@ -157,7 +157,7 @@ signals: void cameraNotWorking(const std::shared_ptr &call, const std::string &cameraName); void videoDisplayErrorOccurred(const std::shared_ptr &call, int errorCode); void audioDeviceChanged(const std::shared_ptr &call, - const std::shared_ptr &audioDevice); + const std::shared_ptr &audioDevice); void remoteRecording(const std::shared_ptr &call, bool recording); }; diff --git a/Linphone/model/tool/ToolModel.cpp b/Linphone/model/tool/ToolModel.cpp index e8d2bb237..af77c5854 100644 --- a/Linphone/model/tool/ToolModel.cpp +++ b/Linphone/model/tool/ToolModel.cpp @@ -94,12 +94,13 @@ QString ToolModel::getDisplayName(QString address) { } QSharedPointer ToolModel::createCall(const QString &sipAddress, - bool withVideo, + const QVariantMap &options, const QString &prepareTransfertAddress, const QHash &headers, linphone::MediaEncryption mediaEncryption) { bool waitRegistrationForCall = true; // getSettingsModel()->getWaitRegistrationForCall() std::shared_ptr core = CoreModel::getInstance()->getCore(); + bool cameraEnabled = options.contains("cameraEnabled") ? options["cameraEnabled"].toBool() : false; std::shared_ptr address = interpretUrl(sipAddress); if (!address) { @@ -109,7 +110,9 @@ QSharedPointer ToolModel::createCall(const QString &sipAddress, } std::shared_ptr params = core->createCallParams(nullptr); - params->enableVideo(withVideo); + params->enableVideo(true); + params->setVideoDirection(cameraEnabled ? linphone::MediaDirection::SendRecv : linphone::MediaDirection::Inactive); + params->setMediaEncryption(mediaEncryption); if (Utils::coreStringToAppString(params->getRecordFile()).isEmpty()) { @@ -129,7 +132,7 @@ QSharedPointer ToolModel::createCall(const QString &sipAddress, if (core->getDefaultAccount()) params->setAccount(core->getDefaultAccount()); auto call = core->inviteAddressWithParams(address, params); - call->enableCamera(withVideo); + call->enableCamera(cameraEnabled); return call ? CallCore::create(call) : nullptr; /* TODO transfer diff --git a/Linphone/model/tool/ToolModel.hpp b/Linphone/model/tool/ToolModel.hpp index b159d5f9a..68cc7222f 100644 --- a/Linphone/model/tool/ToolModel.hpp +++ b/Linphone/model/tool/ToolModel.hpp @@ -47,7 +47,7 @@ public: static QString getDisplayName(QString address); static QSharedPointer createCall(const QString &sipAddress, - bool withVideo = false, + const QVariantMap &options = {}, const QString &prepareTransfertAddress = "", const QHash &headers = {}, linphone::MediaEncryption = linphone::MediaEncryption::None); diff --git a/Linphone/tool/Utils.cpp b/Linphone/tool/Utils.cpp index fc91d59b6..f4b1b0a29 100644 --- a/Linphone/tool/Utils.cpp +++ b/Linphone/tool/Utils.cpp @@ -94,14 +94,14 @@ QString Utils::getInitials(const QString &username) { return QLocale().toUpper(initials.join("")); } -VariantObject *Utils::createCall(const QString &sipAddress, - bool withVideo, - const QString &prepareTransfertAddress, - const QHash &headers) { +VariantObject *Utils::createCall(QString sipAddress, + QVariantMap options, + QString prepareTransfertAddress, + QHash headers) { VariantObject *data = new VariantObject(QVariant()); // Scope : GUI if (!data) return nullptr; - data->makeRequest([sipAddress, withVideo, prepareTransfertAddress, headers]() { - auto call = ToolModel::createCall(sipAddress, withVideo, prepareTransfertAddress, headers); + data->makeRequest([sipAddress, options, prepareTransfertAddress, headers]() { + auto call = ToolModel::createCall(sipAddress, options, prepareTransfertAddress, headers); if (call) { auto callGui = QVariant::fromValue(new CallGui(call)); App::postCoreSync([callGui]() { diff --git a/Linphone/tool/Utils.hpp b/Linphone/tool/Utils.hpp index 5a9829a1e..56a3db233 100644 --- a/Linphone/tool/Utils.hpp +++ b/Linphone/tool/Utils.hpp @@ -58,10 +58,10 @@ public: Q_INVOKABLE static QString getFamilyNameFromFullName(const QString &fullName); Q_INVOKABLE static QString getInitials(const QString &username); // Support UTF32 - Q_INVOKABLE static VariantObject *createCall(const QString &sipAddress, - bool withVideo = false, - const QString &prepareTransfertAddress = "", - const QHash &headers = {}); + Q_INVOKABLE static VariantObject *createCall(QString sipAddress, + QVariantMap options = {}, + QString prepareTransfertAddress = "", + QHash headers = {}); Q_INVOKABLE static void openCallsWindow(CallGui *call); Q_INVOKABLE static void setupConference(ConferenceInfoGui *confGui); Q_INVOKABLE static void setCallsWindowCall(CallGui *call); diff --git a/Linphone/view/App/CallsWindow.qml b/Linphone/view/App/CallsWindow.qml index 2f9baf3ad..9430ecab6 100644 --- a/Linphone/view/App/CallsWindow.qml +++ b/Linphone/view/App/CallsWindow.qml @@ -34,10 +34,10 @@ Window { } property var callObj - function joinConference(withVideo) { + function joinConference(options) { if (!conferenceInfo || conferenceInfo.core.uri.length === 0) UtilsCpp.showInformationPopup(qsTr("Erreur"), qsTr("La conférence n'a pas pu démarrer en raison d'une erreur d'uri."), mainWindow) else { - callObj = UtilsCpp.createCall(conferenceInfo.core.uri, withVideo) + callObj = UtilsCpp.createCall(conferenceInfo.core.uri, options) } } @@ -789,14 +789,7 @@ Window { target: rightPanel onVisibleChanged: if (!visible) waitingRoomIn.settingsButtonChecked = false } - Connections { - target: mainWindow - onCallChanged: if (mainWindow.conferenceInfo && mainWindow.call) { - mainWindow.call.core.lSetCameraEnabled(waitingRoomIn.cameraEnabled) - mainWindow.call.core.lSetMicrophoneMuted(!waitingRoomIn.microEnabled) - } - } - onJoinConfRequested: mainWindow.joinConference(cameraEnabled) + onJoinConfRequested: mainWindow.joinConference({'microEnabled':microEnabled, 'cameraEnabled':cameraEnabled}) } } Component { diff --git a/Linphone/view/App/Layout/MainLayout.qml b/Linphone/view/App/Layout/MainLayout.qml index c29564f9a..16c41a7ea 100644 --- a/Linphone/view/App/Layout/MainLayout.qml +++ b/Linphone/view/App/Layout/MainLayout.qml @@ -245,7 +245,7 @@ Item { height: 24 * DefaultStyle.dp source: AppIcons.videoCamera } - onClicked: mainItem.callObj = UtilsCpp.createCall(sipAddr.text, true) + onClicked: mainItem.callObj = UtilsCpp.createCall(sipAddr.text, {'cameraEnabled':true}) } } Button { diff --git a/Linphone/view/Item/Call/WaitingRoom.qml b/Linphone/view/Item/Call/WaitingRoom.qml index 2a7122365..e5b04e6e5 100644 --- a/Linphone/view/Item/Call/WaitingRoom.qml +++ b/Linphone/view/Item/Call/WaitingRoom.qml @@ -22,6 +22,7 @@ RowLayout { // Layout.leftMargin: 97 * DefaultStyle.dp Sticker { id: preview + previewEnabled: true Layout.preferredHeight: 330 * DefaultStyle.dp Layout.preferredWidth: 558 * DefaultStyle.dp qmlName: "WP" @@ -29,7 +30,6 @@ RowLayout { id: accounts } account: accounts.defaultAccount - previewEnabled: true } RowLayout { Layout.alignment: Qt.AlignHCenter diff --git a/Linphone/view/Item/Contact/ContactsList.qml b/Linphone/view/Item/Contact/ContactsList.qml index a808b98f7..a0a247ce2 100644 --- a/Linphone/view/Item/Contact/ContactsList.qml +++ b/Linphone/view/Item/Contact/ContactsList.qml @@ -169,7 +169,7 @@ ListView { height: 24 * DefaultStyle.dp source: AppIcons.videoCamera } - onClicked: callObj = UtilsCpp.createCall(modelData.core.defaultAddress, true) + onClicked: callObj = UtilsCpp.createCall(modelData.core.defaultAddress, {'cameraEnabled':true}) } } PopupButton { diff --git a/Linphone/view/Item/Contact/Sticker.qml b/Linphone/view/Item/Contact/Sticker.qml index 884f8b1e5..995f28587 100644 --- a/Linphone/view/Item/Contact/Sticker.qml +++ b/Linphone/view/Item/Contact/Sticker.qml @@ -14,19 +14,23 @@ Item { id: mainItem height: 300 width: 200 + required property bool previewEnabled property CallGui call: null property AccountGui account: null property ParticipantDeviceGui participantDevice: null - property bool previewEnabled: false property bool displayBorder : participantDevice && participantDevice.core.isSpeaking || false property color color: DefaultStyle.grey_600 property int radius: 15 * DefaultStyle.dp - property var peerAddressObj: participantDevice && participantDevice.core - ? UtilsCpp.getDisplayName(participantDevice.core.address) - : !previewEnabled && call && call.core - ? UtilsCpp.getDisplayName(call.core.peerAddress) - : null + property var peerAddressObj: previewEnabled + ? UtilsCpp.getDisplayName(account.core.identityAddress) + : participantDevice && participantDevice.core + ? UtilsCpp.getDisplayName(participantDevice.core.address) + : !previewEnabled && call && call.core + ? UtilsCpp.getDisplayName(call.core.peerAddress) + : null + property string peerAddress:peerAddressObj ? peerAddressObj.value : "" + onPeerAddressChanged: console.log("TOTO " +qmlName + " => " +peerAddress) property var identityAddress: account ? UtilsCpp.getDisplayName(account.core.identityAddress) : null property bool cameraEnabled: previewEnabled || participantDevice && participantDevice.core.videoEnabled property string qmlName @@ -97,9 +101,10 @@ Item { anchors.fill: parent visible: false qmlName: mainItem.qmlName + isPreview: mainItem.previewEnabled call: mainItem.call participantDevice: mainItem.participantDevice - isPreview: mainItem.previewEnabled + onRequestNewRenderer: { console.log("Request new renderer for " +mainItem.qmlName) resetTimer.restart() diff --git a/Linphone/view/Layout/Call/ActiveSpeakerLayout.qml b/Linphone/view/Layout/Call/ActiveSpeakerLayout.qml index 910af64c6..d6b053b08 100644 --- a/Linphone/view/Layout/Call/ActiveSpeakerLayout.qml +++ b/Linphone/view/Layout/Call/ActiveSpeakerLayout.qml @@ -31,6 +31,7 @@ Item{ Sticker { id: activeSpeakerSticker + previewEnabled: false Layout.fillWidth: true Layout.fillHeight: true call: mainItem.call @@ -88,14 +89,14 @@ Item{ clip: true delegate: Sticker { - visible: mainItem.callState != LinphoneEnums.CallState.End && mainItem.callState != LinphoneEnums.CallState.Released + previewEnabled: index == 0 + visible: modelData && mainItem.callState != LinphoneEnums.CallState.End && mainItem.callState != LinphoneEnums.CallState.Released && modelData.core.address != activeSpeakerSticker.address height: visible ? 180 * DefaultStyle.dp : 0 width: 300 * DefaultStyle.dp qmlName: 'S_'+index participantDevice: modelData - previewEnabled: index == 0 Component.onCompleted: console.log(qmlName + " is " +modelData.core.address) } } @@ -115,7 +116,8 @@ Item{ //participantDevice: allDevices.me cameraEnabled: preview.visible && mainItem.call && mainItem.call.core.cameraEnabled onCameraEnabledChanged: console.log("P : " +cameraEnabled + " / " +visible +" / " +mainItem.call) - + property AccountProxy accounts: AccountProxy{id: accountProxy} + account: accountProxy.defaultAccount call: mainItem.call MovableMouseArea { diff --git a/Linphone/view/Layout/Call/GridLayout.qml b/Linphone/view/Layout/Call/GridLayout.qml index 3e7346b74..55dceb361 100644 --- a/Linphone/view/Layout/Call/GridLayout.qml +++ b/Linphone/view/Layout/Call/GridLayout.qml @@ -33,12 +33,12 @@ Mosaic { width: grid.cellWidth - 10 Sticker { id: cameraView + previewEnabled: index == 0 visible: mainItem.callState != LinphoneEnums.CallState.End && mainItem.callState != LinphoneEnums.CallState.Released anchors.fill: parent qmlName: 'G_'+index participantDevice: avatarCell.currentDevice - previewEnabled: index == 0 Component.onCompleted: console.log(qmlName + " is " +modelData.core.address) } /* diff --git a/external/linphone-sdk b/external/linphone-sdk index 0dda330ac..1f9db257f 160000 --- a/external/linphone-sdk +++ b/external/linphone-sdk @@ -1 +1 @@ -Subproject commit 0dda330ac9ccd7f5b495ac147e88ff7dbb620762 +Subproject commit 1f9db257fe224ea6d9b067e69ee6b9f72102e129