diff --git a/bin/dbus/cx.ring.Ring.CallManager.xml b/bin/dbus/cx.ring.Ring.CallManager.xml
index ee796b871afc999d14050b4e9e6045afbe64d122..76c1cf8427bddbfb73705238baa0ff7a893bfe80 100644
--- a/bin/dbus/cx.ring.Ring.CallManager.xml
+++ b/bin/dbus/cx.ring.Ring.CallManager.xml
@@ -514,6 +514,26 @@
             <arg type="b" name="isMixed" direction="in"/>
         </method>
 
+
+        <method name="getConferenceInfos" tp:name-for-bindings="getConferenceInfos">
+            <tp:docstring>
+                Retrieve conferences infos with the following format:
+                Layout = {
+                  {
+                    "uri": "participant", "x":"0", "y":"0", "w": "0", "h": "0"
+                  },
+                  {
+                      "uri": "participant1", "x":"0", "y":"0", "w": "0", "h": "0"
+                  }
+                    (...)
+                }
+            </tp:docstring>
+            <tp:added version="9.5.0"/>
+            <arg type="s" name="confId" direction="in" />
+            <annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="VectorMapStringString"/>
+            <arg type="aa{ss}" direction="out" />
+        </method>
+
         <signal name="incomingCall" tp:name-for-bindings="incomingCall">
             <tp:docstring>
               <p>Notify an incoming call.</p>
@@ -815,5 +835,12 @@
             <arg type="s" name="callID" />
             <arg type="b" name="videoMuted" />
         </signal>
+
+        <signal name="onConferenceInfosUpdated" tp:name-for-bindings="onConferenceInfosUpdated">
+            <tp:added version="9.5.0"/>
+            <arg type="s" name="confId" />
+            <annotation name="org.qtproject.QtDBus.QtTypeName.In1" value="VectorMapStringString"/>
+            <arg type="aa{ss}" name="infos" />
+        </signal>
     </interface>
 </node>
diff --git a/bin/dbus/dbuscallmanager.cpp b/bin/dbus/dbuscallmanager.cpp
index 68b1f12ad8255225b22fe3199c12de684c98215d..ff4da488460c2f7a75c08893fd6f49c10ec6399c 100644
--- a/bin/dbus/dbuscallmanager.cpp
+++ b/bin/dbus/dbuscallmanager.cpp
@@ -99,6 +99,12 @@ DBusCallManager::getCallList() -> decltype(DRing::getCallList())
     return DRing::getCallList();
 }
 
+std::vector<std::map<std::string, std::string>>
+DBusCallManager::getConferenceInfos(const std::string& confId)
+{
+    return DRing::getConferenceInfos(confId);
+}
+
 void
 DBusCallManager::removeConference(const std::string& conference_id)
 {
diff --git a/bin/dbus/dbuscallmanager.h b/bin/dbus/dbuscallmanager.h
index 1a3d0b03adbabacfd740a239960ce8af31465351..bb3f547c13f27b29eef16ed9ddc267de51ee7c61 100644
--- a/bin/dbus/dbuscallmanager.h
+++ b/bin/dbus/dbuscallmanager.h
@@ -68,6 +68,7 @@ class DRING_PUBLIC DBusCallManager :
         bool attendedTransfer(const std::string& transferID, const std::string& targetID);
         std::map<std::string, std::string> getCallDetails(const std::string& callID);
         std::vector<std::string> getCallList();
+        std::vector<std::map<std::string, std::string>> getConferenceInfos(const std::string& confId);
         void removeConference(const std::string& conference_id);
         bool joinParticipant(const std::string& sel_callID, const std::string& drag_callID);
         void createConfFromParticipantList(const std::vector< std::string >& participants);
diff --git a/bin/dbus/dbusclient.cpp b/bin/dbus/dbusclient.cpp
index 3f6cff264c4e2e6d43bf64fbbf025cc550ff09d6..18a9c1b42e16de9aad74b5e4ceb0e233edc92a23 100644
--- a/bin/dbus/dbusclient.cpp
+++ b/bin/dbus/dbusclient.cpp
@@ -173,6 +173,7 @@ DBusClient::initLibrary(int flags)
         exportable_callback<CallSignal::SecureSdesOn>(bind(&DBusCallManager::secureSdesOn, callM, _1)),
         exportable_callback<CallSignal::SecureSdesOff>(bind(&DBusCallManager::secureSdesOff, callM, _1)),
         exportable_callback<CallSignal::RtcpReportReceived>(bind(&DBusCallManager::onRtcpReportReceived, callM, _1, _2)),
+        exportable_callback<CallSignal::OnConferenceInfosUpdated>(bind(&DBusCallManager::onConferenceInfosUpdated, callM, _1, _2)),
         exportable_callback<CallSignal::PeerHold>(bind(&DBusCallManager::peerHold, callM, _1, _2)),
         exportable_callback<CallSignal::AudioMuted>(bind(&DBusCallManager::audioMuted, callM, _1, _2)),
         exportable_callback<CallSignal::VideoMuted>(bind(&DBusCallManager::videoMuted, callM, _1, _2)),
diff --git a/bin/jni/callmanager.i b/bin/jni/callmanager.i
index a7008f54f399e953015298fd82422b73f2a62319..b0d5f1e9c61bb2edd475016e5d004dad1d55effc 100644
--- a/bin/jni/callmanager.i
+++ b/bin/jni/callmanager.i
@@ -45,6 +45,7 @@ public:
     virtual void recordingStateChanged(const std::string& call_id, int code){}
     virtual void recordStateChange(const std::string& call_id, int state){}
     virtual void onRtcpReportReceived(const std::string& call_id, const std::map<std::string, int>& stats){}
+    virtual void onConferenceInfosUpdated(const std::string& confId, const std::vector<std::map<std::string, std::string>>& infos) {}
     virtual void peerHold(const std::string& call_id, bool holding){}
     virtual void connectionUpdate(const std::string& id, int state){}
 };
@@ -89,6 +90,7 @@ std::vector<std::string> getParticipantList(const std::string& confID);
 std::vector<std::string> getDisplayNames(const std::string& confID);
 std::string getConferenceId(const std::string& callID);
 std::map<std::string, std::string> getConferenceDetails(const std::string& callID);
+std::vector<std::map<std::string, std::string>> getConferenceInfos(const std::string& confId);
 
 /* File Playback methods */
 bool startRecordedFilePlayback(const std::string& filepath);
@@ -132,6 +134,7 @@ public:
     virtual void recordingStateChanged(const std::string& call_id, int code){}
     virtual void recordStateChange(const std::string& call_id, int state){}
     virtual void onRtcpReportReceived(const std::string& call_id, const std::map<std::string, int>& stats){}
+    virtual void onConferenceInfosUpdated(const std::string& confId, const std::vector<std::map<std::string, std::string>>& infos) {}
     virtual void peerHold(const std::string& call_id, bool holding){}
     virtual void connectionUpdate(const std::string& id, int state){}
 };
diff --git a/bin/jni/jni_interface.i b/bin/jni/jni_interface.i
index c6df5572028abd3f33dfe08025c479e1b6dd954a..67c66631fa4fa6d9af47080cba8f7f4d53824724 100644
--- a/bin/jni/jni_interface.i
+++ b/bin/jni/jni_interface.i
@@ -247,6 +247,7 @@ void init(ConfigurationCallback* confM, Callback* callM, PresenceCallback* presM
         exportable_callback<CallSignal::ConferenceRemoved>(bind(&Callback::conferenceRemoved, callM, _1)),
         exportable_callback<CallSignal::RecordingStateChanged>(bind(&Callback::recordingStateChanged, callM, _1, _2)),
         exportable_callback<CallSignal::RtcpReportReceived>(bind(&Callback::onRtcpReportReceived, callM, _1, _2)),
+        exportable_callback<CallSignal::OnConferenceInfosUpdated>(bind(&Callback::onConferenceInfosUpdated, callM, _1, _2)),
         exportable_callback<CallSignal::PeerHold>(bind(&Callback::peerHold, callM, _1, _2)),
         exportable_callback<CallSignal::ConnectionUpdate>(bind(&Callback::connectionUpdate, callM, _1, _2))
     };
diff --git a/bin/nodejs/callmanager.i b/bin/nodejs/callmanager.i
index 6f584daf4cff158bd4240d8542d78ad0d1203de8..b213005a0b069898987e20100c5786c02a012ec7 100644
--- a/bin/nodejs/callmanager.i
+++ b/bin/nodejs/callmanager.i
@@ -45,6 +45,7 @@ public:
     virtual void recordingStateChanged(const std::string& call_id, int code){}
     virtual void recordStateChange(const std::string& call_id, int state){}
     virtual void onRtcpReportReceived(const std::string& call_id, const std::map<std::string, int>& stats){}
+    virtual void onConferenceInfosUpdated(const std::string& confId, const std::vector<std::map<std::string, std::string>>& infos) {}
     virtual void peerHold(const std::string& call_id, bool holding){}
 };
 
@@ -88,6 +89,7 @@ std::vector<std::string> getParticipantList(const std::string& confID);
 std::vector<std::string> getDisplayNames(const std::string& confID);
 std::string getConferenceId(const std::string& callID);
 std::map<std::string, std::string> getConferenceDetails(const std::string& callID);
+std::vector<std::map<std::string, std::string>> getConferenceInfos(const std::string& confId);
 
 /* File Playback methods */
 bool startRecordedFilePlayback(const std::string& filepath);
@@ -131,5 +133,6 @@ public:
     virtual void recordingStateChanged(const std::string& call_id, int code){}
     virtual void recordStateChange(const std::string& call_id, int state){}
     virtual void onRtcpReportReceived(const std::string& call_id, const std::map<std::string, int>& stats){}
+    virtual void onConferenceInfosUpdated(const std::string& confId, const std::vector<std::map<std::string, std::string>>& infos) {}
     virtual void peerHold(const std::string& call_id, bool holding){}
 };
diff --git a/configure.ac b/configure.ac
index 79ad5e1d3d98e5903157f14f6620123831c25cf6..455f6256a2a27faa3f2e8eed05e9129c0f6e6ad9 100644
--- a/configure.ac
+++ b/configure.ac
@@ -2,7 +2,7 @@ dnl Jami - configure.ac for automake 1.9 and autoconf 2.59
 
 dnl Process this file with autoconf to produce a configure script.
 AC_PREREQ([2.65])
-AC_INIT([Jami Daemon],[9.4.0],[ring@gnu.org],[jami])
+AC_INIT([Jami Daemon],[9.5.0],[ring@gnu.org],[jami])
 
 AC_COPYRIGHT([[Copyright (c) Savoir-faire Linux 2004-2019]])
 AC_REVISION([$Revision$])
diff --git a/meson.build b/meson.build
index b114f8088297cae2e36b7b10fbf7cfa1099fd223..9f49b57ba3e78382bc5c56466766372f870db601 100644
--- a/meson.build
+++ b/meson.build
@@ -1,5 +1,5 @@
 project('jami-daemon', ['c', 'cpp'],
-        version: '9.4.0',
+        version: '9.5.0',
         license: 'GPL3+',
         default_options: ['cpp_std=gnu++17', 'buildtype=debugoptimized'],
         meson_version:'>= 0.54'
diff --git a/src/account.h b/src/account.h
index 4049e33a194bb0b02a233498f266e0573350da32..bffe3c409fadac3df4ef8a37c5a64f7ce1129194 100644
--- a/src/account.h
+++ b/src/account.h
@@ -100,6 +100,8 @@ class Account : public Serializable, public std::enable_shared_from_this<Account
 
         virtual std::map<std::string, std::string> getVolatileAccountDetails() const;
 
+        virtual std::string getFromUri() const = 0;
+
         /**
          * Load the settings for this account.
          */
diff --git a/src/call.cpp b/src/call.cpp
index 875592cf30a07f2e842c334299174ef1bbe3ee4a..ec479febcfb1afc03d88af424381e54bcd953547 100644
--- a/src/call.cpp
+++ b/src/call.cpp
@@ -394,6 +394,12 @@ Call::getNullDetails()
 void
 Call::onTextMessage(std::map<std::string, std::string>&& messages)
 {
+    auto it = messages.find("application/confInfo+json");
+    if (it != messages.end()) {
+        setConferenceInfo(it->second);
+        return;
+    }
+
     {
         std::lock_guard<std::recursive_mutex> lk {callMutex_};
         if (parent_) {
@@ -606,4 +612,32 @@ Call::safePopSubcalls()
     return old_value;
 }
 
+void
+Call::setConferenceInfo(const std::string& msg)
+{
+    ConfInfo newInfo;
+    Json::Value json;
+    std::string err;
+    Json::CharReaderBuilder rbuilder;
+    auto reader = std::unique_ptr<Json::CharReader>(rbuilder.newCharReader());
+    if (reader->parse(msg.data(), msg.data() + msg.size(), &json, &err)) {
+        for (const auto& participantInfo: json) {
+            ParticipantInfo pInfo;
+            if (!participantInfo.isMember("uri")) continue;
+            pInfo.fromJson(participantInfo);
+            newInfo.emplace_back(pInfo);
+        }
+    }
+
+    std::vector<std::map<std::string, std::string>> toSend;
+    {
+        std::lock_guard<std::mutex> lk(confInfoMutex_);
+        confInfo_ = std::move(newInfo);
+        toSend = confInfo_.toVectorMapStringString();
+    }
+
+    // Inform client that layout has changed
+    jami::emitSignal<DRing::CallSignal::OnConferenceInfosUpdated>(id_, std::move(toSend));
+}
+
 } // namespace jami
diff --git a/src/call.h b/src/call.h
index eba94d615534f201ed07539acf17b842291e62b3..968b4537ae6793aceddefa2466834570850a6fca 100644
--- a/src/call.h
+++ b/src/call.h
@@ -31,6 +31,7 @@
 
 #include "recordable.h"
 #include "ip_utils.h"
+#include "conference.h"
 
 #include <atomic>
 #include <mutex>
@@ -330,6 +331,20 @@ class Call : public Recordable, public std::enable_shared_from_this<Call> {
 
         bool hasVideo() const { return not isAudioOnly_; }
 
+        /**
+         * A Call can be in a conference. If this is the case, the other side
+         * will send conference informations describing the rendered image
+         * @msg     A JSON object describing the conference
+         */
+        void setConferenceInfo(const std::string& msg);
+
+        std::vector<std::map<std::string, std::string>>
+        getConferenceInfos() const
+        {
+            return confInfo_.toVectorMapStringString();
+        }
+
+
     protected:
         virtual void merge(Call& scall);
 
@@ -409,6 +424,9 @@ class Call : public Recordable, public std::enable_shared_from_this<Call> {
         // If the call is blocked during the progressing state
         OnNeedFallbackCb onNeedFallback_;
         std::atomic_bool startFallback_ {true};
+
+        mutable std::mutex confInfoMutex_ {};
+        mutable ConfInfo confInfo_ {};
 };
 
 // Helpers
diff --git a/src/client/callmanager.cpp b/src/client/callmanager.cpp
index cd6e591e2656748264d1b041f060f4f0e2267df4..28491d3a8011aa09f65fa0cad74d9ee9bbc579c8 100644
--- a/src/client/callmanager.cpp
+++ b/src/client/callmanager.cpp
@@ -298,6 +298,12 @@ getCallList()
     return jami::Manager::instance().getCallList();
 }
 
+std::vector<std::map<std::string, std::string>>
+getConferenceInfos(const std::string& confId)
+{
+    return jami::Manager::instance().getConferenceInfos(confId);
+}
+
 void
 playDTMF(const std::string& key)
 {
diff --git a/src/client/ring_signal.cpp b/src/client/ring_signal.cpp
index 386d0a6f7a15dc49e896874c84fc38e13fbc6068..30c5cda0f5bc3e3c8337e3b5ef038f5487432173 100644
--- a/src/client/ring_signal.cpp
+++ b/src/client/ring_signal.cpp
@@ -48,6 +48,7 @@ getSignalHandlers()
         exported_callback<DRing::CallSignal::AudioMuted>(),
         exported_callback<DRing::CallSignal::SmartInfo>(),
         exported_callback<DRing::CallSignal::ConnectionUpdate>(),
+        exported_callback<DRing::CallSignal::OnConferenceInfosUpdated>(),
 
         /* Configuration */
         exported_callback<DRing::ConfigurationSignal::VolumeChanged>(),
diff --git a/src/conference.cpp b/src/conference.cpp
index c8a22d874d9d7e23934317219008235a0f01239e..e536a7d410d8ed9e332bd59ca5d93209e3baa268 100644
--- a/src/conference.cpp
+++ b/src/conference.cpp
@@ -44,7 +44,47 @@ Conference::Conference()
 #ifdef ENABLE_VIDEO
     , mediaInput_(Manager::instance().getVideoManager().videoDeviceMonitor.getMRLForDefaultDevice())
 #endif
-{}
+{
+#ifdef ENABLE_VIDEO
+    getVideoMixer()->setOnSourcesUpdated([this](const std::vector<video::SourceInfo>&& infos) {
+        runOnMainThread([w=weak(), infos=std::move(infos)]{
+            auto shared = w.lock();
+            if (!shared)
+                return;
+            ConfInfo newInfo;
+            std::unique_lock<std::mutex> lk(shared->videoToCallMtx_);
+            for (const auto& info: infos) {
+                std::string uri = "local";
+                auto it = shared->videoToCall_.find(info.source);
+                if (it == shared->videoToCall_.end())
+                    it = shared->videoToCall_.emplace_hint(it, info.source, std::string());
+                // If not local
+                if (!it->second.empty()) {
+                    // Retrieve calls participants
+                    // TODO: this is a first version, we assume that the peer is not
+                    // a master of a conference and there is only one remote
+                    // In the future, we should retrieve confInfo from the call
+                    // To merge layouts informations
+                    if (auto call = Manager::instance().callFactory.getCall<SIPCall>(it->second)) {
+                        uri = call->getPeerNumber();
+                    }
+                }
+                newInfo.emplace_back(ParticipantInfo {
+                    std::move(uri), info.x, info.y, info.w, info.h
+                });
+            }
+            lk.unlock();
+
+            {
+                std::lock_guard<std::mutex> lk2(shared->confInfoMutex_);
+                shared->confInfo_ = std::move(newInfo);
+            }
+
+            shared->sendConferenceInfos();
+        });
+    });
+#endif
+}
 
 Conference::~Conference()
 {
@@ -97,6 +137,66 @@ Conference::setActiveParticipant(const std::string &participant_id)
     videoMixer_->setActiveParticipant(nullptr);
 }
 
+std::vector<std::map<std::string, std::string>>
+ConfInfo::toVectorMapStringString() const
+{
+    std::vector<std::map<std::string, std::string>> infos;
+    infos.reserve(size());
+    auto it = cbegin();
+    while (it != cend()) {
+        infos.emplace_back(it->toMap());
+        ++it;
+    }
+    return infos;
+}
+
+void
+Conference::sendConferenceInfos()
+{
+    Json::Value jsonArray;
+    std::vector<std::map<std::string, std::string>> toSend;
+    {
+        std::lock_guard<std::mutex> lk2(confInfoMutex_);
+        for (const auto& info: confInfo_) {
+            jsonArray.append(info.toJson());
+        }
+        toSend = confInfo_.toVectorMapStringString();
+    }
+
+    Json::StreamWriterBuilder builder;
+    const auto confInfo = Json::writeString(builder, jsonArray);
+    // Inform calls that the layout has changed
+    for (const auto &participant_id : participants_) {
+        if (auto call = Manager::instance().callFactory.getCall<SIPCall>(participant_id)) {
+            call->sendTextMessage(
+                    std::map<std::string, std::string> {{"application/confInfo+json", confInfo}},
+                    call->getAccount().getFromUri());
+        }
+    }
+
+    // Inform client that layout has changed
+    jami::emitSignal<DRing::CallSignal::OnConferenceInfosUpdated>(id_, std::move(toSend));
+}
+
+void
+Conference::attachVideo(Observable<std::shared_ptr<MediaFrame>>* frame, const std::string& callId)
+{
+    std::lock_guard<std::mutex> lk(videoToCallMtx_);
+    videoToCall_.emplace(frame, callId);
+    frame->attach(getVideoMixer().get());
+}
+
+void
+Conference::detachVideo(Observable<std::shared_ptr<MediaFrame>>* frame)
+{
+    std::lock_guard<std::mutex> lk(videoToCallMtx_);
+    auto it = videoToCall_.find(frame);
+    if (it != videoToCall_.end()) {
+        it->first->detach(getVideoMixer().get());
+        videoToCall_.erase(it);
+    }
+}
+
 void
 Conference::remove(const std::string &participant_id)
 {
diff --git a/src/conference.h b/src/conference.h
index d165d47a267d54645ffb234c1f09fdf28f272936..004d885a4358008d610f32302183227d09b821cb 100644
--- a/src/conference.h
+++ b/src/conference.h
@@ -29,6 +29,8 @@
 #include <memory>
 #include <vector>
 
+#include <json/json.h>
+
 #include "recordable.h"
 
 namespace jami {
@@ -39,9 +41,54 @@ class VideoMixer;
 }
 #endif
 
+struct ParticipantInfo
+{
+    std::string uri;
+    int x {0};
+    int y {0};
+    int w {0};
+    int h {0};
+
+    void fromJson(const Json::Value& v) {
+        uri = v["uri"].asString();
+        x = v["x"].asInt();
+        y = v["y"].asInt();
+        w = v["w"].asInt();
+        h = v["h"].asInt();
+    }
+
+    Json::Value toJson() const {
+        Json::Value val;
+        val["uri"] = uri;
+        val["x"] = x;
+        val["y"] = y;
+        val["w"] = w;
+        val["h"] = h;
+        return val;
+    }
+
+    std::map<std::string, std::string> toMap() const {
+        return {
+            {"uri", uri},
+            {"x", std::to_string(x)},
+            {"y", std::to_string(y)},
+            {"w", std::to_string(w)},
+            {"h", std::to_string(h)}
+        };
+    }
+};
+
+struct ConfInfo : public std::vector<ParticipantInfo>
+{
+    std::vector<std::map<std::string, std::string>> toVectorMapStringString() const;
+};
+
 using ParticipantSet = std::set<std::string>;
 
-class Conference : public Recordable {
+class Conference
+    : public Recordable
+    , public std::enable_shared_from_this<Conference>
+    {
 public:
     enum class State {
         ACTIVE_ATTACHED,
@@ -136,16 +183,39 @@ public:
 
     void setActiveParticipant(const std::string &participant_id);
 
+
+    void attachVideo(Observable<std::shared_ptr<MediaFrame>>* frame, const std::string& callId);
+    void detachVideo(Observable<std::shared_ptr<MediaFrame>>* frame);
+
 #ifdef ENABLE_VIDEO
     std::shared_ptr<video::VideoMixer> getVideoMixer();
     std::string getVideoInput() const { return mediaInput_; }
 #endif
 
+    std::vector<std::map<std::string, std::string>>
+    getConferenceInfos() const
+    {
+        std::lock_guard<std::mutex> lk(confInfoMutex_);
+        return confInfo_.toVectorMapStringString();
+    }
+
 private:
+
+    std::weak_ptr<Conference> weak() {
+        return std::static_pointer_cast<Conference>(shared_from_this());
+    }
+
     std::string id_;
     State confState_ {State::ACTIVE_ATTACHED};
     ParticipantSet participants_;
 
+    mutable std::mutex confInfoMutex_ {};
+    mutable ConfInfo confInfo_ {};
+    void sendConferenceInfos();
+    // We need to convert call to frame
+    std::mutex videoToCallMtx_;
+    std::map<Observable<std::shared_ptr<MediaFrame>>*, std::string> videoToCall_ {};
+
 #ifdef ENABLE_VIDEO
     std::string mediaInput_ {};
     std::shared_ptr<video::VideoMixer> videoMixer_;
diff --git a/src/dring/callmanager_interface.h b/src/dring/callmanager_interface.h
index 2c68d049a2269d412b04ba2a05881b3dc6276472..34ac9001fc46ca26b86f02b84b2a3a9a2111cd7c 100644
--- a/src/dring/callmanager_interface.h
+++ b/src/dring/callmanager_interface.h
@@ -73,6 +73,7 @@ DRING_PUBLIC std::vector<std::string> getParticipantList(const std::string& conf
 DRING_PUBLIC std::vector<std::string> getDisplayNames(const std::string& confID);
 DRING_PUBLIC std::string getConferenceId(const std::string& callID);
 DRING_PUBLIC std::map<std::string, std::string> getConferenceDetails(const std::string& callID);
+DRING_PUBLIC std::vector<std::map<std::string, std::string>> getConferenceInfos(const std::string& confId);
 
 /* Statistic related methods */
 DRING_PUBLIC void startSmartInfo(uint32_t refreshTimeMs);
@@ -191,6 +192,10 @@ struct DRING_PUBLIC CallSignal {
                 constexpr static const char* name = "ConnectionUpdate";
                 using cb_type = void(const std::string&, int);
         };
+        struct DRING_PUBLIC OnConferenceInfosUpdated {
+                constexpr static const char* name = "OnConferenceInfosUpdated";
+                using cb_type = void(const std::string&, const std::vector<std::map<std::string, std::string>>&);
+        };
 };
 
 } // namespace DRing
diff --git a/src/jamidht/jamiaccount.h b/src/jamidht/jamiaccount.h
index 6ab4cd1270f77e9f59802724868880b2bda3b22f..dddbf5e0d20893527e691a663c0691c90f4dc5ce 100644
--- a/src/jamidht/jamiaccount.h
+++ b/src/jamidht/jamiaccount.h
@@ -190,7 +190,7 @@ public:
      * of the host on which the UA is running, since these are not logical
      * names."
      */
-    std::string getFromUri() const;
+    std::string getFromUri() const override;
 
     /**
      * This method adds the correct scheme, hostname and append
diff --git a/src/manager.cpp b/src/manager.cpp
index 66e875b189e49c1f050d37c9f25d8fbc0cb4ad30..fbb3e147a2065ee59ceffcb953454fc348cbcdc2 100644
--- a/src/manager.cpp
+++ b/src/manager.cpp
@@ -1943,8 +1943,9 @@ Manager::incomingMessage(const std::string& callID,
 
         // in case of a conference we must notify client using conference id
         emitSignal<DRing::CallSignal::IncomingMessage>(conf->getConfID(), from, messages);
-    } else
+    } else {
         emitSignal<DRing::CallSignal::IncomingMessage>(callID, from, messages);
+    }
 }
 
 void
@@ -2912,6 +2913,16 @@ Manager::getCallList() const
     return results;
 }
 
+std::vector<std::map<std::string, std::string>>
+Manager::getConferenceInfos(const std::string& confId) const
+{
+    if (auto conf = getConferenceFromID(confId))
+        return conf->getConferenceInfos();
+    else if (auto call = getCallFromCallID(confId))
+        return call->getConferenceInfos();
+    return {};
+}
+
 std::map<std::string, std::string>
 Manager::getConferenceDetails(const std::string& confID) const
 {
diff --git a/src/manager.h b/src/manager.h
index 44f5c527483f0723a50b271f966ea31db4df2b64..e73caf1f3ee2f649ebe811d89e4bf14245a22076 100644
--- a/src/manager.h
+++ b/src/manager.h
@@ -458,6 +458,13 @@ class DRING_TESTABLE Manager {
          */
         std::vector<std::string> getCallList() const;
 
+        /**
+         * Get conferences informations (participant list + rendered positions in the frame)
+         * @param confId
+         * @return {{"uri":"xxx", "x":"0", "y":"0", "w":"0", "h":"0"}...}
+         */
+        std::vector<std::map<std::string, std::string>> getConferenceInfos(const std::string& confId) const;
+
         /**
          * Retrieve details about a given call
          * @param callID      The account identifier
diff --git a/src/media/video/video_mixer.cpp b/src/media/video/video_mixer.cpp
index 4cc5b5b708c60d4596103cb6970c5823b926cefa..9e7b10ca6dc707101e5665274f6d73b931e5819e 100644
--- a/src/media/video/video_mixer.cpp
+++ b/src/media/video/video_mixer.cpp
@@ -36,6 +36,8 @@
 #include <cmath>
 #include <unistd.h>
 
+#include <opendht/thread_pool.h>
+
 extern "C" {
 #include <libavutil/display.h>
 }
@@ -52,6 +54,12 @@ struct VideoMixer::VideoMixerSource {
         std::lock_guard<std::mutex> lock(mutex_);
         render_frame.swap(other);
     }
+
+    // Current render informations
+    int x {};
+    int y {};
+    int w {};
+    int h {};
 private:
     std::mutex mutex_;
 };
@@ -123,6 +131,7 @@ void
 VideoMixer::setActiveParticipant(Observable<std::shared_ptr<MediaFrame>>* ob)
 {
     activeSource_ = ob;
+    layoutUpdated_ += 1;
 }
 
 void
@@ -133,6 +142,7 @@ VideoMixer::attached(Observable<std::shared_ptr<MediaFrame>>* ob)
     auto src = std::unique_ptr<VideoMixerSource>(new VideoMixerSource);
     src->source = ob;
     sources_.emplace_back(std::move(src));
+    layoutUpdated_ += 1;
 }
 
 void
@@ -148,6 +158,7 @@ VideoMixer::detached(Observable<std::shared_ptr<MediaFrame>>* ob)
                 activeSource_ = nullptr;
             }
             sources_.remove(x);
+            layoutUpdated_ += 1;
             break;
         }
     }
@@ -197,7 +208,9 @@ VideoMixer::process()
 
         int i = 0;
         bool activeFound = false;
-        for (const auto& x : sources_) {
+        bool needsUpdate = layoutUpdated_ > 0;
+        bool successfullyRendered = true;
+        for (auto& x : sources_) {
             /* thread stop pending? */
             if (!loop_.isRunning())
                 return;
@@ -227,24 +240,49 @@ VideoMixer::process()
                 }
 
                 if (input)
-                    render_frame(output, *input, x, wantedIndex);
+                    successfullyRendered &= render_frame(output, *input, x, wantedIndex, needsUpdate);
+                else
+                    successfullyRendered = false;
 
                 x->atomic_swap_render(input);
+            } else if (needsUpdate) {
+                x->x = 0;
+                x->y = 0;
+                x->w = 0;
+                x->h = 0;
             }
 
             ++i;
         }
+        if (needsUpdate and successfullyRendered) {
+            layoutUpdated_ -= 1;
+            if (layoutUpdated_ == 0) {
+                std::vector<SourceInfo> sourcesInfo;
+                sourcesInfo.reserve(sources_.size());
+                for (auto& x : sources_) {
+                    sourcesInfo.emplace_back(SourceInfo {
+                        x->source,
+                        x->x,
+                        x->y,
+                        x->w,
+                        x->h
+                    });
+                }
+                if (onSourcesUpdated_)
+                    (onSourcesUpdated_)(std::move(sourcesInfo));
+            }
+        }
     }
 
     publishFrame();
 }
 
-void
+bool
 VideoMixer::render_frame(VideoFrame& output, const VideoFrame& input,
-    const std::unique_ptr<VideoMixerSource>& source, int index)
+    std::unique_ptr<VideoMixerSource>& source, int index, bool needsUpdate)
 {
     if (!width_ or !height_ or !input.pointer() or input.pointer()->format == -1)
-        return;
+        return false;
 
 #ifdef RING_ACCEL
     std::shared_ptr<VideoFrame> frame { HardwareAccel::transferToMainMemory(input, AV_PIX_FMT_NV12) };
@@ -252,28 +290,44 @@ VideoMixer::render_frame(VideoFrame& output, const VideoFrame& input,
     std::shared_ptr<VideoFrame> frame = input;
 #endif
 
-    const int n = currentLayout_ == Layout::ONE_BIG? 1 : sources_.size();
-    const int zoom = currentLayout_ == Layout::ONE_BIG_WITH_SMALL? std::max(6,n) : ceil(sqrt(n));
-    int cell_width = width_ / zoom;
-    int cell_height = height_ / zoom;
-    if (currentLayout_ == Layout::ONE_BIG_WITH_SMALL && index == 0) {
-        // In ONE_BIG_WITH_SMALL, the first line at the top is the previews
-        // The rest is the active source
-        cell_width  = width_;
-        cell_height = height_ - cell_height;
-    }
-    int xoff = (index % zoom) * cell_width;
-    int yoff = (index / zoom) * cell_height;
-    if (currentLayout_ == Layout::ONE_BIG_WITH_SMALL) {
-        if (index == 0) {
-            xoff = 0;
-            yoff = height_ / zoom; // First line height
+    int cell_width, cell_height, xoff, yoff;
+    if (not needsUpdate) {
+        cell_width = source->w;
+        cell_height = source->h;
+        xoff = source->x;
+        yoff = source->y;
+    } else {
+        const int n = currentLayout_ == Layout::ONE_BIG? 1 : sources_.size();
+        const int zoom = currentLayout_ == Layout::ONE_BIG_WITH_SMALL? std::max(6,n) : ceil(sqrt(n));
+        if (currentLayout_ == Layout::ONE_BIG_WITH_SMALL && index == 0) {
+            // In ONE_BIG_WITH_SMALL, the first line at the top is the previews
+            // The rest is the active source
+            cell_width  = width_;
+            cell_height = height_ - height_ / zoom;
+        } else {
+            cell_width = width_ / zoom;
+            cell_height = height_ / zoom;
+        }
+        if (currentLayout_ == Layout::ONE_BIG_WITH_SMALL) {
+            if (index == 0) {
+                xoff = 0;
+                yoff = height_ / zoom; // First line height
+            } else {
+                xoff = (index-1) * cell_width;
+                // Show sources in center
+                xoff += (width_ - (n - 1) * cell_width) / 2;
+                yoff = 0;
+            }
         } else {
-            xoff = (index-1) * cell_width;
-            // Show sources in center
-            xoff += (width_ - (n - 1) * cell_width) / 2;
-            yoff = 0;
+            xoff = (index % zoom) * cell_width;
+            yoff = (index / zoom) * cell_height;
         }
+
+        // Update source's cache
+        source->w = cell_width;
+        source->h = cell_height;
+        source->x = xoff;
+        source->y = yoff;
     }
 
     AVFrameSideData* sideData = av_frame_get_side_data(frame->pointer(), AV_FRAME_DATA_DISPLAYMATRIX);
@@ -295,6 +349,7 @@ VideoMixer::render_frame(VideoFrame& output, const VideoFrame& input,
     }
 
     scaler_.scale_and_pad(*frame, output, xoff, yoff, cell_width, cell_height, true);
+    return true;
 }
 
 void
@@ -312,6 +367,7 @@ VideoMixer::setParameters(int width, int height, AVPixelFormat format)
         libav_utils::fillWithBlack(previous_p->pointer());
 
     start_sink();
+    layoutUpdated_ += 1;
 }
 
 void
diff --git a/src/media/video/video_mixer.h b/src/media/video/video_mixer.h
index e75d6f82f52a74b0a849083a957b1b82344f6c70..3dd0d90ab5326cfa91b7b81b9f573262f8d3ed1f 100644
--- a/src/media/video/video_mixer.h
+++ b/src/media/video/video_mixer.h
@@ -35,6 +35,15 @@ namespace jami { namespace video {
 
 class SinkClient;
 
+struct SourceInfo {
+    Observable<std::shared_ptr<MediaFrame>>* source;
+    int x;
+    int y;
+    int w;
+    int h;
+};
+using OnSourcesUpdatedCb = std::function<void(const std::vector<SourceInfo>&&)>;
+
 
 enum class Layout {
     GRID,
@@ -68,15 +77,19 @@ public:
 
     void setVideoLayout(Layout newLayout) {
         currentLayout_ = newLayout;
+        layoutUpdated_ += 1;
+    }
+
+    void setOnSourcesUpdated(OnSourcesUpdatedCb&& cb) {
+        onSourcesUpdated_ = std::move(cb);
     }
 
 private:
     NON_COPYABLE(VideoMixer);
-
     struct VideoMixerSource;
 
-    void render_frame(VideoFrame& output, const VideoFrame& input,
-        const std::unique_ptr<VideoMixerSource>& source, int index);
+    bool render_frame(VideoFrame& output, const VideoFrame& input,
+        std::unique_ptr<VideoMixerSource>& source, int index, bool needsUpdate);
 
     void start_sink();
     void stop_sink();
@@ -100,6 +113,9 @@ private:
     Layout currentLayout_ {Layout::GRID};
     Observable<std::shared_ptr<MediaFrame>>* activeSource_ {nullptr};
     std::list<std::unique_ptr<VideoMixerSource>> sources_;
+
+    std::atomic_int layoutUpdated_ {0};
+    OnSourcesUpdatedCb onSourcesUpdated_ {};
 };
 
 }} // namespace jami::video
diff --git a/src/media/video/video_rtp_session.cpp b/src/media/video/video_rtp_session.cpp
index e770d00ca3288f5c9a89f95b1a979dbf451526b1..c67371fbaf4d1cbfebb2eb572eb68d2a8b6fdd77 100644
--- a/src/media/video/video_rtp_session.cpp
+++ b/src/media/video/video_rtp_session.cpp
@@ -306,18 +306,18 @@ VideoRtpSession::setupConferenceVideoPipeline(Conference& conference)
     JAMI_DBG("[call:%s] Setup video pipeline on conference %s", callID_.c_str(),
              conference.getConfID().c_str());
     videoMixer_ = conference.getVideoMixer();
-
     if (sender_) {
         // Swap sender from local video to conference video mixer
         if (videoLocal_)
             videoLocal_->detach(sender_.get());
-        videoMixer_->attach(sender_.get());
+        if (videoMixer_)
+            videoMixer_->attach(sender_.get());
     } else
         JAMI_WARN("[call:%s] no sender", callID_.c_str());
 
     if (receiveThread_) {
         receiveThread_->enterConference();
-        receiveThread_->attach(videoMixer_.get());
+        conference.attachVideo(receiveThread_.get(), callID_);
     } else
         JAMI_WARN("[call:%s] no receiver", callID_.c_str());
 }
@@ -366,7 +366,7 @@ void VideoRtpSession::exitConference()
             videoMixer_->detach(sender_.get());
 
         if (receiveThread_) {
-            receiveThread_->detach(videoMixer_.get());
+            conference_->detachVideo(receiveThread_.get());
             receiveThread_->exitConference();
         }
 
diff --git a/src/sip/sipaccount.h b/src/sip/sipaccount.h
index 0641927a9b4748932ad147210b802876c310de92..aa0e11bc469100550cf9c5d350a8e3bb0cf15cc8 100644
--- a/src/sip/sipaccount.h
+++ b/src/sip/sipaccount.h
@@ -360,7 +360,7 @@ class SIPAccount : public SIPAccountBase {
          * of the host on which the UA is running, since these are not logical
          * names."
          */
-        std::string getFromUri() const;
+        std::string getFromUri() const override;
 
         /**
          * This method adds the correct scheme, hostname and append