Implement call ringing
This commit is contained in:
@@ -24,8 +24,6 @@ add_library(neochat STATIC
|
||||
models/notificationsmodel.h
|
||||
proxycontroller.cpp
|
||||
proxycontroller.h
|
||||
mediamanager.cpp
|
||||
mediamanager.h
|
||||
sharehandler.cpp
|
||||
sharehandler.h
|
||||
foreigntypes.h
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "mediamanager.h"
|
||||
|
||||
void MediaManager::startPlayback()
|
||||
{
|
||||
Q_EMIT playbackStarted();
|
||||
}
|
||||
|
||||
#include "moc_mediamanager.cpp"
|
||||
@@ -23,6 +23,8 @@ target_sources(LibNeoChat PRIVATE
|
||||
urlhelper.cpp
|
||||
utils.cpp
|
||||
enums/chatbartype.h
|
||||
mediamanager.cpp
|
||||
mediamanager.h
|
||||
enums/messagecomponenttype.h
|
||||
enums/messagetype.h
|
||||
enums/powerlevel.cpp
|
||||
@@ -32,6 +34,7 @@ target_sources(LibNeoChat PRIVATE
|
||||
enums/timelinemarkreadcondition.h
|
||||
events/imagepackevent.cpp
|
||||
events/pollevent.cpp
|
||||
events/callmemberevent.cpp
|
||||
jobs/neochatgetcommonroomsjob.cpp
|
||||
models/actionsmodel.cpp
|
||||
models/completionmodel.cpp
|
||||
@@ -51,6 +54,9 @@ if (TARGET KF6::KIOWidgets)
|
||||
target_compile_definitions(LibNeoChat PUBLIC -DHAVE_KIO)
|
||||
endif()
|
||||
|
||||
qt_add_dbus_interface(MediaPlayer_SRCS org.mpris.MediaPlayer2.Player.xml mediaplayer2player)
|
||||
target_sources(LibNeoChat PRIVATE ${MediaPlayer_SRCS})
|
||||
|
||||
ecm_add_qml_module(LibNeoChat GENERATE_PLUGIN_SOURCE
|
||||
URI org.kde.neochat.libneochat
|
||||
OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/src/org/kde/neochat/libneochat
|
||||
|
||||
38
src/libneochat/events/callmemberevent.cpp
Normal file
38
src/libneochat/events/callmemberevent.cpp
Normal file
@@ -0,0 +1,38 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "callmemberevent.h"
|
||||
|
||||
#include <QString>
|
||||
|
||||
using namespace Quotient;
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
|
||||
CallMemberEventContent::CallMemberEventContent(const QJsonObject &json)
|
||||
{
|
||||
for (const auto &membership : json["memberships"_L1].toArray()) {
|
||||
QList<Focus> foci;
|
||||
for (const auto &focus : membership["foci_active"_L1].toArray()) {
|
||||
foci.append(Focus{
|
||||
.livekitAlias = focus["livekit_alias"_L1].toString(),
|
||||
.livekitServiceUrl = focus["livekit_service_url"_L1].toString(),
|
||||
.type = focus["livekit"_L1].toString(),
|
||||
});
|
||||
}
|
||||
memberships.append(CallMembership{
|
||||
.application = membership["application"_L1].toString(),
|
||||
.callId = membership["call_id"_L1].toString(),
|
||||
.deviceId = membership["device_id"_L1].toString(),
|
||||
.expires = membership["expires"_L1].toInt(),
|
||||
.expiresTs = membership["expires"_L1].toVariant().value<uint64_t>(),
|
||||
.fociActive = foci,
|
||||
.membershipId = membership["membershipID"_L1].toString(),
|
||||
.scope = membership["scope"_L1].toString(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
QJsonObject CallMemberEventContent::toJson() const
|
||||
{
|
||||
return {};
|
||||
}
|
||||
59
src/libneochat/events/callmemberevent.h
Normal file
59
src/libneochat/events/callmemberevent.h
Normal file
@@ -0,0 +1,59 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Quotient/events/stateevent.h>
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
|
||||
struct Focus {
|
||||
QString livekitAlias;
|
||||
QString livekitServiceUrl;
|
||||
QString type;
|
||||
};
|
||||
|
||||
struct CallMembership {
|
||||
QString application;
|
||||
QString callId;
|
||||
QString deviceId;
|
||||
int expires;
|
||||
uint64_t expiresTs;
|
||||
QList<Focus> fociActive;
|
||||
QString membershipId;
|
||||
QString scope;
|
||||
};
|
||||
|
||||
class CallMemberEventContent
|
||||
{
|
||||
public:
|
||||
explicit CallMemberEventContent(const QJsonObject &json);
|
||||
QJsonObject toJson() const;
|
||||
|
||||
QList<CallMembership> memberships;
|
||||
};
|
||||
|
||||
/**
|
||||
* @class CallMemberEvent
|
||||
*
|
||||
* Class to define a call member event.
|
||||
*
|
||||
* @sa Quotient::StateEvent
|
||||
*/
|
||||
class CallMemberEvent : public KeyedStateEventBase<CallMemberEvent, CallMemberEventContent>
|
||||
{
|
||||
public:
|
||||
QUO_EVENT(CallMemberEvent, "org.matrix.msc3401.call.member")
|
||||
|
||||
explicit CallMemberEvent(const QJsonObject &obj)
|
||||
: KeyedStateEventBase(obj)
|
||||
{
|
||||
}
|
||||
|
||||
QJsonArray memberships() const
|
||||
{
|
||||
return contentJson()["memberships"_L1].toArray();
|
||||
}
|
||||
};
|
||||
}
|
||||
21
src/libneochat/events/callnotifyevent.h
Normal file
21
src/libneochat/events/callnotifyevent.h
Normal file
@@ -0,0 +1,21 @@
|
||||
// SPDX-FileCopyrightText: 2024 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.1-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <Quotient/events/roomevent.h>
|
||||
|
||||
namespace Quotient
|
||||
{
|
||||
|
||||
class CallNotifyEvent : public RoomEvent
|
||||
{
|
||||
public:
|
||||
QUO_EVENT(CallNotifyEvent, "org.matrix.msc4075.call.notify");
|
||||
explicit CallNotifyEvent(const QJsonObject &obj)
|
||||
: RoomEvent(obj)
|
||||
{
|
||||
}
|
||||
};
|
||||
|
||||
}
|
||||
146
src/libneochat/mediamanager.cpp
Normal file
146
src/libneochat/mediamanager.cpp
Normal file
@@ -0,0 +1,146 @@
|
||||
// SPDX-FileCopyrightText: 2023-2025 Tobias Fella <tobias.fella@kde.org>
|
||||
// SPDX-License-Identifier: LGPL-2.0-or-later
|
||||
|
||||
#include "mediamanager.h"
|
||||
|
||||
#include <QAudioOutput>
|
||||
#include <QDBusConnection>
|
||||
#include <QDirIterator>
|
||||
#include <QMediaPlayer>
|
||||
#include <QMimeDatabase>
|
||||
|
||||
#include <Quotient/qt_connection_util.h>
|
||||
|
||||
#include "events/callmemberevent.h"
|
||||
#include "mediaplayer2player.h"
|
||||
#include "neochatroom.h"
|
||||
|
||||
using namespace Qt::Literals::StringLiterals;
|
||||
using namespace Quotient;
|
||||
|
||||
void MediaManager::startPlayback()
|
||||
{
|
||||
Q_EMIT playbackStarted();
|
||||
}
|
||||
|
||||
void MediaManager::ring(const QJsonObject &json, NeoChatRoom *room)
|
||||
{
|
||||
// todo: check sender != us
|
||||
if (json["content"_L1]["application"_L1].toString() != "m.call"_L1) {
|
||||
return;
|
||||
}
|
||||
if (!json["content"_L1]["m.mentions"_L1]["room"_L1].toBool() || json["sender"_L1].toString() == room->connection()->userId()) {
|
||||
if (std::ranges::none_of(json["content"_L1]["m.mentions"_L1]["user_ids"_L1].toArray(), [room](const auto &user) {
|
||||
return user.toString() == room->connection()->userId();
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (json["content"_L1]["notify_type"_L1].toString() != "ring"_L1) {
|
||||
return;
|
||||
}
|
||||
if (room->pushNotificationState() == PushNotificationState::Mute) {
|
||||
return;
|
||||
}
|
||||
if (isRinging()) {
|
||||
return;
|
||||
}
|
||||
if (const auto &event = room->currentState().get<CallMemberEvent>(room->connection()->userId())) {
|
||||
if (event) {
|
||||
auto memberships = event->contentJson()["memberships"_L1].toArray();
|
||||
for (const auto &m : memberships) {
|
||||
const auto &membership = m.toObject();
|
||||
if (membership["application"_L1] == "m.call"_L1 && membership["call_id"_L1].toString().isEmpty()) {
|
||||
qWarning() << "already in a call";
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
connectUntil(room, &NeoChatRoom::changed, this, [this, room]() {
|
||||
if (const auto &event = room->currentState().get<CallMemberEvent>(room->connection()->userId())) {
|
||||
auto memberships = event->contentJson()["memberships"_L1].toArray();
|
||||
for (const auto &m : memberships) {
|
||||
const auto &membership = m.toObject();
|
||||
if (membership["application"_L1] == "m.call"_L1 && membership["call_id"_L1].toString().isEmpty()) {
|
||||
stopRinging();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
});
|
||||
if (json["unsigned"_L1]["age"_L1].toInt() > 10000) {
|
||||
return;
|
||||
}
|
||||
|
||||
QTimer::singleShot(60000, this, &MediaManager::stopRinging);
|
||||
ringUnchecked();
|
||||
}
|
||||
|
||||
void MediaManager::ringUnchecked()
|
||||
{
|
||||
// Pause all media players registered with the system
|
||||
for (const auto &iface : QDBusConnection::sessionBus().interface()->registeredServiceNames().value()) {
|
||||
if (iface.startsWith("org.mpris.MediaPlayer2"_L1)) {
|
||||
OrgMprisMediaPlayer2PlayerInterface mprisInterface(iface, "/org/mpris/MediaPlayer2"_L1, QDBusConnection::sessionBus());
|
||||
QString status = mprisInterface.playbackStatus();
|
||||
if (status == "Playing"_L1) {
|
||||
if (mprisInterface.canPause()) {
|
||||
mprisInterface.Pause();
|
||||
} else {
|
||||
mprisInterface.Stop();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static QString path;
|
||||
if (path.isEmpty()) {
|
||||
for (const auto &dir : QString::fromUtf8(qgetenv("XDG_DATA_DIRS")).split(u':')) {
|
||||
if (QFileInfo(dir + QStringLiteral("/sounds/freedesktop/stereo/phone-incoming-call.oga")).exists()) {
|
||||
path = dir + QStringLiteral("/sounds/freedesktop/stereo/phone-incoming-call.oga");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (path.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
m_player->setSource(QUrl::fromLocalFile(path));
|
||||
m_player->play();
|
||||
}
|
||||
|
||||
MediaManager::MediaManager()
|
||||
: QObject(nullptr)
|
||||
, m_player(new QMediaPlayer())
|
||||
, m_output(new QAudioOutput())
|
||||
{
|
||||
m_player->setAudioOutput(m_output);
|
||||
m_timer.setInterval(1000);
|
||||
m_timer.setSingleShot(true);
|
||||
connect(&m_timer, &QTimer::timeout, this, [this]() {
|
||||
m_player->play();
|
||||
});
|
||||
connect(m_player, &QMediaPlayer::playbackStateChanged, this, [this]() {
|
||||
if (m_player->playbackState() == QMediaPlayer::StoppedState) {
|
||||
m_timer.start();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
bool MediaManager::isRinging() const
|
||||
{
|
||||
return m_ringing;
|
||||
}
|
||||
|
||||
void MediaManager::stopRinging()
|
||||
{
|
||||
m_ringing = false;
|
||||
m_player->pause();
|
||||
m_timer.stop();
|
||||
Q_EMIT isRingingChanged();
|
||||
}
|
||||
|
||||
#include "moc_mediamanager.cpp"
|
||||
@@ -5,6 +5,11 @@
|
||||
|
||||
#include <QObject>
|
||||
#include <QQmlEngine>
|
||||
#include <QTimer>
|
||||
|
||||
class NeoChatRoom;
|
||||
class QAudioOutput;
|
||||
class QMediaPlayer;
|
||||
|
||||
/**
|
||||
* @class MediaManager
|
||||
@@ -17,6 +22,8 @@ class MediaManager : public QObject
|
||||
QML_ELEMENT
|
||||
QML_SINGLETON
|
||||
|
||||
Q_PROPERTY(bool isRinging READ isRinging NOTIFY isRingingChanged)
|
||||
|
||||
public:
|
||||
static MediaManager &instance()
|
||||
{
|
||||
@@ -34,9 +41,24 @@ public:
|
||||
*/
|
||||
Q_INVOKABLE void startPlayback();
|
||||
|
||||
void ring(const QJsonObject &json, NeoChatRoom *room);
|
||||
void stopRinging();
|
||||
|
||||
bool isRinging() const;
|
||||
|
||||
Q_SIGNALS:
|
||||
/**
|
||||
* @brief Emitted when any media player starts playing. Other objects should stop / pause playback.
|
||||
*/
|
||||
void playbackStarted();
|
||||
void isRingingChanged();
|
||||
|
||||
private:
|
||||
MediaManager();
|
||||
|
||||
void ringUnchecked();
|
||||
bool m_ringing = false;
|
||||
QMediaPlayer *m_player;
|
||||
QAudioOutput *m_output;
|
||||
QTimer m_timer;
|
||||
};
|
||||
@@ -46,8 +46,10 @@
|
||||
|
||||
#include "chatbarcache.h"
|
||||
#include "clipboard.h"
|
||||
#include "events/callnotifyevent.h"
|
||||
#include "events/pollevent.h"
|
||||
#include "filetransferpseudojob.h"
|
||||
#include "mediamanager.h"
|
||||
#include "neochatconnection.h"
|
||||
#include "roomlastmessageprovider.h"
|
||||
#include "spacehierarchycache.h"
|
||||
@@ -117,6 +119,14 @@ NeoChatRoom::NeoChatRoom(Connection *connection, QString roomId, JoinState joinS
|
||||
}
|
||||
connect(this, &Room::addedMessages, this, &NeoChatRoom::cacheLastEvent);
|
||||
|
||||
connect(this, &NeoChatRoom::aboutToAddNewMessages, this, [this](const auto &events) {
|
||||
for (const auto &event : events) {
|
||||
if (event->template is<CallNotifyEvent>()) {
|
||||
MediaManager::instance().ring(event->fullJson(), this);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
connect(this, &Quotient::Room::eventsHistoryJobChanged, this, &NeoChatRoom::lastActiveTimeChanged);
|
||||
|
||||
connect(this, &Room::joinStateChanged, this, [this](JoinState oldState, JoinState newState) {
|
||||
|
||||
107
src/libneochat/org.mpris.MediaPlayer2.Player.xml
Normal file
107
src/libneochat/org.mpris.MediaPlayer2.Player.xml
Normal file
@@ -0,0 +1,107 @@
|
||||
<?xml version="1.0" ?>
|
||||
<node xmlns:tp="http://telepathy.freedesktop.org/wiki/DbusSpec#extensions-v0">
|
||||
<interface name="org.mpris.MediaPlayer2.Player">
|
||||
<method name="Next" tp:name-for-bindings="Next">
|
||||
</method>
|
||||
|
||||
<method name="Previous" tp:name-for-bindings="Previous">
|
||||
</method>
|
||||
|
||||
<method name="Pause" tp:name-for-bindings="Pause">
|
||||
</method>
|
||||
|
||||
<method name="PlayPause" tp:name-for-bindings="PlayPause">
|
||||
</method>
|
||||
|
||||
<method name="Stop" tp:name-for-bindings="Stop">
|
||||
</method>
|
||||
|
||||
<method name="Play" tp:name-for-bindings="Play">
|
||||
</method>
|
||||
|
||||
<method name="Seek" tp:name-for-bindings="Seek">
|
||||
<arg direction="in" type="x" name="Offset" tp:type="Time_In_Us">
|
||||
</arg>
|
||||
</method>
|
||||
|
||||
<method name="SetPosition" tp:name-for-bindings="Set_Position">
|
||||
<arg direction="in" type="o" tp:type="Track_Id" name="TrackId">
|
||||
</arg>
|
||||
<arg direction="in" type="x" tp:type="Time_In_Us" name="Position">
|
||||
</arg>
|
||||
</method>
|
||||
|
||||
<method name="OpenUri" tp:name-for-bindings="Open_Uri">
|
||||
<arg direction="in" type="s" tp:type="Uri" name="Uri">
|
||||
</arg>
|
||||
</method>
|
||||
|
||||
<property name="PlaybackStatus" tp:name-for-bindings="Playback_Status" type="s" tp:type="Playback_Status" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
</property>
|
||||
|
||||
<property name="LoopStatus" type="s" access="readwrite"
|
||||
tp:name-for-bindings="Loop_Status" tp:type="Loop_Status">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
</property>
|
||||
|
||||
<property name="Rate" tp:name-for-bindings="Rate" type="d" tp:type="Playback_Rate" access="readwrite">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
</property>
|
||||
|
||||
<property name="Shuffle" tp:name-for-bindings="Shuffle" type="b" access="readwrite">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
</property>
|
||||
|
||||
<property name="Metadata" tp:name-for-bindings="Metadata" type="a{sv}" tp:type="Metadata_Map" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
<annotation name="org.qtproject.QtDBus.QtTypeName" value="QVariantMap"/>
|
||||
</property>
|
||||
|
||||
<property name="Volume" type="d" tp:type="Volume" tp:name-for-bindings="Volume" access="readwrite">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true" />
|
||||
</property>
|
||||
|
||||
<property name="Position" type="x" tp:type="Time_In_Us" tp:name-for-bindings="Position" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
|
||||
</property>
|
||||
|
||||
<property name="MinimumRate" tp:name-for-bindings="Minimum_Rate" type="d" tp:type="Playback_Rate" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
</property>
|
||||
|
||||
<property name="MaximumRate" tp:name-for-bindings="Maximum_Rate" type="d" tp:type="Playback_Rate" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
</property>
|
||||
|
||||
<property name="CanGoNext" tp:name-for-bindings="Can_Go_Next" type="b" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
</property>
|
||||
|
||||
<property name="CanGoPrevious" tp:name-for-bindings="Can_Go_Previous" type="b" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
</property>
|
||||
|
||||
<property name="CanPlay" tp:name-for-bindings="Can_Play" type="b" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
</property>
|
||||
|
||||
<property name="CanPause" tp:name-for-bindings="Can_Pause" type="b" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
</property>
|
||||
|
||||
<property name="CanSeek" tp:name-for-bindings="Can_Seek" type="b" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="true"/>
|
||||
</property>
|
||||
|
||||
<property name="CanControl" tp:name-for-bindings="Can_Control" type="b" access="read">
|
||||
<annotation name="org.freedesktop.DBus.Property.EmitsChangedSignal" value="false"/>
|
||||
</property>
|
||||
|
||||
<signal name="Seeked" tp:name-for-bindings="Seeked">
|
||||
<arg name="Position" type="x" tp:type="Time_In_Us">
|
||||
</arg>
|
||||
</signal>
|
||||
|
||||
</interface>
|
||||
</node>
|
||||
Reference in New Issue
Block a user