diff --git a/src/libclient/avmodel.cpp b/src/libclient/avmodel.cpp
index 071132a8b85ad276273faaab7612e26c05ee9208..7939563df4f95c0132ee2112f2efecacfcb709dc 100644
--- a/src/libclient/avmodel.cpp
+++ b/src/libclient/avmodel.cpp
@@ -20,7 +20,6 @@
 #include "api/avmodel.h"
 
 #include "api/video.h"
-#include "api/call.h"
 #include "api/lrc.h"
 #ifdef ENABLE_LIBWRAP
 #include "directrenderer.h"
@@ -734,7 +733,7 @@ AVModel::getListWindows() const
         auto finishedloop = true;
     } catch (...) {
     }
- #endif
+#endif
     return ret;
 }
 
@@ -943,44 +942,48 @@ createRenderer(const QString& id, const QSize& res, const QString& shmPath = {})
 void
 AVModelPimpl::addRenderer(const QString& id, const QSize& res, const QString& shmPath)
 {
-    auto connectRenderer = [this](Renderer* renderer, const QString& id) {
-        connect(
-            renderer,
-            &Renderer::fpsChanged,
-            this,
-            [this, id](void) { Q_EMIT linked_.updateRenderersFPSInfo(id); },
-            Qt::QueuedConnection);
-        connect(
-            renderer,
-            &Renderer::started,
-            this,
-            [this, id](const QSize& size) { Q_EMIT linked_.rendererStarted(id, size); },
-            Qt::DirectConnection);
-        connect(
-            renderer,
-            &Renderer::frameBufferRequested,
-            this,
-            [this, id](AVFrame* frame) { Q_EMIT linked_.frameBufferRequested(id, frame); },
-            Qt::DirectConnection);
-        connect(
-            renderer,
-            &Renderer::frameUpdated,
-            this,
-            [this, id] { Q_EMIT linked_.frameUpdated(id); },
-            Qt::DirectConnection);
-        connect(
-            renderer,
-            &Renderer::stopped,
-            this,
-            [this, id] { Q_EMIT linked_.rendererStopped(id); },
-            Qt::DirectConnection);
-    };
-    std::lock_guard<std::mutex> lk(renderers_mtx_);
-    renderers_.erase(id); // Because it should be done before creating the renderer
+    // First remove the existing renderer.
+    renderers_.erase(id);
+
+    // Create a new one and add it.
     auto renderer = createRenderer(id, res, shmPath);
+    std::lock_guard<std::mutex> lk(renderers_mtx_);
     auto& r = renderers_[id];
     r = std::move(renderer);
-    connectRenderer(r.get(), id);
+    renderers_mtx_.unlock();
+
+    // Listen and forward id-bound signals upwards.
+    connect(
+        r.get(),
+        &Renderer::fpsChanged,
+        this,
+        [this, id](void) { linked_.updateRenderersFPSInfo(id); },
+        Qt::QueuedConnection);
+    connect(
+        r.get(),
+        &Renderer::started,
+        this,
+        [this, id](const QSize& size) { Q_EMIT linked_.rendererStarted(id, size); },
+        Qt::DirectConnection);
+    connect(
+        r.get(),
+        &Renderer::frameBufferRequested,
+        this,
+        [this, id](AVFrame* frame) { Q_EMIT linked_.frameBufferRequested(id, frame); },
+        Qt::DirectConnection);
+    connect(
+        r.get(),
+        &Renderer::frameUpdated,
+        this,
+        [this, id] { Q_EMIT linked_.frameUpdated(id); },
+        Qt::DirectConnection);
+    connect(
+        r.get(),
+        &Renderer::stopped,
+        this,
+        [this, id] { Q_EMIT linked_.rendererStopped(id); },
+        Qt::DirectConnection);
+
     r->startRendering();
 }
 
diff --git a/src/libclient/directrenderer.cpp b/src/libclient/directrenderer.cpp
index e5e5e42498ae4ad39d189e7c3d06bc187619fede..008a29127b1d8f635133eeaa1960713ca4ff4ee6 100644
--- a/src/libclient/directrenderer.cpp
+++ b/src/libclient/directrenderer.cpp
@@ -33,19 +33,10 @@ using namespace lrc::api::video;
 struct DirectRenderer::Impl : public QObject
 {
     Q_OBJECT
-private:
-    int fpsC;
-    int fps;
-
 public:
-    std::chrono::time_point<std::chrono::system_clock> lastFrameDebug;
-
     Impl(DirectRenderer* parent)
         : QObject(nullptr)
         , parent_(parent)
-        , fpsC(0)
-        , fps(0)
-        , lastFrameDebug(std::chrono::system_clock::now())
     {
         configureTarget();
         if (!VideoManager::instance().registerSinkTarget(parent_->id(), target))
@@ -90,17 +81,8 @@ public:
             QMutexLocker lk(&mutex);
             frameBufferPtr = std::move(buf);
         }
-        // compute FPS
-        ++fpsC;
-        auto currentTime = std::chrono::system_clock::now();
-        const std::chrono::duration<double> seconds = currentTime - lastFrameDebug;
-        if (seconds.count() >= FPS_RATE_SEC) {
-            fps = static_cast<int>(fpsC / seconds.count());
-            fpsC = 0;
-            lastFrameDebug = currentTime;
-            parent_->setFPS(fps);
-        }
 
+        parent_->updateFpsTracker();
         Q_EMIT parent_->frameUpdated();
     };
 
@@ -109,6 +91,7 @@ private:
 
 public:
     libjami::SinkTarget target;
+    FpsTracker fpsTracker;
     QMutex mutex;
     libjami::FrameBuffer frameBufferPtr;
 };
diff --git a/src/libclient/renderer.cpp b/src/libclient/renderer.cpp
index f1f542808e6c26c24b3fb893984edc2bb964c9d4..063772b930e4fe219951777db66af02d613493e2 100644
--- a/src/libclient/renderer.cpp
+++ b/src/libclient/renderer.cpp
@@ -21,20 +21,34 @@
 #include <QSize>
 #include <QMutex>
 
+// Uncomment following line to output in console the FPS value for the
+// current renderer type (DirectRenderer, ShmRenderer, etc.).
+// #define DEBUG_FPS
+
 namespace lrc {
 namespace video {
 
 using namespace lrc::api::video;
 
 Renderer::Renderer(const QString& id, const QSize& res)
-    : id_(id)
+    : QObject(nullptr)
+    , id_(id)
     , size_(res)
-    , QObject(nullptr)
-{}
+    , fps_(0.0)
+    , fpsTracker_(new FpsTracker(this))
+{
+    // Subscribe to frame rate updates.
+    connect(fpsTracker_, &FpsTracker::fpsUpdated, this, [this](double fps) {
+        setFPS(fps);
+#ifdef DEBUG_FPS
+        qDebug() << this << ": FPS " << fps;
+#endif
+    });
+}
 
 Renderer::~Renderer() {}
 
-int
+double
 Renderer::fps() const
 {
     return fps_;
@@ -52,12 +66,18 @@ Renderer::size() const
     return size_;
 }
 void
-Renderer::setFPS(int fps)
+Renderer::setFPS(double fps)
 {
     fps_ = fps;
     Q_EMIT fpsChanged();
 }
 
+void
+Renderer::updateFpsTracker()
+{
+    fpsTracker_->update();
+}
+
 MapStringString
 Renderer::getInfos() const
 {
@@ -68,5 +88,24 @@ Renderer::getInfos() const
     return map;
 }
 
+FpsTracker::FpsTracker(QObject* parent)
+    : QObject(parent)
+    , lastTime_(clock_type::now())
+{}
+
+void
+FpsTracker::update()
+{
+    frameCount_++;
+    auto now = clock_type::now();
+    const std::chrono::duration<double> elapsed = now - lastTime_;
+    if (elapsed.count() >= checkInterval_) {
+        double fps = static_cast<double>(frameCount_) / elapsed.count();
+        Q_EMIT fpsUpdated(fps);
+        frameCount_ = 0;
+        lastTime_ = now;
+    }
+}
+
 } // namespace video
 } // namespace lrc
diff --git a/src/libclient/renderer.h b/src/libclient/renderer.h
index 9c83e426e6e124d7942ae13d94423374dec6e9ba..3ff18260b20e2bb3f6510049cf2f1fe392d26d3e 100644
--- a/src/libclient/renderer.h
+++ b/src/libclient/renderer.h
@@ -32,6 +32,8 @@
 namespace lrc {
 namespace video {
 
+class FpsTracker;
+
 class Renderer : public QObject
 {
     Q_OBJECT
@@ -39,7 +41,6 @@ public:
     constexpr static const char RENDERER_ID[] = "RENDERER_ID";
     constexpr static const char FPS[] = "FPS";
     constexpr static const char RES[] = "RES";
-    constexpr static const int FPS_RATE_SEC = 1;
 
     Renderer(const QString& id, const QSize& res);
     virtual ~Renderer();
@@ -47,7 +48,7 @@ public:
     /**
      * @return renderer's fps
      */
-    int fps() const;
+    double fps() const;
 
     /**
      * @return renderer's id
@@ -67,7 +68,12 @@ public:
     /**
      * set fps
      */
-    void setFPS(int fps);
+    void setFPS(double fps);
+
+    /**
+     * Update the FPS tracker.
+     */
+    void updateFpsTracker();
 
     MapStringString getInfos() const;
 
@@ -85,7 +91,30 @@ Q_SIGNALS:
 private:
     QString id_;
     QSize size_;
-    int fps_;
+    double fps_;
+
+    FpsTracker* fpsTracker_;
+};
+
+// Helper that counts ticks, and notifies of FPS changes.
+class FpsTracker : public QObject
+{
+    Q_OBJECT
+public:
+    FpsTracker(QObject* parent = nullptr);
+    ~FpsTracker() = default;
+
+    // Call this function every frame.
+    void update();
+
+    // Emitted after every checkInterval_ when update() is called.
+    Q_SIGNAL void fpsUpdated(double fps);
+
+private:
+    using clock_type = std::chrono::high_resolution_clock;
+    const double checkInterval_ {1.0};
+    unsigned frameCount_ {0};
+    std::chrono::time_point<clock_type> lastTime_;
 };
 
 } // namespace video
diff --git a/src/libclient/shmrenderer.cpp b/src/libclient/shmrenderer.cpp
index 74110e8cbd03ae7c3d69d72c55aeec03cd44afae..734023f0ddc7452c2cc6fa8bbe7be5dc5cec7d5f 100644
--- a/src/libclient/shmrenderer.cpp
+++ b/src/libclient/shmrenderer.cpp
@@ -20,7 +20,6 @@
 #include "shmrenderer.h"
 
 #include "dbus/videomanager.h"
-#include "videomanager_interface.h"
 
 #include <QDebug>
 #include <QMutex>
@@ -39,19 +38,12 @@
 #define CLOCK_REALTIME 0
 #endif
 
-#include <QTimer>
-
-#include <chrono>
-
 namespace lrc {
 
 using namespace api::video;
 
 namespace video {
 
-// Uncomment following line to output in console the FPS value
-//#define DEBUG_FPS
-
 /* Shared memory object
  * Implementation note: double-buffering
  * Shared memory is divided in two regions, each representing one frame.
@@ -87,27 +79,46 @@ public:
         , shmArea((SHMHeader*) MAP_FAILED)
         , shmAreaLen(0)
         , frameGen(0)
-        , fpsC(0)
-        , fps(0)
-        , timer(new QTimer(this))
-        , lastFrameDebug(std::chrono::system_clock::now())
     {
-        timer->setInterval(33);
-        connect(timer, &QTimer::timeout, [this]() { Q_EMIT parent_->frameUpdated(); });
         VideoManager::instance().startShmSink(parent_->id(), true);
 
-        parent_->moveToThread(&thread);
-        connect(&thread, &QThread::finished, [this] { parent_->stopRendering(); });
-        thread.start();
+        // Continuously check for new frames on a separate thread.
+        // This is necessary because the frame rate is not constant.
+        // The function getNewFrame() will return false if no new frame is available.
+        thread = QThread::create([this] {
+            forever {
+                if (QThread::currentThread()->isInterruptionRequested()) {
+                    return;
+                }
+
+                if (!waitForNewFrame()) {
+                    continue;
+                }
+
+                parent_->updateFpsTracker();
+                Q_EMIT parent_->frameUpdated();
+            }
+        });
     };
-    ~Impl()
+    ~Impl() {} // Thread is stopped by parent in ShmRenderer::stopShm.
+
+    void stopThread()
     {
-        thread.quit();
-        thread.wait();
-    }
+        // Request thread loop interruption and then unblock the sem_wait.
+        thread->requestInterruption();
+
+        // Set the isDestroying flag to true so that the thread loop can exit
+        // without emitting the frameUpdated signal for an invalid resolution
+        // (e.g. smartphone rotation).
+        // This works as ShmHolder::renderFrame should reset frameSize appropriately.
+        shmLock();
+        shmArea->frameSize = 0;
+        shmUnlock();
 
-    // Constants
-    constexpr static const int FRAME_CHECK_RATE_HZ = 120;
+        ::sem_post(&shmArea->frameGenMutex);
+
+        thread->wait();
+    }
 
     // Lock the memory while the copy is being made
     bool shmLock()
@@ -122,7 +133,7 @@ public:
     };
 
     // Wait for new frame data from shared memory and save pointer.
-    bool getNewFrame(bool wait)
+    bool waitForNewFrame()
     {
         if (!shmLock())
             return false;
@@ -130,12 +141,7 @@ public:
         if (frameGen == shmArea->frameGen) {
             shmUnlock();
 
-            if (not wait)
-                return false;
-
-            // wait for a new frame, max 33ms
-            static const struct timespec timeout = {0, 33000000};
-            if (::sem_timedwait(&shmArea->frameGenMutex, &timeout) < 0)
+            if (::sem_wait(&shmArea->frameGenMutex) < 0)
                 return false;
 
             if (!shmLock())
@@ -161,22 +167,6 @@ public:
         frameGen = shmArea->frameGen;
 
         shmUnlock();
-
-        ++fpsC;
-
-        // Compute the FPS shown to the client
-        auto currentTime = std::chrono::system_clock::now();
-        const std::chrono::duration<double> seconds = currentTime - lastFrameDebug;
-        if (seconds.count() >= FPS_RATE_SEC) {
-            fps = static_cast<int>(fpsC / seconds.count());
-            fpsC = 0;
-            lastFrameDebug = currentTime;
-            parent_->setFPS(fps);
-#ifdef DEBUG_FPS
-            qDebug() << this << ": FPS " << fps;
-#endif
-        }
-
         return true;
     };
 
@@ -222,13 +212,8 @@ public:
     unsigned shmAreaLen;
     uint frameGen;
 
-    int fpsC;
-    int fps;
-    std::chrono::time_point<std::chrono::system_clock> lastFrameDebug;
-
-    QTimer* timer;
     QMutex mutex;
-    QThread thread;
+    QThread* thread;
     std::shared_ptr<lrc::api::video::Frame> frame;
 };
 
@@ -249,10 +234,8 @@ Frame
 ShmRenderer::currentFrame() const
 {
     QMutexLocker lk {&pimpl_->mutex};
-    if (pimpl_->getNewFrame(false)) {
-        if (auto frame_ptr = pimpl_->frame)
-            return std::move(*frame_ptr);
-    }
+    if (auto frame_ptr = pimpl_->frame)
+        return std::move(*frame_ptr);
     return {};
 }
 
@@ -283,6 +266,7 @@ ShmRenderer::startShm()
     }
 
     pimpl_->shmAreaLen = mapSize;
+    pimpl_->thread->start();
     return true;
 }
 
@@ -292,12 +276,12 @@ ShmRenderer::stopShm()
     if (pimpl_->fd < 0)
         return;
 
-    pimpl_->timer->stop();
-
     // Emit the signal before closing the file, this lower the risk of invalid
     // memory access
     Q_EMIT stopped();
 
+    pimpl_->stopThread();
+
     {
         QMutexLocker lk(&pimpl_->mutex);
         // reset the frame so it doesn't point to an old value
@@ -323,8 +307,6 @@ ShmRenderer::startRendering()
     if (!startShm())
         return;
 
-    pimpl_->timer->start();
-
     Q_EMIT started(size());
 }