diff --git a/CMakeLists.txt b/CMakeLists.txt
index 1d22759811fb7a74ed2c2a652fc9bd4a78aa2c39..c25be7896468b9ace027b97e54319da2200baa8c 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -277,6 +277,7 @@ SET( libringclient_LIB_SRCS
 
   #Models
   src/contactmodel.cpp
+  src/callparticipantsmodel.cpp
   src/newcallmodel.cpp
   src/newdevicemodel.cpp
   src/newcodecmodel.cpp
@@ -342,6 +343,7 @@ SET(libringclient_api_LIB_HDRS
   src/api/member.h
   src/api/newaccountmodel.h
   src/api/newcallmodel.h
+  src/api/callparticipantsmodel.h
   src/api/newcodecmodel.h
   src/api/newdevicemodel.h
   src/api/pluginmodel.h
diff --git a/src/api/call.h b/src/api/call.h
index 993bf4a51751f0e6ca58592e05c62b059e47e2f4..54741a966dab3bca1891bb42007561bfd7b6aba9 100644
--- a/src/api/call.h
+++ b/src/api/call.h
@@ -137,9 +137,9 @@ struct Info
     bool videoMuted = false; // this flag is used to check main video status
     bool isAudioOnly = false;
     Layout layout = Layout::GRID;
-    VectorMapStringString participantsInfos = {};
     VectorMapStringString mediaList = {};
     QSet<QString> peerRec {};
+    bool isConference = false;
 };
 
 static inline bool
diff --git a/src/api/callparticipantsmodel.h b/src/api/callparticipantsmodel.h
new file mode 100644
index 0000000000000000000000000000000000000000..9abc14682cea7444782c5b578125967150842e4e
--- /dev/null
+++ b/src/api/callparticipantsmodel.h
@@ -0,0 +1,147 @@
+/*!
+ *   Copyright (C) 2022 Savoir-faire Linux Inc.
+ *   Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com>
+ *
+ *   This library is free software; you can redistribute it and/or
+ *   modify it under the terms of the GNU Lesser General Public
+ *   License as published by the Free Software Foundation; either
+ *   version 2.1 of the License, or (at your option) any later version.
+ *
+ *   This library is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *   Lesser General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#pragma once
+
+// std
+#include <string>
+#include <ctime>
+#include <chrono>
+#include <mutex>
+
+// Qt
+#include <QObject>
+#include <QJsonObject>
+
+#include "typedefs.h"
+
+namespace lrc {
+
+namespace api {
+class NewCallModel;
+
+struct ParticipantInfos
+{
+    ParticipantInfos() {}
+
+    ParticipantInfos(const MapStringString& infos, const QString& callId, const QString& peerId)
+    {
+        uri = infos["uri"];
+        if (uri.lastIndexOf("@") > 0)
+            uri.truncate(uri.lastIndexOf("@"));
+        if (uri.isEmpty())
+            uri = peerId;
+        device = infos["device"];
+        active = infos["active"] == "true";
+        x = infos["x"].toInt();
+        y = infos["y"].toInt();
+        width = infos["w"].toInt();
+        height = infos["h"].toInt();
+        videoMuted = infos["videoMuted"] == "true";
+        audioLocalMuted = infos["audioLocalMuted"] == "true";
+        audioModeratorMuted = infos["audioModeratorMuted"] == "true";
+        isModerator = infos["isModerator"] == "true";
+        handRaised = infos["handRaised"] == "true";
+
+        if (infos["sinkId"].isEmpty())
+            sinkId = callId + uri + device;
+        else
+            sinkId = infos["sinkId"];
+
+        bestName = "";
+    }
+
+    QString uri;
+    QString device;
+    QString sinkId;
+    QString bestName;
+    QString avatar;
+    bool active {false};
+    int x = 0;
+    int y = 0;
+    int width = 0;
+    int height = 0;
+    bool audioLocalMuted {false};
+    bool audioModeratorMuted {false};
+    bool videoMuted {false};
+    bool isModerator {false};
+    bool islocal {false};
+    bool isContact {false};
+    bool handRaised {false};
+
+    bool operator==(const ParticipantInfos& other) const
+    {
+        return uri == other.uri && sinkId == other.sinkId && active == other.active
+               && audioLocalMuted == other.audioLocalMuted
+               && audioModeratorMuted == other.audioModeratorMuted && avatar == other.avatar
+               && bestName == other.bestName && isContact == other.isContact
+               && islocal == other.islocal && videoMuted == other.videoMuted
+               && handRaised == other.handRaised;
+    }
+};
+
+class LIB_EXPORT CallParticipants : public QObject
+{
+    Q_OBJECT
+
+public:
+    CallParticipants(const VectorMapStringString& infos,
+                     const QString& callId,
+                     const NewCallModel& linked);
+    ~CallParticipants() {}
+
+    /**
+     * @return The list of participants that can have a widget in the client
+     */
+    QList<ParticipantInfos> getParticipants() const;
+
+    /**
+     * Update the participants
+     */
+    void update(const VectorMapStringString& infos);
+
+    /**
+     * @param index participant index
+     * @return informations of the participant in index
+     */
+    QJsonObject toQJsonObject(uint index) const;
+
+private:
+    /**
+     * Filter the participants the might appear for the end user
+     */
+    void filterCandidates(const VectorMapStringString& infos);
+
+    void removeParticipant(int index);
+
+    void addParticipant(const ParticipantInfos& participant);
+
+    // Participants in the conference
+    QMap<QString, ParticipantInfos> candidates_;
+    // Participants ordered
+    QMap<QString, ParticipantInfos> participants_;
+    QList<QString> validUris_;
+    int idx_;
+
+    const NewCallModel& linked;
+    std::mutex streamMtx_ {};
+    const QString callId_;
+};
+} // end namespace api
+} // end namespace lrc
+Q_DECLARE_METATYPE(lrc::api::CallParticipants*)
diff --git a/src/api/newcallmodel.h b/src/api/newcallmodel.h
index 6c786f11486aaaf9b3136033d3e1716d6182de5f..363bdc5f224e27cd7f30c68884b97ed68a044b1a 100644
--- a/src/api/newcallmodel.h
+++ b/src/api/newcallmodel.h
@@ -50,6 +50,7 @@ struct PendingConferenceeInfo
 };
 } // namespace call
 class NewAccountModel;
+class CallParticipants;
 
 /**
  *  @brief Class that manages call informations.
@@ -60,6 +61,7 @@ class LIB_EXPORT NewCallModel : public QObject
 
 public:
     using CallInfoMap = std::map<QString, std::shared_ptr<call::Info>>;
+    using CallParticipantsModelMap = std::map<QString, std::shared_ptr<CallParticipants>>;
 
     const account::Info& owner;
 
@@ -105,6 +107,14 @@ public:
      */
     const call::Info& getCall(const QString& uid) const;
 
+    /**
+     * Get the call participantsInfos from its call id
+     * @param  callId
+     * @return the call participantsInfos
+     * @throw out_of_range exception if not found
+     */
+    const CallParticipants& getParticipantsInfos(const QString& callId);
+
     /**
      * Get the call from the peer uri
      * @param  uri
@@ -383,6 +393,28 @@ public:
      */
     void setDisplay(int idx, int x, int y, int w, int h, const QString& callId = {});
 Q_SIGNALS:
+
+    /**
+     * Emitted when a participant video is added to a conference
+     * @param callId
+     * @param index
+     */
+    void participantAdded(const QString& callId, int index) const;
+
+    /**
+     * Emitted when a participant video is removed from a conference
+     * @param callId
+     * @param index
+     */
+    void participantRemoved(const QString& callId, int index) const;
+
+    /**
+     * Emitted when, in a conference, participant parameters are changed
+     * @param callId
+     * @param index
+     */
+    void participantUpdated(const QString& callId, int index) const;
+
     /**
      * Emitted when a call state changes
      * @param callId
diff --git a/src/callparticipantsmodel.cpp b/src/callparticipantsmodel.cpp
new file mode 100644
index 0000000000000000000000000000000000000000..c089011963d29296fe9fa1a3d69fba1abf3c9032
--- /dev/null
+++ b/src/callparticipantsmodel.cpp
@@ -0,0 +1,151 @@
+/*!
+ *   Copyright (C) 2022 Savoir-faire Linux Inc.
+ *   Author: Aline Gondim Santos <aline.gondimsantos@savoirfairelinux.com>
+ *
+ *   This library is free software; you can redistribute it and/or
+ *   modify it under the terms of the GNU Lesser General Public
+ *   License as published by the Free Software Foundation; either
+ *   version 2.1 of the License, or (at your option) any later version.
+ *
+ *   This library is distributed in the hope that it will be useful,
+ *   but WITHOUT ANY WARRANTY; without even the implied warranty of
+ *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ *   Lesser General Public License for more details.
+ *
+ *   You should have received a copy of the GNU General Public License
+ *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#include "api/callparticipantsmodel.h"
+
+#include "api/account.h"
+#include "api/contactmodel.h"
+#include "api/contact.h"
+#include "api/newcallmodel.h"
+#include "api/newaccountmodel.h"
+
+namespace lrc {
+
+namespace api {
+
+CallParticipants::CallParticipants(const VectorMapStringString& infos,
+                                   const QString& callId,
+                                   const NewCallModel& linked)
+    : linked(linked)
+    , callId_(callId)
+{
+    update(infos);
+}
+
+QList<ParticipantInfos>
+CallParticipants::getParticipants() const
+{
+    return participants_.values();
+}
+
+void
+CallParticipants::update(const VectorMapStringString& infos)
+{
+    validUris_.clear();
+    filterCandidates(infos);
+    validUris_.sort();
+
+    idx_ = 0;
+    auto keys = participants_.keys();
+    for (const auto& key : keys) {
+        auto keyIdx = validUris_.indexOf(key);
+        if (keyIdx < 0 || keyIdx >= validUris_.size())
+            removeParticipant(idx_);
+        else
+            idx_++;
+    }
+
+    idx_ = 0;
+    for (const auto& partUri : validUris_) {
+        addParticipant(candidates_[partUri]);
+        idx_++;
+    }
+}
+
+void
+CallParticipants::removeParticipant(int index)
+{
+    std::lock_guard<std::mutex> lk(streamMtx_);
+    auto it = participants_.begin() + index;
+    participants_.erase(it);
+    Q_EMIT linked.participantRemoved(callId_, idx_);
+}
+
+void
+CallParticipants::addParticipant(const ParticipantInfos& participant)
+{
+    std::lock_guard<std::mutex> lk(streamMtx_);
+    auto it = participants_.find(participant.uri);
+    if (it == participants_.end()) {
+        participants_.insert(participants_.begin() + idx_, participant.uri, participant);
+        Q_EMIT linked.participantAdded(callId_, idx_);
+    } else {
+        if (participant == (*it))
+            return;
+        (*it) = participant;
+        Q_EMIT linked.participantUpdated(callId_, idx_);
+    }
+}
+
+void
+CallParticipants::filterCandidates(const VectorMapStringString& infos)
+{
+    candidates_.clear();
+    for (const auto& candidate : infos) {
+        auto peerId = candidate["uri"];
+        peerId.truncate(peerId.lastIndexOf("@"));
+        if (peerId.isEmpty()) {
+            for (const auto& accId : linked.owner.accountModel->getAccountList()) {
+                try {
+                    auto& accountInfo = linked.owner.accountModel->getAccountInfo(accId);
+                    if (accountInfo.callModel->hasCall(callId_)) {
+                        peerId = accountInfo.profileInfo.uri;
+                    }
+                } catch (...) {
+                }
+            }
+        }
+        if (candidate["w"].toInt() != 0 && candidate["h"].toInt() != 0) {
+            validUris_.append(peerId);
+            candidates_.insert(peerId, ParticipantInfos(candidate, callId_, peerId));
+        }
+    }
+}
+
+QJsonObject
+CallParticipants::toQJsonObject(uint index) const
+{
+    if (index >= participants_.size())
+        return {};
+
+    QJsonObject ret;
+    const auto& participant = participants_.begin() + index;
+
+    ret["uri"] = participant->uri;
+    ret["device"] = participant->device;
+    ret["sinkId"] = participant->sinkId;
+    ret["bestName"] = participant->bestName;
+    ret["avatar"] = participant->avatar;
+    ret["active"] = participant->active;
+    ret["x"] = participant->x;
+    ret["y"] = participant->y;
+    ret["width"] = participant->width;
+    ret["height"] = participant->height;
+    ret["audioLocalMuted"] = participant->audioLocalMuted;
+    ret["audioModeratorMuted"] = participant->audioModeratorMuted;
+    ret["videoMuted"] = participant->videoMuted;
+    ret["isModerator"] = participant->isModerator;
+    ret["islocal"] = participant->islocal;
+    ret["isContact"] = participant->isContact;
+    ret["handRaised"] = participant->handRaised;
+    ret["callId"] = callId_;
+
+    return ret;
+}
+} // end namespace api
+} // end namespace lrc
diff --git a/src/newcallmodel.cpp b/src/newcallmodel.cpp
index 3bd8cb3ee1b4af421c49110265169500c647b817..6a16d522dce32af7349608aaab9a06e5ba2f85b6 100644
--- a/src/newcallmodel.cpp
+++ b/src/newcallmodel.cpp
@@ -27,6 +27,7 @@
 #include "api/contact.h"
 #include "api/contactmodel.h"
 #include "api/pluginmodel.h"
+#include "api/callparticipantsmodel.h"
 #include "api/lrc.h"
 #include "api/newaccountmodel.h"
 #include "authority/storagehelper.h"
@@ -134,6 +135,7 @@ public:
     void sendProfile(const QString& callId);
 
     NewCallModel::CallInfoMap calls;
+    NewCallModel::CallParticipantsModelMap participantsModel;
     const CallbacksHandler& callbacksHandler;
     const NewCallModel& linked;
     const BehaviorController& behaviorController;
@@ -328,6 +330,17 @@ NewCallModel::getCall(const QString& uid) const
     return *pimpl_->calls.at(uid);
 }
 
+const CallParticipants&
+NewCallModel::getParticipantsInfos(const QString& callId)
+{
+    if (pimpl_->participantsModel.find(callId) == pimpl_->participantsModel.end()) {
+        VectorMapStringString infos = {};
+        pimpl_->participantsModel
+            .emplace(callId, std::make_shared<CallParticipants>(infos, callId, pimpl_->linked));
+    }
+    return *pimpl_->participantsModel.at(callId);
+}
+
 void
 NewCallModel::updateCallMediaList(const QString& callId, bool acceptVideo)
 {
@@ -957,7 +970,7 @@ NewCallModelPimpl::initCallFromDaemon()
         callInfo->type = call::Type::DIALOG;
         VectorMapStringString infos = CallManager::instance().getConferenceInfos(linked.owner.id,
                                                                                  callId);
-        callInfo->participantsInfos = infos;
+        participantsModel.emplace(callId, std::make_shared<CallParticipants>(infos, callId, linked));
         calls.emplace(callId, std::move(callInfo));
         // NOTE/BUG: the videorenderer can't know that the client has restarted
         // So, for now, a user will have to manually restart the medias until
@@ -1000,7 +1013,7 @@ NewCallModelPimpl::initConferencesFromDaemon()
         callInfo->type = call::Type::CONFERENCE;
         VectorMapStringString infos = CallManager::instance().getConferenceInfos(linked.owner.id,
                                                                                  callId);
-        callInfo->participantsInfos = infos;
+        participantsModel.emplace(callId, std::make_shared<CallParticipants>(infos, callId, linked));
         calls.emplace(callId, std::move(callInfo));
     }
 }
@@ -1105,6 +1118,10 @@ NewCallModel::isModerator(const QString& confId, const QString& uri)
     auto call = pimpl_->calls.find(confId);
     if (call == pimpl_->calls.end() or not call->second)
         return false;
+    auto participantsModel = pimpl_->participantsModel.find(confId);
+    if (participantsModel == pimpl_->participantsModel.end()
+        or participantsModel->second->getParticipants().size() == 0)
+        return false;
     auto ownerUri = owner.profileInfo.uri;
     auto uriToCheck = uri;
     if (uriToCheck.isEmpty()) {
@@ -1113,10 +1130,10 @@ NewCallModel::isModerator(const QString& confId, const QString& uri)
     auto isModerator = uriToCheck == ownerUri
                            ? call->second->type == lrc::api::call::Type::CONFERENCE
                            : false;
-    if (!isModerator && call->second->participantsInfos.size() != 0) {
-        for (const auto& participant : call->second->participantsInfos) {
-            if (participant["uri"] == uriToCheck) {
-                isModerator = participant["isModerator"] == "true";
+    if (!isModerator && participantsModel->second->getParticipants().size() != 0) {
+        for (const auto& participant : participantsModel->second->getParticipants()) {
+            if (participant.uri == uriToCheck) {
+                isModerator = participant.isModerator;
                 break;
             }
         }
@@ -1136,17 +1153,20 @@ NewCallModel::isHandRaised(const QString& confId, const QString& uri) noexcept
     auto call = pimpl_->calls.find(confId);
     if (call == pimpl_->calls.end() or not call->second)
         return false;
+
+    auto participantsModel = pimpl_->participantsModel.find(confId);
+    if (participantsModel == pimpl_->participantsModel.end())
+        return false;
+
     auto ownerUri = owner.profileInfo.uri;
     auto uriToCheck = uri;
     if (uriToCheck.isEmpty()) {
         uriToCheck = ownerUri;
     }
     auto handRaised = false;
-    for (const auto& participant : call->second->participantsInfos) {
-        auto itUri = participant.find("uri");
-        auto itHand = participant.find("handRaised");
-        if (itUri != participant.end() && itHand != participant.end() && *itUri == uriToCheck) {
-            handRaised = participant["handRaised"] == "true";
+    for (const auto& participant : participantsModel->second->getParticipants()) {
+        if (participant.uri == uriToCheck) {
+            handRaised = participant.handRaised;
             break;
         }
     }
@@ -1458,17 +1478,19 @@ NewCallModelPimpl::slotOnConferenceInfosUpdated(const QString& confId,
     if (it == calls.end() or not it->second)
         return;
 
-    qDebug() << "New conference layout received for call " << confId;
+    if (participantsModel.find(confId) == participantsModel.end())
+        participantsModel.emplace(confId, std::make_shared<CallParticipants>(infos, confId, linked));
+    else
+        participantsModel[confId]->update(infos);
 
     // if Jami, remove @ring.dht
-    it->second->participantsInfos = infos;
-    for (auto& i : it->second->participantsInfos) {
-        i["uri"].replace("@ring.dht", "");
-        if (i["uri"].isEmpty()) {
+    for (auto& i : participantsModel[confId]->getParticipants()) {
+        i.uri.replace("@ring.dht", "");
+        if (i.uri.isEmpty()) {
             if (it->second->type == call::Type::CONFERENCE) {
-                i["uri"] = linked.owner.profileInfo.uri;
+                i.uri = linked.owner.profileInfo.uri;
             } else {
-                i["uri"] = it->second->peerUri.replace("ring:", "");
+                i.uri = it->second->peerUri.replace("ring:", "");
             }
         }
     }
@@ -1519,11 +1541,13 @@ NewCallModelPimpl::slotConferenceCreated(const QString& accountId, const QString
     callInfo->status = call::Status::IN_PROGRESS;
     callInfo->type = call::Type::CONFERENCE;
     callInfo->startTime = std::chrono::steady_clock::now();
-    callInfo->participantsInfos = CallManager::instance().getConferenceInfos(linked.owner.id,
+
+    VectorMapStringString infos = CallManager::instance().getConferenceInfos(linked.owner.id,
                                                                              confId);
-    for (auto& i : callInfo->participantsInfos)
-        i["uri"].replace("@ring.dht", "");
+    participantsModel[confId] = std::make_shared<CallParticipants>(infos, confId, linked);
+
     calls[confId] = callInfo;
+
     foreach (const auto& call, callList) {
         emit linked.callAddedToConference(call, confId);
         // Remove call from pendingConferences_