diff --git a/src/media/audio/audio_input.cpp b/src/media/audio/audio_input.cpp
index 83fa190878e4b01901dc85a703736e9de65ae355..592268544390e17edc16277f3eaa6fa2730bbaec 100644
--- a/src/media/audio/audio_input.cpp
+++ b/src/media/audio/audio_input.cpp
@@ -47,8 +47,6 @@ AudioInput::AudioInput(const std::string& id) :
 
 AudioInput::~AudioInput()
 {
-    if (auto rec = recorder_.lock())
-        rec->stopRecording();
     loop_.join();
 }
 
@@ -71,13 +69,6 @@ AudioInput::process()
     frame->pointer()->pts = sent_samples;
     sent_samples += frame->pointer()->nb_samples;
 
-    {
-        auto rec = recorder_.lock();
-        if (rec && rec->isRecording()) {
-            rec->recordData(frame->pointer(), ms);
-        }
-    }
-
     notify(frame);
 }
 
@@ -197,11 +188,4 @@ AudioInput::setMuted(bool isMuted)
     muteState_ = isMuted;
 }
 
-void
-AudioInput::initRecorder(const std::shared_ptr<MediaRecorder>& rec)
-{
-    rec->incrementExpectedStreams(1);
-    recorder_ = rec;
-}
-
 } // namespace ring
diff --git a/src/media/audio/audio_input.h b/src/media/audio/audio_input.h
index 43fffa1bbcac45b6b3902e5fe87cab87ee36f9da..efdd1274598392e729793d27354638e402079986 100644
--- a/src/media/audio/audio_input.h
+++ b/src/media/audio/audio_input.h
@@ -33,7 +33,6 @@
 
 namespace ring {
 
-class MediaRecorder;
 class Resampler;
 
 class AudioInput : public Observable<std::shared_ptr<AudioFrame>>
@@ -47,7 +46,6 @@ public:
     bool isCapturing() const { return loop_.isRunning(); }
     void setFormat(const AudioFormat& fmt);
     void setMuted(bool isMuted);
-    void initRecorder(const std::shared_ptr<MediaRecorder>& rec);
 
 private:
     bool nextFromDevice(AudioFrame& frame);
@@ -61,7 +59,6 @@ private:
     AudioFormat format_;
 
     std::unique_ptr<Resampler> resampler_;
-    std::weak_ptr<MediaRecorder> recorder_;
 
     std::string currentResource_;
     std::atomic_bool switchPending_ {false};
diff --git a/src/media/audio/audio_receive_thread.cpp b/src/media/audio/audio_receive_thread.cpp
index f320a8b7c9ba1440287a1a050be05b63e9fc110a..b01d9f73d4f9daa869ab5919aa0220aae7c0b7ff 100644
--- a/src/media/audio/audio_receive_thread.cpp
+++ b/src/media/audio/audio_receive_thread.cpp
@@ -49,8 +49,6 @@ AudioReceiveThread::AudioReceiveThread(const std::string& id,
 
 AudioReceiveThread::~AudioReceiveThread()
 {
-    if (auto rec = recorder_.lock())
-        rec->stopRecording();
     loop_.join();
 }
 
@@ -96,11 +94,6 @@ AudioReceiveThread::process()
     auto decodedFrame = std::make_shared<AudioFrame>();
     switch (audioDecoder_->decode(*decodedFrame)) {
         case MediaDecoder::Status::FrameFinished:
-            {
-                auto rec = recorder_.lock();
-                if (rec && rec->isRecording())
-                    rec->recordData(decodedFrame->pointer(), audioDecoder_->getStream("a:remote"));
-            }
             audioDecoder_->writeToRingBuffer(*decodedFrame, *ringbuffer_,
                                              mainBuffFormat);
             notify(decodedFrame);
@@ -163,11 +156,4 @@ AudioReceiveThread::startLoop()
     loop_.start();
 }
 
-void
-AudioReceiveThread::initRecorder(std::shared_ptr<MediaRecorder>& rec)
-{
-    recorder_ = rec;
-    rec->incrementExpectedStreams(1);
-}
-
 }; // namespace ring
diff --git a/src/media/audio/audio_receive_thread.h b/src/media/audio/audio_receive_thread.h
index 38a75a6a1d696141b181441dc29390c82fb30089..8ee6b6a93b5bb0b0749dad1a655c4d8b960fd702 100644
--- a/src/media/audio/audio_receive_thread.h
+++ b/src/media/audio/audio_receive_thread.h
@@ -33,10 +33,9 @@ namespace ring {
 
 class MediaDecoder;
 class MediaIOHandle;
-class MediaRecorder;
 class RingBuffer;
 
-class AudioReceiveThread : Observable<std::shared_ptr<AudioFrame>>
+class AudioReceiveThread : public Observable<std::shared_ptr<AudioFrame>>
 {
 public:
     AudioReceiveThread(const std::string &id,
@@ -47,8 +46,6 @@ public:
     void addIOContext(SocketPair &socketPair);
     void startLoop();
 
-    void initRecorder(std::shared_ptr<MediaRecorder>& rec);
-
 private:
     NON_COPYABLE(AudioReceiveThread);
 
@@ -60,8 +57,6 @@ private:
     void openDecoder();
     bool decodeFrame();
 
-    std::weak_ptr<MediaRecorder> recorder_;
-
     /*-----------------------------------------------------------------*/
     /* These variables should be used in thread (i.e. process()) only! */
     /*-----------------------------------------------------------------*/
diff --git a/src/media/audio/audio_rtp_session.cpp b/src/media/audio/audio_rtp_session.cpp
index 7e326d371a88c564ad6c552e33cd27fd0c08260c..93613fb1a9402f38f87e1337725b36644ec31b7b 100644
--- a/src/media/audio/audio_rtp_session.cpp
+++ b/src/media/audio/audio_rtp_session.cpp
@@ -197,9 +197,18 @@ void
 AudioRtpSession::initRecorder(std::shared_ptr<MediaRecorder>& rec)
 {
     if (receiveThread_)
-        receiveThread_->initRecorder(rec);
-    if (sender_)
-        sender_->initRecorder(rec);
+        receiveThread_->attach(rec.get());
+    if (auto input = ring::getAudioInput(callID_))
+        input->attach(rec.get());
+}
+
+void
+AudioRtpSession::deinitRecorder(std::shared_ptr<MediaRecorder>& rec)
+{
+    if (receiveThread_)
+        receiveThread_->detach(rec.get());
+    if (auto input = ring::getAudioInput(callID_))
+        input->detach(rec.get());
 }
 
 } // namespace ring
diff --git a/src/media/audio/audio_rtp_session.h b/src/media/audio/audio_rtp_session.h
index 03f1e6be067cdee4a82b4593642c1cb24ee0e438..eea60715645c256b87d2600d787c71d827a1fb66 100644
--- a/src/media/audio/audio_rtp_session.h
+++ b/src/media/audio/audio_rtp_session.h
@@ -53,6 +53,7 @@ class AudioRtpSession : public RtpSession {
         void switchInput(const std::string& resource) { input_ = resource; }
 
         void initRecorder(std::shared_ptr<MediaRecorder>& rec) override;
+        void deinitRecorder(std::shared_ptr<MediaRecorder>& rec) override;
 
     private:
         void startSender();
diff --git a/src/media/audio/audio_sender.cpp b/src/media/audio/audio_sender.cpp
index 3d4167ad6493114d2f799e0644bfa9835ddca1a9..04c62f8dba4c8aa757126238db766282f3ca30f7 100644
--- a/src/media/audio/audio_sender.cpp
+++ b/src/media/audio/audio_sender.cpp
@@ -26,7 +26,6 @@
 #include "logger.h"
 #include "media_encoder.h"
 #include "media_io_handle.h"
-#include "media_recorder.h"
 #include "media_stream.h"
 #include "resampler.h"
 #include "smartools.h"
@@ -52,8 +51,6 @@ AudioSender::AudioSender(const std::string& id,
 
 AudioSender::~AudioSender()
 {
-    if (auto rec = recorder_.lock())
-        rec->stopRecording();
     audioInput_->detach(this);
     audioInput_.reset();
     audioEncoder_.reset();
@@ -106,12 +103,6 @@ AudioSender::update(Observable<std::shared_ptr<ring::AudioFrame>>* /*obs*/, cons
     ms.firstTimestamp = frame->pts;
     sent_samples += frame->nb_samples;
 
-    {
-        auto rec = recorder_.lock();
-        if (rec && rec->isRecording())
-            rec->recordData(frame, ms);
-    }
-
     if (audioEncoder_->encodeAudio(*framePtr) < 0)
         RING_ERR("encoding failed");
 }
@@ -129,11 +120,4 @@ AudioSender::getLastSeqValue()
     return audioEncoder_->getLastSeqValue();
 }
 
-void
-AudioSender::initRecorder(std::shared_ptr<MediaRecorder>& rec)
-{
-    recorder_ = rec;
-    rec->incrementExpectedStreams(1);
-}
-
 } // namespace ring
diff --git a/src/media/audio/audio_sender.h b/src/media/audio/audio_sender.h
index b51d9db7f54bae6227049009d15c228fd30974c4..3b2e9736af3bf5dfa0d1884451434e81d6251dcd 100644
--- a/src/media/audio/audio_sender.h
+++ b/src/media/audio/audio_sender.h
@@ -31,7 +31,6 @@ namespace ring {
 class AudioInput;
 class MediaEncoder;
 class MediaIOHandle;
-class MediaRecorder;
 class Resampler;
 
 class AudioSender : public Observer<std::shared_ptr<AudioFrame>> {
@@ -51,8 +50,6 @@ public:
     void update(Observable<std::shared_ptr<ring::AudioFrame>>*,
                 const std::shared_ptr<ring::AudioFrame>&) override;
 
-    void initRecorder(std::shared_ptr<MediaRecorder>& rec);
-
 private:
     NON_COPYABLE(AudioSender);
 
@@ -65,7 +62,6 @@ private:
     std::unique_ptr<MediaIOHandle> muxContext_;
     std::unique_ptr<Resampler> resampler_;
     std::shared_ptr<AudioInput> audioInput_;
-    std::weak_ptr<MediaRecorder> recorder_;
 
     uint64_t sent_samples = 0;
 
diff --git a/src/media/localrecorder.cpp b/src/media/localrecorder.cpp
index 1283e1f6a7ccec167ad615e3717a45d7843498c3..2aaebc94f27a3fb1d4d73ce6cd182c4029fbed3e 100644
--- a/src/media/localrecorder.cpp
+++ b/src/media/localrecorder.cpp
@@ -73,14 +73,14 @@ LocalRecorder::startRecording()
 
     audioInput_ = ring::getAudioInput(path_);
     audioInput_->setFormat(AudioFormat::STEREO());
-    audioInput_->initRecorder(recorder_);
+    audioInput_->attach(recorder_.get());
 
 #ifdef RING_VIDEO
     // video recording
     if (!isAudioOnly_) {
         videoInput_ = std::static_pointer_cast<video::VideoInput>(ring::getVideoCamera());
         if (videoInput_) {
-            videoInput_->initRecorder(recorder_);
+            videoInput_->attach(recorder_.get());
         } else {
             RING_ERR() << "Unable to record video (no video input)";
             return false;
@@ -97,8 +97,6 @@ LocalRecorder::stopRecording()
     Recordable::stopRecording();
     Manager::instance().getRingBufferPool().unBindHalfDuplexOut(path_, RingBufferPool::DEFAULT_ID);
     audioInput_.reset();
-    if (videoInput_)
-        videoInput_->initRecorder(nullptr); // workaround for deiniting recorder
     videoInput_.reset();
 }
 
diff --git a/src/media/media_recorder.cpp b/src/media/media_recorder.cpp
index fa6f436cb49cb1809e5e13136ab8fbf68b94b354..589fc74a2ed3c67118c71ab8456c007de0dc095b 100644
--- a/src/media/media_recorder.cpp
+++ b/src/media/media_recorder.cpp
@@ -19,16 +19,17 @@
  */
 
 #include "libav_deps.h" // MUST BE INCLUDED FIRST
+#include "audio/audio_input.h"
+#include "audio/audio_receive_thread.h"
+#include "audio/audio_sender.h"
 #include "client/ring_signal.h"
 #include "fileutils.h"
 #include "logger.h"
 #include "media_io_handle.h"
 #include "media_recorder.h"
 #include "system_codec_container.h"
-
-extern "C" {
-#include <libavutil/frame.h>
-}
+#include "video/video_input.h"
+#include "video/video_receive_thread.h"
 
 #include <algorithm>
 #include <iomanip>
@@ -38,8 +39,6 @@ extern "C" {
 
 namespace ring {
 
-static constexpr auto FRAME_DEQUEUE_INTERVAL = std::chrono::milliseconds(200);
-
 static std::string
 replaceAll(const std::string& str, const std::string& from, const std::string& to)
 {
@@ -119,12 +118,6 @@ MediaRecorder::setPath(const std::string& path)
     RING_DBG() << "Recording will be saved as '" << getPath() << "'";
 }
 
-void
-MediaRecorder::incrementExpectedStreams(int n)
-{
-    nbExpectedStreams_ += n;
-}
-
 bool
 MediaRecorder::isRecording() const
 {
@@ -185,6 +178,42 @@ MediaRecorder::stopRecording()
     resetToDefaults();
 }
 
+void
+MediaRecorder::update(Observable<std::shared_ptr<AudioFrame>>* ob, const std::shared_ptr<AudioFrame>& a)
+{
+    MediaStream ms;
+    if (dynamic_cast<AudioReceiveThread*>(ob))
+        ms.name = "a:remote";
+    else // if (dynamic_cast<AudioSender*>(ob) || dynamic_cast<AudioInput*>(ob))
+        ms.name = "a:local";
+    ms.isVideo = false;
+    ms.update(a->pointer());
+    ms.firstTimestamp = a->pointer()->pts;
+    recordData(a->pointer(), ms);
+}
+
+void MediaRecorder::attached(Observable<std::shared_ptr<AudioFrame>>* /*ob*/)
+{
+    ++nbExpectedStreams_;
+}
+
+void MediaRecorder::update(Observable<std::shared_ptr<VideoFrame>>* ob, const std::shared_ptr<VideoFrame>& v)
+{
+    MediaStream ms;
+    if (auto receiver = dynamic_cast<video::VideoReceiveThread*>(ob)) {
+        ms = receiver->getStream();
+    } else if (auto input = dynamic_cast<video::VideoInput*>(ob)) {
+        ms = input->getStream();
+    }
+    ms.firstTimestamp = v->pointer()->pts;
+    recordData(v->pointer(), ms);
+}
+
+void MediaRecorder::attached(Observable<std::shared_ptr<VideoFrame>>* /*ob*/)
+{
+    ++nbExpectedStreams_;
+}
+
 int
 MediaRecorder::addStream(const MediaStream& ms)
 {
@@ -402,7 +431,7 @@ MediaRecorder::buildVideoFilter(const std::vector<MediaStream>& peers, const Med
 
     switch (peers.size()) {
     case 0:
-        v << "[" << local.name << "] format=pix_fmts=yuv420p";
+        v << "[" << local.name << "] fps=30, format=pix_fmts=yuv420p";
         break;
     case 1:
         {
diff --git a/src/media/media_recorder.h b/src/media/media_recorder.h
index e8b3bae30293946b3067cf541257caa2b6713242..829b052450d787e82596fce78524f910879337ea 100644
--- a/src/media/media_recorder.h
+++ b/src/media/media_recorder.h
@@ -21,11 +21,16 @@
 #pragma once
 
 #include "config.h"
+#include "media_buffer.h"
 #include "media_encoder.h"
 #include "media_filter.h"
 #include "media_stream.h"
 #include "noncopyable.h"
+#include "observer.h"
 #include "threadloop.h"
+#ifdef RING_VIDEO
+#include "video/video_base.h"
+#endif
 
 #include <map>
 #include <memory>
@@ -39,95 +44,102 @@ struct AVFrame;
 
 namespace ring {
 
-class MediaRecorder {
-    public:
-        MediaRecorder();
-        ~MediaRecorder();
-
-        std::string getPath() const;
-
-        void setPath(const std::string& path);
-
-        void audioOnly(bool audioOnly);
-
-        // replaces %TIMESTAMP with time at start of recording
-        // default title: "Conversation at %Y-%m-%d %H:%M:%S"
-        // default description: "Recorded with Jami https://jami.net"
-        void setMetadata(const std::string& title, const std::string& desc);
-
-        [[deprecated("use setPath to set full recording path")]]
-        void setRecordingPath(const std::string& dir);
-
-        // adjust nb of streams before recording
-        // used to know when all streams are set up
-        void incrementExpectedStreams(int n);
-
-        bool isRecording() const;
-
-        bool toggleRecording();
-
-        int startRecording();
-
-        void stopRecording();
-
-        int recordData(AVFrame* frame, const MediaStream& ms);
-
-    private:
-        NON_COPYABLE(MediaRecorder);
-
-        int addStream(const MediaStream& ms);
-        int initRecord();
-        MediaStream setupVideoOutput();
-        std::string buildVideoFilter(const std::vector<MediaStream>& peers, const MediaStream& local) const;
-        MediaStream setupAudioOutput();
-        std::string buildAudioFilter(const std::vector<MediaStream>& peers, const MediaStream& local) const;
-        void emptyFilterGraph();
-        int sendToEncoder(AVFrame* frame, int streamIdx);
-        int flush();
-        void resetToDefaults(); // clear saved data for next recording
-
-        std::unique_ptr<MediaEncoder> encoder_;
-        std::unique_ptr<MediaFilter> videoFilter_;
-        std::unique_ptr<MediaFilter> audioFilter_;
-
-        std::mutex mutex_; // protect against concurrent file writes
-
-        std::map<std::string, const MediaStream> streams_;
-
-        std::tm startTime_;
-        std::string title_;
-        std::string description_;
-
-        std::string path_;
-
-        // NOTE do not use dir_ or filename_, use path_ instead
-        std::string dir_;
-        std::string filename_;
-
-        unsigned nbExpectedStreams_ = 0;
-        unsigned nbReceivedVideoStreams_ = 0;
-        unsigned nbReceivedAudioStreams_ = 0;
-        int videoIdx_ = -1;
-        int audioIdx_ = -1;
-        bool isRecording_ = false;
-        bool isReady_ = false;
-        bool audioOnly_ = false;
-
-        struct RecordFrame {
-            AVFrame* frame;
-            bool isVideo;
-            bool fromPeer;
-            RecordFrame() {}
-            RecordFrame(AVFrame* f, bool v, bool p)
-                : frame(f)
-                , isVideo(v)
-                , fromPeer(p)
-            {}
-        };
-        InterruptedThreadLoop loop_;
-        void process();
-        std::mutex qLock_;
-        std::deque<RecordFrame> frames_;
+class MediaRecorder : public Observer<std::shared_ptr<AudioFrame>>
+#ifdef RING_VIDEO
+                    , public video::VideoFramePassiveReader
+#endif
+{
+public:
+    MediaRecorder();
+    ~MediaRecorder();
+
+    std::string getPath() const;
+
+    void setPath(const std::string& path);
+
+    void audioOnly(bool audioOnly);
+
+    // replaces %TIMESTAMP with time at start of recording
+    // default title: "Conversation at %Y-%m-%d %H:%M:%S"
+    // default description: "Recorded with Jami https://jami.net"
+    void setMetadata(const std::string& title, const std::string& desc);
+
+    [[deprecated("use setPath to set full recording path")]]
+    void setRecordingPath(const std::string& dir);
+
+    bool isRecording() const;
+
+    bool toggleRecording();
+
+    int startRecording();
+
+    void stopRecording();
+
+    /* Observer methods*/
+    void update(Observable<std::shared_ptr<AudioFrame>>* ob, const std::shared_ptr<AudioFrame>& a) override;
+    void attached(Observable<std::shared_ptr<AudioFrame>>* ob) override;
+
+    void update(Observable<std::shared_ptr<VideoFrame>>* ob, const std::shared_ptr<VideoFrame>& v) override;
+    void attached(Observable<std::shared_ptr<VideoFrame>>* ob) override;
+
+private:
+    NON_COPYABLE(MediaRecorder);
+
+    int recordData(AVFrame* frame, const MediaStream& ms);
+
+    int addStream(const MediaStream& ms);
+    int initRecord();
+    MediaStream setupVideoOutput();
+    std::string buildVideoFilter(const std::vector<MediaStream>& peers, const MediaStream& local) const;
+    MediaStream setupAudioOutput();
+    std::string buildAudioFilter(const std::vector<MediaStream>& peers, const MediaStream& local) const;
+    void emptyFilterGraph();
+    int sendToEncoder(AVFrame* frame, int streamIdx);
+    int flush();
+    void resetToDefaults(); // clear saved data for next recording
+
+    std::unique_ptr<MediaEncoder> encoder_;
+    std::unique_ptr<MediaFilter> videoFilter_;
+    std::unique_ptr<MediaFilter> audioFilter_;
+
+    std::mutex mutex_; // protect against concurrent file writes
+
+    std::map<std::string, const MediaStream> streams_;
+
+    std::tm startTime_;
+    std::string title_;
+    std::string description_;
+
+    std::string path_;
+
+    // NOTE do not use dir_ or filename_, use path_ instead
+    std::string dir_;
+    std::string filename_;
+
+    unsigned nbExpectedStreams_ = 0;
+    unsigned nbReceivedVideoStreams_ = 0;
+    unsigned nbReceivedAudioStreams_ = 0;
+    int videoIdx_ = -1;
+    int audioIdx_ = -1;
+    bool isRecording_ = false;
+    bool isReady_ = false;
+    bool audioOnly_ = false;
+
+    struct RecordFrame {
+        AVFrame* frame;
+        bool isVideo;
+        bool fromPeer;
+        RecordFrame() {}
+        RecordFrame(AVFrame* f, bool v, bool p)
+            : frame(f)
+            , isVideo(v)
+            , fromPeer(p)
+        {}
+    };
+    InterruptedThreadLoop loop_;
+    void process();
+    std::mutex qLock_;
+    std::deque<RecordFrame> frames_;
 };
 
 }; // namespace ring
diff --git a/src/media/rtp_session.h b/src/media/rtp_session.h
index 6c85997efeb83cda525b08e2e359e00fa8fe3a36..f64ac3768e2e300de6c3786e1761eecca7f32bb5 100644
--- a/src/media/rtp_session.h
+++ b/src/media/rtp_session.h
@@ -56,6 +56,7 @@ public:
     void setMtu(uint16_t mtu) { mtu_ = mtu; }
 
     virtual void initRecorder(std::shared_ptr<MediaRecorder>& rec) = 0;
+    virtual void deinitRecorder(std::shared_ptr<MediaRecorder>& rec) = 0;
 
 protected:
     std::recursive_mutex mutex_;
diff --git a/src/media/video/video_input.cpp b/src/media/video/video_input.cpp
index d7a6334ddb672f56b3312e5b42f4e64820af6b32..61ace8c1ff298d1ab51d045d6eb629216cecf20a 100644
--- a/src/media/video/video_input.cpp
+++ b/src/media/video/video_input.cpp
@@ -63,8 +63,6 @@ VideoInput::VideoInput()
 
 VideoInput::~VideoInput()
 {
-    if (auto rec = recorder_.lock())
-        rec->stopRecording();
 #if defined(__ANDROID__) || defined(RING_UWP) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS)
     /* we need to stop the loop and notify the condition variable
      * to unblock the process loop */
@@ -232,11 +230,6 @@ bool VideoInput::captureFrame()
             return static_cast<bool>(decoder_);
 
         case MediaDecoder::Status::FrameFinished:
-            {
-                auto rec = recorder_.lock();
-                if (rec && rec->isRecording())
-                    rec->recordData(frame.pointer(), decoder_->getStream("v:local"));
-            }
             publishFrame();
             return true;
         // continue decoding
@@ -613,6 +606,12 @@ int VideoInput::getPixelFormat() const
 DeviceParams VideoInput::getParams() const
 { return decOpts_; }
 
+MediaStream
+VideoInput::getStream() const
+{
+    return decoder_->getStream("v:local");
+}
+
 void
 VideoInput::foundDecOpts(const DeviceParams& params)
 {
@@ -622,15 +621,4 @@ VideoInput::foundDecOpts(const DeviceParams& params)
     }
 }
 
-void
-VideoInput::initRecorder(const std::shared_ptr<MediaRecorder>& rec)
-{
-    if (rec) {
-        recorder_ = rec;
-        rec->incrementExpectedStreams(1);
-    } else {
-        recorder_.reset();
-    }
-}
-
 }} // namespace ring::video
diff --git a/src/media/video/video_input.h b/src/media/video/video_input.h
index 0e8d16ef2919511158d439e75dba28510dc9a5e4..1ccfc544f81c28f11c1996ad8790fcc2ce71d3d9 100644
--- a/src/media/video/video_input.h
+++ b/src/media/video/video_input.h
@@ -25,6 +25,7 @@
 
 #include "noncopyable.h"
 #include "threadloop.h"
+#include "media_stream.h"
 #include "media/media_device.h" // DeviceParams
 #include "media/video/video_base.h"
 
@@ -42,7 +43,6 @@
 
 namespace ring {
 class MediaDecoder;
-class MediaRecorder;
 }
 
 namespace ring { namespace video {
@@ -78,6 +78,7 @@ public:
     int getHeight() const;
     int getPixelFormat() const;
     DeviceParams getParams() const;
+    MediaStream getStream() const;
 
     std::shared_future<DeviceParams> switchInput(const std::string& resource);
 #if defined(__ANDROID__) || defined(RING_UWP) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS)
@@ -89,8 +90,6 @@ public:
     void releaseFrame(void *frame);
 #endif
 
-    void initRecorder(const std::shared_ptr<MediaRecorder>& rec);
-
 private:
     NON_COPYABLE(VideoInput);
 
@@ -144,8 +143,6 @@ private:
     void releaseBufferCb(uint8_t* ptr);
     std::array<struct VideoFrameBuffer, 8> buffers_;
 #endif
-
-    std::weak_ptr<MediaRecorder> recorder_;
 };
 
 }} // namespace ring::video
diff --git a/src/media/video/video_receive_thread.cpp b/src/media/video/video_receive_thread.cpp
index a82a66a264ec9f9d1f61c64991ba73eadd6b2436..174fc09274a5b7cac15afbf76047dc7c02a788b7 100644
--- a/src/media/video/video_receive_thread.cpp
+++ b/src/media/video/video_receive_thread.cpp
@@ -56,8 +56,6 @@ VideoReceiveThread::VideoReceiveThread(const std::string& id,
 
 VideoReceiveThread::~VideoReceiveThread()
 {
-    if (auto rec = recorder_.lock())
-        rec->stopRecording();
     loop_.join();
 }
 
@@ -187,11 +185,6 @@ bool VideoReceiveThread::decodeFrame()
 
     switch (ret) {
         case MediaDecoder::Status::FrameFinished:
-            {
-                auto rec = recorder_.lock();
-                if (rec && rec->isRecording())
-                    rec->recordData(frame.pointer(), videoDecoder_->getStream("v:remote"));
-            }
             publishFrame();
             return true;
 
@@ -253,18 +246,17 @@ int VideoReceiveThread::getHeight() const
 int VideoReceiveThread::getPixelFormat() const
 { return videoDecoder_->getPixelFormat(); }
 
-void
-VideoReceiveThread::triggerKeyFrameRequest()
+MediaStream
+VideoReceiveThread::getStream() const
 {
-    if (requestKeyFrameCallback_)
-        requestKeyFrameCallback_(id_);
+    return videoDecoder_->getStream("v:remote");
 }
 
 void
-VideoReceiveThread::initRecorder(std::shared_ptr<ring::MediaRecorder>& rec)
+VideoReceiveThread::triggerKeyFrameRequest()
 {
-    recorder_ = rec;
-    rec->incrementExpectedStreams(1);
+    if (requestKeyFrameCallback_)
+        requestKeyFrameCallback_(id_);
 }
 
 }} // namespace ring::video
diff --git a/src/media/video/video_receive_thread.h b/src/media/video/video_receive_thread.h
index 273a51e1f1cb5fe016f9cf769b59621444cb7301..7641f4627cfd6b6c20d0c60f8b30970bb93eb86d 100644
--- a/src/media/video/video_receive_thread.h
+++ b/src/media/video/video_receive_thread.h
@@ -26,6 +26,7 @@
 #include "media_codec.h"
 #include "media_io_handle.h"
 #include "media_device.h"
+#include "media_stream.h"
 #include "threadloop.h"
 #include "noncopyable.h"
 
@@ -38,7 +39,6 @@
 namespace ring {
 class SocketPair;
 class MediaDecoder;
-class MediaRecorder;
 } // namespace ring
 
 namespace ring { namespace video {
@@ -60,10 +60,9 @@ public:
     int getWidth() const;
     int getHeight() const;
     int getPixelFormat() const;
+    MediaStream getStream() const;
     void triggerKeyFrameRequest();
 
-    void initRecorder(std::shared_ptr<ring::MediaRecorder>& rec);
-
 private:
     NON_COPYABLE(VideoReceiveThread);
 
@@ -89,8 +88,6 @@ private:
     static int interruptCb(void *ctx);
     static int readFunction(void *opaque, uint8_t *buf, int buf_size);
 
-    std::weak_ptr<MediaRecorder> recorder_;
-
     ThreadLoop loop_;
 
     // used by ThreadLoop
diff --git a/src/media/video/video_rtp_session.cpp b/src/media/video/video_rtp_session.cpp
index 3d6f91e0c3251895eda0978855515df98e67a23c..07818aad6dba160cb396e4ecbb6e35ccfa82934e 100644
--- a/src/media/video/video_rtp_session.cpp
+++ b/src/media/video/video_rtp_session.cpp
@@ -567,11 +567,19 @@ VideoRtpSession::processPacketLoss()
 void
 VideoRtpSession::initRecorder(std::shared_ptr<MediaRecorder>& rec)
 {
-    // video recording needs to start with keyframes
     if (receiveThread_)
-        receiveThread_->initRecorder(rec);
+        receiveThread_->attach(rec.get());
     if (auto vidInput = std::static_pointer_cast<VideoInput>(videoLocal_))
-        vidInput->initRecorder(rec);
+        vidInput->attach(rec.get());
+}
+
+void
+VideoRtpSession::deinitRecorder(std::shared_ptr<MediaRecorder>& rec)
+{
+    if (receiveThread_)
+        receiveThread_->detach(rec.get());
+    if (auto vidInput = std::static_pointer_cast<VideoInput>(videoLocal_))
+        vidInput->detach(rec.get());
 }
 
 }} // namespace ring::video
diff --git a/src/media/video/video_rtp_session.h b/src/media/video/video_rtp_session.h
index bb1bade19f3efd5fc213e470228e2ba0db00be91..b4a468025f126de5cca256b31c8997ae2aa94de1 100644
--- a/src/media/video/video_rtp_session.h
+++ b/src/media/video/video_rtp_session.h
@@ -81,6 +81,7 @@ public:
     bool useCodec(const AccountVideoCodecInfo* codec) const;
 
     void initRecorder(std::shared_ptr<MediaRecorder>& rec) override;
+    void deinitRecorder(std::shared_ptr<MediaRecorder>& rec) override;
 
 private:
     void setupConferenceVideoPipeline(Conference& conference);
diff --git a/src/sip/sipcall.cpp b/src/sip/sipcall.cpp
index b7e5b7c9174e8f8ab0d3dfc2d5e39f394af7ff70..c08533fe805007d800bc01ddd93c816423f2e7d9 100644
--- a/src/sip/sipcall.cpp
+++ b/src/sip/sipcall.cpp
@@ -1198,6 +1198,13 @@ SIPCall::toggleRecording()
 #ifdef RING_VIDEO
         if (!isAudioOnly_ && videortp_)
             videortp_->initRecorder(recorder_);
+#endif
+    } else {
+        if (avformatrtp_)
+            avformatrtp_->deinitRecorder(recorder_);
+#ifdef RING_VIDEO
+        if (!isAudioOnly_ && videortp_)
+            videortp_->deinitRecorder(recorder_);
 #endif
     }
     return startRecording;