diff --git a/resources/qml/ActiveCallBar.qml b/resources/qml/ActiveCallBar.qml
index 3059e213ab251ee037f7926a1bb79da7c0f431df..57b0877c6e0d37f5b743e8395f20b67ba91b4603 100644
--- a/resources/qml/ActiveCallBar.qml
+++ b/resources/qml/ActiveCallBar.qml
@@ -6,14 +6,14 @@ import im.nheko 1.0
 Rectangle {
     id: activeCallBar
 
-    visible: TimelineManager.callState != WebRTCState.DISCONNECTED
+    visible: CallManager.isOnCall
     color: "#2ECC71"
     implicitHeight: visible ? rowLayout.height + 8 : 0
 
     MouseArea {
         anchors.fill: parent
         onClicked: {
-            if (TimelineManager.onVideoCall)
+            if (CallManager.isOnVideoCall)
                 stackLayout.currentIndex = stackLayout.currentIndex ? 0 : 1;
 
         }
@@ -30,19 +30,19 @@ Rectangle {
         Avatar {
             width: avatarSize
             height: avatarSize
-            url: TimelineManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
-            displayName: TimelineManager.callPartyName
+            url: CallManager.callPartyAvatarUrl.replace("mxc://", "image://MxcImage/")
+            displayName: CallManager.callPartyName
         }
 
         Label {
             font.pointSize: fontMetrics.font.pointSize * 1.1
-            text: "  " + TimelineManager.callPartyName + " "
+            text: "  " + CallManager.callPartyName + " "
         }
 
         Image {
             Layout.preferredWidth: 24
             Layout.preferredHeight: 24
-            source: TimelineManager.onVideoCall ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
+            source: CallManager.isOnVideoCall ? "qrc:/icons/icons/ui/video-call.png" : "qrc:/icons/icons/ui/place-call.png"
         }
 
         Label {
@@ -52,11 +52,10 @@ Rectangle {
         }
 
         Item {
-            state: TimelineManager.callState
             states: [
                 State {
                     name: "OFFERSENT"
-                    when: state == WebRTCState.OFFERSENT
+                    when: CallManager.callState == WebRTCState.OFFERSENT
 
                     PropertyChanges {
                         target: callStateLabel
@@ -66,7 +65,7 @@ Rectangle {
                 },
                 State {
                     name: "CONNECTING"
-                    when: state == WebRTCState.CONNECTING
+                    when: CallManager.callState == WebRTCState.CONNECTING
 
                     PropertyChanges {
                         target: callStateLabel
@@ -76,7 +75,7 @@ Rectangle {
                 },
                 State {
                     name: "ANSWERSENT"
-                    when: state == WebRTCState.ANSWERSENT
+                    when: CallManager.callState == WebRTCState.ANSWERSENT
 
                     PropertyChanges {
                         target: callStateLabel
@@ -86,7 +85,7 @@ Rectangle {
                 },
                 State {
                     name: "CONNECTED"
-                    when: state == WebRTCState.CONNECTED
+                    when: CallManager.callState == WebRTCState.CONNECTED
 
                     PropertyChanges {
                         target: callStateLabel
@@ -100,13 +99,13 @@ Rectangle {
 
                     PropertyChanges {
                         target: stackLayout
-                        currentIndex: TimelineManager.onVideoCall ? 1 : 0
+                        currentIndex: CallManager.isOnVideoCall ? 1 : 0
                     }
 
                 },
                 State {
                     name: "DISCONNECTED"
-                    when: state == WebRTCState.DISCONNECTED
+                    when: CallManager.callState == WebRTCState.DISCONNECTED
 
                     PropertyChanges {
                         target: callStateLabel
@@ -132,7 +131,7 @@ Rectangle {
             }
 
             interval: 1000
-            running: TimelineManager.callState == WebRTCState.CONNECTED
+            running: CallManager.callState == WebRTCState.CONNECTED
             repeat: true
             onTriggered: {
                 var d = new Date();
@@ -149,7 +148,7 @@ Rectangle {
         }
 
         ImageButton {
-            visible: TimelineManager.onVideoCall
+            visible: CallManager.isOnVideoCall
             width: 24
             height: 24
             buttonTextColor: "#000000"
@@ -157,7 +156,7 @@ Rectangle {
             hoverEnabled: true
             ToolTip.visible: hovered
             ToolTip.text: "Toggle camera view"
-            onClicked: TimelineManager.toggleCameraView()
+            onClicked: CallManager.toggleCameraView()
         }
 
         Item {
@@ -168,11 +167,11 @@ Rectangle {
             width: 24
             height: 24
             buttonTextColor: "#000000"
-            image: TimelineManager.isMicMuted ? ":/icons/icons/ui/microphone-unmute.png" : ":/icons/icons/ui/microphone-mute.png"
+            image: CallManager.isMicMuted ? ":/icons/icons/ui/microphone-unmute.png" : ":/icons/icons/ui/microphone-mute.png"
             hoverEnabled: true
             ToolTip.visible: hovered
-            ToolTip.text: TimelineManager.isMicMuted ? qsTr("Unmute Mic") : qsTr("Mute Mic")
-            onClicked: TimelineManager.toggleMicMute()
+            ToolTip.text: CallManager.isMicMuted ? qsTr("Unmute Mic") : qsTr("Mute Mic")
+            onClicked: CallManager.toggleMicMute()
         }
 
         Item {
diff --git a/resources/qml/MessageInput.qml b/resources/qml/MessageInput.qml
index e8ebd5fc1923844b8f62854e46c443fd98746bb4..2847d51d635020f34b5082491471fdb4399963b6 100644
--- a/resources/qml/MessageInput.qml
+++ b/resources/qml/MessageInput.qml
@@ -17,14 +17,14 @@ Rectangle {
         spacing: 16
 
         ImageButton {
-            visible: TimelineManager.callsSupported
+            visible: CallManager.callsSupported
             Layout.alignment: Qt.AlignBottom
             hoverEnabled: true
             width: 22
             height: 22
-            image: TimelineManager.isOnCall ? ":/icons/icons/ui/end-call.png" : ":/icons/icons/ui/place-call.png"
+            image: CallManager.isOnCall ? ":/icons/icons/ui/end-call.png" : ":/icons/icons/ui/place-call.png"
             ToolTip.visible: hovered
-            ToolTip.text: TimelineManager.isOnCall ? qsTr("Hang up") : qsTr("Place a call")
+            ToolTip.text: CallManager.isOnCall ? qsTr("Hang up") : qsTr("Place a call")
             Layout.topMargin: 8
             Layout.bottomMargin: 8
             Layout.leftMargin: 16
@@ -39,7 +39,7 @@ Rectangle {
             image: ":/icons/icons/ui/paper-clip-outline.png"
             Layout.topMargin: 8
             Layout.bottomMargin: 8
-            Layout.leftMargin: TimelineManager.callsSupported ? 0 : 16
+            Layout.leftMargin: CallManager.callsSupported ? 0 : 16
             onClicked: TimelineManager.timeline.input.openFileSelection()
             ToolTip.visible: hovered
             ToolTip.text: qsTr("Send a file")
diff --git a/resources/qml/TimelineView.qml b/resources/qml/TimelineView.qml
index 6e9cd6651cba81db3638665b2a3f9396ad7c49c0..c71eb89f4ad5d10408d7177586d2ee27852fff42 100644
--- a/resources/qml/TimelineView.qml
+++ b/resources/qml/TimelineView.qml
@@ -210,7 +210,7 @@ Page {
                         }
 
                         Loader {
-                            source: TimelineManager.onVideoCall ? "VideoCall.qml" : ""
+                            source: CallManager.isOnVideoCall ? "VideoCall.qml" : ""
                             onLoaded: TimelineManager.setVideoCallItem()
                         }
 
diff --git a/src/CallManager.cpp b/src/CallManager.cpp
index 89cfeaf998a486d30a118b1a946124885307ff60..cb523bc284a214a53b620e13007cfb07ff851892 100644
--- a/src/CallManager.cpp
+++ b/src/CallManager.cpp
@@ -13,7 +13,6 @@
 #include "MainWindow.h"
 #include "MatrixClient.h"
 #include "Utils.h"
-#include "WebRTCSession.h"
 #include "dialogs/AcceptCall.h"
 
 #include "mtx/responses/turn_server.hpp"
@@ -112,6 +111,7 @@ CallManager::CallManager(QObject *parent)
                 default:
                         break;
                 }
+                emit newCallState();
         });
 
         connect(&player_,
@@ -144,7 +144,7 @@ CallManager::CallManager(QObject *parent)
 void
 CallManager::sendInvite(const QString &roomid, bool isVideo)
 {
-        if (onActiveCall())
+        if (isOnCall())
                 return;
 
         auto roomInfo = cache::singleRoomInfo(roomid.toStdString());
@@ -206,12 +206,6 @@ CallManager::hangUp(CallHangUp::Reason reason)
         }
 }
 
-bool
-CallManager::onActiveCall() const
-{
-        return session_.state() != webrtc::State::DISCONNECTED;
-}
-
 void
 CallManager::syncEvent(const mtx::events::collections::TimelineEvents &event)
 {
@@ -257,7 +251,7 @@ CallManager::handleEvent(const RoomEvent<CallInvite> &callInviteEvent)
                 return;
 
         auto roomInfo = cache::singleRoomInfo(callInviteEvent.room_id);
-        if (onActiveCall() || roomInfo.member_count != 2) {
+        if (isOnCall() || roomInfo.member_count != 2) {
                 emit newMessage(QString::fromStdString(callInviteEvent.room_id),
                                 CallHangUp{callInviteEvent.content.call_id,
                                            0,
@@ -332,7 +326,7 @@ CallManager::handleEvent(const RoomEvent<CallCandidates> &callCandidatesEvent)
                            callCandidatesEvent.sender);
 
         if (callid_ == callCandidatesEvent.content.call_id) {
-                if (onActiveCall())
+                if (isOnCall())
                         session_.acceptICECandidates(callCandidatesEvent.content.candidates);
                 else {
                         // CallInvite has been received and we're awaiting localUser to accept or
@@ -350,7 +344,7 @@ CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent)
                            callAnswerEvent.content.call_id,
                            callAnswerEvent.sender);
 
-        if (!onActiveCall() && callAnswerEvent.sender == utils::localUser().toStdString() &&
+        if (!isOnCall() && callAnswerEvent.sender == utils::localUser().toStdString() &&
             callid_ == callAnswerEvent.content.call_id) {
                 emit ChatPage::instance()->showNotification("Call answered on another device.");
                 stopRingtone();
@@ -358,7 +352,7 @@ CallManager::handleEvent(const RoomEvent<CallAnswer> &callAnswerEvent)
                 return;
         }
 
-        if (onActiveCall() && callid_ == callAnswerEvent.content.call_id) {
+        if (isOnCall() && callid_ == callAnswerEvent.content.call_id) {
                 stopRingtone();
                 if (!session_.acceptAnswer(callAnswerEvent.content.sdp)) {
                         emit ChatPage::instance()->showNotification("Problem setting up call.");
@@ -381,6 +375,23 @@ CallManager::handleEvent(const RoomEvent<CallHangUp> &callHangUpEvent)
         }
 }
 
+void
+CallManager::toggleMicMute()
+{
+        session_.toggleMicMute();
+        emit micMuteChanged();
+}
+
+bool
+CallManager::callsSupported() const
+{
+#ifdef GSTREAMER_AVAILABLE
+        return true;
+#else
+        return false;
+#endif
+}
+
 void
 CallManager::generateCallID()
 {
diff --git a/src/CallManager.h b/src/CallManager.h
index 8004e83865672985a99bf19b2c14b9fe0f3da444..d59a6249d415007036b66d8cbf2f3cfdf5df044c 100644
--- a/src/CallManager.h
+++ b/src/CallManager.h
@@ -8,6 +8,7 @@
 #include <QString>
 #include <QTimer>
 
+#include "WebRTCSession.h"
 #include "mtx/events/collections.hpp"
 #include "mtx/events/voip.hpp"
 
@@ -16,11 +17,17 @@ struct TurnServer;
 }
 
 class QUrl;
-class WebRTCSession;
 
 class CallManager : public QObject
 {
         Q_OBJECT
+        Q_PROPERTY(bool isOnCall READ isOnCall NOTIFY newCallState)
+        Q_PROPERTY(bool isOnVideoCall READ isOnVideoCall NOTIFY newVideoCallState)
+        Q_PROPERTY(webrtc::State callState READ callState NOTIFY newCallState)
+        Q_PROPERTY(QString callPartyName READ callPartyName NOTIFY newCallParty)
+        Q_PROPERTY(QString callPartyAvatarUrl READ callPartyAvatarUrl NOTIFY newCallParty)
+        Q_PROPERTY(bool isMicMuted READ isMicMuted NOTIFY micMuteChanged)
+        Q_PROPERTY(bool callsSupported READ callsSupported CONSTANT)
 
 public:
         CallManager(QObject *);
@@ -28,21 +35,29 @@ public:
         void sendInvite(const QString &roomid, bool isVideo);
         void hangUp(
           mtx::events::msg::CallHangUp::Reason = mtx::events::msg::CallHangUp::Reason::User);
-        bool onActiveCall() const;
+        bool isOnCall() const { return session_.state() != webrtc::State::DISCONNECTED; }
+        bool isOnVideoCall() const { return session_.isVideo(); }
+        webrtc::State callState() const { return session_.state(); }
         QString callPartyName() const { return callPartyName_; }
         QString callPartyAvatarUrl() const { return callPartyAvatarUrl_; }
+        bool isMicMuted() const { return session_.isMicMuted(); }
+        bool callsSupported() const;
         void refreshTurnServer();
 
 public slots:
         void syncEvent(const mtx::events::collections::TimelineEvents &event);
+        void toggleMicMute();
+        void toggleCameraView() { session_.toggleCameraView(); }
 
 signals:
         void newMessage(const QString &roomid, const mtx::events::msg::CallInvite &);
         void newMessage(const QString &roomid, const mtx::events::msg::CallCandidates &);
         void newMessage(const QString &roomid, const mtx::events::msg::CallAnswer &);
         void newMessage(const QString &roomid, const mtx::events::msg::CallHangUp &);
-        void newCallParty();
+        void newCallState();
         void newVideoCallState();
+        void newCallParty();
+        void micMuteChanged();
         void turnServerRetrieved(const mtx::responses::TurnServer &);
 
 private slots:
diff --git a/src/timeline/InputBar.cpp b/src/timeline/InputBar.cpp
index 5cbc33e00434dc6132dbe49b9703bfdce9d91d6f..2f50a7cca6f5fb2e60e6ae1e6962d9d180ee2f3d 100644
--- a/src/timeline/InputBar.cpp
+++ b/src/timeline/InputBar.cpp
@@ -597,7 +597,7 @@ void
 InputBar::callButton()
 {
         auto callManager_ = ChatPage::instance()->callManager();
-        if (callManager_->onActiveCall()) {
+        if (callManager_->isOnCall()) {
                 callManager_->hangUp();
         } else {
                 auto current_room_ = room->roomId();
diff --git a/src/timeline/TimelineViewManager.cpp b/src/timeline/TimelineViewManager.cpp
index f10c2c0dece0c27fbd485d7cfb7f396850093978..97af0065b08572a66aea464eb8f6d6667a492d93 100644
--- a/src/timeline/TimelineViewManager.cpp
+++ b/src/timeline/TimelineViewManager.cpp
@@ -136,6 +136,10 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
           "im.nheko", 1, 0, "Settings", [](QQmlEngine *, QJSEngine *) -> QObject * {
                   return ChatPage::instance()->userSettings().data();
           });
+        qmlRegisterSingletonType<CallManager>(
+          "im.nheko", 1, 0, "CallManager", [](QQmlEngine *, QJSEngine *) -> QObject * {
+                  return ChatPage::instance()->callManager();
+          });
 
         qRegisterMetaType<mtx::events::collections::TimelineEvents>();
         qRegisterMetaType<std::vector<DeviceInfo>>();
@@ -237,36 +241,6 @@ TimelineViewManager::TimelineViewManager(CallManager *callManager, ChatPage *par
                 isInitialSync_ = true;
                 emit initialSyncChanged(true);
         });
-        connect(&WebRTCSession::instance(),
-                &WebRTCSession::stateChanged,
-                this,
-                &TimelineViewManager::callStateChanged);
-        connect(
-          callManager_, &CallManager::newCallParty, this, &TimelineViewManager::callPartyChanged);
-        connect(callManager_,
-                &CallManager::newVideoCallState,
-                this,
-                &TimelineViewManager::videoCallChanged);
-
-        connect(&WebRTCSession::instance(),
-                &WebRTCSession::stateChanged,
-                this,
-                &TimelineViewManager::onCallChanged);
-}
-
-bool
-TimelineViewManager::isOnCall() const
-{
-        return callManager_->onActiveCall();
-}
-bool
-TimelineViewManager::callsSupported() const
-{
-#ifdef GSTREAMER_AVAILABLE
-        return true;
-#else
-        return false;
-#endif
 }
 
 void
@@ -354,19 +328,6 @@ TimelineViewManager::escapeEmoji(QString str) const
         return utils::replaceEmoji(str);
 }
 
-void
-TimelineViewManager::toggleMicMute()
-{
-        WebRTCSession::instance().toggleMicMute();
-        emit micMuteChanged();
-}
-
-void
-TimelineViewManager::toggleCameraView()
-{
-        WebRTCSession::instance().toggleCameraView();
-}
-
 void
 TimelineViewManager::openImageOverlay(QString mxcUrl, QString eventId) const
 {
diff --git a/src/timeline/TimelineViewManager.h b/src/timeline/TimelineViewManager.h
index 1cec0939200675c3c4b88ee6659d5a54799ce863..23a960b8307ab018ca7df41010bec6ecde0594a5 100644
--- a/src/timeline/TimelineViewManager.h
+++ b/src/timeline/TimelineViewManager.h
@@ -36,13 +36,6 @@ class TimelineViewManager : public QObject
           bool isInitialSync MEMBER isInitialSync_ READ isInitialSync NOTIFY initialSyncChanged)
         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)
-        Q_PROPERTY(bool isOnCall READ isOnCall NOTIFY onCallChanged)
-        Q_PROPERTY(bool callsSupported READ callsSupported CONSTANT)
 
 public:
         TimelineViewManager(CallManager *callManager, ChatPage *parent = nullptr);
@@ -61,14 +54,6 @@ public:
         Q_INVOKABLE TimelineModel *activeTimeline() const { return timeline_; }
         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(); }
-        Q_INVOKABLE void toggleMicMute();
-        Q_INVOKABLE void toggleCameraView();
         Q_INVOKABLE void openImageOverlay(QString mxcUrl, QString eventId) const;
         Q_INVOKABLE QColor userColor(QString id, QColor background);
         Q_INVOKABLE QString escapeEmoji(QString str) const;
@@ -98,11 +83,6 @@ signals:
         void inviteUsers(QStringList users);
         void showRoomList();
         void narrowViewChanged();
-        void callStateChanged(webrtc::State);
-        void videoCallChanged();
-        void callPartyChanged();
-        void micMuteChanged();
-        void onCallChanged();
 
 public slots:
         void updateReadReceipts(const QString &room_id, const std::vector<QString> &event_ids);
@@ -130,8 +110,7 @@ public slots:
         void queueCallMessage(const QString &roomid, const mtx::events::msg::CallHangUp &);
 
         void updateEncryptedDescriptions();
-        bool isOnCall() const;
-        bool callsSupported() const;
+        void setVideoCallItem();
 
         void enableBackButton()
         {