diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index a0c6818d1..ef568c335 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -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 diff --git a/src/app/mediamanager.cpp b/src/app/mediamanager.cpp deleted file mode 100644 index 1667a1374..000000000 --- a/src/app/mediamanager.cpp +++ /dev/null @@ -1,11 +0,0 @@ -// SPDX-FileCopyrightText: 2023 Tobias Fella -// SPDX-License-Identifier: LGPL-2.0-or-later - -#include "mediamanager.h" - -void MediaManager::startPlayback() -{ - Q_EMIT playbackStarted(); -} - -#include "moc_mediamanager.cpp" diff --git a/src/libneochat/CMakeLists.txt b/src/libneochat/CMakeLists.txt index 07b6dc4a2..1d66e905c 100644 --- a/src/libneochat/CMakeLists.txt +++ b/src/libneochat/CMakeLists.txt @@ -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 diff --git a/src/libneochat/events/callmemberevent.cpp b/src/libneochat/events/callmemberevent.cpp new file mode 100644 index 000000000..556f21f2f --- /dev/null +++ b/src/libneochat/events/callmemberevent.cpp @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2024 Tobias Fella +// SPDX-License-Identifier: LGPL-2.0-or-later + +#include "callmemberevent.h" + +#include + +using namespace Quotient; +using namespace Qt::Literals::StringLiterals; + +CallMemberEventContent::CallMemberEventContent(const QJsonObject &json) +{ + for (const auto &membership : json["memberships"_L1].toArray()) { + QList 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(), + .fociActive = foci, + .membershipId = membership["membershipID"_L1].toString(), + .scope = membership["scope"_L1].toString(), + }); + } +} + +QJsonObject CallMemberEventContent::toJson() const +{ + return {}; +} diff --git a/src/libneochat/events/callmemberevent.h b/src/libneochat/events/callmemberevent.h new file mode 100644 index 000000000..c50ce5551 --- /dev/null +++ b/src/libneochat/events/callmemberevent.h @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2024 Tobias Fella +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include + +namespace Quotient +{ + +struct Focus { + QString livekitAlias; + QString livekitServiceUrl; + QString type; +}; + +struct CallMembership { + QString application; + QString callId; + QString deviceId; + int expires; + uint64_t expiresTs; + QList fociActive; + QString membershipId; + QString scope; +}; + +class CallMemberEventContent +{ +public: + explicit CallMemberEventContent(const QJsonObject &json); + QJsonObject toJson() const; + + QList memberships; +}; + +/** + * @class CallMemberEvent + * + * Class to define a call member event. + * + * @sa Quotient::StateEvent + */ +class CallMemberEvent : public KeyedStateEventBase +{ +public: + QUO_EVENT(CallMemberEvent, "org.matrix.msc3401.call.member") + + explicit CallMemberEvent(const QJsonObject &obj) + : KeyedStateEventBase(obj) + { + } + + QJsonArray memberships() const + { + return contentJson()["memberships"_L1].toArray(); + } +}; +} diff --git a/src/libneochat/events/callnotifyevent.h b/src/libneochat/events/callnotifyevent.h new file mode 100644 index 000000000..4656b9046 --- /dev/null +++ b/src/libneochat/events/callnotifyevent.h @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2024 Tobias Fella +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include + +namespace Quotient +{ + +class CallNotifyEvent : public RoomEvent +{ +public: + QUO_EVENT(CallNotifyEvent, "org.matrix.msc4075.call.notify"); + explicit CallNotifyEvent(const QJsonObject &obj) + : RoomEvent(obj) + { + } +}; + +} diff --git a/src/libneochat/mediamanager.cpp b/src/libneochat/mediamanager.cpp new file mode 100644 index 000000000..8357d40b6 --- /dev/null +++ b/src/libneochat/mediamanager.cpp @@ -0,0 +1,146 @@ +// SPDX-FileCopyrightText: 2023-2025 Tobias Fella +// SPDX-License-Identifier: LGPL-2.0-or-later + +#include "mediamanager.h" + +#include +#include +#include +#include +#include + +#include + +#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(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(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" diff --git a/src/app/mediamanager.h b/src/libneochat/mediamanager.h similarity index 67% rename from src/app/mediamanager.h rename to src/libneochat/mediamanager.h index b5386c73f..7e3d18fda 100644 --- a/src/app/mediamanager.h +++ b/src/libneochat/mediamanager.h @@ -5,6 +5,11 @@ #include #include +#include + +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; }; diff --git a/src/libneochat/neochatroom.cpp b/src/libneochat/neochatroom.cpp index 177b6abd4..a12e1a5d7 100644 --- a/src/libneochat/neochatroom.cpp +++ b/src/libneochat/neochatroom.cpp @@ -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()) { + MediaManager::instance().ring(event->fullJson(), this); + } + } + }); + connect(this, &Quotient::Room::eventsHistoryJobChanged, this, &NeoChatRoom::lastActiveTimeChanged); connect(this, &Room::joinStateChanged, this, [this](JoinState oldState, JoinState newState) { diff --git a/src/libneochat/org.mpris.MediaPlayer2.Player.xml b/src/libneochat/org.mpris.MediaPlayer2.Player.xml new file mode 100644 index 000000000..8b094bd1c --- /dev/null +++ b/src/libneochat/org.mpris.MediaPlayer2.Player.xml @@ -0,0 +1,107 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +