diff --git a/src/media/audio/audio_input.cpp b/src/media/audio/audio_input.cpp
index f0e2e63f0e58aca1fd40e5e1388af16086aae01c..96a9bc3f7627ce2d8b4739a8dc863993b6373f4e 100644
--- a/src/media/audio/audio_input.cpp
+++ b/src/media/audio/audio_input.cpp
@@ -28,6 +28,7 @@
 
 #include <future>
 #include <chrono>
+#include <memory>
 
 namespace ring {
 
@@ -69,7 +70,7 @@ AudioInput::process()
     frame->pointer()->pts = sent_samples;
     sent_samples += frame->pointer()->nb_samples;
 
-    notify(frame);
+    notify(std::static_pointer_cast<MediaFrame>(frame));
 }
 
 bool
diff --git a/src/media/audio/audio_input.h b/src/media/audio/audio_input.h
index 8baecb17317ec67d69dd728fceb6d508fe1dd863..5d442fb2bc800175cadbfa03fffcd943e4429776 100644
--- a/src/media/audio/audio_input.h
+++ b/src/media/audio/audio_input.h
@@ -36,7 +36,7 @@ namespace ring {
 struct MediaStream;
 class Resampler;
 
-class AudioInput : public Observable<std::shared_ptr<AudioFrame>>
+class AudioInput : public Observable<std::shared_ptr<MediaFrame>>
 {
 public:
     AudioInput(const std::string& id);
diff --git a/src/media/audio/audio_receive_thread.cpp b/src/media/audio/audio_receive_thread.cpp
index bfd12b56d2bdeca0c9bf1ae959a6b8ae4d8378df..9865b5a357c0844013373606620c539f74cb7f6d 100644
--- a/src/media/audio/audio_receive_thread.cpp
+++ b/src/media/audio/audio_receive_thread.cpp
@@ -30,6 +30,8 @@
 #include "ringbufferpool.h"
 #include "smartools.h"
 
+#include <memory>
+
 namespace ring {
 
 AudioReceiveThread::AudioReceiveThread(const std::string& id,
@@ -96,7 +98,7 @@ AudioReceiveThread::process()
         case MediaDecoder::Status::FrameFinished:
             audioDecoder_->writeToRingBuffer(*decodedFrame, *ringbuffer_,
                                              mainBuffFormat);
-            notify(decodedFrame);
+            notify(std::static_pointer_cast<MediaFrame>(decodedFrame));
             return;
         case MediaDecoder::Status::DecodeError:
             RING_WARN("decoding failure, trying to reset decoder...");
diff --git a/src/media/audio/audio_receive_thread.h b/src/media/audio/audio_receive_thread.h
index 11f3068c043206beed9e956accdbf10a118d0275..f67dd3fd0a721a81b1dc7c44f3127fdbc27dea02 100644
--- a/src/media/audio/audio_receive_thread.h
+++ b/src/media/audio/audio_receive_thread.h
@@ -36,7 +36,7 @@ class MediaIOHandle;
 struct MediaStream;
 class RingBuffer;
 
-class AudioReceiveThread : public Observable<std::shared_ptr<AudioFrame>>
+class AudioReceiveThread : public Observable<std::shared_ptr<MediaFrame>>
 {
 public:
     AudioReceiveThread(const std::string &id,
diff --git a/src/media/audio/audio_sender.cpp b/src/media/audio/audio_sender.cpp
index 04c62f8dba4c8aa757126238db766282f3ca30f7..340d4ae5a600bebd04761743bccfeb89f810de84 100644
--- a/src/media/audio/audio_sender.cpp
+++ b/src/media/audio/audio_sender.cpp
@@ -30,6 +30,8 @@
 #include "resampler.h"
 #include "smartools.h"
 
+#include <memory>
+
 namespace ring {
 
 AudioSender::AudioSender(const std::string& id,
@@ -94,7 +96,7 @@ AudioSender::setup(SocketPair& socketPair)
 }
 
 void
-AudioSender::update(Observable<std::shared_ptr<ring::AudioFrame>>* /*obs*/, const std::shared_ptr<ring::AudioFrame>& framePtr)
+AudioSender::update(Observable<std::shared_ptr<ring::MediaFrame>>* /*obs*/, const std::shared_ptr<ring::MediaFrame>& framePtr)
 {
     auto frame = framePtr->pointer();
     auto ms = MediaStream("a:local", frame->format, rational<int>(1, frame->sample_rate),
@@ -103,7 +105,7 @@ AudioSender::update(Observable<std::shared_ptr<ring::AudioFrame>>* /*obs*/, cons
     ms.firstTimestamp = frame->pts;
     sent_samples += frame->nb_samples;
 
-    if (audioEncoder_->encodeAudio(*framePtr) < 0)
+    if (audioEncoder_->encodeAudio(*std::static_pointer_cast<AudioFrame>(framePtr)) < 0)
         RING_ERR("encoding failed");
 }
 
diff --git a/src/media/audio/audio_sender.h b/src/media/audio/audio_sender.h
index 3b2e9736af3bf5dfa0d1884451434e81d6251dcd..673b48927bbba80a4b93572b3bc1ee70a8b30d36 100644
--- a/src/media/audio/audio_sender.h
+++ b/src/media/audio/audio_sender.h
@@ -33,7 +33,7 @@ class MediaEncoder;
 class MediaIOHandle;
 class Resampler;
 
-class AudioSender : public Observer<std::shared_ptr<AudioFrame>> {
+class AudioSender : public Observer<std::shared_ptr<MediaFrame>> {
 public:
     AudioSender(const std::string& id,
                 const std::string& dest,
@@ -47,8 +47,8 @@ public:
     void setMuted(bool isMuted);
     uint16_t getLastSeqValue();
 
-    void update(Observable<std::shared_ptr<ring::AudioFrame>>*,
-                const std::shared_ptr<ring::AudioFrame>&) override;
+    void update(Observable<std::shared_ptr<ring::MediaFrame>>*,
+                const std::shared_ptr<ring::MediaFrame>&) override;
 
 private:
     NON_COPYABLE(AudioSender);
diff --git a/src/media/media_recorder.cpp b/src/media/media_recorder.cpp
index 04c0f4f3c7db0dc3f621f375dee169dcfd52c1a9..baf35522ece8469c2dd8df3b3ea1b66be3833bba 100644
--- a/src/media/media_recorder.cpp
+++ b/src/media/media_recorder.cpp
@@ -141,36 +141,27 @@ MediaRecorder::addStream(const MediaStream& ms)
 }
 
 void
-MediaRecorder::update(Observable<std::shared_ptr<AudioFrame>>* ob, const std::shared_ptr<AudioFrame>& a)
+MediaRecorder::update(Observable<std::shared_ptr<MediaFrame>>* ob, const std::shared_ptr<MediaFrame>& m)
 {
     if (!isRecording_)
         return;
     std::string name;
     if (dynamic_cast<AudioReceiveThread*>(ob))
         name = "a:remote";
-    else // ob is of type AudioInput*
+    else if (dynamic_cast<AudioInput*>(ob))
         name = "a:local";
-    // copy frame to not mess with the original frame's pts
-    AudioFrame clone;
-    clone.copyFrom(*a);
-    clone.pointer()->pts -= streams_[name].firstTimestamp;
-    audioFilter_->feedInput(clone.pointer(), name);
-}
-
-void MediaRecorder::update(Observable<std::shared_ptr<VideoFrame>>* ob, const std::shared_ptr<VideoFrame>& v)
-{
-    if (!isRecording_)
-        return;
-    std::string name;
-    if (dynamic_cast<video::VideoReceiveThread*>(ob))
+    else if (dynamic_cast<video::VideoReceiveThread*>(ob))
         name = "v:remote";
-    else // ob is of type VideoInput*
+    else if (dynamic_cast<video::VideoInput*>(ob))
         name = "v:local";
-    // copy frame to not mess with the original frame's pts
-    VideoFrame clone;
-    clone.copyFrom(*v);
+    // copy frame to not mess with the original frame's pts (does not actually copy frame data)
+    MediaFrame clone;
+    clone.copyFrom(*m);
     clone.pointer()->pts -= streams_[name].firstTimestamp;
-    videoFilter_->feedInput(clone.pointer(), name);
+    if (clone.pointer()->width > 0 && clone.pointer()->height > 0)
+        videoFilter_->feedInput(clone.pointer(), name);
+    else
+        audioFilter_->feedInput(clone.pointer(), name);
 }
 
 int
diff --git a/src/media/media_recorder.h b/src/media/media_recorder.h
index c4796db8c2cb236a82a74f947e058456165074c1..0bcb632f0ab1ea7918f570761ec62a785f12de80 100644
--- a/src/media/media_recorder.h
+++ b/src/media/media_recorder.h
@@ -41,10 +41,7 @@
 
 namespace ring {
 
-class MediaRecorder : public Observer<std::shared_ptr<AudioFrame>>
-#ifdef RING_VIDEO
-                    , public video::VideoFramePassiveReader
-#endif
+class MediaRecorder : public Observer<std::shared_ptr<MediaFrame>>
                     , public std::enable_shared_from_this<MediaRecorder>
 {
 public:
@@ -104,8 +101,7 @@ public:
     /**
      * Updates the recorder with an audio or video frame.
      */
-    void update(Observable<std::shared_ptr<AudioFrame>>* ob, const std::shared_ptr<AudioFrame>& a) override;
-    void update(Observable<std::shared_ptr<VideoFrame>>* ob, const std::shared_ptr<VideoFrame>& v) override;
+    void update(Observable<std::shared_ptr<MediaFrame>>* ob, const std::shared_ptr<MediaFrame>& a) override;
 
 private:
     NON_COPYABLE(MediaRecorder);
diff --git a/src/media/video/sinkclient.cpp b/src/media/video/sinkclient.cpp
index ef4265a91163814e1c386fb8f688510c56ed3f2f..6736fe154c12209c22c2ecfcaabf2f50364846a6 100644
--- a/src/media/video/sinkclient.cpp
+++ b/src/media/video/sinkclient.cpp
@@ -313,10 +313,10 @@ SinkClient::SinkClient(const std::string& id, bool mixer)
 {}
 
 void
-SinkClient::update(Observable<std::shared_ptr<VideoFrame>>* /*obs*/,
-                   const std::shared_ptr<VideoFrame>& frame_p)
+SinkClient::update(Observable<std::shared_ptr<MediaFrame>>* /*obs*/,
+                   const std::shared_ptr<MediaFrame>& frame_p)
 {
-    auto& f = *frame_p;
+    auto& f = *std::static_pointer_cast<VideoFrame>(frame_p);
 
 #ifdef DEBUG_FPS
     auto currentTime = std::chrono::system_clock::now();
diff --git a/src/media/video/sinkclient.h b/src/media/video/sinkclient.h
index 8068e8623c8b51c927a8665d153ede54616b4d42..e7a8136ab469454b2b1d5fcf517e0b0a8311b901 100644
--- a/src/media/video/sinkclient.h
+++ b/src/media/video/sinkclient.h
@@ -63,8 +63,8 @@ class SinkClient : public VideoFramePassiveReader
         }
 
         // as VideoFramePassiveReader
-        void update(Observable<std::shared_ptr<ring::VideoFrame>>*,
-                    const std::shared_ptr<ring::VideoFrame>&) override;
+        void update(Observable<std::shared_ptr<ring::MediaFrame>>*,
+                    const std::shared_ptr<ring::MediaFrame>&) override;
 
         bool start() noexcept;
         bool stop() noexcept;
diff --git a/src/media/video/video_base.cpp b/src/media/video/video_base.cpp
index 4b8759c0a68102e5eadc1f39b97f1ea94aa54ece..9aca0944718ee1f9c9b60254597acb5ca6a108b7 100644
--- a/src/media/video/video_base.cpp
+++ b/src/media/video/video_base.cpp
@@ -44,7 +44,7 @@ VideoGenerator::publishFrame()
 {
     std::lock_guard<std::mutex> lk(mutex_);
     lastFrame_ = std::move(writableFrame_);
-    notify(lastFrame_);
+    notify(std::static_pointer_cast<MediaFrame>(lastFrame_));
 }
 
 void
diff --git a/src/media/video/video_base.h b/src/media/video/video_base.h
index 6954d1aac704f0b69df4a00bfa0ec295da0f687c..d7e38727be954833c5e5abee62ae998b5e1d7cb7 100644
--- a/src/media/video/video_base.h
+++ b/src/media/video/video_base.h
@@ -49,17 +49,19 @@ struct AVIOContext;
 #endif
 
 namespace DRing {
+class MediaFrame;
 class VideoFrame;
 }
 
 namespace ring {
+using MediaFrame = DRing::MediaFrame;
 using VideoFrame = DRing::VideoFrame;
 }
 
 namespace ring { namespace video {
 
-struct VideoFrameActiveWriter: Observable<std::shared_ptr<VideoFrame>> {};
-struct VideoFramePassiveReader: Observer<std::shared_ptr<VideoFrame>> {};
+struct VideoFrameActiveWriter: Observable<std::shared_ptr<MediaFrame>> {};
+struct VideoFramePassiveReader: Observer<std::shared_ptr<MediaFrame>> {};
 
 /*=== VideoGenerator =========================================================*/
 
diff --git a/src/media/video/video_mixer.cpp b/src/media/video/video_mixer.cpp
index 6757bf4c28de9baced0b0f1114a6b253e83e6ccc..365f57dfd9a28008ba07f326f57e92e6bbcfd25e 100644
--- a/src/media/video/video_mixer.cpp
+++ b/src/media/video/video_mixer.cpp
@@ -33,7 +33,7 @@
 namespace ring { namespace video {
 
 struct VideoMixer::VideoMixerSource {
-    Observable<std::shared_ptr<VideoFrame>>* source = nullptr;
+    Observable<std::shared_ptr<MediaFrame>>* source = nullptr;
     std::unique_ptr<VideoFrame> update_frame;
     std::unique_ptr<VideoFrame> render_frame;
     void atomic_swap_render(std::unique_ptr<VideoFrame>& other) {
@@ -77,7 +77,7 @@ VideoMixer::~VideoMixer()
 }
 
 void
-VideoMixer::attached(Observable<std::shared_ptr<VideoFrame>>* ob)
+VideoMixer::attached(Observable<std::shared_ptr<MediaFrame>>* ob)
 {
     auto lock(rwMutex_.write());
 
@@ -87,7 +87,7 @@ VideoMixer::attached(Observable<std::shared_ptr<VideoFrame>>* ob)
 }
 
 void
-VideoMixer::detached(Observable<std::shared_ptr<VideoFrame>>* ob)
+VideoMixer::detached(Observable<std::shared_ptr<MediaFrame>>* ob)
 {
     auto lock(rwMutex_.write());
 
@@ -100,8 +100,8 @@ VideoMixer::detached(Observable<std::shared_ptr<VideoFrame>>* ob)
 }
 
 void
-VideoMixer::update(Observable<std::shared_ptr<VideoFrame>>* ob,
-                   const std::shared_ptr<VideoFrame>& frame_p)
+VideoMixer::update(Observable<std::shared_ptr<MediaFrame>>* ob,
+                   const std::shared_ptr<MediaFrame>& frame_p)
 {
     auto lock(rwMutex_.read());
 
@@ -111,7 +111,7 @@ VideoMixer::update(Observable<std::shared_ptr<VideoFrame>>* ob,
                 x->update_frame.reset(new VideoFrame);
             else
                 x->update_frame->reset();
-            x->update_frame->copyFrom(*frame_p); // copy frame content, it will be destroyed after return
+            x->update_frame->copyFrom(*std::static_pointer_cast<VideoFrame>(frame_p)); // copy frame content, it will be destroyed after return
             x->atomic_swap_render(x->update_frame);
             return;
         }
diff --git a/src/media/video/video_mixer.h b/src/media/video/video_mixer.h
index a7ccc08df4bd68802f61dc4c3c8ba8965c3a45db..57454026bcd0307683a45bac42dfa3377502bea6 100644
--- a/src/media/video/video_mixer.h
+++ b/src/media/video/video_mixer.h
@@ -49,9 +49,9 @@ public:
     int getPixelFormat() const override;
 
     // as VideoFramePassiveReader
-    void update(Observable<std::shared_ptr<VideoFrame>>* ob, const std::shared_ptr<VideoFrame>& v) override;
-    void attached(Observable<std::shared_ptr<VideoFrame>>* ob) override;
-    void detached(Observable<std::shared_ptr<VideoFrame>>* ob) override;
+    void update(Observable<std::shared_ptr<MediaFrame>>* ob, const std::shared_ptr<MediaFrame>& v) override;
+    void attached(Observable<std::shared_ptr<MediaFrame>>* ob) override;
+    void detached(Observable<std::shared_ptr<MediaFrame>>* ob) override;
 
 private:
     NON_COPYABLE(VideoMixer);
diff --git a/src/media/video/video_sender.cpp b/src/media/video/video_sender.cpp
index 93cfe1e0bff9bfd4212a6d49156c5360b6266ae7..d7133641b78afb158cd7b3cc6663c53b4e3103ab 100644
--- a/src/media/video/video_sender.cpp
+++ b/src/media/video/video_sender.cpp
@@ -78,10 +78,10 @@ VideoSender::encodeAndSendVideo(VideoFrame& input_frame)
 }
 
 void
-VideoSender::update(Observable<std::shared_ptr<VideoFrame>>* /*obs*/,
-                    const std::shared_ptr<VideoFrame>& frame_p)
+VideoSender::update(Observable<std::shared_ptr<MediaFrame>>* /*obs*/,
+                    const std::shared_ptr<MediaFrame>& frame_p)
 {
-    encodeAndSendVideo(*frame_p);
+    encodeAndSendVideo(*std::static_pointer_cast<VideoFrame>(frame_p));
 }
 
 void
diff --git a/src/media/video/video_sender.h b/src/media/video/video_sender.h
index 322aa618926aa648144a6846f03df951d6553fcf..cf8eb8174459ba09c237eb780d7965f6e5e2012a 100644
--- a/src/media/video/video_sender.h
+++ b/src/media/video/video_sender.h
@@ -54,8 +54,8 @@ public:
     void forceKeyFrame();
 
     // as VideoFramePassiveReader
-    void update(Observable<std::shared_ptr<VideoFrame>>* obs,
-                const std::shared_ptr<VideoFrame>& frame_p) override;
+    void update(Observable<std::shared_ptr<MediaFrame>>* obs,
+                const std::shared_ptr<MediaFrame>& frame_p) override;
 
     void setMuted(bool isMuted);
     uint16_t getLastSeqValue();