mirror of
https://github.com/Nheko-Reborn/nheko.git
synced 2024-11-25 20:48:52 +03:00
Support video calls
This commit is contained in:
parent
3499abd99a
commit
d1f3a3ef40
18 changed files with 1171 additions and 447 deletions
BIN
resources/icons/ui/video-call.png
Normal file
BIN
resources/icons/ui/video-call.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 353 B |
|
@ -10,6 +10,12 @@ Rectangle {
|
|||
color: "#2ECC71"
|
||||
implicitHeight: rowLayout.height + 8
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: if (TimelineManager.onVideoCall)
|
||||
stackLayout.currentIndex = stackLayout.currentIndex ? 0 : 1;
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
id: rowLayout
|
||||
|
||||
|
@ -33,7 +39,8 @@ Rectangle {
|
|||
Image {
|
||||
Layout.preferredWidth: 24
|
||||
Layout.preferredHeight: 24
|
||||
source: "qrc:/icons/icons/ui/place-call.png"
|
||||
source: TimelineManager.onVideoCall ?
|
||||
"qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
|
||||
}
|
||||
|
||||
Label {
|
||||
|
@ -58,9 +65,12 @@ Rectangle {
|
|||
callStateLabel.text = "00:00";
|
||||
var d = new Date();
|
||||
callTimer.startTime = Math.floor(d.getTime() / 1000);
|
||||
if (TimelineManager.onVideoCall)
|
||||
stackLayout.currentIndex = 1;
|
||||
break;
|
||||
case WebRTCState.DISCONNECTED:
|
||||
callStateLabel.text = "";
|
||||
stackLayout.currentIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -4,7 +4,7 @@ import "./emoji"
|
|||
import QtGraphicalEffects 1.0
|
||||
import QtQuick 2.9
|
||||
import QtQuick.Controls 2.3
|
||||
import QtQuick.Layouts 1.2
|
||||
import QtQuick.Layouts 1.3
|
||||
import QtQuick.Window 2.2
|
||||
import im.nheko 1.0
|
||||
import im.nheko.EmojiModel 1.0
|
||||
|
@ -282,144 +282,157 @@ Page {
|
|||
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: chat
|
||||
StackLayout {
|
||||
id: stackLayout
|
||||
currentIndex: 0
|
||||
|
||||
property int delegateMaxWidth: (Settings.timelineMaxWidth > 100 && (parent.width - Settings.timelineMaxWidth) > scrollbar.width * 2) ? Settings.timelineMaxWidth : (parent.width - scrollbar.width * 2)
|
||||
|
||||
visible: TimelineManager.timeline != null
|
||||
cacheBuffer: 400
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
model: TimelineManager.timeline
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
pixelAligned: true
|
||||
spacing: 4
|
||||
verticalLayoutDirection: ListView.BottomToTop
|
||||
onCountChanged: {
|
||||
if (atYEnd)
|
||||
model.currentIndex = 0;
|
||||
|
||||
} // Mark last event as read, since we are at the bottom
|
||||
|
||||
ScrollHelper {
|
||||
flickable: parent
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.MoveToPreviousPage
|
||||
onActivated: {
|
||||
chat.contentY = chat.contentY - chat.height / 2;
|
||||
chat.returnToBounds();
|
||||
Connections {
|
||||
target: TimelineManager
|
||||
function onActiveTimelineChanged() {
|
||||
stackLayout.currentIndex = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.MoveToNextPage
|
||||
onActivated: {
|
||||
chat.contentY = chat.contentY + chat.height / 2;
|
||||
chat.returnToBounds();
|
||||
}
|
||||
}
|
||||
ListView {
|
||||
id: chat
|
||||
|
||||
Shortcut {
|
||||
sequence: StandardKey.Cancel
|
||||
onActivated: chat.model.reply = undefined
|
||||
}
|
||||
property int delegateMaxWidth: (Settings.timelineMaxWidth > 100 && (parent.width - Settings.timelineMaxWidth) > scrollbar.width * 2) ? Settings.timelineMaxWidth : (parent.width - scrollbar.width * 2)
|
||||
|
||||
Shortcut {
|
||||
sequence: "Alt+Up"
|
||||
onActivated: chat.model.reply = chat.model.indexToId(chat.model.reply ? chat.model.idToIndex(chat.model.reply) + 1 : 0)
|
||||
}
|
||||
visible: TimelineManager.timeline != null
|
||||
cacheBuffer: 400
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
model: TimelineManager.timeline
|
||||
boundsBehavior: Flickable.StopAtBounds
|
||||
pixelAligned: true
|
||||
spacing: 4
|
||||
verticalLayoutDirection: ListView.BottomToTop
|
||||
onCountChanged: {
|
||||
if (atYEnd)
|
||||
model.currentIndex = 0;
|
||||
|
||||
Shortcut {
|
||||
sequence: "Alt+Down"
|
||||
onActivated: {
|
||||
var idx = chat.model.reply ? chat.model.idToIndex(chat.model.reply) - 1 : -1;
|
||||
chat.model.reply = idx >= 0 ? chat.model.indexToId(idx) : undefined;
|
||||
}
|
||||
}
|
||||
} // Mark last event as read, since we are at the bottom
|
||||
|
||||
Component {
|
||||
id: userProfileComponent
|
||||
|
||||
UserProfile {
|
||||
ScrollHelper {
|
||||
flickable: parent
|
||||
anchors.fill: parent
|
||||
}
|
||||
|
||||
}
|
||||
Shortcut {
|
||||
sequence: StandardKey.MoveToPreviousPage
|
||||
onActivated: {
|
||||
chat.contentY = chat.contentY - chat.height / 2;
|
||||
chat.returnToBounds();
|
||||
}
|
||||
}
|
||||
|
||||
section {
|
||||
property: "section"
|
||||
}
|
||||
Shortcut {
|
||||
sequence: StandardKey.MoveToNextPage
|
||||
onActivated: {
|
||||
chat.contentY = chat.contentY + chat.height / 2;
|
||||
chat.returnToBounds();
|
||||
}
|
||||
}
|
||||
|
||||
Component {
|
||||
id: sectionHeader
|
||||
Shortcut {
|
||||
sequence: StandardKey.Cancel
|
||||
onActivated: chat.model.reply = undefined
|
||||
}
|
||||
|
||||
Column {
|
||||
property var modelData
|
||||
property string section
|
||||
property string nextSection
|
||||
Shortcut {
|
||||
sequence: "Alt+Up"
|
||||
onActivated: chat.model.reply = chat.model.indexToId(chat.model.reply ? chat.model.idToIndex(chat.model.reply) + 1 : 0)
|
||||
}
|
||||
|
||||
topPadding: 4
|
||||
bottomPadding: 4
|
||||
spacing: 8
|
||||
visible: !!modelData
|
||||
width: parent.width
|
||||
height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8
|
||||
Shortcut {
|
||||
sequence: "Alt+Down"
|
||||
onActivated: {
|
||||
var idx = chat.model.reply ? chat.model.idToIndex(chat.model.reply) - 1 : -1;
|
||||
chat.model.reply = idx >= 0 ? chat.model.indexToId(idx) : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
id: dateBubble
|
||||
|
||||
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
|
||||
visible: section.includes(" ")
|
||||
text: chat.model.formatDateSeparator(modelData.timestamp)
|
||||
color: colors.text
|
||||
height: fontMetrics.height * 1.4
|
||||
width: contentWidth * 1.2
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
background: Rectangle {
|
||||
radius: parent.height / 2
|
||||
color: colors.base
|
||||
}
|
||||
Component {
|
||||
id: userProfileComponent
|
||||
|
||||
UserProfile {
|
||||
}
|
||||
|
||||
Row {
|
||||
height: userName.height
|
||||
}
|
||||
|
||||
section {
|
||||
property: "section"
|
||||
}
|
||||
|
||||
Component {
|
||||
id: sectionHeader
|
||||
|
||||
Column {
|
||||
property var modelData
|
||||
property string section
|
||||
property string nextSection
|
||||
|
||||
topPadding: 4
|
||||
bottomPadding: 4
|
||||
spacing: 8
|
||||
|
||||
Avatar {
|
||||
width: avatarSize
|
||||
height: avatarSize
|
||||
url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/")
|
||||
displayName: modelData.userName
|
||||
userid: modelData.userId
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: chat.model.openUserProfile(modelData.userId)
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
propagateComposedEvents: true
|
||||
}
|
||||
|
||||
}
|
||||
visible: !!modelData
|
||||
width: parent.width
|
||||
height: (section.includes(" ") ? dateBubble.height + 8 + userName.height : userName.height) + 8
|
||||
|
||||
Label {
|
||||
id: userName
|
||||
id: dateBubble
|
||||
|
||||
text: TimelineManager.escapeEmoji(modelData.userName)
|
||||
color: TimelineManager.userColor(modelData.userId, colors.window)
|
||||
textFormat: Text.RichText
|
||||
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
|
||||
visible: section.includes(" ")
|
||||
text: chat.model.formatDateSeparator(modelData.timestamp)
|
||||
color: colors.text
|
||||
height: fontMetrics.height * 1.4
|
||||
width: contentWidth * 1.2
|
||||
horizontalAlignment: Text.AlignHCenter
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
|
||||
background: Rectangle {
|
||||
radius: parent.height / 2
|
||||
color: colors.base
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Row {
|
||||
height: userName.height
|
||||
spacing: 8
|
||||
|
||||
Avatar {
|
||||
width: avatarSize
|
||||
height: avatarSize
|
||||
url: chat.model.avatarUrl(modelData.userId).replace("mxc://", "image://MxcImage/")
|
||||
displayName: modelData.userName
|
||||
userid: modelData.userId
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: chat.model.openUserProfile(modelData.userId)
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
propagateComposedEvents: true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Label {
|
||||
id: userName
|
||||
|
||||
text: TimelineManager.escapeEmoji(modelData.userName)
|
||||
color: TimelineManager.userColor(modelData.userId, colors.window)
|
||||
textFormat: Text.RichText
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: chat.model.openUserProfile(modelData.userId)
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
propagateComposedEvents: true
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
Layout.alignment: Qt.AlignHCenter
|
||||
onClicked: chat.model.openUserProfile(modelData.userId)
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
propagateComposedEvents: true
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -428,62 +441,67 @@ Page {
|
|||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
id: scrollbar
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
id: wrapper
|
||||
|
||||
// This would normally be previousSection, but our model's order is inverted.
|
||||
property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1
|
||||
property Item section
|
||||
|
||||
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
|
||||
width: chat.delegateMaxWidth
|
||||
height: section ? section.height + timelinerow.height : timelinerow.height
|
||||
onSectionBoundaryChanged: {
|
||||
if (sectionBoundary) {
|
||||
var properties = {
|
||||
"modelData": model.dump,
|
||||
"section": ListView.section,
|
||||
"nextSection": ListView.nextSection
|
||||
};
|
||||
section = sectionHeader.createObject(wrapper, properties);
|
||||
} else {
|
||||
section.destroy();
|
||||
section = null;
|
||||
}
|
||||
ScrollBar.vertical: ScrollBar {
|
||||
id: scrollbar
|
||||
}
|
||||
|
||||
TimelineRow {
|
||||
id: timelinerow
|
||||
delegate: Item {
|
||||
id: wrapper
|
||||
|
||||
y: section ? section.y + section.height : 0
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onMovementEnded() {
|
||||
if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
|
||||
chat.model.currentIndex = index;
|
||||
// This would normally be previousSection, but our model's order is inverted.
|
||||
property bool sectionBoundary: (ListView.nextSection != "" && ListView.nextSection !== ListView.section) || model.index === chat.count - 1
|
||||
property Item section
|
||||
|
||||
anchors.horizontalCenter: parent ? parent.horizontalCenter : undefined
|
||||
width: chat.delegateMaxWidth
|
||||
height: section ? section.height + timelinerow.height : timelinerow.height
|
||||
onSectionBoundaryChanged: {
|
||||
if (sectionBoundary) {
|
||||
var properties = {
|
||||
"modelData": model.dump,
|
||||
"section": ListView.section,
|
||||
"nextSection": ListView.nextSection
|
||||
};
|
||||
section = sectionHeader.createObject(wrapper, properties);
|
||||
} else {
|
||||
section.destroy();
|
||||
section = null;
|
||||
}
|
||||
}
|
||||
|
||||
target: chat
|
||||
TimelineRow {
|
||||
id: timelinerow
|
||||
|
||||
y: section ? section.y + section.height : 0
|
||||
}
|
||||
|
||||
Connections {
|
||||
function onMovementEnded() {
|
||||
if (y + height + 2 * chat.spacing > chat.contentY + chat.height && y < chat.contentY + chat.height)
|
||||
chat.model.currentIndex = index;
|
||||
|
||||
}
|
||||
|
||||
target: chat
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
footer: BusyIndicator {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
running: chat.model && chat.model.paginationInProgress
|
||||
height: 50
|
||||
width: 50
|
||||
z: 3
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
footer: BusyIndicator {
|
||||
anchors.horizontalCenter: parent.horizontalCenter
|
||||
running: chat.model && chat.model.paginationInProgress
|
||||
height: 50
|
||||
width: 50
|
||||
z: 3
|
||||
Loader {
|
||||
id: videoCallLoader
|
||||
source: TimelineManager.onVideoCall ? "VideoCall.qml" : ""
|
||||
onLoaded: TimelineManager.setVideoCallItem()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Item {
|
||||
|
|
7
resources/qml/VideoCall.qml
Normal file
7
resources/qml/VideoCall.qml
Normal file
|
@ -0,0 +1,7 @@
|
|||
import QtQuick 2.9
|
||||
|
||||
import org.freedesktop.gstreamer.GLVideoItem 1.0
|
||||
|
||||
GstGLVideoItem {
|
||||
objectName: "videoCallItem"
|
||||
}
|
|
@ -74,6 +74,7 @@
|
|||
<file>icons/ui/end-call.png</file>
|
||||
<file>icons/ui/microphone-mute.png</file>
|
||||
<file>icons/ui/microphone-unmute.png</file>
|
||||
<file>icons/ui/video-call.png</file>
|
||||
|
||||
<file>icons/emoji-categories/people.png</file>
|
||||
<file>icons/emoji-categories/people@2x.png</file>
|
||||
|
@ -130,6 +131,7 @@
|
|||
<file>qml/Reactions.qml</file>
|
||||
<file>qml/ScrollHelper.qml</file>
|
||||
<file>qml/TimelineRow.qml</file>
|
||||
<file>qml/VideoCall.qml</file>
|
||||
<file>qml/emoji/EmojiButton.qml</file>
|
||||
<file>qml/emoji/EmojiPicker.qml</file>
|
||||
<file>qml/UserProfile.qml</file>
|
||||
|
|
|
@ -25,9 +25,6 @@ Q_DECLARE_METATYPE(mtx::responses::TurnServer)
|
|||
using namespace mtx::events;
|
||||
using namespace mtx::events::msg;
|
||||
|
||||
// https://github.com/vector-im/riot-web/issues/10173
|
||||
#define STUN_SERVER "stun://turn.matrix.org:3478"
|
||||
|
||||
namespace {
|
||||
std::vector<std::string>
|
||||
getTurnURIs(const mtx::responses::TurnServer &turnServer);
|
||||
|
@ -43,6 +40,8 @@ CallManager::CallManager(QSharedPointer<UserSettings> userSettings)
|
|||
qRegisterMetaType<mtx::events::msg::CallCandidates::Candidate>();
|
||||
qRegisterMetaType<mtx::responses::TurnServer>();
|
||||
|
||||
session_.setSettings(userSettings);
|
||||
|
||||
connect(
|
||||
&session_,
|
||||
&WebRTCSession::offerCreated,
|
||||
|
@ -128,30 +127,29 @@ CallManager::CallManager(QSharedPointer<UserSettings> userSettings)
|
|||
}
|
||||
|
||||
void
|
||||
CallManager::sendInvite(const QString &roomid)
|
||||
CallManager::sendInvite(const QString &roomid, bool isVideo)
|
||||
{
|
||||
if (onActiveCall())
|
||||
return;
|
||||
|
||||
auto roomInfo = cache::singleRoomInfo(roomid.toStdString());
|
||||
if (roomInfo.member_count != 2) {
|
||||
emit ChatPage::instance()->showNotification(
|
||||
"Voice calls are limited to 1:1 rooms.");
|
||||
emit ChatPage::instance()->showNotification("Calls are limited to 1:1 rooms.");
|
||||
return;
|
||||
}
|
||||
|
||||
std::string errorMessage;
|
||||
if (!session_.init(&errorMessage)) {
|
||||
if (!session_.havePlugins(false, &errorMessage) ||
|
||||
(isVideo && !session_.havePlugins(true, &errorMessage))) {
|
||||
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
|
||||
return;
|
||||
}
|
||||
|
||||
roomid_ = roomid;
|
||||
session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : "");
|
||||
session_.setTurnServers(turnURIs_);
|
||||
|
||||
generateCallID();
|
||||
nhlog::ui()->debug("WebRTC: call id: {} - creating invite", callid_);
|
||||
nhlog::ui()->debug(
|
||||
"WebRTC: call id: {} - creating {} invite", callid_, isVideo ? "video" : "voice");
|
||||
std::vector<RoomMember> members(cache::getMembers(roomid.toStdString()));
|
||||
const RoomMember &callee =
|
||||
members.front().user_id == utils::localUser() ? members.back() : members.front();
|
||||
|
@ -159,10 +157,12 @@ CallManager::sendInvite(const QString &roomid)
|
|||
callPartyAvatarUrl_ = QString::fromStdString(roomInfo.avatar_url);
|
||||
emit newCallParty();
|
||||
playRingtone("qrc:/media/media/ringback.ogg", true);
|
||||
if (!session_.createOffer()) {
|
||||
if (!session_.createOffer(isVideo)) {
|
||||
emit ChatPage::instance()->showNotification("Problem setting up call.");
|
||||
endCall();
|
||||
}
|
||||
if (isVideo)
|
||||
emit newVideoCallState();
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
@ -242,7 +242,7 @@ CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent)
|
|||
return;
|
||||
|
||||
auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id);
|
||||
if (onActiveCall() || roomInfo.member_count != 2 || isVideo) {
|
||||
if (onActiveCall() || roomInfo.member_count != 2) {
|
||||
emit newMessage(QString::fromStdString(callInviteEvent.room_id),
|
||||
CallHangUp{callInviteEvent.content.call_id,
|
||||
0,
|
||||
|
@ -266,10 +266,11 @@ CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent)
|
|||
QString::fromStdString(roomInfo.name),
|
||||
QString::fromStdString(roomInfo.avatar_url),
|
||||
settings_,
|
||||
isVideo,
|
||||
MainWindow::instance());
|
||||
connect(dialog, &dialogs::AcceptCall::accept, this, [this, callInviteEvent]() {
|
||||
connect(dialog, &dialogs::AcceptCall::accept, this, [this, callInviteEvent, isVideo]() {
|
||||
MainWindow::instance()->hideOverlay();
|
||||
answerInvite(callInviteEvent.content);
|
||||
answerInvite(callInviteEvent.content, isVideo);
|
||||
});
|
||||
connect(dialog, &dialogs::AcceptCall::reject, this, [this]() {
|
||||
MainWindow::instance()->hideOverlay();
|
||||
|
@ -279,19 +280,18 @@ CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent)
|
|||
}
|
||||
|
||||
void
|
||||
CallManager::answerInvite(const CallInvite &invite)
|
||||
CallManager::answerInvite(const CallInvite &invite, bool isVideo)
|
||||
{
|
||||
stopRingtone();
|
||||
std::string errorMessage;
|
||||
if (!session_.init(&errorMessage)) {
|
||||
if (!session_.havePlugins(false, &errorMessage) ||
|
||||
(isVideo && !session_.havePlugins(true, &errorMessage))) {
|
||||
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
|
||||
hangUp();
|
||||
return;
|
||||
}
|
||||
|
||||
session_.setStunServer(settings_->useStunServer() ? STUN_SERVER : "");
|
||||
session_.setTurnServers(turnURIs_);
|
||||
|
||||
if (!session_.acceptOffer(invite.sdp)) {
|
||||
emit ChatPage::instance()->showNotification("Problem setting up call.");
|
||||
hangUp();
|
||||
|
@ -299,6 +299,8 @@ CallManager::answerInvite(const CallInvite &invite)
|
|||
}
|
||||
session_.acceptICECandidates(remoteICECandidates_);
|
||||
remoteICECandidates_.clear();
|
||||
if (isVideo)
|
||||
emit newVideoCallState();
|
||||
}
|
||||
|
||||
void
|
||||
|
@ -384,7 +386,10 @@ CallManager::endCall()
|
|||
{
|
||||
stopRingtone();
|
||||
clear();
|
||||
bool isVideo = session_.isVideo();
|
||||
session_.end();
|
||||
if (isVideo)
|
||||
emit newVideoCallState();
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
@ -26,7 +26,7 @@ class CallManager : public QObject
|
|||
public:
|
||||
CallManager(QSharedPointer<UserSettings>);
|
||||
|
||||
void sendInvite(const QString &roomid);
|
||||
void sendInvite(const QString &roomid, bool isVideo);
|
||||
void hangUp(
|
||||
mtx::events::msg::CallHangUp::Reason = mtx::events::msg::CallHangUp::Reason::User);
|
||||
bool onActiveCall() const;
|
||||
|
@ -43,6 +43,7 @@ signals:
|
|||
void newMessage(const QString &roomid, const mtx::events::msg::CallAnswer &);
|
||||
void newMessage(const QString &roomid, const mtx::events::msg::CallHangUp &);
|
||||
void newCallParty();
|
||||
void newVideoCallState();
|
||||
void turnServerRetrieved(const mtx::responses::TurnServer &);
|
||||
|
||||
private slots:
|
||||
|
@ -67,7 +68,7 @@ private:
|
|||
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallCandidates> &);
|
||||
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallAnswer> &);
|
||||
void handleEvent(const mtx::events::RoomEvent<mtx::events::msg::CallHangUp> &);
|
||||
void answerInvite(const mtx::events::msg::CallInvite &);
|
||||
void answerInvite(const mtx::events::msg::CallInvite &, bool isVideo);
|
||||
void generateCallID();
|
||||
void clear();
|
||||
void endCall();
|
||||
|
|
|
@ -437,7 +437,7 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
|
|||
} else {
|
||||
if (auto roomInfo = cache::singleRoomInfo(current_room_.toStdString());
|
||||
roomInfo.member_count != 2) {
|
||||
showNotification("Voice calls are limited to 1:1 rooms.");
|
||||
showNotification("Calls are limited to 1:1 rooms.");
|
||||
} else {
|
||||
std::vector<RoomMember> members(
|
||||
cache::getMembers(current_room_.toStdString()));
|
||||
|
@ -452,7 +452,10 @@ ChatPage::ChatPage(QSharedPointer<UserSettings> userSettings, QWidget *parent)
|
|||
userSettings_,
|
||||
MainWindow::instance());
|
||||
connect(dialog, &dialogs::PlaceCall::voice, this, [this]() {
|
||||
callManager_.sendInvite(current_room_);
|
||||
callManager_.sendInvite(current_room_, false);
|
||||
});
|
||||
connect(dialog, &dialogs::PlaceCall::video, this, [this]() {
|
||||
callManager_.sendInvite(current_room_, true);
|
||||
});
|
||||
utils::centerWidget(dialog, MainWindow::instance());
|
||||
dialog->show();
|
||||
|
|
|
@ -42,6 +42,7 @@
|
|||
#include "Olm.h"
|
||||
#include "UserSettingsPage.h"
|
||||
#include "Utils.h"
|
||||
#include "WebRTCSession.h"
|
||||
#include "ui/FlatButton.h"
|
||||
#include "ui/ToggleButton.h"
|
||||
|
||||
|
@ -77,8 +78,11 @@ UserSettings::load()
|
|||
presence_ =
|
||||
settings.value("user/presence", QVariant::fromValue(Presence::AutomaticPresence))
|
||||
.value<Presence>();
|
||||
useStunServer_ = settings.value("user/use_stun_server", false).toBool();
|
||||
defaultAudioSource_ = settings.value("user/default_audio_source", QString()).toString();
|
||||
microphone_ = settings.value("user/microphone", QString()).toString();
|
||||
camera_ = settings.value("user/camera", QString()).toString();
|
||||
cameraResolution_ = settings.value("user/camera_resolution", QString()).toString();
|
||||
cameraFrameRate_ = settings.value("user/camera_frame_rate", QString()).toString();
|
||||
useStunServer_ = settings.value("user/use_stun_server", false).toBool();
|
||||
|
||||
applyTheme();
|
||||
}
|
||||
|
@ -292,12 +296,42 @@ UserSettings::setUseStunServer(bool useStunServer)
|
|||
}
|
||||
|
||||
void
|
||||
UserSettings::setDefaultAudioSource(const QString &defaultAudioSource)
|
||||
UserSettings::setMicrophone(QString microphone)
|
||||
{
|
||||
if (defaultAudioSource == defaultAudioSource_)
|
||||
if (microphone == microphone_)
|
||||
return;
|
||||
defaultAudioSource_ = defaultAudioSource;
|
||||
emit defaultAudioSourceChanged(defaultAudioSource);
|
||||
microphone_ = microphone;
|
||||
emit microphoneChanged(microphone);
|
||||
save();
|
||||
}
|
||||
|
||||
void
|
||||
UserSettings::setCamera(QString camera)
|
||||
{
|
||||
if (camera == camera_)
|
||||
return;
|
||||
camera_ = camera;
|
||||
emit cameraChanged(camera);
|
||||
save();
|
||||
}
|
||||
|
||||
void
|
||||
UserSettings::setCameraResolution(QString resolution)
|
||||
{
|
||||
if (resolution == cameraResolution_)
|
||||
return;
|
||||
cameraResolution_ = resolution;
|
||||
emit cameraResolutionChanged(resolution);
|
||||
save();
|
||||
}
|
||||
|
||||
void
|
||||
UserSettings::setCameraFrameRate(QString frameRate)
|
||||
{
|
||||
if (frameRate == cameraFrameRate_)
|
||||
return;
|
||||
cameraFrameRate_ = frameRate;
|
||||
emit cameraFrameRateChanged(frameRate);
|
||||
save();
|
||||
}
|
||||
|
||||
|
@ -386,8 +420,11 @@ UserSettings::save()
|
|||
settings.setValue("font_family", font_);
|
||||
settings.setValue("emoji_font_family", emojiFont_);
|
||||
settings.setValue("presence", QVariant::fromValue(presence_));
|
||||
settings.setValue("microphone", microphone_);
|
||||
settings.setValue("camera", camera_);
|
||||
settings.setValue("camera_resolution", cameraResolution_);
|
||||
settings.setValue("camera_frame_rate", cameraFrameRate_);
|
||||
settings.setValue("use_stun_server", useStunServer_);
|
||||
settings.setValue("default_audio_source", defaultAudioSource_);
|
||||
|
||||
settings.endGroup();
|
||||
|
||||
|
@ -458,6 +495,10 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
|
|||
fontSizeCombo_ = new QComboBox{this};
|
||||
fontSelectionCombo_ = new QComboBox{this};
|
||||
emojiFontSelectionCombo_ = new QComboBox{this};
|
||||
microphoneCombo_ = new QComboBox{this};
|
||||
cameraCombo_ = new QComboBox{this};
|
||||
cameraResolutionCombo_ = new QComboBox{this};
|
||||
cameraFrameRateCombo_ = new QComboBox{this};
|
||||
timelineMaxWidthSpin_ = new QSpinBox{this};
|
||||
|
||||
if (!settings_->tray())
|
||||
|
@ -645,6 +686,14 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
|
|||
|
||||
formLayout_->addRow(callsLabel);
|
||||
formLayout_->addRow(new HorizontalLine{this});
|
||||
boxWrap(tr("Microphone"), microphoneCombo_);
|
||||
boxWrap(tr("Camera"), cameraCombo_);
|
||||
boxWrap(tr("Camera resolution"), cameraResolutionCombo_);
|
||||
boxWrap(tr("Camera frame rate"), cameraFrameRateCombo_);
|
||||
microphoneCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents);
|
||||
cameraCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents);
|
||||
cameraResolutionCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents);
|
||||
cameraFrameRateCombo_->setSizeAdjustPolicy(QComboBox::AdjustToContents);
|
||||
boxWrap(tr("Allow fallback call assist server"),
|
||||
useStunServer_,
|
||||
tr("Will use turn.matrix.org as assist when your home server does not offer one."));
|
||||
|
@ -698,6 +747,38 @@ UserSettingsPage::UserSettingsPage(QSharedPointer<UserSettings> settings, QWidge
|
|||
connect(emojiFontSelectionCombo_,
|
||||
static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
|
||||
[this](const QString &family) { settings_->setEmojiFontFamily(family.trimmed()); });
|
||||
|
||||
connect(microphoneCombo_,
|
||||
static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
|
||||
[this](const QString µphone) { settings_->setMicrophone(microphone); });
|
||||
|
||||
connect(cameraCombo_,
|
||||
static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
|
||||
[this](const QString &camera) {
|
||||
settings_->setCamera(camera);
|
||||
std::vector<std::string> resolutions =
|
||||
WebRTCSession::instance().getResolutions(camera.toStdString());
|
||||
cameraResolutionCombo_->clear();
|
||||
for (const auto &resolution : resolutions)
|
||||
cameraResolutionCombo_->addItem(QString::fromStdString(resolution));
|
||||
});
|
||||
|
||||
connect(cameraResolutionCombo_,
|
||||
static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
|
||||
[this](const QString &resolution) {
|
||||
settings_->setCameraResolution(resolution);
|
||||
std::vector<std::string> frameRates =
|
||||
WebRTCSession::instance().getFrameRates(settings_->camera().toStdString(),
|
||||
resolution.toStdString());
|
||||
cameraFrameRateCombo_->clear();
|
||||
for (const auto &frameRate : frameRates)
|
||||
cameraFrameRateCombo_->addItem(QString::fromStdString(frameRate));
|
||||
});
|
||||
|
||||
connect(cameraFrameRateCombo_,
|
||||
static_cast<void (QComboBox::*)(const QString &)>(&QComboBox::currentTextChanged),
|
||||
[this](const QString &frameRate) { settings_->setCameraFrameRate(frameRate); });
|
||||
|
||||
connect(trayToggle_, &Toggle::toggled, this, [this](bool disabled) {
|
||||
settings_->setTray(!disabled);
|
||||
if (disabled) {
|
||||
|
@ -807,6 +888,26 @@ UserSettingsPage::showEvent(QShowEvent *)
|
|||
enlargeEmojiOnlyMessages_->setState(!settings_->enlargeEmojiOnlyMessages());
|
||||
deviceIdValue_->setText(QString::fromStdString(http::client()->device_id()));
|
||||
timelineMaxWidthSpin_->setValue(settings_->timelineMaxWidth());
|
||||
|
||||
WebRTCSession::instance().refreshDevices();
|
||||
auto mics =
|
||||
WebRTCSession::instance().getDeviceNames(false, settings_->microphone().toStdString());
|
||||
microphoneCombo_->clear();
|
||||
for (const auto &m : mics)
|
||||
microphoneCombo_->addItem(QString::fromStdString(m));
|
||||
|
||||
auto cameraResolution = settings_->cameraResolution();
|
||||
auto cameraFrameRate = settings_->cameraFrameRate();
|
||||
|
||||
auto cameras =
|
||||
WebRTCSession::instance().getDeviceNames(true, settings_->camera().toStdString());
|
||||
cameraCombo_->clear();
|
||||
for (const auto &c : cameras)
|
||||
cameraCombo_->addItem(QString::fromStdString(c));
|
||||
|
||||
utils::restoreCombobox(cameraResolutionCombo_, cameraResolution);
|
||||
utils::restoreCombobox(cameraFrameRateCombo_, cameraFrameRate);
|
||||
|
||||
useStunServer_->setState(!settings_->useStunServer());
|
||||
|
||||
deviceFingerprintValue_->setText(
|
||||
|
|
|
@ -73,8 +73,12 @@ class UserSettings : public QObject
|
|||
Q_PROPERTY(Presence presence READ presence WRITE setPresence NOTIFY presenceChanged)
|
||||
Q_PROPERTY(
|
||||
bool useStunServer READ useStunServer WRITE setUseStunServer NOTIFY useStunServerChanged)
|
||||
Q_PROPERTY(QString defaultAudioSource READ defaultAudioSource WRITE setDefaultAudioSource
|
||||
NOTIFY defaultAudioSourceChanged)
|
||||
Q_PROPERTY(QString microphone READ microphone WRITE setMicrophone NOTIFY microphoneChanged)
|
||||
Q_PROPERTY(QString camera READ camera WRITE setCamera NOTIFY cameraChanged)
|
||||
Q_PROPERTY(QString cameraResolution READ cameraResolution WRITE setCameraResolution NOTIFY
|
||||
cameraResolutionChanged)
|
||||
Q_PROPERTY(QString cameraFrameRate READ cameraFrameRate WRITE setCameraFrameRate NOTIFY
|
||||
cameraFrameRateChanged)
|
||||
|
||||
public:
|
||||
UserSettings();
|
||||
|
@ -111,8 +115,11 @@ public:
|
|||
void setAvatarCircles(bool state);
|
||||
void setDecryptSidebar(bool state);
|
||||
void setPresence(Presence state);
|
||||
void setMicrophone(QString microphone);
|
||||
void setCamera(QString camera);
|
||||
void setCameraResolution(QString resolution);
|
||||
void setCameraFrameRate(QString frameRate);
|
||||
void setUseStunServer(bool state);
|
||||
void setDefaultAudioSource(const QString &deviceName);
|
||||
|
||||
QString theme() const { return !theme_.isEmpty() ? theme_ : defaultTheme_; }
|
||||
bool messageHoverHighlight() const { return messageHoverHighlight_; }
|
||||
|
@ -138,8 +145,11 @@ public:
|
|||
QString font() const { return font_; }
|
||||
QString emojiFont() const { return emojiFont_; }
|
||||
Presence presence() const { return presence_; }
|
||||
QString microphone() const { return microphone_; }
|
||||
QString camera() const { return camera_; }
|
||||
QString cameraResolution() const { return cameraResolution_; }
|
||||
QString cameraFrameRate() const { return cameraFrameRate_; }
|
||||
bool useStunServer() const { return useStunServer_; }
|
||||
QString defaultAudioSource() const { return defaultAudioSource_; }
|
||||
|
||||
signals:
|
||||
void groupViewStateChanged(bool state);
|
||||
|
@ -162,8 +172,11 @@ signals:
|
|||
void fontChanged(QString state);
|
||||
void emojiFontChanged(QString state);
|
||||
void presenceChanged(Presence state);
|
||||
void microphoneChanged(QString microphone);
|
||||
void cameraChanged(QString camera);
|
||||
void cameraResolutionChanged(QString resolution);
|
||||
void cameraFrameRateChanged(QString frameRate);
|
||||
void useStunServerChanged(bool state);
|
||||
void defaultAudioSourceChanged(const QString &deviceName);
|
||||
|
||||
private:
|
||||
// Default to system theme if QT_QPA_PLATFORMTHEME var is set.
|
||||
|
@ -191,8 +204,11 @@ private:
|
|||
QString font_;
|
||||
QString emojiFont_;
|
||||
Presence presence_;
|
||||
QString microphone_;
|
||||
QString camera_;
|
||||
QString cameraResolution_;
|
||||
QString cameraFrameRate_;
|
||||
bool useStunServer_;
|
||||
QString defaultAudioSource_;
|
||||
};
|
||||
|
||||
class HorizontalLine : public QFrame
|
||||
|
@ -256,6 +272,10 @@ private:
|
|||
QComboBox *fontSizeCombo_;
|
||||
QComboBox *fontSelectionCombo_;
|
||||
QComboBox *emojiFontSelectionCombo_;
|
||||
QComboBox *microphoneCombo_;
|
||||
QComboBox *cameraCombo_;
|
||||
QComboBox *cameraResolutionCombo_;
|
||||
QComboBox *cameraFrameRateCombo_;
|
||||
|
||||
QSpinBox *timelineMaxWidthSpin_;
|
||||
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -4,10 +4,13 @@
|
|||
#include <vector>
|
||||
|
||||
#include <QObject>
|
||||
#include <QSharedPointer>
|
||||
|
||||
#include "mtx/events/voip.hpp"
|
||||
|
||||
typedef struct _GstElement GstElement;
|
||||
class QQuickItem;
|
||||
class UserSettings;
|
||||
|
||||
namespace webrtc {
|
||||
Q_NAMESPACE
|
||||
|
@ -39,10 +42,13 @@ public:
|
|||
return instance;
|
||||
}
|
||||
|
||||
bool init(std::string *errorMessage = nullptr);
|
||||
bool havePlugins(bool isVideo, std::string *errorMessage = nullptr);
|
||||
webrtc::State state() const { return state_; }
|
||||
bool isVideo() const { return isVideo_; }
|
||||
bool isOffering() const { return isOffering_; }
|
||||
bool isRemoteVideoRecvOnly() const { return isRemoteVideoRecvOnly_; }
|
||||
|
||||
bool createOffer();
|
||||
bool createOffer(bool isVideo);
|
||||
bool acceptOffer(const std::string &sdp);
|
||||
bool acceptAnswer(const std::string &sdp);
|
||||
void acceptICECandidates(const std::vector<mtx::events::msg::CallCandidates::Candidate> &);
|
||||
|
@ -51,11 +57,18 @@ public:
|
|||
bool toggleMicMute();
|
||||
void end();
|
||||
|
||||
void setStunServer(const std::string &stunServer) { stunServer_ = stunServer; }
|
||||
void setSettings(QSharedPointer<UserSettings> settings) { settings_ = settings; }
|
||||
void setTurnServers(const std::vector<std::string> &uris) { turnServers_ = uris; }
|
||||
|
||||
std::vector<std::string> getAudioSourceNames(const std::string &defaultDevice);
|
||||
void setAudioSource(int audioDeviceIndex) { audioSourceIndex_ = audioDeviceIndex; }
|
||||
void refreshDevices();
|
||||
std::vector<std::string> getDeviceNames(bool isVideo,
|
||||
const std::string &defaultDevice) const;
|
||||
std::vector<std::string> getResolutions(const std::string &cameraName) const;
|
||||
std::vector<std::string> getFrameRates(const std::string &cameraName,
|
||||
const std::string &resolution) const;
|
||||
|
||||
void setVideoItem(QQuickItem *item) { videoItem_ = item; }
|
||||
QQuickItem *getVideoItem() const { return videoItem_; }
|
||||
|
||||
signals:
|
||||
void offerCreated(const std::string &sdp,
|
||||
|
@ -71,18 +84,24 @@ private slots:
|
|||
private:
|
||||
WebRTCSession();
|
||||
|
||||
bool initialised_ = false;
|
||||
webrtc::State state_ = webrtc::State::DISCONNECTED;
|
||||
GstElement *pipe_ = nullptr;
|
||||
GstElement *webrtc_ = nullptr;
|
||||
unsigned int busWatchId_ = 0;
|
||||
std::string stunServer_;
|
||||
bool initialised_ = false;
|
||||
bool haveVoicePlugins_ = false;
|
||||
bool haveVideoPlugins_ = false;
|
||||
webrtc::State state_ = webrtc::State::DISCONNECTED;
|
||||
bool isVideo_ = false;
|
||||
bool isOffering_ = false;
|
||||
bool isRemoteVideoRecvOnly_ = false;
|
||||
QQuickItem *videoItem_ = nullptr;
|
||||
GstElement *pipe_ = nullptr;
|
||||
GstElement *webrtc_ = nullptr;
|
||||
unsigned int busWatchId_ = 0;
|
||||
QSharedPointer<UserSettings> settings_;
|
||||
std::vector<std::string> turnServers_;
|
||||
int audioSourceIndex_ = -1;
|
||||
|
||||
bool startPipeline(int opusPayloadType);
|
||||
bool createPipeline(int opusPayloadType);
|
||||
void refreshDevices();
|
||||
bool init(std::string *errorMessage = nullptr);
|
||||
bool startPipeline(int opusPayloadType, int vp8PayloadType);
|
||||
bool createPipeline(int opusPayloadType, int vp8PayloadType);
|
||||
bool addVideoPipeline(int vp8PayloadType);
|
||||
void startDeviceMonitor();
|
||||
|
||||
public:
|
||||
|
|
|
@ -19,23 +19,32 @@ AcceptCall::AcceptCall(const QString &caller,
|
|||
const QString &roomName,
|
||||
const QString &avatarUrl,
|
||||
QSharedPointer<UserSettings> settings,
|
||||
bool isVideo,
|
||||
QWidget *parent)
|
||||
: QWidget(parent)
|
||||
{
|
||||
std::string errorMessage;
|
||||
if (!WebRTCSession::instance().init(&errorMessage)) {
|
||||
WebRTCSession *session = &WebRTCSession::instance();
|
||||
if (!session->havePlugins(false, &errorMessage)) {
|
||||
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
|
||||
emit close();
|
||||
return;
|
||||
}
|
||||
audioDevices_ = WebRTCSession::instance().getAudioSourceNames(
|
||||
settings->defaultAudioSource().toStdString());
|
||||
if (audioDevices_.empty()) {
|
||||
emit ChatPage::instance()->showNotification(
|
||||
"Incoming call: No audio sources found.");
|
||||
if (isVideo && !session->havePlugins(true, &errorMessage)) {
|
||||
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
|
||||
emit close();
|
||||
return;
|
||||
}
|
||||
session->refreshDevices();
|
||||
microphones_ = session->getDeviceNames(false, settings->microphone().toStdString());
|
||||
if (microphones_.empty()) {
|
||||
emit ChatPage::instance()->showNotification(
|
||||
tr("Incoming call: No microphone found."));
|
||||
emit close();
|
||||
return;
|
||||
}
|
||||
if (isVideo)
|
||||
cameras_ = session->getDeviceNames(true, settings->camera().toStdString());
|
||||
|
||||
setAutoFillBackground(true);
|
||||
setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
|
||||
|
@ -77,9 +86,10 @@ AcceptCall::AcceptCall(const QString &caller,
|
|||
const int iconSize = 22;
|
||||
QLabel *callTypeIndicator = new QLabel(this);
|
||||
callTypeIndicator->setPixmap(
|
||||
QIcon(":/icons/icons/ui/place-call.png").pixmap(QSize(iconSize * 2, iconSize * 2)));
|
||||
QIcon(isVideo ? ":/icons/icons/ui/video-call.png" : ":/icons/icons/ui/place-call.png")
|
||||
.pixmap(QSize(iconSize * 2, iconSize * 2)));
|
||||
|
||||
QLabel *callTypeLabel = new QLabel("Voice Call", this);
|
||||
QLabel *callTypeLabel = new QLabel(isVideo ? tr("Video Call") : tr("Voice Call"), this);
|
||||
labelFont.setPointSizeF(f.pointSizeF() * 1.1);
|
||||
callTypeLabel->setFont(labelFont);
|
||||
callTypeLabel->setAlignment(Qt::AlignCenter);
|
||||
|
@ -88,7 +98,8 @@ AcceptCall::AcceptCall(const QString &caller,
|
|||
buttonLayout->setSpacing(18);
|
||||
acceptBtn_ = new QPushButton(tr("Accept"), this);
|
||||
acceptBtn_->setDefault(true);
|
||||
acceptBtn_->setIcon(QIcon(":/icons/icons/ui/place-call.png"));
|
||||
acceptBtn_->setIcon(
|
||||
QIcon(isVideo ? ":/icons/icons/ui/video-call.png" : ":/icons/icons/ui/place-call.png"));
|
||||
acceptBtn_->setIconSize(QSize(iconSize, iconSize));
|
||||
|
||||
rejectBtn_ = new QPushButton(tr("Reject"), this);
|
||||
|
@ -97,18 +108,17 @@ AcceptCall::AcceptCall(const QString &caller,
|
|||
buttonLayout->addWidget(acceptBtn_);
|
||||
buttonLayout->addWidget(rejectBtn_);
|
||||
|
||||
auto deviceLayout = new QHBoxLayout;
|
||||
auto audioLabel = new QLabel(this);
|
||||
audioLabel->setPixmap(
|
||||
QIcon(":/icons/icons/ui/microphone-unmute.png").pixmap(QSize(iconSize, iconSize)));
|
||||
microphoneCombo_ = new QComboBox(this);
|
||||
for (const auto &m : microphones_)
|
||||
microphoneCombo_->addItem(QIcon(":/icons/icons/ui/microphone-unmute.png"),
|
||||
QString::fromStdString(m));
|
||||
|
||||
auto deviceList = new QComboBox(this);
|
||||
for (const auto &d : audioDevices_)
|
||||
deviceList->addItem(QString::fromStdString(d));
|
||||
|
||||
deviceLayout->addStretch();
|
||||
deviceLayout->addWidget(audioLabel);
|
||||
deviceLayout->addWidget(deviceList);
|
||||
if (!cameras_.empty()) {
|
||||
cameraCombo_ = new QComboBox(this);
|
||||
for (const auto &c : cameras_)
|
||||
cameraCombo_->addItem(QIcon(":/icons/icons/ui/video-call.png"),
|
||||
QString::fromStdString(c));
|
||||
}
|
||||
|
||||
if (displayNameLabel)
|
||||
layout->addWidget(displayNameLabel, 0, Qt::AlignCenter);
|
||||
|
@ -117,12 +127,17 @@ AcceptCall::AcceptCall(const QString &caller,
|
|||
layout->addWidget(callTypeIndicator, 0, Qt::AlignCenter);
|
||||
layout->addWidget(callTypeLabel, 0, Qt::AlignCenter);
|
||||
layout->addLayout(buttonLayout);
|
||||
layout->addLayout(deviceLayout);
|
||||
layout->addWidget(microphoneCombo_);
|
||||
if (cameraCombo_)
|
||||
layout->addWidget(cameraCombo_);
|
||||
|
||||
connect(acceptBtn_, &QPushButton::clicked, this, [this, deviceList, settings]() {
|
||||
WebRTCSession::instance().setAudioSource(deviceList->currentIndex());
|
||||
settings->setDefaultAudioSource(
|
||||
QString::fromStdString(audioDevices_[deviceList->currentIndex()]));
|
||||
connect(acceptBtn_, &QPushButton::clicked, this, [this, settings, session]() {
|
||||
settings->setMicrophone(
|
||||
QString::fromStdString(microphones_[microphoneCombo_->currentIndex()]));
|
||||
if (cameraCombo_) {
|
||||
settings->setCamera(
|
||||
QString::fromStdString(cameras_[cameraCombo_->currentIndex()]));
|
||||
}
|
||||
emit accept();
|
||||
emit close();
|
||||
});
|
||||
|
@ -131,4 +146,5 @@ AcceptCall::AcceptCall(const QString &caller,
|
|||
emit close();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include <QSharedPointer>
|
||||
#include <QWidget>
|
||||
|
||||
class QComboBox;
|
||||
class QPushButton;
|
||||
class QString;
|
||||
class UserSettings;
|
||||
|
@ -22,6 +23,7 @@ public:
|
|||
const QString &roomName,
|
||||
const QString &avatarUrl,
|
||||
QSharedPointer<UserSettings> settings,
|
||||
bool isVideo,
|
||||
QWidget *parent = nullptr);
|
||||
|
||||
signals:
|
||||
|
@ -29,8 +31,12 @@ signals:
|
|||
void reject();
|
||||
|
||||
private:
|
||||
QPushButton *acceptBtn_;
|
||||
QPushButton *rejectBtn_;
|
||||
std::vector<std::string> audioDevices_;
|
||||
QPushButton *acceptBtn_ = nullptr;
|
||||
QPushButton *rejectBtn_ = nullptr;
|
||||
QComboBox *microphoneCombo_ = nullptr;
|
||||
QComboBox *cameraCombo_ = nullptr;
|
||||
std::vector<std::string> microphones_;
|
||||
std::vector<std::string> cameras_;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -23,18 +23,20 @@ PlaceCall::PlaceCall(const QString &callee,
|
|||
: QWidget(parent)
|
||||
{
|
||||
std::string errorMessage;
|
||||
if (!WebRTCSession::instance().init(&errorMessage)) {
|
||||
WebRTCSession *session = &WebRTCSession::instance();
|
||||
if (!session->havePlugins(false, &errorMessage)) {
|
||||
emit ChatPage::instance()->showNotification(QString::fromStdString(errorMessage));
|
||||
emit close();
|
||||
return;
|
||||
}
|
||||
audioDevices_ = WebRTCSession::instance().getAudioSourceNames(
|
||||
settings->defaultAudioSource().toStdString());
|
||||
if (audioDevices_.empty()) {
|
||||
emit ChatPage::instance()->showNotification("No audio sources found.");
|
||||
session->refreshDevices();
|
||||
microphones_ = session->getDeviceNames(false, settings->microphone().toStdString());
|
||||
if (microphones_.empty()) {
|
||||
emit ChatPage::instance()->showNotification(tr("No microphone found."));
|
||||
emit close();
|
||||
return;
|
||||
}
|
||||
cameras_ = session->getDeviceNames(true, settings->camera().toStdString());
|
||||
|
||||
setAutoFillBackground(true);
|
||||
setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint);
|
||||
|
@ -56,48 +58,74 @@ PlaceCall::PlaceCall(const QString &callee,
|
|||
avatar->setImage(avatarUrl);
|
||||
else
|
||||
avatar->setLetter(utils::firstChar(roomName));
|
||||
const int iconSize = 18;
|
||||
voiceBtn_ = new QPushButton(tr("Voice"), this);
|
||||
|
||||
voiceBtn_ = new QPushButton(tr("Voice"), this);
|
||||
voiceBtn_->setIcon(QIcon(":/icons/icons/ui/place-call.png"));
|
||||
voiceBtn_->setIconSize(QSize(iconSize, iconSize));
|
||||
voiceBtn_->setIconSize(QSize(iconSize_, iconSize_));
|
||||
voiceBtn_->setDefault(true);
|
||||
|
||||
if (!cameras_.empty()) {
|
||||
videoBtn_ = new QPushButton(tr("Video"), this);
|
||||
videoBtn_->setIcon(QIcon(":/icons/icons/ui/video-call.png"));
|
||||
videoBtn_->setIconSize(QSize(iconSize_, iconSize_));
|
||||
}
|
||||
cancelBtn_ = new QPushButton(tr("Cancel"), this);
|
||||
|
||||
buttonLayout->addWidget(avatar);
|
||||
buttonLayout->addStretch();
|
||||
buttonLayout->addWidget(voiceBtn_);
|
||||
if (videoBtn_)
|
||||
buttonLayout->addWidget(videoBtn_);
|
||||
buttonLayout->addWidget(cancelBtn_);
|
||||
|
||||
QString name = displayName.isEmpty() ? callee : displayName;
|
||||
QLabel *label = new QLabel("Place a call to " + name + "?", this);
|
||||
QLabel *label = new QLabel(tr("Place a call to ") + name + "?", this);
|
||||
|
||||
auto deviceLayout = new QHBoxLayout;
|
||||
auto audioLabel = new QLabel(this);
|
||||
audioLabel->setPixmap(QIcon(":/icons/icons/ui/microphone-unmute.png")
|
||||
.pixmap(QSize(iconSize * 1.2, iconSize * 1.2)));
|
||||
microphoneCombo_ = new QComboBox(this);
|
||||
for (const auto &m : microphones_)
|
||||
microphoneCombo_->addItem(QIcon(":/icons/icons/ui/microphone-unmute.png"),
|
||||
QString::fromStdString(m));
|
||||
|
||||
auto deviceList = new QComboBox(this);
|
||||
for (const auto &d : audioDevices_)
|
||||
deviceList->addItem(QString::fromStdString(d));
|
||||
|
||||
deviceLayout->addStretch();
|
||||
deviceLayout->addWidget(audioLabel);
|
||||
deviceLayout->addWidget(deviceList);
|
||||
if (videoBtn_) {
|
||||
cameraCombo_ = new QComboBox(this);
|
||||
for (const auto &c : cameras_)
|
||||
cameraCombo_->addItem(QIcon(":/icons/icons/ui/video-call.png"),
|
||||
QString::fromStdString(c));
|
||||
}
|
||||
|
||||
layout->addWidget(label);
|
||||
layout->addLayout(buttonLayout);
|
||||
layout->addLayout(deviceLayout);
|
||||
layout->addStretch();
|
||||
layout->addWidget(microphoneCombo_);
|
||||
if (videoBtn_)
|
||||
layout->addWidget(cameraCombo_);
|
||||
|
||||
connect(voiceBtn_, &QPushButton::clicked, this, [this, deviceList, settings]() {
|
||||
WebRTCSession::instance().setAudioSource(deviceList->currentIndex());
|
||||
settings->setDefaultAudioSource(
|
||||
QString::fromStdString(audioDevices_[deviceList->currentIndex()]));
|
||||
connect(voiceBtn_, &QPushButton::clicked, this, [this, settings, session]() {
|
||||
settings->setMicrophone(
|
||||
QString::fromStdString(microphones_[microphoneCombo_->currentIndex()]));
|
||||
emit voice();
|
||||
emit close();
|
||||
});
|
||||
if (videoBtn_)
|
||||
connect(videoBtn_, &QPushButton::clicked, this, [this, settings, session]() {
|
||||
std::string error;
|
||||
if (!session->havePlugins(true, &error)) {
|
||||
emit ChatPage::instance()->showNotification(
|
||||
QString::fromStdString(error));
|
||||
emit close();
|
||||
return;
|
||||
}
|
||||
settings->setMicrophone(
|
||||
QString::fromStdString(microphones_[microphoneCombo_->currentIndex()]));
|
||||
settings->setCamera(
|
||||
QString::fromStdString(cameras_[cameraCombo_->currentIndex()]));
|
||||
emit video();
|
||||
emit close();
|
||||
});
|
||||
connect(cancelBtn_, &QPushButton::clicked, this, [this]() {
|
||||
emit cancel();
|
||||
emit close();
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include <QSharedPointer>
|
||||
#include <QWidget>
|
||||
|
||||
class QComboBox;
|
||||
class QPushButton;
|
||||
class QString;
|
||||
class UserSettings;
|
||||
|
@ -26,11 +27,18 @@ public:
|
|||
|
||||
signals:
|
||||
void voice();
|
||||
void video();
|
||||
void cancel();
|
||||
|
||||
private:
|
||||
QPushButton *voiceBtn_;
|
||||
QPushButton *cancelBtn_;
|
||||
std::vector<std::string> audioDevices_;
|
||||
const int iconSize_ = 18;
|
||||
QPushButton *voiceBtn_ = nullptr;
|
||||
QPushButton *videoBtn_ = nullptr;
|
||||
QPushButton *cancelBtn_ = nullptr;
|
||||
QComboBox *microphoneCombo_ = nullptr;
|
||||
QComboBox *cameraCombo_ = nullptr;
|
||||
std::vector<std::string> microphones_;
|
||||
std::vector<std::string> cameras_;
|
||||
};
|
||||
|
||||
}
|
||||
|
|
|
@ -242,6 +242,17 @@ TimelineViewManager::TimelineViewManager(QSharedPointer<UserSettings> userSettin
|
|||
&TimelineViewManager::callStateChanged);
|
||||
connect(
|
||||
callManager_, &CallManager::newCallParty, this, &TimelineViewManager::callPartyChanged);
|
||||
connect(callManager_,
|
||||
&CallManager::newVideoCallState,
|
||||
this,
|
||||
&TimelineViewManager::videoCallChanged);
|
||||
}
|
||||
|
||||
void
|
||||
TimelineViewManager::setVideoCallItem()
|
||||
{
|
||||
WebRTCSession::instance().setVideoItem(
|
||||
view->rootObject()->findChild<QQuickItem *>("videoCallItem"));
|
||||
}
|
||||
|
||||
void
|
||||
|
|
|
@ -36,6 +36,7 @@ class TimelineViewManager : public QObject
|
|||
Q_PROPERTY(
|
||||
bool isNarrowView MEMBER isNarrowView_ READ isNarrowView NOTIFY narrowViewChanged)
|
||||
Q_PROPERTY(webrtc::State callState READ callState NOTIFY callStateChanged)
|
||||
Q_PROPERTY(bool onVideoCall READ onVideoCall NOTIFY videoCallChanged)
|
||||
Q_PROPERTY(QString callPartyName READ callPartyName NOTIFY callPartyChanged)
|
||||
Q_PROPERTY(QString callPartyAvatarUrl READ callPartyAvatarUrl NOTIFY callPartyChanged)
|
||||
Q_PROPERTY(bool isMicMuted READ isMicMuted NOTIFY micMuteChanged)
|
||||
|
@ -55,6 +56,8 @@ public:
|
|||
Q_INVOKABLE bool isInitialSync() const { return isInitialSync_; }
|
||||
bool isNarrowView() const { return isNarrowView_; }
|
||||
webrtc::State callState() const { return WebRTCSession::instance().state(); }
|
||||
bool onVideoCall() const { return WebRTCSession::instance().isVideo(); }
|
||||
Q_INVOKABLE void setVideoCallItem();
|
||||
QString callPartyName() const { return callManager_->callPartyName(); }
|
||||
QString callPartyAvatarUrl() const { return callManager_->callPartyAvatarUrl(); }
|
||||
bool isMicMuted() const { return WebRTCSession::instance().isMicMuted(); }
|
||||
|
@ -89,6 +92,7 @@ signals:
|
|||
void showRoomList();
|
||||
void narrowViewChanged();
|
||||
void callStateChanged(webrtc::State);
|
||||
void videoCallChanged();
|
||||
void callPartyChanged();
|
||||
void micMuteChanged();
|
||||
|
||||
|
|
Loading…
Reference in a new issue