mirror of
https://git.jami.net/savoirfairelinux/jami-client-qt.git
synced 2025-08-01 05:15:44 +02:00
localvideo: refactor preview component device control
Change-Id: Ibcd88c5a3c73a0e67f94d70bc420845aa7b8c822
This commit is contained in:
parent
afde816b23
commit
ff7acf9932
11 changed files with 318 additions and 306 deletions
|
@ -33,7 +33,7 @@ VideoView {
|
||||||
stop();
|
stop();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const forceRestart = rendererId === id;
|
const forceRestart = rendererId === id || force;
|
||||||
if (!forceRestart) {
|
if (!forceRestart) {
|
||||||
// Stop previous device
|
// Stop previous device
|
||||||
VideoDevices.stopDevice(rendererId);
|
VideoDevices.stopDevice(rendererId);
|
||||||
|
|
|
@ -223,43 +223,14 @@ CurrentCall::updateCallInfo()
|
||||||
set_isGrid(callInfo.layout == call::Layout::GRID);
|
set_isGrid(callInfo.layout == call::Layout::GRID);
|
||||||
set_isAudioOnly(callInfo.isAudioOnly);
|
set_isAudioOnly(callInfo.isAudioOnly);
|
||||||
|
|
||||||
bool isAudioMuted {};
|
auto callInfoEx = callInfo.getCallInfoEx();
|
||||||
bool isVideoMuted {};
|
set_previewId(callInfoEx["preview_id"].toString());
|
||||||
bool isSharing {};
|
set_isAudioMuted(callInfoEx["is_audio_muted"].toBool());
|
||||||
QString sharingSource {};
|
set_isVideoMuted(callInfoEx["is_video_muted"].toBool());
|
||||||
bool isCapturing {};
|
set_isSharing(callInfoEx["is_sharing"].toBool());
|
||||||
QString previewId {};
|
set_sharingSource(isSharing_ ? callInfoEx["preview_id"].toString() : QString());
|
||||||
using namespace libjami::Media;
|
set_isCapturing(callInfoEx["is_capturing"].toBool());
|
||||||
if (callInfo.status != lrc::api::call::Status::ENDED) {
|
|
||||||
for (const auto& media : callInfo.mediaList) {
|
|
||||||
if (media[MediaAttributeKey::MEDIA_TYPE] == Details::MEDIA_TYPE_VIDEO) {
|
|
||||||
if (media[MediaAttributeKey::SOURCE].startsWith(VideoProtocolPrefix::DISPLAY)
|
|
||||||
|| media[MediaAttributeKey::SOURCE].startsWith(VideoProtocolPrefix::FILE)) {
|
|
||||||
isSharing = true;
|
|
||||||
sharingSource = media[MediaAttributeKey::SOURCE];
|
|
||||||
}
|
|
||||||
if (media[MediaAttributeKey::ENABLED] == TRUE_STR
|
|
||||||
&& media[MediaAttributeKey::MUTED] == FALSE_STR && previewId.isEmpty()) {
|
|
||||||
previewId = media[libjami::Media::MediaAttributeKey::SOURCE];
|
|
||||||
}
|
|
||||||
if (media[libjami::Media::MediaAttributeKey::SOURCE].startsWith(
|
|
||||||
libjami::Media::VideoProtocolPrefix::CAMERA)) {
|
|
||||||
isVideoMuted |= media[MediaAttributeKey::MUTED] == TRUE_STR;
|
|
||||||
isCapturing = media[MediaAttributeKey::MUTED] == FALSE_STR;
|
|
||||||
}
|
|
||||||
} else if (media[MediaAttributeKey::MEDIA_TYPE] == Details::MEDIA_TYPE_AUDIO) {
|
|
||||||
if (media[MediaAttributeKey::LABEL] == "audio_0") {
|
|
||||||
isAudioMuted |= media[libjami::Media::MediaAttributeKey::MUTED] == TRUE_STR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
set_previewId(previewId);
|
|
||||||
set_isAudioMuted(isAudioMuted);
|
|
||||||
set_isVideoMuted(isVideoMuted);
|
|
||||||
set_isSharing(isSharing);
|
|
||||||
set_sharingSource(sharingSource);
|
|
||||||
set_isCapturing(isCapturing);
|
|
||||||
set_isHandRaised(callModel->isHandRaised(id_));
|
set_isHandRaised(callModel->isHandRaised(id_));
|
||||||
set_isModerator(callModel->isModerator(id_));
|
set_isModerator(callModel->isModerator(id_));
|
||||||
|
|
||||||
|
|
|
@ -18,6 +18,8 @@
|
||||||
|
|
||||||
#include "currentconversation.h"
|
#include "currentconversation.h"
|
||||||
|
|
||||||
|
#include "global.h"
|
||||||
|
|
||||||
#include <api/conversationmodel.h>
|
#include <api/conversationmodel.h>
|
||||||
#include <api/contact.h>
|
#include <api/contact.h>
|
||||||
|
|
||||||
|
@ -264,51 +266,39 @@ void
|
||||||
CurrentConversation::connectModel()
|
CurrentConversation::connectModel()
|
||||||
{
|
{
|
||||||
membersModel_->setMembers({}, {}, {});
|
membersModel_->setMembers({}, {}, {});
|
||||||
auto convModel = lrcInstance_->getCurrentConversationModel();
|
|
||||||
if (!convModel)
|
auto currentConversationModel = lrcInstance_->getCurrentConversationModel();
|
||||||
|
auto currentCallModel = lrcInstance_->getCurrentCallModel();
|
||||||
|
if (!currentConversationModel || !currentCallModel) {
|
||||||
|
C_DBG << "CurrentConversation: can't connect to unavailable models";
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
auto connectObjectSignal = [this](auto obj, auto signal, auto slot) {
|
auto connectObjectSignal = [this](auto obj, auto signal, auto slot) {
|
||||||
connect(obj, signal, this, slot, Qt::UniqueConnection);
|
connect(obj, signal, this, slot, Qt::UniqueConnection);
|
||||||
};
|
};
|
||||||
|
|
||||||
connectObjectSignal(convModel,
|
connectObjectSignal(currentConversationModel,
|
||||||
&ConversationModel::conversationUpdated,
|
&ConversationModel::conversationUpdated,
|
||||||
&CurrentConversation::onConversationUpdated);
|
&CurrentConversation::onConversationUpdated);
|
||||||
connectObjectSignal(convModel,
|
connectObjectSignal(currentConversationModel,
|
||||||
&ConversationModel::profileUpdated,
|
&ConversationModel::profileUpdated,
|
||||||
&CurrentConversation::updateProfile);
|
&CurrentConversation::updateProfile);
|
||||||
|
connectObjectSignal(currentConversationModel,
|
||||||
connect(lrcInstance_->getCurrentConversationModel(),
|
&ConversationModel::conversationErrorsUpdated,
|
||||||
&ConversationModel::profileUpdated,
|
&CurrentConversation::updateErrors);
|
||||||
this,
|
connectObjectSignal(currentConversationModel,
|
||||||
&CurrentConversation::updateProfile,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(lrcInstance_->getCurrentConversationModel(),
|
|
||||||
&ConversationModel::onConversationErrorsUpdated,
|
|
||||||
this,
|
|
||||||
&CurrentConversation::updateErrors,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(lrcInstance_->getCurrentConversationModel(),
|
|
||||||
&ConversationModel::activeCallsChanged,
|
&ConversationModel::activeCallsChanged,
|
||||||
this,
|
&CurrentConversation::updateActiveCalls);
|
||||||
&CurrentConversation::updateActiveCalls,
|
connectObjectSignal(currentConversationModel,
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(lrcInstance_->getCurrentConversationModel(),
|
|
||||||
&ConversationModel::conversationPreferencesUpdated,
|
&ConversationModel::conversationPreferencesUpdated,
|
||||||
this,
|
&CurrentConversation::updateConversationPreferences);
|
||||||
&CurrentConversation::updateConversationPreferences,
|
connectObjectSignal(currentConversationModel,
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(lrcInstance_->getCurrentConversationModel(),
|
|
||||||
&ConversationModel::needsHost,
|
&ConversationModel::needsHost,
|
||||||
this,
|
&CurrentConversation::onNeedsHost);
|
||||||
&CurrentConversation::onNeedsHost,
|
connectObjectSignal(currentCallModel,
|
||||||
Qt::UniqueConnection);
|
|
||||||
connect(lrcInstance_->getCurrentCallModel(),
|
|
||||||
&CallModel::callStatusChanged,
|
&CallModel::callStatusChanged,
|
||||||
this,
|
&CurrentConversation::onCallStatusChanged);
|
||||||
&CurrentConversation::onCallStatusChanged,
|
|
||||||
Qt::UniqueConnection);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|
211
src/app/mainview/components/InCallLocalVideo.qml
Normal file
211
src/app/mainview/components/InCallLocalVideo.qml
Normal file
|
@ -0,0 +1,211 @@
|
||||||
|
/*
|
||||||
|
* Copyright (C) 2024 Savoir-faire Linux Inc.
|
||||||
|
*
|
||||||
|
* 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, see <https://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import Qt5Compat.GraphicalEffects
|
||||||
|
|
||||||
|
import net.jami.Enums 1.1
|
||||||
|
import net.jami.Constants 1.1
|
||||||
|
import net.jami.Adapters 1.1
|
||||||
|
|
||||||
|
import "../../commoncomponents"
|
||||||
|
|
||||||
|
// This component uses anchors and they are set within this component.
|
||||||
|
LocalVideo {
|
||||||
|
id: localPreview
|
||||||
|
|
||||||
|
required property var container
|
||||||
|
required property real opacityModifier
|
||||||
|
|
||||||
|
readonly property int previewMargin: 15
|
||||||
|
readonly property int previewMarginYTop: previewMargin + 42
|
||||||
|
readonly property int previewMarginYBottom: previewMargin + 84
|
||||||
|
|
||||||
|
anchors.bottomMargin: previewMarginYBottom
|
||||||
|
anchors.leftMargin: sideMargin
|
||||||
|
anchors.rightMargin: sideMargin
|
||||||
|
anchors.topMargin: previewMarginYTop
|
||||||
|
|
||||||
|
visibilityCondition: (CurrentCall.isSharing || !CurrentCall.isVideoMuted) &&
|
||||||
|
!CurrentCall.isConference
|
||||||
|
height: width * invAspectRatio
|
||||||
|
width: Math.max(container.width / 5, JamiTheme.minimumPreviewWidth)
|
||||||
|
flip: CurrentCall.flipSelf && !CurrentCall.isSharing
|
||||||
|
blurRadius: hidden ? 25 : 0
|
||||||
|
|
||||||
|
opacity: hidden ? opacityModifier : 1
|
||||||
|
|
||||||
|
// Allow hiding the preview (available when anchored)
|
||||||
|
readonly property bool hovered: hoverHandler.hovered
|
||||||
|
readonly property bool anchored: state !== "unanchored"
|
||||||
|
property bool hidden: false
|
||||||
|
readonly property real hiddenHandleSize: 32
|
||||||
|
// Compute the margin as a function of the preview width in order to
|
||||||
|
// apply a negative margin and expose a constant width handle.
|
||||||
|
// If not hidden, return the previewMargin.
|
||||||
|
property real sideMargin: !hidden ? previewMargin : -(width - hiddenHandleSize)
|
||||||
|
// Animate the hiddenSize with a Behavior.
|
||||||
|
Behavior on sideMargin { NumberAnimation { duration: 250; easing.type: Easing.OutExpo }}
|
||||||
|
readonly property bool onLeft: state.indexOf("left") !== -1
|
||||||
|
PushButton {
|
||||||
|
id: hidePreviewButton
|
||||||
|
objectName: "hidePreviewButton"
|
||||||
|
|
||||||
|
width: localPreview.hiddenHandleSize
|
||||||
|
state: localPreview.onLeft ?
|
||||||
|
(localPreview.hidden ? "right" : "left") :
|
||||||
|
(localPreview.hidden ? "left" : "right")
|
||||||
|
states: [
|
||||||
|
State {
|
||||||
|
name: "left"
|
||||||
|
AnchorChanges {
|
||||||
|
target: hidePreviewButton
|
||||||
|
anchors.left: parent.left
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "right"
|
||||||
|
AnchorChanges {
|
||||||
|
target: hidePreviewButton
|
||||||
|
anchors.right: parent.right
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
anchors.top: parent.top
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
opacity: (localPreview.anchored && localPreview.hovered) || localPreview.hidden
|
||||||
|
Behavior on opacity { NumberAnimation { duration: 250; easing.type: Easing.OutExpo }}
|
||||||
|
visible: opacity > 0
|
||||||
|
background: Rectangle {
|
||||||
|
readonly property color normalColor: JamiTheme.mediumGrey
|
||||||
|
color: JamiTheme.mediumGrey
|
||||||
|
opacity: hidePreviewButton.hovered ? 0.7 : 0.5
|
||||||
|
Behavior on opacity { NumberAnimation { duration: 250; easing.type: Easing.OutExpo }}
|
||||||
|
}
|
||||||
|
normalImageSource: hidePreviewButton.state === "left" ?
|
||||||
|
JamiResources.chevron_left_black_24dp_svg :
|
||||||
|
JamiResources.chevron_right_black_24dp_svg
|
||||||
|
imageColor: JamiTheme.darkGreyColor
|
||||||
|
onClicked: localPreview.hidden = !localPreview.hidden
|
||||||
|
toolTipText: localPreview.hidden ?
|
||||||
|
JamiStrings.showLocalVideo :
|
||||||
|
JamiStrings.hideLocalVideo
|
||||||
|
}
|
||||||
|
|
||||||
|
state: "anchor_top_right"
|
||||||
|
states: [
|
||||||
|
State {
|
||||||
|
name: "unanchored"
|
||||||
|
AnchorChanges {
|
||||||
|
target: localPreview
|
||||||
|
anchors.top: undefined
|
||||||
|
anchors.right: undefined
|
||||||
|
anchors.bottom: undefined
|
||||||
|
anchors.left: undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "anchor_top_left"
|
||||||
|
AnchorChanges {
|
||||||
|
target: localPreview
|
||||||
|
anchors.top: localPreview.container.top
|
||||||
|
anchors.left: localPreview.container.left
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "anchor_top_right"
|
||||||
|
AnchorChanges {
|
||||||
|
target: localPreview
|
||||||
|
anchors.top: localPreview.container.top
|
||||||
|
anchors.right: localPreview.container.right
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "anchor_bottom_right"
|
||||||
|
AnchorChanges {
|
||||||
|
target: localPreview
|
||||||
|
anchors.bottom: localPreview.container.bottom
|
||||||
|
anchors.right: localPreview.container.right
|
||||||
|
}
|
||||||
|
},
|
||||||
|
State {
|
||||||
|
name: "anchor_bottom_left"
|
||||||
|
AnchorChanges {
|
||||||
|
target: localPreview
|
||||||
|
anchors.bottom: localPreview.container.bottom
|
||||||
|
anchors.left: localPreview.container.left
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
transitions: Transition {
|
||||||
|
AnchorAnimation {
|
||||||
|
duration: 250
|
||||||
|
easing.type: Easing.OutBack
|
||||||
|
easing.overshoot: 1.5
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HoverHandler {
|
||||||
|
id: hoverHandler
|
||||||
|
}
|
||||||
|
|
||||||
|
DragHandler {
|
||||||
|
id: dragHandler
|
||||||
|
readonly property var container: localPreview.container
|
||||||
|
target: parent
|
||||||
|
dragThreshold: 4
|
||||||
|
enabled: !localPreview.hidden
|
||||||
|
xAxis.maximum: container.width - parent.width - previewMargin
|
||||||
|
xAxis.minimum: previewMargin
|
||||||
|
yAxis.maximum: container.height - parent.height - previewMarginYBottom
|
||||||
|
yAxis.minimum: previewMarginYTop
|
||||||
|
onActiveChanged: {
|
||||||
|
if (active) {
|
||||||
|
localPreview.state = "unanchored";
|
||||||
|
} else {
|
||||||
|
const center = Qt.point(target.x + target.width / 2,
|
||||||
|
target.y + target.height / 2);
|
||||||
|
const containerCenter = Qt.point(container.x + container.width / 2,
|
||||||
|
container.y + container.height / 2);
|
||||||
|
if (center.x >= containerCenter.x) {
|
||||||
|
if (center.y >= containerCenter.y) {
|
||||||
|
localPreview.state = "anchor_bottom_right";
|
||||||
|
} else {
|
||||||
|
localPreview.state = "anchor_top_right";
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (center.y >= containerCenter.y) {
|
||||||
|
localPreview.state = "anchor_bottom_left";
|
||||||
|
} else {
|
||||||
|
localPreview.state = "anchor_top_left";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
layer.enabled: true
|
||||||
|
layer.effect: OpacityMask {
|
||||||
|
maskSource: Rectangle {
|
||||||
|
width: localPreview.width
|
||||||
|
height: localPreview.height
|
||||||
|
radius: JamiTheme.primaryRadius
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -30,11 +30,6 @@ import "../../commoncomponents"
|
||||||
Rectangle {
|
Rectangle {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
// Constraints for the preview component.
|
|
||||||
property int previewMargin: 15
|
|
||||||
property int previewMarginYTop: previewMargin + 42
|
|
||||||
property int previewMarginYBottom: previewMargin + 84
|
|
||||||
|
|
||||||
property alias chatViewContainer: chatViewContainer
|
property alias chatViewContainer: chatViewContainer
|
||||||
property string callPreviewId
|
property string callPreviewId
|
||||||
|
|
||||||
|
@ -166,222 +161,15 @@ Rectangle {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalVideo {
|
// Note: this component should not be used within a layout, as
|
||||||
|
// it implements anchor management itself.
|
||||||
|
InCallLocalVideo {
|
||||||
id: localPreview
|
id: localPreview
|
||||||
objectName: "localPreview"
|
objectName: "localPreview"
|
||||||
|
|
||||||
readonly property var container: parent
|
container: parent
|
||||||
readonly property string callPreviewId: root.callPreviewId
|
rendererId: CurrentCall.previewId
|
||||||
|
opacityModifier: callOverlay.mainOverlayOpacity
|
||||||
visibilityCondition: (CurrentCall.isSharing || !CurrentCall.isVideoMuted) &&
|
|
||||||
!CurrentCall.isConference
|
|
||||||
height: width * invAspectRatio
|
|
||||||
|
|
||||||
// Keep the area of the preview a proportion of the screen size plus a
|
|
||||||
// modifier to allow the user to scale it.
|
|
||||||
readonly property real containerArea: container.width * container.height
|
|
||||||
property real scalingFactor: 1
|
|
||||||
width: Math.sqrt(containerArea / 16) * scalingFactor
|
|
||||||
flip: CurrentCall.flipSelf && !CurrentCall.isSharing
|
|
||||||
blurRadius: hidden ? 25 : 0
|
|
||||||
onCallPreviewIdChanged: startWithId(callPreviewId)
|
|
||||||
onVisibleChanged: if (!visible) stop()
|
|
||||||
|
|
||||||
anchors.topMargin: previewMarginYTop
|
|
||||||
anchors.leftMargin: sideMargin
|
|
||||||
anchors.rightMargin: sideMargin
|
|
||||||
anchors.bottomMargin: previewMarginYBottom
|
|
||||||
|
|
||||||
opacity: hidden ? callOverlay.mainOverlayOpacity : 1
|
|
||||||
|
|
||||||
// Allow hiding the preview (available when anchored)
|
|
||||||
readonly property bool hovered: hoverHandler.hovered
|
|
||||||
readonly property bool anchored: state !== "unanchored"
|
|
||||||
property bool hidden: false
|
|
||||||
readonly property real hiddenHandleSize: 32
|
|
||||||
// Compute the margin as a function of the preview width in order to
|
|
||||||
// apply a negative margin and expose a constant width handle.
|
|
||||||
// If not hidden, return the previewMargin.
|
|
||||||
property real sideMargin: !hidden ? previewMargin : -(width - hiddenHandleSize)
|
|
||||||
// Animate the hiddenSize with a Behavior.
|
|
||||||
Behavior on sideMargin { NumberAnimation { duration: 250; easing.type: Easing.OutExpo }}
|
|
||||||
readonly property bool onLeft: state.indexOf("left") !== -1
|
|
||||||
PushButton {
|
|
||||||
id: hidePreviewButton
|
|
||||||
objectName: "hidePreviewButton"
|
|
||||||
|
|
||||||
width: localPreview.hiddenHandleSize
|
|
||||||
state: {
|
|
||||||
if (!localPreview.anchored) {
|
|
||||||
return "none";
|
|
||||||
}
|
|
||||||
return localPreview.onLeft ?
|
|
||||||
(localPreview.hidden ? "right" : "left") :
|
|
||||||
(localPreview.hidden ? "left" : "right")
|
|
||||||
}
|
|
||||||
states: [
|
|
||||||
State {
|
|
||||||
name: "none"
|
|
||||||
// Override visible to false when the localPreview isn't anchored.
|
|
||||||
PropertyChanges {
|
|
||||||
target: hidePreviewButton
|
|
||||||
visible: false
|
|
||||||
}
|
|
||||||
},
|
|
||||||
State {
|
|
||||||
name: "left"
|
|
||||||
AnchorChanges {
|
|
||||||
target: hidePreviewButton
|
|
||||||
anchors.left: parent.left
|
|
||||||
}
|
|
||||||
},
|
|
||||||
State {
|
|
||||||
name: "right"
|
|
||||||
AnchorChanges {
|
|
||||||
target: hidePreviewButton
|
|
||||||
anchors.right: parent.right
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
anchors.top: parent.top
|
|
||||||
anchors.bottom: parent.bottom
|
|
||||||
opacity: (localPreview.anchored && localPreview.hovered) || localPreview.hidden
|
|
||||||
Behavior on opacity { NumberAnimation { duration: 250; easing.type: Easing.OutExpo }}
|
|
||||||
visible: opacity > 0
|
|
||||||
background: Rectangle {
|
|
||||||
readonly property color normalColor: JamiTheme.mediumGrey
|
|
||||||
color: JamiTheme.mediumGrey
|
|
||||||
opacity: hidePreviewButton.hovered ? 0.7 : 0.5
|
|
||||||
Behavior on opacity { NumberAnimation { duration: 250; easing.type: Easing.OutExpo }}
|
|
||||||
}
|
|
||||||
normalImageSource: hidePreviewButton.state === "left" ?
|
|
||||||
JamiResources.chevron_left_black_24dp_svg :
|
|
||||||
JamiResources.chevron_right_black_24dp_svg
|
|
||||||
imageColor: JamiTheme.darkGreyColor
|
|
||||||
onClicked: localPreview.hidden = !localPreview.hidden
|
|
||||||
toolTipText: localPreview.hidden ?
|
|
||||||
JamiStrings.showLocalVideo :
|
|
||||||
JamiStrings.hideLocalVideo
|
|
||||||
}
|
|
||||||
|
|
||||||
state: "anchor_top_right"
|
|
||||||
states: [
|
|
||||||
State {
|
|
||||||
name: "unanchored"
|
|
||||||
AnchorChanges {
|
|
||||||
target: localPreview
|
|
||||||
anchors.top: undefined
|
|
||||||
anchors.right: undefined
|
|
||||||
anchors.bottom: undefined
|
|
||||||
anchors.left: undefined
|
|
||||||
}
|
|
||||||
},
|
|
||||||
State {
|
|
||||||
name: "anchor_top_left"
|
|
||||||
AnchorChanges {
|
|
||||||
target: localPreview
|
|
||||||
anchors.top: localPreview.container.top
|
|
||||||
anchors.left: localPreview.container.left
|
|
||||||
}
|
|
||||||
},
|
|
||||||
State {
|
|
||||||
name: "anchor_top_right"
|
|
||||||
AnchorChanges {
|
|
||||||
target: localPreview
|
|
||||||
anchors.top: localPreview.container.top
|
|
||||||
anchors.right: localPreview.container.right
|
|
||||||
}
|
|
||||||
},
|
|
||||||
State {
|
|
||||||
name: "anchor_bottom_right"
|
|
||||||
AnchorChanges {
|
|
||||||
target: localPreview
|
|
||||||
anchors.bottom: localPreview.container.bottom
|
|
||||||
anchors.right: localPreview.container.right
|
|
||||||
}
|
|
||||||
},
|
|
||||||
State {
|
|
||||||
name: "anchor_bottom_left"
|
|
||||||
AnchorChanges {
|
|
||||||
target: localPreview
|
|
||||||
anchors.bottom: localPreview.container.bottom
|
|
||||||
anchors.left: localPreview.container.left
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
|
|
||||||
transitions: Transition {
|
|
||||||
AnchorAnimation {
|
|
||||||
duration: 250
|
|
||||||
easing.type: Easing.OutBack
|
|
||||||
easing.overshoot: 1.5
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
HoverHandler {
|
|
||||||
id: hoverHandler
|
|
||||||
}
|
|
||||||
|
|
||||||
WheelHandler {
|
|
||||||
onWheel: function(event) {
|
|
||||||
const delta = event.angleDelta.y / 120 * 0.1;
|
|
||||||
parent.opacity = JamiQmlUtils.clamp(parent.opacity + delta, 0.25, 1);
|
|
||||||
}
|
|
||||||
acceptedModifiers: Qt.CTRL
|
|
||||||
}
|
|
||||||
|
|
||||||
WheelHandler {
|
|
||||||
onWheel: function(event) {
|
|
||||||
const delta = event.angleDelta.y / 120 * 0.1;
|
|
||||||
localPreview.scalingFactor = JamiQmlUtils.clamp(localPreview.scalingFactor + delta, 0.5, 4);
|
|
||||||
}
|
|
||||||
acceptedModifiers: Qt.NoModifier
|
|
||||||
enabled: !localPreview.hidden
|
|
||||||
}
|
|
||||||
|
|
||||||
DragHandler {
|
|
||||||
id: dragHandler
|
|
||||||
readonly property var container: localPreview.container
|
|
||||||
target: parent
|
|
||||||
dragThreshold: 4
|
|
||||||
enabled: !localPreview.hidden
|
|
||||||
xAxis.maximum: container.width - parent.width - previewMargin
|
|
||||||
xAxis.minimum: previewMargin
|
|
||||||
yAxis.maximum: container.height - parent.height - previewMarginYBottom
|
|
||||||
yAxis.minimum: previewMarginYTop
|
|
||||||
onActiveChanged: {
|
|
||||||
if (active) {
|
|
||||||
localPreview.state = "unanchored";
|
|
||||||
} else {
|
|
||||||
const center = Qt.point(target.x + target.width / 2,
|
|
||||||
target.y + target.height / 2);
|
|
||||||
const containerCenter = Qt.point(container.x + container.width / 2,
|
|
||||||
container.y + container.height / 2);
|
|
||||||
if (center.x >= containerCenter.x) {
|
|
||||||
if (center.y >= containerCenter.y) {
|
|
||||||
localPreview.state = "anchor_bottom_right";
|
|
||||||
} else {
|
|
||||||
localPreview.state = "anchor_top_right";
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (center.y >= containerCenter.y) {
|
|
||||||
localPreview.state = "anchor_bottom_left";
|
|
||||||
} else {
|
|
||||||
localPreview.state = "anchor_top_left";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
layer.enabled: true
|
|
||||||
layer.effect: OpacityMask {
|
|
||||||
maskSource: Rectangle {
|
|
||||||
width: localPreview.width
|
|
||||||
height: localPreview.height
|
|
||||||
radius: JamiTheme.primaryRadius
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CallOverlay {
|
CallOverlay {
|
||||||
|
|
|
@ -80,13 +80,10 @@ SettingsPageBase {
|
||||||
Component.onCompleted: {
|
Component.onCompleted: {
|
||||||
flipControl.checked = UtilsAdapter.getAppValue(Settings.FlipSelf);
|
flipControl.checked = UtilsAdapter.getAppValue(Settings.FlipSelf);
|
||||||
hardwareAccelControl.checked = AvAdapter.getHardwareAcceleration();
|
hardwareAccelControl.checked = AvAdapter.getHardwareAcceleration();
|
||||||
if (previewWidget.visible)
|
|
||||||
startPreviewing(true);
|
startPreviewing(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
Component.onDestruction: {
|
Component.onDestruction: previewWidget.stop()
|
||||||
previewWidget.startWithId("");
|
|
||||||
}
|
|
||||||
|
|
||||||
// video Preview
|
// video Preview
|
||||||
Rectangle {
|
Rectangle {
|
||||||
|
|
|
@ -256,14 +256,16 @@ VideoDevices::startDevice(const QString& id, bool force)
|
||||||
void
|
void
|
||||||
VideoDevices::stopDevice(const QString& id)
|
VideoDevices::stopDevice(const QString& id)
|
||||||
{
|
{
|
||||||
if (!id.isEmpty()) {
|
if (id.isEmpty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
qInfo() << "Stopping device" << id;
|
qInfo() << "Stopping device" << id;
|
||||||
if (lrcInstance_->avModel().stopPreview(id)) {
|
if (lrcInstance_->avModel().stopPreview(id)) {
|
||||||
deviceOpen_ = false;
|
deviceOpen_ = false;
|
||||||
} else {
|
} else {
|
||||||
qWarning() << "Failed to stop device" << id;
|
qWarning() << "Failed to stop device" << id;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
|
|
@ -149,6 +149,48 @@ struct Info
|
||||||
return true;
|
return true;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Extract some common meta data for this call including:
|
||||||
|
// - the video preview ID
|
||||||
|
// - audio/video muted status
|
||||||
|
// - if the call is sharing (indicating that the preview is a screen share)
|
||||||
|
QVariantMap getCallInfoEx() const
|
||||||
|
{
|
||||||
|
bool isAudioMuted = false;
|
||||||
|
bool isVideoMuted = false;
|
||||||
|
QString previewId;
|
||||||
|
QVariantMap callInfo;
|
||||||
|
using namespace libjami::Media;
|
||||||
|
if (status == lrc::api::call::Status::ENDED) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
for (const auto& media : mediaList) {
|
||||||
|
if (media[MediaAttributeKey::MEDIA_TYPE] == Details::MEDIA_TYPE_VIDEO) {
|
||||||
|
if (media[MediaAttributeKey::SOURCE].startsWith(VideoProtocolPrefix::DISPLAY)
|
||||||
|
|| media[MediaAttributeKey::SOURCE].startsWith(VideoProtocolPrefix::FILE)) {
|
||||||
|
callInfo["is_sharing"] = true;
|
||||||
|
callInfo["preview_id"] = media[MediaAttributeKey::SOURCE];
|
||||||
|
}
|
||||||
|
if (media[MediaAttributeKey::ENABLED] == TRUE_STR
|
||||||
|
&& media[MediaAttributeKey::MUTED] == FALSE_STR && previewId.isEmpty()) {
|
||||||
|
previewId = media[libjami::Media::MediaAttributeKey::SOURCE];
|
||||||
|
}
|
||||||
|
if (media[libjami::Media::MediaAttributeKey::SOURCE].startsWith(
|
||||||
|
libjami::Media::VideoProtocolPrefix::CAMERA)) {
|
||||||
|
isVideoMuted |= media[MediaAttributeKey::MUTED] == TRUE_STR;
|
||||||
|
callInfo["is_capturing"] = media[MediaAttributeKey::MUTED] == FALSE_STR;
|
||||||
|
}
|
||||||
|
} else if (media[MediaAttributeKey::MEDIA_TYPE] == Details::MEDIA_TYPE_AUDIO) {
|
||||||
|
if (media[MediaAttributeKey::LABEL] == "audio_0") {
|
||||||
|
isAudioMuted |= media[libjami::Media::MediaAttributeKey::MUTED] == TRUE_STR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
callInfo["preview_id"] = previewId;
|
||||||
|
callInfo["is_audio_muted"] = isAudioMuted;
|
||||||
|
callInfo["is_video_muted"] = isVideoMuted;
|
||||||
|
return callInfo;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
static inline bool
|
static inline bool
|
||||||
|
|
|
@ -464,7 +464,7 @@ Q_SIGNALS:
|
||||||
* Emitted when a conversation detects an error
|
* Emitted when a conversation detects an error
|
||||||
* @param uid
|
* @param uid
|
||||||
*/
|
*/
|
||||||
void onConversationErrorsUpdated(const QString& uid) const;
|
void conversationErrorsUpdated(const QString& uid) const;
|
||||||
/**
|
/**
|
||||||
* Emitted when conversation's preferences has been updated
|
* Emitted when conversation's preferences has been updated
|
||||||
* @param uid
|
* @param uid
|
||||||
|
|
|
@ -1089,7 +1089,7 @@ ConversationModel::popFrontError(const QString& conversationId)
|
||||||
|
|
||||||
auto& conversation = conversationOpt->get();
|
auto& conversation = conversationOpt->get();
|
||||||
conversation.errors.pop_front();
|
conversation.errors.pop_front();
|
||||||
Q_EMIT onConversationErrorsUpdated(conversationId);
|
Q_EMIT conversationErrorsUpdated(conversationId);
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
|
@ -2706,7 +2706,7 @@ ConversationModelPimpl::slotOnConversationError(const QString& accountId,
|
||||||
try {
|
try {
|
||||||
auto& conversation = getConversationForUid(conversationId).get();
|
auto& conversation = getConversationForUid(conversationId).get();
|
||||||
conversation.errors.push_back({code, what});
|
conversation.errors.push_back({code, what});
|
||||||
Q_EMIT linked.onConversationErrorsUpdated(conversationId);
|
Q_EMIT linked.conversationErrorsUpdated(conversationId);
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -180,6 +180,17 @@ TestWrapper {
|
||||||
compare(localPreview.hidden, false);
|
compare(localPreview.hidden, false);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function test_localPreviewRemainsVisibleWhenOngoingCallPageIsToggled() {
|
||||||
|
localPreviewTestWrapper(function(localPreview) {
|
||||||
|
// The local preview should remain visible when the OngoingCallPage is toggled.
|
||||||
|
compare(localPreview.visible, true);
|
||||||
|
uut.visible = false;
|
||||||
|
compare(localPreview.visible, false);
|
||||||
|
uut.visible = true;
|
||||||
|
compare(localPreview.visible, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue