Skip to content
Snippets Groups Projects
Commit b38e2167 authored by Andreas Traczyk's avatar Andreas Traczyk
Browse files

ongoingcallpage: local-preview: add a hide-preview feature

Gitlab: #1555
Change-Id: Ifa196b91fed4d13d1cd0acf535cc3e1802c22a29
parent 91f32f24
No related branches found
No related tags found
No related merge requests found
......@@ -23,19 +23,26 @@ import net.jami.Adapters 1.1
VideoView {
id: root
property bool visibilityCondition: true
crop: true
visible: isRendering && visibilityCondition
function startWithId(id, force = false) {
if (id !== undefined && id.length === 0) {
stop();
return;
}
const forceRestart = rendererId === id;
if (!forceRestart) {
// Stop previous device
VideoDevices.stopDevice(rendererId);
rendererId = id;
} else {
const forceRestart = rendererId === id;
if (!forceRestart) {
// Stop previous device
VideoDevices.stopDevice(rendererId);
}
rendererId = VideoDevices.startDevice(id, forceRestart);
}
rendererId = VideoDevices.startDevice(id, forceRestart);
}
function stop() {
VideoDevices.stopDevice(rendererId);
rendererId = "";
}
}
......@@ -29,6 +29,10 @@ Item {
property real invAspectRatio: (videoOutput.sourceRect.height / videoOutput.sourceRect.width) || 0.5625 // 16:9 default
property bool crop: false
property bool flip: false
property real blurRadius: 0
// We need to know if the frames are being rendered to the screen or not.
readonly property bool isRendering: videoProvider.activeRenderers[rendererId] !== undefined
// This rect describes the actual rendered content rectangle
// as the VideoOutput component may use PreserveAspectFit
......@@ -55,7 +59,7 @@ Item {
antialiasing: true
anchors.fill: parent
opacity: videoProvider.activeRenderers[rendererId] === true
opacity: isRendering
visible: opacity
fillMode: crop ? VideoOutput.PreserveAspectCrop : VideoOutput.PreserveAspectFit
......@@ -70,7 +74,7 @@ Item {
layer.effect: FastBlur {
source: videoOutput
anchors.fill: root
radius: (1. - opacity) * 100
radius: blurRadius ? blurRadius : (1. - opacity) * 100
}
transform: Scale {
......
......@@ -32,6 +32,7 @@ Item {
id: root
property bool participantsSide: UtilsAdapter.getAppValue(Settings.ParticipantsSide)
property alias mainOverlayOpacity: mainOverlay.opacity
signal chatButtonClicked
signal fullScreenClicked
......
......@@ -36,34 +36,28 @@ Rectangle {
color: "black"
LocalVideo {
id: previewRenderer
id: localPreview
anchors.centerIn: parent
anchors.fill: parent
visible: !CurrentCall.isAudioOnly && CurrentAccount.videoEnabled_Video && VideoDevices.listSize !== 0 && ((CurrentCall.status >= Call.Status.INCOMING_RINGING && CurrentCall.status <= Call.Status.SEARCHING) || CurrentCall.status === Call.Status.CONNECTED)
opacity: 0.5
// HACK: this is a workaround to the preview video starting
// and stopping a few times. The root cause should be investigated ASAP.
Timer {
id: controlPreview
property bool startVideo
interval: 1000
onTriggered: {
var rendId = visible && startVideo ? VideoDevices.getDefaultDevice() : "";
previewRenderer.startWithId(rendId);
readonly property bool start: {
if (CurrentCall.isAudioOnly || !CurrentAccount.videoEnabled_Video) {
return false;
}
}
onVisibleChanged: {
controlPreview.stop();
if (visible) {
controlPreview.startVideo = true;
controlPreview.interval = 1000;
} else {
controlPreview.startVideo = false;
controlPreview.interval = 0;
if (!VideoDevices.listSize) {
return false;
}
controlPreview.start();
const isCallStatusEligible =
(CurrentCall.status >= Call.Status.INCOMING_RINGING &&
CurrentCall.status <= Call.Status.SEARCHING) ||
CurrentCall.status === Call.Status.CONNECTED;
if (!isCallStatusEligible) {
return false;
}
return true;
}
onStartChanged: localPreview.startWithId(start ? VideoDevices.getDefaultDevice() : "")
opacity: 0.5
}
ListModel {
......
......@@ -41,10 +41,6 @@ Rectangle {
// A link to the first child will provide access to the chat view.
property var chatView: chatViewContainer.children[0]
onCallPreviewIdChanged: {
controlPreview.start();
}
color: "black"
Connections {
......@@ -131,8 +127,8 @@ Rectangle {
onTapped: function (eventPoint, button) {
if (button === Qt.RightButton) {
var isOnLocal = eventPoint.position.x >= previewRenderer.x && eventPoint.position.x <= previewRenderer.x + previewRenderer.width;
isOnLocal &= eventPoint.position.y >= previewRenderer.y && eventPoint.position.y <= previewRenderer.y + previewRenderer.height;
var isOnLocal = eventPoint.position.x >= localPreview.x && eventPoint.position.x <= localPreview.x + localPreview.width;
isOnLocal &= eventPoint.position.y >= localPreview.y && eventPoint.position.y <= localPreview.y + localPreview.height;
isOnLocal |= participantsLayer.hoveredOverlaySinkId.indexOf("camera://") === 0;
callOverlay.openCallViewContextMenuInPos(eventPoint.position.x, eventPoint.position.y, participantsLayer.hoveredOverlayUri, participantsLayer.hoveredOverlaySinkId, participantsLayer.hoveredOverVideoMuted, isOnLocal);
}
......@@ -171,53 +167,90 @@ Rectangle {
}
LocalVideo {
id: previewRenderer
id: localPreview
objectName: "localPreview"
visible: (CurrentCall.isSharing || !CurrentCall.isVideoMuted) && !CurrentCall.isConference
readonly property var container: parent
readonly property string callPreviewId: root.callPreviewId
visibilityCondition: (CurrentCall.isSharing || !CurrentCall.isVideoMuted) &&
!CurrentCall.isConference
height: width * invAspectRatio
width: Math.max(callPageMainRect.width / 5, JamiTheme.minimumPreviewWidth)
x: callPageMainRect.width - previewRenderer.width - previewMargin
y: previewMarginYTop
width: Math.max(container.width / 5, JamiTheme.minimumPreviewWidth)
flip: CurrentCall.flipSelf && !CurrentCall.isSharing
blurRadius: hidden ? 25 : 0
onCallPreviewIdChanged: startWithId(callPreviewId)
onVisibleChanged: if (!visible) stop()
// HACK: this is a workaround to the preview video starting
// and stopping a few times. The root cause should be investigated ASAP.
Timer {
id: controlPreview
property bool startVideo
interval: 1000
onTriggered: {
var rendId = visible && startVideo ? root.callPreviewId : "";
previewRenderer.startWithId(rendId);
}
}
anchors.topMargin: previewMarginYTop
anchors.leftMargin: sideMargin
anchors.rightMargin: sideMargin
anchors.bottomMargin: previewMarginYBottom
onVisibleChanged: {
controlPreview.stop();
if (visible) {
controlPreview.startVideo = true;
controlPreview.interval = 1000;
} else {
controlPreview.startVideo = false;
controlPreview.interval = 0;
opacity: hidden ? callOverlay.mainOverlayOpacity : 1
// Allow hiding the preview (available when anchored)
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 && hoverHandler.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 }}
}
controlPreview.start();
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
}
anchors.topMargin: previewMarginYTop
anchors.leftMargin: previewMargin
anchors.rightMargin: previewMargin
anchors.bottomMargin: previewMarginYBottom
state: "anchor_top_right"
states: [
// Not anchored
State {
name: "unanchored"
AnchorChanges {
target: previewRenderer
target: localPreview
anchors.top: undefined
anchors.right: undefined
anchors.bottom: undefined
......@@ -227,33 +260,33 @@ Rectangle {
State {
name: "anchor_top_left"
AnchorChanges {
target: previewRenderer
anchors.top: callPageMainRect.top
anchors.left: callPageMainRect.left
target: localPreview
anchors.top: localPreview.container.top
anchors.left: localPreview.container.left
}
},
State {
name: "anchor_top_right"
AnchorChanges {
target: previewRenderer
anchors.top: callPageMainRect.top
anchors.right: callPageMainRect.right
target: localPreview
anchors.top: localPreview.container.top
anchors.right: localPreview.container.right
}
},
State {
name: "anchor_bottom_right"
AnchorChanges {
target: previewRenderer
anchors.bottom: callPageMainRect.bottom
anchors.right: callPageMainRect.right
target: localPreview
anchors.bottom: localPreview.container.bottom
anchors.right: localPreview.container.right
}
},
State {
name: "anchor_bottom_left"
AnchorChanges {
target: previewRenderer
anchors.bottom: callPageMainRect.bottom
anchors.left: callPageMainRect.left
target: localPreview
anchors.bottom: localPreview.container.bottom
anchors.left: localPreview.container.left
}
}
]
......@@ -266,17 +299,23 @@ Rectangle {
}
}
HoverHandler {
id: hoverHandler
}
DragHandler {
readonly property var container: callPageMainRect
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) {
previewRenderer.state = "unanchored";
localPreview.state = "unanchored";
} else {
const center = Qt.point(target.x + target.width / 2,
target.y + target.height / 2);
......@@ -284,15 +323,15 @@ Rectangle {
container.y + container.height / 2);
if (center.x >= containerCenter.x) {
if (center.y >= containerCenter.y) {
previewRenderer.state = "anchor_bottom_right";
localPreview.state = "anchor_bottom_right";
} else {
previewRenderer.state = "anchor_top_right";
localPreview.state = "anchor_top_right";
}
} else {
if (center.y >= containerCenter.y) {
previewRenderer.state = "anchor_bottom_left";
localPreview.state = "anchor_bottom_left";
} else {
previewRenderer.state = "anchor_top_left";
localPreview.state = "anchor_top_left";
}
}
}
......@@ -302,8 +341,8 @@ Rectangle {
layer.enabled: true
layer.effect: OpacityMask {
maskSource: Rectangle {
width: previewRenderer.width
height: previewRenderer.height
width: localPreview.width
height: localPreview.height
radius: JamiTheme.primaryRadius
}
}
......
......@@ -785,6 +785,8 @@ Item {
property string becomeHostOneCall: qsTr("Host only this call")
property string hostThisCall: qsTr("Host this call")
property string becomeDefaultHost: qsTr("Make me the default host for future calls")
property string showLocalVideo: qsTr("Show local video")
property string hideLocalVideo: qsTr("Hide local video")
// Invitation View
property string invitationViewSentRequest: qsTr("%1 has sent you a request for a conversation.")
......
......@@ -257,8 +257,12 @@ void
VideoDevices::stopDevice(const QString& id)
{
if (!id.isEmpty()) {
lrcInstance_->avModel().stopPreview(id);
deviceOpen_ = false;
qInfo() << "Stopping device" << id;
if (lrcInstance_->avModel().stopPreview(id)) {
deviceOpen_ = false;
} else {
qWarning() << "Failed to stop device" << id;
}
}
}
......
......@@ -243,7 +243,7 @@ public:
* Stop preview renderer and the camera.
* @param resource
*/
Q_INVOKABLE void stopPreview(const QString& resource);
Q_INVOKABLE bool stopPreview(const QString& resource);
/**
* Get the list of available windows ids
* X11: a id is of the form 0x0000000
......
......@@ -560,10 +560,10 @@ AVModel::startPreview(const QString& resource)
return VideoManager::instance().openVideoInput(resource);
}
void
bool
AVModel::stopPreview(const QString& resource)
{
VideoManager::instance().closeVideoInput(resource);
return VideoManager::instance().closeVideoInput(resource);
}
#ifdef WIN32
......@@ -795,42 +795,50 @@ AVModel::useDirectRenderer() const
#endif
}
QString AVModel::createMediaPlayer(const QString& resource)
QString
AVModel::createMediaPlayer(const QString& resource)
{
return VideoManager::instance().createMediaPlayer(resource);
}
void AVModel::closeMediaPlayer(const QString& resource)
void
AVModel::closeMediaPlayer(const QString& resource)
{
VideoManager::instance().closeMediaPlayer(resource);
}
bool AVModel::pausePlayer(const QString& id, bool pause)
bool
AVModel::pausePlayer(const QString& id, bool pause)
{
return VideoManager::instance().pausePlayer(id, pause);
}
bool AVModel::mutePlayerAudio(const QString& id, bool mute)
bool
AVModel::mutePlayerAudio(const QString& id, bool mute)
{
return VideoManager::instance().mutePlayerAudio(id, mute);
}
bool AVModel::playerSeekToTime(const QString& id, int time)
bool
AVModel::playerSeekToTime(const QString& id, int time)
{
return VideoManager::instance().playerSeekToTime(id, time);
}
qint64 AVModel::getPlayerPosition(const QString& id)
qint64
AVModel::getPlayerPosition(const QString& id)
{
return VideoManager::instance().getPlayerPosition(id);
}
qint64 AVModel::getPlayerDuration(const QString& id)
qint64
AVModel::getPlayerDuration(const QString& id)
{
return VideoManager::instance().getPlayerDuration(id);
}
void AVModel::setAutoRestart(const QString& id, bool restart)
void
AVModel::setAutoRestart(const QString& id, bool restart)
{
VideoManager::instance().setAutoRestart(id, restart);
}
......
......@@ -130,10 +130,10 @@ public Q_SLOTS: // METHODS
#endif
}
void closeVideoInput(const QString& resource)
bool closeVideoInput(const QString& resource)
{
#ifdef ENABLE_VIDEO
libjami::closeVideoInput(resource.toLatin1().toStdString());
return libjami::closeVideoInput(resource.toLatin1().toStdString());
#endif
}
......
......@@ -88,11 +88,9 @@ public Q_SLOTS:
auto downloadPath = settingsManager_->getValue(Settings::Key::DownloadPath);
lrcInstance_->accountModel().downloadDirectory = downloadPath.toString() + "/";
// Create 2 Account
QSignalSpy accountStatusChangedSpy(&lrcInstance_->accountModel(),
&AccountModel::accountStatusChanged);
// Create 2 Accounts
QSignalSpy accountAddedSpy(&lrcInstance_->accountModel(), &AccountModel::accountAdded);
aliceId = lrcInstance_->accountModel().createNewAccount(profile::Type::JAMI, "Alice");
accountAddedSpy.wait(15000);
QCOMPARE(accountAddedSpy.count(), 1);
......
......@@ -32,6 +32,20 @@ Item {
width: childrenRect.width
height: childrenRect.height
// This is a helper function to wait for a signal to be emitted and check a condition.
function waitForSignalAndCheck(signalObject, signalName, action, checkExpression) {
// Create the SignalSpy component dynamically with the provided signal object and name.
const spy = Qt.createQmlObject('import QtTest 1.0; SignalSpy {}', this);
spy.target = signalObject;
spy.signalName = signalName;
// Perform the action that should emit the signal.
action();
// Wait a maximum of 1 second for the signal to be emitted.
spy.wait(1000);
// Check the signal count and the provided expression.
return spy.count > 0 && checkExpression();
}
// A binding to the windowShown property
Binding {
tw.appWindow: uut.Window.window
......
......@@ -38,7 +38,12 @@ TestWrapper {
when: windowShown // Mouse events can only be handled
// after the window has been shown.
property string dummyImgUrl
function initTestCase() {
// Create a dummy image file to use for the local preview.
dummyImgUrl = UtilsAdapter.urlFromLocalPath(UtilsAdapter.createDummyImage());
// The CallActionBar on the OngoingCallPage starts out invisible and
// is made visible whenever the user moves their mouse.
// This is implemented via an event filter in the CallOverlayModel
......@@ -79,50 +84,88 @@ TestWrapper {
compare(callActionBar.visible, true)
}
function test_checkLocalPreviewAnchoring() {
// Define a generic local preview test wrapper function that starts and stops
// the local preview, and calls the provided test function in between.
function localPreviewTestWrapper(testFunction) {
const localPreview = findChild(uut, "localPreview");
const container = localPreview.parent;
// The preview should normally be invisible at first, but there is a bug
// in the current implementation that makes it visible. This will need to
// be adjusted once the bug is fixed.
compare(localPreview.visible, true);
// Start a preview of a local resource.
const dummyImgFile = UtilsAdapter.createDummyImage();
localPreview.startWithId(UtilsAdapter.urlFromLocalPath(dummyImgFile));
// First check that the preview is anchored.
verify(localPreview.state.indexOf("unanchored") === -1);
const containerCenter = Qt.point(container.width / 2, container.height / 2);
function moveAndVerifyState(dx, dy, expectedState) {
const previewCenter = Qt.point(localPreview.x + localPreview.width / 2,
localPreview.y + localPreview.height / 2);
const destination = Qt.point(containerCenter.x + dx,
containerCenter.y + dy);
// Position the mouse at the center of the preview, then drag it until
// we reach the destination point.
mouseDrag(container, previewCenter.x, previewCenter.y,
destination.x - previewCenter.x, destination.y - previewCenter.y);
wait(250);
compare(localPreview.state, expectedState);
}
const dx = 1;
const dy = 1;
moveAndVerifyState(-dx, -dy, "anchor_top_left");
moveAndVerifyState(dx, -dy, "anchor_top_right");
moveAndVerifyState(-dx, dy, "anchor_bottom_left");
moveAndVerifyState(dx, dy, "anchor_bottom_right");
// Verify that during a drag process, the preview is unanchored.
mousePress(localPreview);
mouseMove(localPreview, 100, 100);
verify(localPreview.state.indexOf("unanchored") !== -1);
// The preview should be invisible at first.
compare(localPreview.visible, false);
// Start a preview of a local resource and wait for the preview to become visible.
verify(waitForSignalAndCheck(localPreview,
"visibleChanged",
() => localPreview.startWithId(dummyImgUrl),
() => localPreview.visible));
// Call the provided test function.
testFunction(localPreview);
// Stop the preview.
localPreview.startWithId("");
verify(waitForSignalAndCheck(localPreview,
"visibleChanged",
() => localPreview.stop(),
() => !localPreview.visible));
}
function test_localPreviewAnchoring() {
localPreviewTestWrapper(function(localPreview) {
const container = localPreview.parent;
// First check that the preview is anchored.
verify(localPreview.state.indexOf("unanchored") === -1);
const containerCenter = Qt.point(container.width / 2, container.height / 2);
function moveAndVerifyState(dx, dy, expectedState) {
const previewCenter = Qt.point(localPreview.x + localPreview.width / 2,
localPreview.y + localPreview.height / 2);
const destination = Qt.point(containerCenter.x + dx,
containerCenter.y + dy);
// Position the mouse at the center of the preview, then drag it until
// we reach the destination point.
mouseDrag(container, previewCenter.x, previewCenter.y,
destination.x - previewCenter.x, destination.y - previewCenter.y);
wait(250);
compare(localPreview.state, expectedState);
}
const dx = 1;
const dy = 1;
moveAndVerifyState(-dx, -dy, "anchor_top_left");
moveAndVerifyState(dx, -dy, "anchor_top_right");
moveAndVerifyState(-dx, dy, "anchor_bottom_left");
moveAndVerifyState(dx, dy, "anchor_bottom_right");
// Verify that during a drag process, the preview is unanchored.
mousePress(localPreview);
mouseMove(localPreview, 100, 100);
verify(localPreview.state.indexOf("unanchored") !== -1);
mouseRelease(localPreview);
});
}
function test_localPreviewHiding() {
localPreviewTestWrapper(function(localPreview) {
// Make sure the preview is anchored.
verify(localPreview.state.indexOf("unanchored") === -1);
// It should also not be hidden.
compare(localPreview.hidden, false);
// We presume that the preview is anchored and that once we hover over the
// local preview, that the hide button will become visible.
// Note: There is currently an issue where the CallOverlay is detecting hover
// events, but child controls are not. This should be addressed as it seems
// to affect MouseArea, HoverHandler, Buttons, and other controls that rely
// on hover events. For now, we'll manually trigger the onClicked event.
const hidePreviewButton = findChild(localPreview, "hidePreviewButton");
hidePreviewButton.onClicked();
compare(localPreview.hidden, true);
// "Click" the hide button again to unhide the preview.
hidePreviewButton.onClicked();
compare(localPreview.hidden, false);
});
}
}
}
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment