diff --git a/bin/dbus/cx.ring.Ring.VideoManager.xml b/bin/dbus/cx.ring.Ring.VideoManager.xml
index c5a79d3ab1d199375db4a592692be6a0412f96f2..c3f73bf51ff0ae2e6ae99fae3d9bcb2b4e28fb22 100644
--- a/bin/dbus/cx.ring.Ring.VideoManager.xml
+++ b/bin/dbus/cx.ring.Ring.VideoManager.xml
@@ -117,6 +117,15 @@
             </arg>
         </method>
 
+        <method name="setDeviceOrientation" tp:name-for-bindings="setDeviceOrientation">
+            <arg type="s" name="name" direction="in">
+                <tp:docstring>Device name</tp:docstring>
+            </arg>
+            <arg type="i" name="angle" direction="in">
+                <tp:docstring>Angle of device in degrees (counterclockwise)</tp:docstring>
+            </arg>
+        </method>
+
         <method name="getRenderer" tp:name-for-bindings="getRenderer">
             <tp:docstring>Returns a map of information about a call's renderer.</tp:docstring>
             <annotation name="org.qtproject.QtDBus.QtTypeName.Out0" value="MapStringString"/>
diff --git a/bin/dbus/dbusvideomanager.cpp b/bin/dbus/dbusvideomanager.cpp
index e867e5c1a7d7df6fc915cd3ad1f0a8d11486e19a..0ccda3f53dc010e635e54446702a4b01098614df 100644
--- a/bin/dbus/dbusvideomanager.cpp
+++ b/bin/dbus/dbusvideomanager.cpp
@@ -109,6 +109,12 @@ DBusVideoManager::setDecodingAccelerated(const bool& state)
     DRing::setDecodingAccelerated(state);
 }
 
+void
+DBusVideoManager::setDeviceOrientation(const std::string& name, const int& angle)
+{
+    DRing::setDeviceOrientation(name, angle);
+}
+
 std::map<std::string, std::string>
 DBusVideoManager::getRenderer(const std::string& callId)
 {
diff --git a/bin/dbus/dbusvideomanager.h b/bin/dbus/dbusvideomanager.h
index c0d0427d3cbf0a90836ab99e8165b3091cc99c1b..ea3b7f61f10423a6c2d3f819bdc9f7b83520d208 100644
--- a/bin/dbus/dbusvideomanager.h
+++ b/bin/dbus/dbusvideomanager.h
@@ -65,6 +65,7 @@ class DRING_PUBLIC DBusVideoManager :
         bool hasCameraStarted();
         bool getDecodingAccelerated();
         void setDecodingAccelerated(const bool& state);
+        void setDeviceOrientation(const std::string& name, const int& angle);
         std::map<std::string, std::string> getRenderer(const std::string& callId);
         std::string startLocalRecorder(const bool& audioOnly, const std::string& filepath);
         void stopLocalRecorder(const std::string& filepath);
diff --git a/bin/jni/videomanager.i b/bin/jni/videomanager.i
index a84d6c83f96839c1949518641075b299337e0dd4..2cc0d02d3d7dd3de94fcd67c5f71bae7b8129b40 100644
--- a/bin/jni/videomanager.i
+++ b/bin/jni/videomanager.i
@@ -35,6 +35,7 @@
 extern "C" {
 #include <libavutil/pixdesc.h>
 #include <libavutil/imgutils.h>
+#include <libavutil/display.h>
 #include <libavcodec/avcodec.h>
 }
 
@@ -50,6 +51,7 @@ public:
     virtual void decodingStopped(const std::string& id, const std::string& shm_path, bool is_mixer) {}
     virtual std::string startLocalRecorder(const bool& audioOnly, const std::string& filepath) {}
     virtual void stopLocalRecorder(const std::string& filepath) {}
+    virtual void setDeviceOrientation(const std::string&, int angle) {}
 };
 %}
 
@@ -61,8 +63,25 @@ std::map<ANativeWindow*, std::unique_ptr<DRing::FrameBuffer>> windows {};
 std::mutex windows_mutex;
 
 std::vector<uint8_t> workspace;
+int rotAngle = 0;
+AVBufferRef* rotMatrix = nullptr;
+
 extern JavaVM *gJavaVM;
 
+void setRotation(int angle)
+{
+    if (angle == rotAngle)
+        return;
+    AVBufferRef* localFrameDataBuffer = angle == 0 ? nullptr : av_buffer_alloc(sizeof(int32_t) * 9);
+    if (localFrameDataBuffer)
+        av_display_rotation_set(reinterpret_cast<int32_t*>(localFrameDataBuffer->data), angle);
+
+    std::swap(rotMatrix, localFrameDataBuffer);
+    rotAngle = angle;
+
+    av_buffer_unref(&localFrameDataBuffer);
+}
+
 void rotateNV21(uint8_t* yinput, uint8_t* uvinput, unsigned ystride, unsigned uvstride, unsigned width, unsigned height, int rotation, uint8_t* youtput, uint8_t* uvoutput)
 {
     if (rotation == 0) {
@@ -203,22 +222,10 @@ JNIEXPORT void JNICALL Java_cx_ring_daemon_RingserviceJNI_captureVideoFrame(JNIE
             // False YUV422, actually NV12 or NV21
             auto uvdata = std::min(udata, vdata);
             avframe->format = uvdata == udata ? AV_PIX_FMT_NV12 : AV_PIX_FMT_NV21;
-            if (rotation == 0) {
-                avframe->data[0] = ydata;
-                avframe->linesize[0] = ystride;
-                avframe->data[1] = uvdata;
-                avframe->linesize[1] = uvstride;
-            } else {
-                directPointer = false;
-                bool swap = rotation != 0 && rotation != 180;
-                auto ow = avframe->width;
-                auto oh = avframe->height;
-                avframe->width = swap ? oh : ow;
-                avframe->height = swap ? ow : oh;
-                av_frame_get_buffer(avframe, 1);
-                rotateNV21(ydata, uvdata, ystride, uvstride, ow, oh, rotation, avframe->data[0], avframe->data[1]);
-                jenv->CallVoidMethod(image, jenv->GetMethodID(imageClass, "close", "()V"));
-            }
+            avframe->data[0] = ydata;
+            avframe->linesize[0] = ystride;
+            avframe->data[1] = uvdata;
+            avframe->linesize[1] = uvstride;
         }
     } else {
         for (int i=0; i<planeCount; i++) {
@@ -232,6 +239,10 @@ JNIEXPORT void JNICALL Java_cx_ring_daemon_RingserviceJNI_captureVideoFrame(JNIE
         }
     }
 
+    setRotation(rotation);
+    if (rotMatrix)
+        av_frame_new_side_data_from_buf(avframe, AV_FRAME_DATA_DISPLAYMATRIX, av_buffer_ref(rotMatrix));
+
     if (directPointer) {
         image = jenv->NewGlobalRef(image);
         imageClass = (jclass)jenv->NewGlobalRef(imageClass);
@@ -390,6 +401,7 @@ void applySettings(const std::string& name, const std::map<std::string, std::str
 
 void addVideoDevice(const std::string &node);
 void removeVideoDevice(const std::string &node);
+void setDeviceOrientation(const std::string& name, int angle);
 uint8_t* obtainFrame(int length);
 void releaseFrame(uint8_t* frame);
 void registerSinkTarget(const std::string& sinkId, const DRing::SinkTarget& target);
diff --git a/bin/nodejs/videomanager.i b/bin/nodejs/videomanager.i
index ba119b4d92017c3640643f0661d7c7441e9dbc4e..82fed9556a299c8762381a28c439aed19e396935 100644
--- a/bin/nodejs/videomanager.i
+++ b/bin/nodejs/videomanager.i
@@ -38,6 +38,7 @@ public:
     virtual void decodingStopped(const std::string& id, const std::string& shm_path, bool is_mixer) {}
     virtual std::string startLocalRecorder(const bool& audioOnly, const std::string& filepath) {}
     virtual void stopLocalRecorder(const std::string& filepath) {}
+    virtual void setDeviceOrientation(const std::string& name, int angle) {}
 };
 %}
 
@@ -59,6 +60,7 @@ std::map<std::string, std::string> getSettings(const std::string& name);
 void applySettings(const std::string& name, const std::map<std::string, std::string>& settings);
 
 void registerSinkTarget(const std::string& sinkId, const DRing::SinkTarget& target);
+void setDeviceOrientation(const std::string& name, int angle);
 }
 
 class VideoCallback {
diff --git a/configure.ac b/configure.ac
index 28a045b2aa269ca5cf5df195059c0b36a6c7d680..3f208cc44502db1c4f80c5a099e52d766062e881 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([Ring Daemon],[7.4.0],[ring@gnu.org],[ring])
+AC_INIT([Ring Daemon],[7.5.0],[ring@gnu.org],[ring])
 
 AC_COPYRIGHT([[Copyright (c) Savoir-faire Linux 2004-2018]])
 AC_REVISION([$Revision$])
diff --git a/src/client/videomanager.cpp b/src/client/videomanager.cpp
index 7875cbb2c6c76532dc3a53f146fe3c0f8fc05ca8..6d1a42b168b9b58a74ee79fc17d8879e815cf067 100644
--- a/src/client/videomanager.cpp
+++ b/src/client/videomanager.cpp
@@ -347,6 +347,12 @@ setDefaultDevice(const std::string& name)
     ring::Manager::instance().saveConfig();
 }
 
+void
+setDeviceOrientation(const std::string& name, int angle)
+{
+    ring::Manager::instance().getVideoManager().setDeviceOrientation(name, angle);
+}
+
 std::map<std::string, std::string>
 getDeviceParams(const std::string& name)
 {
@@ -619,4 +625,10 @@ getAudioInput(const std::string& id)
     return input;
 }
 
+void
+VideoManager::setDeviceOrientation(const std::string& name, int angle)
+{
+    videoDeviceMonitor.setDeviceOrientation(name, angle);
+}
+
 } // namespace ring
diff --git a/src/client/videomanager.h b/src/client/videomanager.h
index 98e8912b8b5a38827d8b188e01508d669f22d7ea..9c55f689e17fab9579984a2dc7ab09b00d082568 100644
--- a/src/client/videomanager.h
+++ b/src/client/videomanager.h
@@ -38,28 +38,31 @@ namespace ring {
 
 struct VideoManager
 {
-    public:
-        /**
-         * VideoManager acts as a cache of the active VideoInput.
-         * When this input is needed, you must use getVideoCamera
-         * to create the instance if not done yet and obtain a shared pointer
-         * for your own usage.
-         * VideoManager instance doesn't increment the reference count of
-         * this video input instance: this instance is destroyed when the last
-         * external user has released its shared pointer.
-         */
-        std::weak_ptr<video::VideoInput> videoInput;
-        std::shared_ptr<video::VideoFrameActiveWriter> videoPreview;
-        video::VideoDeviceMonitor videoDeviceMonitor;
-        std::atomic_bool started;
-        /**
-         * VideoManager also acts as a cache of the active AudioInput(s).
-         * When one of these is needed, you must use getAudioInput, which will
-         * create an instance if need be and return a shared_ptr.
-         */
-        std::map<std::string, std::weak_ptr<AudioInput>> audioInputs;
-        std::mutex audioMutex;
-        std::shared_ptr<AudioInput> audioPreview;
+public:
+
+    void setDeviceOrientation(const std::string& name, int angle);
+
+    /**
+     * VideoManager acts as a cache of the active VideoInput.
+     * When this input is needed, you must use getVideoCamera
+     * to create the instance if not done yet and obtain a shared pointer
+     * for your own usage.
+     * VideoManager instance doesn't increment the reference count of
+     * this video input instance: this instance is destroyed when the last
+     * external user has released its shared pointer.
+     */
+    std::weak_ptr<video::VideoInput> videoInput;
+    std::shared_ptr<video::VideoFrameActiveWriter> videoPreview;
+    video::VideoDeviceMonitor videoDeviceMonitor;
+    std::atomic_bool started;
+    /**
+     * VideoManager also acts as a cache of the active AudioInput(s).
+     * When one of these is needed, you must use getAudioInput, which will
+     * create an instance if need be and return a shared_ptr.
+     */
+    std::map<std::string, std::weak_ptr<AudioInput>> audioInputs;
+    std::mutex audioMutex;
+    std::shared_ptr<AudioInput> audioPreview;
 };
 
 std::shared_ptr<video::VideoFrameActiveWriter> getVideoCamera();
diff --git a/src/dring/videomanager_interface.h b/src/dring/videomanager_interface.h
index 2fdf7ae0f3ad2e57ceccddcb53e639c5e3b20842..d696bfcf881ee03f6108c870cf57beaadc53c821 100644
--- a/src/dring/videomanager_interface.h
+++ b/src/dring/videomanager_interface.h
@@ -163,6 +163,7 @@ DRING_PUBLIC VideoCapabilities getCapabilities(const std::string& name);
 DRING_PUBLIC std::map<std::string, std::string> getSettings(const std::string& name);
 DRING_PUBLIC void applySettings(const std::string& name, const std::map<std::string, std::string>& settings);
 DRING_PUBLIC void setDefaultDevice(const std::string& name);
+DRING_PUBLIC void setDeviceOrientation(const std::string& name, int angle);
 
 DRING_PUBLIC std::map<std::string, std::string> getDeviceParams(const std::string& name);
 
diff --git a/src/media/media_device.h b/src/media/media_device.h
index 802ea2f2f7c44bea63d3f6a4c8e10c05b5347481..b590b1d3ab7aa136fa2f1f57eddea5398f042650 100644
--- a/src/media/media_device.h
+++ b/src/media/media_device.h
@@ -48,6 +48,7 @@ struct DeviceParams {
     std::string sdp_flags {};
     unsigned offset_x {};
     unsigned offset_y {};
+    int orientation {};
 };
 
 }
diff --git a/src/media/media_filter.cpp b/src/media/media_filter.cpp
index c5c70ab0afe855e6e76be5bfeb401d18e8790d2e..7d3ac7cfee7c0fdbf1f85069d3a684948ff9728c 100644
--- a/src/media/media_filter.cpp
+++ b/src/media/media_filter.cpp
@@ -189,28 +189,36 @@ MediaFilter::feedInput(AVFrame* frame, const std::string& inputName)
     return fail(ss.str(), AVERROR(EINVAL));
 }
 
-AVFrame*
+std::unique_ptr<MediaFrame>
 MediaFilter::readOutput()
 {
     if (!initialized_) {
         fail("Not properly initialized", -1);
-        return nullptr;
+        return {};
     }
 
-    int ret = 0;
-    AVFrame* frame = av_frame_alloc();
-    ret = av_buffersink_get_frame_flags(output_, frame, 0);
-    if (ret >= 0) {
+    std::unique_ptr<MediaFrame> frame;
+    switch (av_buffersink_get_type(output_)) {
+    case AVMEDIA_TYPE_VIDEO:
+        frame = std::make_unique<VideoFrame>();
+        break;
+    case AVMEDIA_TYPE_AUDIO:
+        frame = std::make_unique<AudioFrame>();
+        break;
+    default:
+        return {};
+    }
+    auto err = av_buffersink_get_frame(output_, frame->pointer());
+    if (err >= 0) {
         return frame;
-    } else if (ret == AVERROR(EAGAIN)) {
+    } else if (err == AVERROR(EAGAIN)) {
         // no data available right now, try again
-    } else if (ret == AVERROR_EOF) {
+    } else if (err == AVERROR_EOF) {
         RING_WARN() << "Filters have reached EOF, no more frames will be output";
     } else {
-        fail("Error occurred while pulling from filter graph", ret);
+        fail("Error occurred while pulling from filter graph", err);
     }
-    av_frame_free(&frame);
-    return nullptr;
+    return {};
 }
 
 void
diff --git a/src/media/media_filter.h b/src/media/media_filter.h
index 36f2a464212bce6f329d7d596a309deba13552c5..8791219369632d3ae0785e177f4e937be4823c8a 100644
--- a/src/media/media_filter.h
+++ b/src/media/media_filter.h
@@ -97,7 +97,7 @@ class MediaFilter {
          *
          * NOTE Frame reference belongs to the caller
          */
-        AVFrame* readOutput();
+        std::unique_ptr<MediaFrame> readOutput();
 
         /**
          * Flush filter to indicate EOF.
diff --git a/src/media/media_recorder.cpp b/src/media/media_recorder.cpp
index f6b02fc461a80360d38ec08c635e45337368ec65..0385e2886aca427da552def13e0d1d4292a289b7 100644
--- a/src/media/media_recorder.cpp
+++ b/src/media/media_recorder.cpp
@@ -461,11 +461,10 @@ MediaRecorder::filterAndEncode(MediaFilter* filter, int streamIdx)
         while (auto frame = filter->readOutput()) {
             try {
                 std::lock_guard<std::mutex> lk(mutex_);
-                encoder_->encode(frame, streamIdx);
+                encoder_->encode(frame->pointer(), streamIdx);
             } catch (const MediaEncoderException& e) {
                 RING_ERR() << "Failed to record frame: " << e.what();
             }
-            av_frame_free(&frame);
         }
     }
 }
diff --git a/src/media/video/accel.cpp b/src/media/video/accel.cpp
index 1f0502ef2d1e7d616caa97ee7965b0a516ae7e68..1e7e7216c7bc38dbe3ba0d72d5d70b2f8fff4400 100644
--- a/src/media/video/accel.cpp
+++ b/src/media/video/accel.cpp
@@ -226,6 +226,8 @@ HardwareAccel::transferToMainMemory(const VideoFrame& frame, AVPixelFormat desir
     }
 
     output->pts = input->pts;
+    if (AVFrameSideData* side_data = av_frame_get_side_data(input, AV_FRAME_DATA_DISPLAYMATRIX))
+        av_frame_new_side_data_from_buf(output, AV_FRAME_DATA_DISPLAYMATRIX, av_buffer_ref(side_data->buf));
     return out;
 }
 
diff --git a/src/media/video/sinkclient.cpp b/src/media/video/sinkclient.cpp
index 09b0cf8c2cc7438d46494ca0a8df50af5910ca77..b51c1cc2cb2b8c3679a41ff49536ea1284ed107d 100644
--- a/src/media/video/sinkclient.cpp
+++ b/src/media/video/sinkclient.cpp
@@ -38,6 +38,7 @@
 #include "libav_utils.h"
 #include "video_scaler.h"
 #include "smartools.h"
+#include "media_filter.h"
 
 #ifdef RING_ACCEL
 #include "accel.h"
@@ -54,9 +55,16 @@
 #include <cerrno>
 #include <cstring>
 #include <stdexcept>
+#include <cmath>
+
+extern "C" {
+#include <libavutil/display.h>
+}
 
 namespace ring { namespace video {
 
+const constexpr char FILTER_INPUT_NAME[] = "in";
+
 #if HAVE_SHM
 // RAII class helper on sem_wait/sem_post sempahore operations
 class SemGuardLock {
@@ -82,7 +90,7 @@ class SemGuardLock {
 class ShmHolder
 {
     public:
-        ShmHolder(const std::string& name={});
+        ShmHolder(const std::string& name = {});
         ~ShmHolder();
 
         std::string name() const noexcept {
@@ -316,12 +324,60 @@ SinkClient::SinkClient(const std::string& id, bool mixer)
 #endif
 {}
 
+void
+SinkClient::setRotation(int rotation)
+{
+    if (rotation_ == rotation || width_ == 0 || height_ == 0)
+        return;
+
+    rotation_ = rotation;
+    RING_WARN("Rotation set to %d", rotation_);
+    auto in_name = FILTER_INPUT_NAME;
+
+    std::stringstream ss;
+
+    ss << "[" << in_name << "] " << "format=rgb32,";  // avoid https://trac.ffmpeg.org/ticket/5356
+
+    switch (rotation_) {
+        case 90 :
+        case -270 :
+            ss << "transpose=2";
+            break;
+        case 180 :
+        case -180 :
+            ss << "rotate=PI";
+            break;
+        case 270 :
+        case -90 :
+            ss << "transpose=1";
+            break;
+        default :
+            ss << "null";
+    }
+
+    const auto format = AV_PIX_FMT_RGB32;
+    const auto one = rational<int>(1);
+    std::vector<MediaStream> msv;
+    msv.emplace_back(in_name, format, one, width_, height_, one, one);
+
+    if (!rotation_) {
+        filter_.reset();
+    }
+    else {
+        filter_.reset(new MediaFilter);
+        auto ret = filter_->initialize(ss.str(), msv);
+        if (ret < 0) {
+            RING_ERR() << "filter init fail";
+            filter_ = nullptr;
+            rotation_ = 0;
+        }
+    }
+}
+
 void
 SinkClient::update(Observable<std::shared_ptr<MediaFrame>>* /*obs*/,
                    const std::shared_ptr<MediaFrame>& frame_p)
 {
-    auto& f = *std::static_pointer_cast<VideoFrame>(frame_p);
-
 #ifdef DEBUG_FPS
     auto currentTime = std::chrono::system_clock::now();
     const std::chrono::duration<double> seconds = currentTime - lastFrameDebug_;
@@ -338,7 +394,7 @@ SinkClient::update(Observable<std::shared_ptr<MediaFrame>>* /*obs*/,
 
     if (avTarget_.push) {
         auto outFrame = std::make_unique<VideoFrame>();
-        outFrame->copyFrom(f);
+        outFrame->copyFrom(*std::static_pointer_cast<VideoFrame>(frame_p));
         avTarget_.push(std::move(outFrame));
     }
 
@@ -349,18 +405,32 @@ SinkClient::update(Observable<std::shared_ptr<MediaFrame>>* /*obs*/,
 
     if (doTransfer) {
 #ifdef RING_ACCEL
-        auto framePtr = HardwareAccel::transferToMainMemory(f, AV_PIX_FMT_NV12);
-        const auto& swFrame = *framePtr;
+        std::shared_ptr<VideoFrame> frame {HardwareAccel::transferToMainMemory(*std::static_pointer_cast<VideoFrame>(frame_p), AV_PIX_FMT_NV12)};
 #else
-        const auto& swFrame = f;
+        std::shared_ptr<VideoFrame> frame {std::static_pointer_cast<VideoFrame>(frame_p)};
 #endif
+        AVFrameSideData* side_data = av_frame_get_side_data(frame->pointer(), AV_FRAME_DATA_DISPLAYMATRIX);
+        if (side_data) {
+            auto matrix_rotation = reinterpret_cast<int32_t*>(side_data->data);
+            auto angle = av_display_rotation_get(matrix_rotation);
+            if (!std::isnan(angle))
+                setRotation(angle);
+            if (filter_) {
+                filter_->feedInput(frame->pointer(), FILTER_INPUT_NAME);
+                frame = std::static_pointer_cast<VideoFrame>(std::shared_ptr<MediaFrame>(filter_->readOutput()));
+            }
+            if (frame->height() != height_ || frame->width() != width_) {
+                setFrameSize(0, 0);
+                setFrameSize(frame->width(), frame->height());
+            }
+        }
 #if HAVE_SHM
-        shm_->renderFrame(swFrame);
+        shm_->renderFrame(*frame);
 #endif
         if (target_.pull) {
             VideoFrame dst;
-            const int width = swFrame.width();
-            const int height = swFrame.height();
+            const int width = frame->width();
+            const int height = frame->height();
 #if defined(__ANDROID__) || (defined(__APPLE__) && !TARGET_OS_IPHONE)
             const int format = AV_PIX_FMT_RGBA;
 #else
@@ -373,7 +443,7 @@ SinkClient::update(Observable<std::shared_ptr<MediaFrame>>* /*obs*/,
                     buffer_ptr->width = width;
                     buffer_ptr->height = height;
                     dst.setFromMemory(buffer_ptr->ptr, format, width, height);
-                    scaler_->scale(swFrame, dst);
+                    scaler_->scale(*frame, dst);
                     target_.push(std::move(buffer_ptr));
                 }
             }
diff --git a/src/media/video/sinkclient.h b/src/media/video/sinkclient.h
index 5d76751523bef426b683c79633b494bb0e584f04..def2e47d6f9b3481a48c44f1cceef479a8a41c0f 100644
--- a/src/media/video/sinkclient.h
+++ b/src/media/video/sinkclient.h
@@ -35,6 +35,8 @@
 
 #define DEBUG_FPS
 
+namespace ring {class MediaFilter;}
+
 namespace ring { namespace video {
 
 #if HAVE_SHM
@@ -88,9 +90,13 @@ class SinkClient : public VideoFramePassiveReader
         int width_ {0};
         int height_ {0};
         bool started_ {false}; // used to arbitrate client's stop signal.
+        int rotation_ {0};
         DRing::SinkTarget target_;
         DRing::AVSinkTarget avTarget_;
         std::unique_ptr<VideoScaler> scaler_;
+        std::unique_ptr<MediaFilter> filter_;
+
+        void setRotation(int rotation);
 
 #ifdef DEBUG_FPS
         unsigned frameCount_;
diff --git a/src/media/video/v4l2/video_device_impl.cpp b/src/media/video/v4l2/video_device_impl.cpp
index 57386c119b5de784f30d9bf79f82ac1f11f6700d..c6cc3e88ef8cefb4ae9804ffb4556a61be7c5613 100644
--- a/src/media/video/v4l2/video_device_impl.cpp
+++ b/src/media/video/v4l2/video_device_impl.cpp
@@ -578,7 +578,9 @@ VideoDevice::VideoDevice(const std::string& path, const std::vector<std::map<std
 DeviceParams
 VideoDevice::getDeviceParams() const
 {
-    return deviceImpl_->getDeviceParams();
+    auto params = deviceImpl_->getDeviceParams();
+    params.orientation = orientation_;
+    return params;
 }
 
 void
diff --git a/src/media/video/video_device.h b/src/media/video/video_device.h
index 645a15701867e56b45a89089c576a8d968c73191..9b319274d82ffa50c2bea2d10728612541933e7e 100644
--- a/src/media/video/video_device.h
+++ b/src/media/video/video_device.h
@@ -163,6 +163,10 @@ public:
         setDeviceParams(params);
     }
 
+    void setOrientation(int orientation) {
+      orientation_ = orientation;
+    }
+
     /**
      * Returns the parameters needed for actual use of the device
      */
@@ -220,6 +224,8 @@ private:
      */
     std::string node_ {};
 
+    int orientation_ {0};
+
     /*
      * Device specific implementation.
      * On Linux, V4L2 stuffs go there.
diff --git a/src/media/video/video_device_monitor.cpp b/src/media/video/video_device_monitor.cpp
index 81513ff40cbb93ec19323dfd72962f8aa1157c4b..568cb463f44d4988b230838779b122235535e322 100644
--- a/src/media/video/video_device_monitor.cpp
+++ b/src/media/video/video_device_monitor.cpp
@@ -130,6 +130,15 @@ VideoDeviceMonitor::setDefaultDevice(const std::string& name)
     }
 }
 
+void
+VideoDeviceMonitor::setDeviceOrientation(const std::string& name, int angle)
+{
+    const auto itd = findDeviceByName(name);
+    if (itd != devices_.cend()) {
+        itd->setOrientation(angle);
+    }
+}
+
 DeviceParams
 VideoDeviceMonitor::getDeviceParams(const std::string& name) const
 {
diff --git a/src/media/video/video_device_monitor.h b/src/media/video/video_device_monitor.h
index 63f14e3ac3900c4a1bbc660137949dee781711a3..03adfe7eb303c213569525badb505849ee3e75cf 100644
--- a/src/media/video/video_device_monitor.h
+++ b/src/media/video/video_device_monitor.h
@@ -56,6 +56,7 @@ class VideoDeviceMonitor : public Serializable
         std::string getDefaultDevice() const;
         std::string getMRLForDefaultDevice() const;
         void setDefaultDevice(const std::string& name);
+        void setDeviceOrientation(const std::string& name, int angle);
 
         void addDevice(const std::string &node, const std::vector<std::map<std::string, std::string>>* devInfo=nullptr);
         void removeDevice(const std::string &node);
diff --git a/src/media/video/video_input.cpp b/src/media/video/video_input.cpp
index 168795c0dcbf2dbaa305c526442f92cf01057926..0d5981c41f4524e3a56156fdcd50c8d620a6e927 100644
--- a/src/media/video/video_input.cpp
+++ b/src/media/video/video_input.cpp
@@ -44,6 +44,9 @@
 #else
 #include <unistd.h>
 #endif
+extern "C" {
+#include <libavutil/display.h>
+}
 
 namespace ring { namespace video {
 
@@ -70,6 +73,9 @@ VideoInput::~VideoInput()
     frame_cv_.notify_one();
 #endif
     loop_.join();
+
+    if (auto localFrameDataBuffer = frameDataBuffer_.exchange(nullptr))
+        av_buffer_unref(&localFrameDataBuffer);
 }
 
 #if defined(__ANDROID__) || defined(RING_UWP) || (defined(TARGET_OS_IOS) && TARGET_OS_IOS)
@@ -111,11 +117,19 @@ void VideoInput::process()
         return;
     }
 
+    if (decOpts_.orientation != rotation_) {
+        setRotation(decOpts_.orientation);
+        rotation_ = decOpts_.orientation;
+    }
+
     for (auto& buffer : buffers_) {
         if (buffer.status == BUFFER_FULL && buffer.index == publish_index_) {
             auto& frame = getNewFrame();
             AVPixelFormat format = getPixelFormat();
 
+            if (auto localFDB = frameDataBuffer_.load())
+                av_frame_new_side_data_from_buf(frame.pointer(), AV_FRAME_DATA_DISPLAYMATRIX, av_buffer_ref(localFDB));
+
             buffer.status = BUFFER_PUBLISHED;
             frame.setFromMemory((uint8_t*)buffer.data, format, decOpts_.width, decOpts_.height,
                                 [wthis](uint8_t* ptr) {
@@ -132,6 +146,18 @@ void VideoInput::process()
     }
 }
 
+void
+VideoInput::setRotation(int angle)
+{
+    auto localFrameDataBuffer = (angle == 0) ? nullptr : av_buffer_alloc(sizeof(int32_t) * 9);
+    if (localFrameDataBuffer)
+        av_display_rotation_set(reinterpret_cast<int32_t*>(localFrameDataBuffer->data), angle);
+
+    localFrameDataBuffer = frameDataBuffer_.exchange(localFrameDataBuffer);
+
+    av_buffer_unref(&localFrameDataBuffer);
+}
+
 void VideoInput::cleanup()
 {
     emitSignal<DRing::VideoSignal::StopCapture>();
@@ -148,6 +174,8 @@ void VideoInput::cleanup()
             RING_ERR("Failed to free buffer [%p]", buffer.data);
         }
     }
+
+    setRotation(0);
 }
 #else
 
diff --git a/src/media/video/video_input.h b/src/media/video/video_input.h
index 36a1c26ea0bf67bcc842d2105ba5cbf2489deb4e..ff08614a6be4047100c96d50dd57d01ab8b8df21 100644
--- a/src/media/video/video_input.h
+++ b/src/media/video/video_input.h
@@ -74,6 +74,9 @@ public:
     ~VideoInput();
 
     // as VideoGenerator
+    const std::string& getName() const {
+      return currentResource_;
+    }
     int getWidth() const;
     int getHeight() const;
     AVPixelFormat getPixelFormat() const;
@@ -114,6 +117,10 @@ private:
     void createDecoder();
     void deleteDecoder();
 
+    int rotation_ {0};
+    std::atomic<AVBufferRef*> frameDataBuffer_ {nullptr};
+    void setRotation(int angle);
+
     // true if decOpts_ is ready to use, false if using promise/future
     bool initCamera(const std::string& device);
     bool initX11(std::string display);
diff --git a/src/media/video/video_receive_thread.cpp b/src/media/video/video_receive_thread.cpp
index 091263c46d22189cfd4f9349d4937634321bf98b..a454f680b30ff42552d7c1cba9d8bafec1e4f283 100644
--- a/src/media/video/video_receive_thread.cpp
+++ b/src/media/video/video_receive_thread.cpp
@@ -29,6 +29,10 @@
 #include "logger.h"
 #include "smartools.h"
 
+extern "C" {
+#include <libavutil/display.h>
+}
+
 #include <unistd.h>
 #include <map>
 
@@ -48,6 +52,7 @@ VideoReceiveThread::VideoReceiveThread(const std::string& id,
     , sdpContext_(stream_.str().size(), false, &readFunction, 0, 0, this)
     , sink_ {Manager::instance().createSinkClient(id)}
     , mtu_(mtu)
+    , rotation_(0)
     , requestKeyFrameCallback_(0)
     , loop_(std::bind(&VideoReceiveThread::setup, this),
             std::bind(&VideoReceiveThread::process, this),
@@ -57,6 +62,8 @@ VideoReceiveThread::VideoReceiveThread(const std::string& id,
 VideoReceiveThread::~VideoReceiveThread()
 {
     loop_.join();
+    auto localFDB = frameDataBuffer.exchange(nullptr);
+    av_buffer_unref(&localFDB);
 }
 
 void
@@ -183,6 +190,9 @@ bool VideoReceiveThread::decodeFrame()
     auto& frame = getNewFrame();
     const auto ret = videoDecoder_->decode(frame);
 
+    if (auto localFDB = frameDataBuffer.load())
+        av_frame_new_side_data_from_buf(frame.pointer(), AV_FRAME_DATA_DISPLAYMATRIX, av_buffer_ref(localFDB));
+
     switch (ret) {
         case MediaDecoder::Status::FrameFinished:
             publishFrame();
@@ -258,4 +268,17 @@ VideoReceiveThread::triggerKeyFrameRequest()
         requestKeyFrameCallback_();
 }
 
+void
+VideoReceiveThread::setRotation(int angle)
+{
+    auto localFrameDataBuffer = av_buffer_alloc(sizeof(int32_t) * 9);  // matrix 3x3 of int32_t
+
+    if (localFrameDataBuffer)
+        av_display_rotation_set(reinterpret_cast<int32_t*>(localFrameDataBuffer->data), angle);
+
+    localFrameDataBuffer = frameDataBuffer.exchange(localFrameDataBuffer);
+
+    av_buffer_unref(&localFrameDataBuffer);
+}
+
 }} // namespace ring::video
diff --git a/src/media/video/video_receive_thread.h b/src/media/video/video_receive_thread.h
index b92e452f6bb7756f50b3eaef8c4c59ed35b8236e..563f5d4f6c6dd93f54494634d098dd670aa3045b 100644
--- a/src/media/video/video_receive_thread.h
+++ b/src/media/video/video_receive_thread.h
@@ -63,6 +63,13 @@ public:
     MediaStream getInfo() const;
     void triggerKeyFrameRequest();
 
+    /**
+      * Set angle of rotation to apply to the video by the decoder
+      *
+      * @param angle Angle of rotation in degrees (counterclockwise)
+      */
+    void setRotation(int angle);
+
 private:
     NON_COPYABLE(VideoReceiveThread);
 
@@ -81,6 +88,8 @@ private:
     std::shared_ptr<SinkClient> sink_;
     bool isReset_;
     uint16_t mtu_;
+    int rotation_;
+    std::atomic<AVBufferRef*> frameDataBuffer {nullptr};
 
     std::function<void(void)> requestKeyFrameCallback_;
     void openDecoder();
diff --git a/src/media/video/video_rtp_session.cpp b/src/media/video/video_rtp_session.cpp
index f4557830c9005e0b7fa7ead32bb99e9ad67a91d5..c8370318b23e758c6a244019a9998b384fbc84a4 100644
--- a/src/media/video/video_rtp_session.cpp
+++ b/src/media/video/video_rtp_session.cpp
@@ -100,9 +100,9 @@ void VideoRtpSession::startSender()
                 auto newParams = input->switchInput(input_);
                 try {
                     if (newParams.valid() &&
-                        newParams.wait_for(NEWPARAMS_TIMEOUT) == std::future_status::ready)
+                        newParams.wait_for(NEWPARAMS_TIMEOUT) == std::future_status::ready) {
                         localVideoParams_ = newParams.get();
-                    else {
+                    } else {
                         RING_ERR("No valid new video parameters.");
                         return;
                     }
@@ -127,6 +127,9 @@ void VideoRtpSession::startSender()
             socketPair_->stopSendOp(false);
             sender_.reset(new VideoSender(getRemoteRtpUri(), localVideoParams_,
                                           send_, *socketPair_, initSeqVal_, mtu_));
+            if (changeOrientationCallback_)
+                sender_->setChangeOrientationCallback(changeOrientationCallback_);
+
         } catch (const MediaEncoderException &e) {
             RING_ERR("%s", e.what());
             send_.enabled = false;
@@ -246,6 +249,12 @@ void VideoRtpSession::forceKeyFrame()
         sender_->forceKeyFrame();
 }
 
+void
+VideoRtpSession::setRotation(int rotation)
+{
+    receiveThread_->setRotation(rotation);
+}
+
 void
 VideoRtpSession::setupVideoPipeline()
 {
@@ -357,7 +366,7 @@ unsigned
 VideoRtpSession::getLowerQuality()
 {
     // if lower quality was stored we return it
-    unsigned quality = videoBitrateInfo_.videoQualityCurrent;
+    unsigned quality = 0;
     while ( not histoQuality_.empty()) {
         quality = histoQuality_.back();
         histoQuality_.pop_back();
@@ -374,7 +383,7 @@ unsigned
 VideoRtpSession::getLowerBitrate()
 {
     // if a lower bitrate was stored we return it
-    unsigned bitrate = videoBitrateInfo_.videoBitrateCurrent;
+    unsigned bitrate = 0;
     while ( not histoBitrate_.empty()) {
         bitrate = histoBitrate_.back();
         histoBitrate_.pop_back();
@@ -599,4 +608,10 @@ VideoRtpSession::deinitRecorder(std::shared_ptr<MediaRecorder>& rec)
     }
 }
 
+void
+VideoRtpSession::setChangeOrientationCallback(std::function<void(int)> cb)
+{
+    changeOrientationCallback_ = cb;
+}
+
 }} // namespace ring::video
diff --git a/src/media/video/video_rtp_session.h b/src/media/video/video_rtp_session.h
index 4ed06bad7146b13c9c6ef2ae4a30dd81f8046293..b840f4721ce95dad608ee9c94b7165b3a0aa4a19 100644
--- a/src/media/video/video_rtp_session.h
+++ b/src/media/video/video_rtp_session.h
@@ -71,6 +71,14 @@ public:
     void restartSender() override;
     void stop() override;
 
+    /**
+      * Set video orientation
+      *
+      * Send to the receive thread rotation to apply to the video (counterclockwise)
+      *
+      * @param rotation Rotation in degrees (counterclockwise)
+      */
+    void setRotation(int rotation);
     void forceKeyFrame();
     void bindMixer(VideoMixer* mixer);
     void unbindMixer();
@@ -79,6 +87,11 @@ public:
     void switchInput(const std::string& input) {
         input_ = input;
     }
+    const std::string& getInput() const {
+      return input_;
+    }
+
+    void setChangeOrientationCallback(std::function<void(int)> cb);
 
     bool useCodec(const AccountVideoCodecInfo* codec) const;
 
@@ -140,6 +153,8 @@ private:
 
     InterruptedThreadLoop packetLossThread_;
     void processPacketLoss();
+
+    std::function<void(int)> changeOrientationCallback_;
 };
 
 }} // namespace ring::video
diff --git a/src/media/video/video_sender.cpp b/src/media/video/video_sender.cpp
index f9a9237b83e8c83b8d059171f1f9c8596b8e63fb..ec5ff42b54c9a22b81c5e7b2274dd87a8043e26f 100644
--- a/src/media/video/video_sender.cpp
+++ b/src/media/video/video_sender.cpp
@@ -34,6 +34,9 @@
 
 #include <map>
 #include <unistd.h>
+extern "C" {
+#include <libavutil/display.h>
+}
 
 namespace ring { namespace video {
 
@@ -88,6 +91,17 @@ VideoSender::encodeAndSendVideo(VideoFrame& input_frame)
         if (is_keyframe)
             --forceKeyFrame_;
 
+        AVFrameSideData* side_data = av_frame_get_side_data(input_frame.pointer(), AV_FRAME_DATA_DISPLAYMATRIX);
+        if (side_data) {
+            auto matrix_rotation = reinterpret_cast<int32_t*>(side_data->data);
+            auto angle = av_display_rotation_get(matrix_rotation);
+            if (rotation_ != angle) {
+                rotation_ = angle;
+                if (changeOrientationCallback_)
+                    changeOrientationCallback_(rotation_);
+            }
+        }
+
 #ifdef RING_ACCEL
         auto framePtr = HardwareAccel::transferToMainMemory(input_frame, AV_PIX_FMT_NV12);
         auto& swFrame = *framePtr;
@@ -125,4 +139,10 @@ VideoSender::useCodec(const ring::AccountVideoCodecInfo* codec) const
     return videoEncoder_->useCodec(codec);
 }
 
+void
+VideoSender::setChangeOrientationCallback(std::function<void(int)> cb)
+{
+    changeOrientationCallback_ = cb;
+}
+
 }} // namespace ring::video
diff --git a/src/media/video/video_sender.h b/src/media/video/video_sender.h
index 551808dab4c4b263603482e7be3bf2e364d35221..b98c178b0b630da1fc38dc21658c6559aad3cc2b 100644
--- a/src/media/video/video_sender.h
+++ b/src/media/video/video_sender.h
@@ -61,6 +61,8 @@ public:
 
     bool useCodec(const AccountVideoCodecInfo* codec) const;
 
+    void setChangeOrientationCallback(std::function<void(int)> cb);
+
 private:
     static constexpr int KEYFRAMES_AT_START {4}; // Number of keyframes to enforce at stream startup
     static constexpr unsigned KEY_FRAME_PERIOD {0}; // seconds before forcing a keyframe
@@ -77,6 +79,9 @@ private:
     std::atomic<int> forceKeyFrame_ {KEYFRAMES_AT_START};
     int keyFrameFreq_ {0}; // Set keyframe rate, 0 to disable auto-keyframe. Computed in constructor
     int64_t frameNumber_ = 0;
+
+    int rotation_ = 0;
+    std::function<void(int)> changeOrientationCallback_;
 };
 }} // namespace ring::video
 
diff --git a/src/sip/sipcall.cpp b/src/sip/sipcall.cpp
index c2f2cf89ade3132d581bd797f6222675b41e3788..e66dc0ba84acc8a47a58d065f537e1e6e3326607 100644
--- a/src/sip/sipcall.cpp
+++ b/src/sip/sipcall.cpp
@@ -693,6 +693,20 @@ SIPCall::carryingDTMFdigits(char code)
     }
 }
 
+void
+SIPCall::setVideoOrientation(int rotation)
+{
+    std::string sip_body =
+        "<?xml version=\"1.0\" encoding=\"utf-8\" ?>"
+        "<media_control><vc_primitive><to_encoder>"
+        "<device_orientation=" + std::to_string(rotation) + "/>"
+        "</to_encoder></vc_primitive></media_control>";
+
+    RING_DBG("Sending device orientation via SIP INFO");
+
+    sendSIPInfo(sip_body.c_str(), "media_control+xml");
+}
+
 void
 SIPCall::sendTextMessage(const std::map<std::string, std::string>& messages,
                          const std::string& from)
@@ -895,6 +909,12 @@ SIPCall::startAllMedia()
                 this_->requestKeyframe();
         });
     });
+    videortp_->setChangeOrientationCallback([wthis = weak()] (int angle) {
+        runOnMainThread([wthis, angle] {
+            if (auto this_ = wthis.lock())
+                this_->setVideoOrientation(angle);
+        });
+    });
 #endif
 
     for (const auto& slot : slots) {
diff --git a/src/sip/sipcall.h b/src/sip/sipcall.h
index 3a572a1aadaf7b7236a8b43ab8b5541836514d62..22f77f8e6635dddf9349ee070d189ebdca0c57b1 100644
--- a/src/sip/sipcall.h
+++ b/src/sip/sipcall.h
@@ -97,6 +97,13 @@ public: // overridden
     void switchInput(const std::string& resource) override;
     void peerHungup() override;
     void carryingDTMFdigits(char code) override;
+
+    /**
+      * Send device orientation through SIP INFO
+      * @param rotation Device orientation (0/90/180/270) (counterclockwise)
+      */
+    void setVideoOrientation(int rotation);
+
     void sendTextMessage(const std::map<std::string, std::string>& messages,
                          const std::string& from) override;
     void removeCall() override;
diff --git a/src/sip/sipvoiplink.cpp b/src/sip/sipvoiplink.cpp
index 11c702b1a0e1344da6266de24a067eceb42d4056..c15d9e9a6cc0fdc2ca0712cea1e34d21e3e3b93a 100644
--- a/src/sip/sipvoiplink.cpp
+++ b/src/sip/sipvoiplink.cpp
@@ -64,6 +64,7 @@
 
 #include <istream>
 #include <algorithm>
+#include <regex>
 
 namespace ring {
 
@@ -958,10 +959,27 @@ handleMediaControl(SIPCall& call, pjsip_msg_body* body)
         /* Apply and answer the INFO request */
         pj_strset(&control_st, (char *) body->data, body->len);
         const pj_str_t PICT_FAST_UPDATE = CONST_PJ_STR("picture_fast_update");
+        const pj_str_t DEVICE_ORIENTATION = CONST_PJ_STR("device_orientation");
 
         if (pj_strstr(&control_st, &PICT_FAST_UPDATE)) {
             call.sendKeyframe();
             return true;
+        } else if (pj_strstr(&control_st, &DEVICE_ORIENTATION)) {
+            int rotation = 0;
+            std::string body_msg = control_st.ptr;
+            std::smatch matched_pattern;
+            std::regex str_pattern("device_orientation=([-+]?[0-9]+)");
+
+            std::regex_search(body_msg, matched_pattern, str_pattern);
+            if (matched_pattern.ready() && !matched_pattern.empty() && matched_pattern[1].matched) {
+                rotation = std::stoi(matched_pattern[1]);
+
+                RING_WARN("Rotate video %d deg.", rotation);
+
+                call.getVideoRtp().setRotation(rotation);
+
+                return true;
+            }
         }
     }
 
diff --git a/test/unitTest/media/test_media_filter.cpp b/test/unitTest/media/test_media_filter.cpp
index d99d4f62579ef0839d73fd4e00653da50a63269b..ce5c100062b09ab554328442acf41081f6c5b952 100644
--- a/test/unitTest/media/test_media_filter.cpp
+++ b/test/unitTest/media/test_media_filter.cpp
@@ -161,10 +161,10 @@ MediaFilterTest::testAudioFilter()
     CPPUNIT_ASSERT(filter_->feedInput(frame, "in1") >= 0);
     auto out = filter_->readOutput();
     CPPUNIT_ASSERT(out);
+    CPPUNIT_ASSERT(out->pointer());
 
     // check if the filter worked
-    CPPUNIT_ASSERT(out->format == AV_SAMPLE_FMT_U8);
-    av_frame_free(&out);
+    CPPUNIT_ASSERT(out->pointer()->format == AV_SAMPLE_FMT_U8);
 }
 
 void
@@ -210,7 +210,7 @@ MediaFilterTest::testAudioMixing()
         // read output
         auto out = filter_->readOutput();
         CPPUNIT_ASSERT(out);
-        av_frame_free(&out);
+        CPPUNIT_ASSERT(out->pointer());
 
         av_frame_unref(frame1);
         av_frame_unref(frame2);
@@ -266,10 +266,10 @@ MediaFilterTest::testVideoFilter()
     CPPUNIT_ASSERT(filter_->feedInput(extra, top) >= 0);
     auto out = filter_->readOutput();
     CPPUNIT_ASSERT(out);
+    CPPUNIT_ASSERT(out->pointer());
 
     // check if the filter worked
-    CPPUNIT_ASSERT(out->width == width1 && out->height == height1);
-    av_frame_free(&out);
+    CPPUNIT_ASSERT(out->pointer()->width == width1 && out->pointer()->height == height1);
 }
 
 void