diff --git a/contrib/src/ffmpeg/rules.mak b/contrib/src/ffmpeg/rules.mak
index dd18fc40f289a564b685abd211bc2d38610a05d2..20b8a018f8104d72341b0162bf31f883c66457f8 100644
--- a/contrib/src/ffmpeg/rules.mak
+++ b/contrib/src/ffmpeg/rules.mak
@@ -177,7 +177,10 @@ FFMPEGCONF += \
 	--enable-hwaccel=mpeg4_vaapi \
 	--enable-hwaccel=h263_vaapi \
 	--enable-hwaccel=vp8_vaapi \
-	--enable-hwaccel=mjpeg_vaapi
+	--enable-hwaccel=mjpeg_vaapi \
+	--enable-encoder=h264_vaapi \
+	--enable-encoder=vp8_vaapi \
+	--enable-encoder=mjpeg_vaapi
 endif
 endif
 
@@ -194,6 +197,7 @@ FFMPEGCONF += \
 	--enable-hwaccel=h263_videotoolbox \
 	--enable-hwaccel=h264_videotoolbox \
 	--enable-hwaccel=mpeg4_videotoolbox \
+	--enable-encoder=h264_videotoolbox \
 	--disable-securetransport
 endif
 
diff --git a/src/media/media_decoder.cpp b/src/media/media_decoder.cpp
index 767e397ec06effb36c9dbc9f9b32d1c19471725e..40e2f05b410d14d7fa522535fe5140257ec182c4 100644
--- a/src/media/media_decoder.cpp
+++ b/src/media/media_decoder.cpp
@@ -61,10 +61,6 @@ MediaDecoder::MediaDecoder() :
 
 MediaDecoder::~MediaDecoder()
 {
-#ifdef RING_ACCEL
-    if (decoderCtx_ && decoderCtx_->hw_device_ctx)
-        av_buffer_unref(&decoderCtx_->hw_device_ctx);
-#endif
     if (decoderCtx_)
         avcodec_free_context(&decoderCtx_);
     if (inputCtx_)
@@ -231,8 +227,11 @@ MediaDecoder::setupStream(AVMediaType mediaType)
 #ifdef RING_ACCEL
     if (mediaType == AVMEDIA_TYPE_VIDEO) {
         if (enableAccel_) {
-            accel_ = video::HardwareAccel::setupDecoder(decoderCtx_);
-            decoderCtx_->opaque = accel_.get();
+            accel_ = video::HardwareAccel::setupDecoder(decoderCtx_->codec_id);
+            if (accel_) {
+                accel_->setDetails(decoderCtx_, &options_);
+                decoderCtx_->opaque = accel_.get();
+            }
         } else if (Manager::instance().videoPreferences.getDecodingAccelerated()) {
             RING_WARN() << "Hardware decoding disabled because of previous failure";
         } else {
@@ -479,7 +478,7 @@ MediaDecoder::getStream(std::string name) const
 #ifdef RING_ACCEL
     // accel_ is null if not using accelerated codecs
     if (accel_)
-        ms.format = AV_PIX_FMT_NV12; // TODO option me!
+        ms.format = accel_->getSoftwareFormat();
 #endif
     return ms;
 }
diff --git a/src/media/media_encoder.cpp b/src/media/media_encoder.cpp
index 0e38a413f902ba10dd97add85a38afe564aceff1..affc929814c50e47bb23ee64979e85893fc63787 100644
--- a/src/media/media_encoder.cpp
+++ b/src/media/media_encoder.cpp
@@ -25,9 +25,15 @@
 #include "media_encoder.h"
 #include "media_buffer.h"
 
+#include "client/ring_signal.h"
 #include "fileutils.h"
-#include "string_utils.h"
 #include "logger.h"
+#include "manager.h"
+#include "string_utils.h"
+
+#ifdef RING_ACCEL
+#include "video/accel.h"
+#endif
 
 extern "C" {
 #include <libavutil/parseutils.h>
@@ -171,6 +177,10 @@ MediaEncoder::openOutput(const std::string& filename, const std::string& format)
         avformat_alloc_output_context2(&outputCtx_, nullptr, nullptr, filename.c_str());
     else
         avformat_alloc_output_context2(&outputCtx_, nullptr, format.c_str(), filename.c_str());
+
+#ifdef RING_ACCEL
+    enableAccel_ = Manager::instance().videoPreferences.getEncodingAccelerated();
+#endif
 }
 
 int
@@ -178,21 +188,44 @@ MediaEncoder::addStream(const SystemCodecInfo& systemCodecInfo)
 {
     AVCodec* outputCodec = nullptr;
     AVCodecContext* encoderCtx = nullptr;
-    /* find the video encoder */
-    if (systemCodecInfo.avcodecId == AV_CODEC_ID_H263)
-        // For H263 encoding, we force the use of AV_CODEC_ID_H263P (H263-1998)
-        // H263-1998 can manage all frame sizes while H263 don't
-        // AV_CODEC_ID_H263 decoder will be used for decoding
-        outputCodec = avcodec_find_encoder(AV_CODEC_ID_H263P);
-    else
-        outputCodec = avcodec_find_encoder(static_cast<AVCodecID>(systemCodecInfo.avcodecId));
+#ifdef RING_ACCEL
+    if (systemCodecInfo.mediaType == MEDIA_VIDEO) {
+        if (enableAccel_) {
+            if (accel_ = video::HardwareAccel::setupEncoder(
+                static_cast<AVCodecID>(systemCodecInfo.avcodecId), device_.width, device_.height)) {
+                outputCodec = avcodec_find_encoder_by_name(accel_->getCodecName().c_str());
+            }
+        } else {
+            RING_WARN() << "Hardware encoding disabled";
+        }
+    }
+#endif
+
     if (!outputCodec) {
-        RING_ERR("Encoder \"%s\" not found!", systemCodecInfo.name.c_str());
-        throw MediaEncoderException("No output encoder");
+        /* find the video encoder */
+        if (systemCodecInfo.avcodecId == AV_CODEC_ID_H263)
+            // For H263 encoding, we force the use of AV_CODEC_ID_H263P (H263-1998)
+            // H263-1998 can manage all frame sizes while H263 don't
+            // AV_CODEC_ID_H263 decoder will be used for decoding
+            outputCodec = avcodec_find_encoder(AV_CODEC_ID_H263P);
+        else
+            outputCodec = avcodec_find_encoder(static_cast<AVCodecID>(systemCodecInfo.avcodecId));
+        if (!outputCodec) {
+            RING_ERR("Encoder \"%s\" not found!", systemCodecInfo.name.c_str());
+            throw MediaEncoderException("No output encoder");
+        }
     }
 
     encoderCtx = prepareEncoderContext(outputCodec, systemCodecInfo.mediaType == MEDIA_VIDEO);
     encoders_.push_back(encoderCtx);
+
+#ifdef RING_ACCEL
+    if (accel_) {
+        accel_->setDetails(encoderCtx, &options_);
+        encoderCtx->opaque = accel_.get();
+    }
+#endif
+
     auto maxBitrate = 1000 * std::atoi(libav_utils::getDictValue(options_, "max_rate"));
     auto bufSize = 2 * maxBitrate; // as recommended (TODO: make it customizable)
     auto crf = std::atoi(libav_utils::getDictValue(options_, "crf"));
@@ -201,6 +234,12 @@ MediaEncoder::addStream(const SystemCodecInfo& systemCodecInfo)
     if (systemCodecInfo.avcodecId == AV_CODEC_ID_H264) {
         auto profileLevelId = libav_utils::getDictValue(options_, "parameters");
         extractProfileLevelID(profileLevelId, encoderCtx);
+#ifdef RING_ACCEL
+        if (accel_)
+            // limit the bitrate else it will easily go up to a few MiB/s
+            encoderCtx->bit_rate = maxBitrate;
+        else
+#endif
         forcePresetX264(encoderCtx);
         // For H264 :
         // Streaming => VBV (constrained encoding) + CRF (Constant Rate Factor)
@@ -274,7 +313,15 @@ MediaEncoder::addStream(const SystemCodecInfo& systemCodecInfo)
         // allocate buffers for both scaled (pre-encoder) and encoded frames
         const int width = encoderCtx->width;
         const int height = encoderCtx->height;
-        const int format = encoderCtx->pix_fmt;
+        int format = encoderCtx->pix_fmt;
+#ifdef RING_ACCEL
+        if (accel_) {
+            // hardware encoders require a specific pixel format
+            auto desc = av_pix_fmt_desc_get(encoderCtx->pix_fmt);
+            if (desc && (desc->flags & AV_PIX_FMT_FLAG_HWACCEL))
+                format = accel_->getSoftwareFormat();
+        }
+#endif
         scaledFrameBufferSize_ = videoFrameSize(format, width, height);
         if (scaledFrameBufferSize_ < 0)
             throw MediaEncoderException(("Could not compute buffer size: " + libav_utils::getError(scaledFrameBufferSize_)).c_str());
@@ -339,7 +386,11 @@ MediaEncoder::encode(VideoFrame& input, bool is_keyframe,
 
     scaler_.scale_with_aspect(input, scaledFrame_);
 
-    auto frame = scaledFrame_.pointer();
+    // Copy frame so the VideoScaler can still use the software frame (input)
+    VideoFrame copy;
+    copy.copyFrom(scaledFrame_);
+
+    auto frame = copy.pointer();
     AVCodecContext* enc = encoders_[currentStreamIdx_];
     // ideally, time base is the inverse of framerate, but this may not always be the case
     if (enc->framerate.num == enc->time_base.den && enc->framerate.den == enc->time_base.num)
@@ -355,6 +406,19 @@ MediaEncoder::encode(VideoFrame& input, bool is_keyframe,
         frame->key_frame = 0;
     }
 
+#ifdef RING_ACCEL
+    // NOTE needs to be at same scope as call to encode
+    std::unique_ptr<VideoFrame> framePtr;
+    if (accel_) {
+        framePtr = accel_->transfer(copy);
+        if (!framePtr) {
+            RING_ERR() << "Hardware encoding failure";
+            return -1;
+        }
+        frame = framePtr->pointer();
+    }
+#endif
+
     return encode(frame, currentStreamIdx_);
 }
 #endif // RING_VIDEO
@@ -465,10 +529,7 @@ MediaEncoder::prepareEncoderContext(AVCodec* outputCodec, bool is_video)
 {
     AVCodecContext* encoderCtx = avcodec_alloc_context3(outputCodec);
 
-    auto encoderName = encoderCtx->av_class->item_name ?
-        encoderCtx->av_class->item_name(encoderCtx) : nullptr;
-    if (encoderName == nullptr)
-        encoderName = "encoder?";
+    auto encoderName = outputCodec->name; // guaranteed to be non null if AVCodec is not null
 
     encoderCtx->thread_count = std::min(std::thread::hardware_concurrency(), is_video ? 16u : 4u);
     RING_DBG("[%s] Using %d threads", encoderName, encoderCtx->thread_count);
@@ -506,7 +567,11 @@ MediaEncoder::prepareEncoderContext(AVCodec* outputCodec, bool is_video)
 
         // emit one intra frame every gop_size frames
         encoderCtx->max_b_frames = 0;
-        encoderCtx->pix_fmt = AV_PIX_FMT_YUV420P; // TODO: option me !
+        encoderCtx->pix_fmt = AV_PIX_FMT_YUV420P;
+#ifdef RING_ACCEL
+        if (accel_)
+            encoderCtx->pix_fmt = accel_->getFormat();
+#endif
 
         // Fri Jul 22 11:37:59 EDT 2011:tmatth:XXX: DON'T set this, we want our
         // pps and sps to be sent in-band for RTP
@@ -617,6 +682,20 @@ MediaEncoder::useCodec(const ring::AccountCodecInfo* codec) const noexcept
     return codec_.get() == codec;
 }
 
+#ifdef RING_ACCEL
+void
+MediaEncoder::enableAccel(bool enableAccel)
+{
+    enableAccel_ = enableAccel;
+    emitSignal<DRing::ConfigurationSignal::HardwareEncodingChanged>(enableAccel_);
+    if (!enableAccel_) {
+        accel_.reset();
+        for (auto enc : encoders_)
+            enc->opaque = nullptr;
+    }
+}
+#endif
+
 unsigned
 MediaEncoder::getStreamCount() const
 {
@@ -637,7 +716,12 @@ MediaEncoder::getStream(const std::string& name, int streamIdx) const
         return {};
     auto enc = encoders_[streamIdx];
     // TODO set firstTimestamp
-    return MediaStream(name, enc);
+    auto ms = MediaStream(name, enc);
+#ifdef RING_ACCEL
+    if (accel_)
+        ms.format = accel_->getSoftwareFormat();
+#endif
+    return ms;
 }
 
 void
diff --git a/src/media/media_encoder.h b/src/media/media_encoder.h
index 9487a69e8dd711c80935286e47a2e7de1f83185b..0a6d598de6a9a710309880a8b147b04b35f70eae 100644
--- a/src/media/media_encoder.h
+++ b/src/media/media_encoder.h
@@ -50,6 +50,12 @@ namespace ring {
 struct MediaDescription;
 struct AccountCodecInfo;
 
+#ifdef RING_ACCEL
+namespace video {
+class HardwareAccel;
+}
+#endif
+
 class MediaEncoderException : public std::runtime_error {
     public:
         MediaEncoderException(const char *msg) : std::runtime_error(msg) {}
@@ -94,6 +100,10 @@ public:
 
     bool useCodec(const AccountCodecInfo* codec) const noexcept;
 
+#ifdef RING_ACCEL
+    void enableAccel(bool enableAccel);
+#endif
+
     unsigned getStreamCount() const;
     MediaStream getStream(const std::string& name, int streamIdx = -1) const;
 
@@ -116,6 +126,11 @@ private:
     std::vector<uint8_t> scaledFrameBuffer_;
     int scaledFrameBufferSize_ = 0;
 
+#ifdef RING_ACCEL
+    bool enableAccel_ = true;
+    std::unique_ptr<video::HardwareAccel> accel_;
+#endif
+
 protected:
     void readConfig(AVDictionary** dict, AVCodecContext* encoderCtx);
     AVDictionary *options_ = nullptr;
diff --git a/src/media/video/accel.cpp b/src/media/video/accel.cpp
index f1c3a010ce0717b5f241a21b7949fc7571632235..1f0502ef2d1e7d616caa97ee7965b0a516ae7e68 100644
--- a/src/media/video/accel.cpp
+++ b/src/media/video/accel.cpp
@@ -37,6 +37,7 @@ struct HardwareAPI
 {
     std::string name;
     AVPixelFormat format;
+    AVPixelFormat swFormat;
     std::vector<AVCodecID> supportedCodecs;
 };
 
@@ -52,7 +53,7 @@ getFormatCb(AVCodecContext* codecCtx, const AVPixelFormat* formats)
             // found hardware format for codec with api
             RING_DBG() << "Found compatible hardware format for "
                 << avcodec_get_name(static_cast<AVCodecID>(accel->getCodecId()))
-                << " with " << accel->getName();
+                << " decoder with " << accel->getName();
             return formats[i];
         }
     }
@@ -61,54 +62,146 @@ getFormatCb(AVCodecContext* codecCtx, const AVPixelFormat* formats)
     return fallback;
 }
 
-HardwareAccel::HardwareAccel(AVCodecID id, const std::string& name, AVPixelFormat format)
+HardwareAccel::HardwareAccel(AVCodecID id, const std::string& name, AVPixelFormat format, AVPixelFormat swFormat, CodecType type)
     : id_(id)
     , name_(name)
     , format_(format)
+    , swFormat_(swFormat)
+    , type_(type)
 {}
 
+HardwareAccel::~HardwareAccel()
+{
+    if (deviceCtx_)
+        av_buffer_unref(&deviceCtx_);
+    if (framesCtx_)
+        av_buffer_unref(&framesCtx_);
+}
+
+std::string
+HardwareAccel::getCodecName() const
+{
+    if (type_ == CODEC_DECODER) {
+        return avcodec_get_name(id_);
+    } else if (type_ == CODEC_ENCODER) {
+        std::stringstream ss;
+        ss << avcodec_get_name(id_) << '_' << name_;
+        return ss.str();
+    }
+    return "";
+}
+
 std::unique_ptr<VideoFrame>
 HardwareAccel::transfer(const VideoFrame& frame)
 {
-    auto input = frame.pointer();
-    if (input->format != format_) {
-        RING_ERR("Frame format mismatch: expected %s, got %s",
-                 av_get_pix_fmt_name(static_cast<AVPixelFormat>(format_)),
-                 av_get_pix_fmt_name(static_cast<AVPixelFormat>(input->format)));
+    int ret = 0;
+    if (type_ == CODEC_DECODER) {
+        auto input = frame.pointer();
+        if (input->format != format_) {
+            RING_ERR() << "Frame format mismatch: expected "
+                << av_get_pix_fmt_name(format_) << ", got "
+                << av_get_pix_fmt_name(static_cast<AVPixelFormat>(input->format));
+            return nullptr;
+        }
+
+        return transferToMainMemory(frame, AV_PIX_FMT_NV12);
+    } else if (type_ == CODEC_ENCODER) {
+        auto input = frame.pointer();
+        if (input->format != swFormat_) {
+            RING_ERR() << "Frame format mismatch: expected "
+                << av_get_pix_fmt_name(swFormat_) << ", got "
+                << av_get_pix_fmt_name(static_cast<AVPixelFormat>(input->format));
+            return nullptr;
+        }
+
+        auto framePtr = std::make_unique<VideoFrame>();
+        auto hwFrame = framePtr->pointer();
+
+        if ((ret = av_hwframe_get_buffer(framesCtx_, hwFrame, 0)) < 0) {
+            RING_ERR() << "Failed to allocate hardware buffer: " << libav_utils::getError(ret);
+            return nullptr;
+        }
+
+        if (!hwFrame->hw_frames_ctx) {
+            RING_ERR() << "Failed to allocate hardware buffer: Cannot allocate memory";
+            return nullptr;
+        }
+
+        if ((ret = av_hwframe_transfer_data(hwFrame, input, 0)) < 0) {
+            RING_ERR() << "Failed to push frame to GPU: " << libav_utils::getError(ret);
+            return nullptr;
+        }
+
+        hwFrame->pts = input->pts; // transfer does not copy timestamp
+        return framePtr;
+    } else {
+        RING_ERR() << "Invalid hardware accelerator";
         return nullptr;
     }
+}
 
-    return transferToMainMemory(frame, AV_PIX_FMT_NV12);
+void
+HardwareAccel::setDetails(AVCodecContext* codecCtx, AVDictionary** /*d*/)
+{
+    if (type_ == CODEC_DECODER) {
+        codecCtx->hw_device_ctx = av_buffer_ref(deviceCtx_);
+        codecCtx->get_format = getFormatCb;
+        codecCtx->thread_safe_callbacks = 1;
+    } else if (type_ == CODEC_ENCODER) {
+        codecCtx->hw_device_ctx = av_buffer_ref(deviceCtx_);
+        codecCtx->hw_frames_ctx = av_buffer_ref(framesCtx_);
+    }
 }
 
-static int
-initDevice(const HardwareAPI& api, AVCodecContext* codecCtx)
+bool
+HardwareAccel::initDevice()
 {
     int ret = 0;
-    AVBufferRef* hardwareDeviceCtx = nullptr;
-    auto hwType = av_hwdevice_find_type_by_name(api.name.c_str());
+    auto hwType = av_hwdevice_find_type_by_name(name_.c_str());
 #ifdef HAVE_VAAPI_ACCEL_DRM
     // default DRM device may not work on multi GPU computers, so check all possible values
-    if (api.name == "vaapi") {
+    if (name_ == "vaapi") {
         const std::string path = "/dev/dri/";
         auto files = ring::fileutils::readDirectory(path);
         // renderD* is preferred over card*
         std::sort(files.rbegin(), files.rend());
         for (auto& entry : files) {
             std::string deviceName = path + entry;
-            if ((ret = av_hwdevice_ctx_create(&hardwareDeviceCtx, hwType, deviceName.c_str(), nullptr, 0)) >= 0) {
-                codecCtx->hw_device_ctx = hardwareDeviceCtx;
-                return ret;
+            if ((ret = av_hwdevice_ctx_create(&deviceCtx_, hwType, deviceName.c_str(), nullptr, 0)) >= 0) {
+                return true;
             }
         }
     }
 #endif
     // default device (nullptr) works for most cases
-    if ((ret = av_hwdevice_ctx_create(&hardwareDeviceCtx, hwType, nullptr, nullptr, 0)) >= 0) {
-        codecCtx->hw_device_ctx = hardwareDeviceCtx;
+    ret = av_hwdevice_ctx_create(&deviceCtx_, hwType, nullptr, nullptr, 0);
+    return ret >= 0;
+}
+
+bool
+HardwareAccel::initFrame(int width, int height)
+{
+    int ret = 0;
+    if (!deviceCtx_) {
+        RING_ERR() << "Cannot initialize hardware frames without a valid hardware device";
+        return false;
     }
 
-    return ret;
+    framesCtx_ = av_hwframe_ctx_alloc(deviceCtx_);
+    if (!framesCtx_)
+        return false;
+
+    auto ctx = reinterpret_cast<AVHWFramesContext*>(framesCtx_->data);
+    ctx->format = format_;
+    ctx->sw_format = swFormat_;
+    ctx->width = width;
+    ctx->height = height;
+    ctx->initial_pool_size = 20; // TODO try other values
+
+    if ((ret = av_hwframe_ctx_init(framesCtx_)) < 0)
+        av_buffer_unref(&framesCtx_);
+
+    return ret >= 0;
 }
 
 std::unique_ptr<VideoFrame>
@@ -137,25 +230,50 @@ HardwareAccel::transferToMainMemory(const VideoFrame& frame, AVPixelFormat desir
 }
 
 std::unique_ptr<HardwareAccel>
-HardwareAccel::setupDecoder(AVCodecContext* codecCtx)
+HardwareAccel::setupDecoder(AVCodecID id)
 {
     static const HardwareAPI apiList[] = {
-        { "vaapi", AV_PIX_FMT_VAAPI, { AV_CODEC_ID_H264, AV_CODEC_ID_MPEG4, AV_CODEC_ID_VP8, AV_CODEC_ID_MJPEG } },
-        { "vdpau", AV_PIX_FMT_VDPAU, { AV_CODEC_ID_H264, AV_CODEC_ID_MPEG4 } },
-        { "videotoolbox", AV_PIX_FMT_VIDEOTOOLBOX, { AV_CODEC_ID_H264, AV_CODEC_ID_MPEG4 } },
+        { "vaapi", AV_PIX_FMT_VAAPI, AV_PIX_FMT_NV12, { AV_CODEC_ID_H264, AV_CODEC_ID_MPEG4, AV_CODEC_ID_VP8, AV_CODEC_ID_MJPEG } },
+        { "vdpau", AV_PIX_FMT_VDPAU, AV_PIX_FMT_NV12, { AV_CODEC_ID_H264, AV_CODEC_ID_MPEG4 } },
+        { "videotoolbox", AV_PIX_FMT_VIDEOTOOLBOX, AV_PIX_FMT_NV12, { AV_CODEC_ID_H264, AV_CODEC_ID_MPEG4 } },
     };
 
     for (const auto& api : apiList) {
-        if (std::find(api.supportedCodecs.begin(), api.supportedCodecs.end(), codecCtx->codec_id) != api.supportedCodecs.end()) {
-            if (initDevice(api, codecCtx) >= 0) {
-                codecCtx->get_format = getFormatCb;
-                codecCtx->thread_safe_callbacks = 1;
-                RING_DBG() << "Attempting to use hardware accelerated decoding with " << api.name;
-                return std::make_unique<HardwareAccel>(codecCtx->codec_id, api.name, api.format);
+        if (std::find(api.supportedCodecs.begin(), api.supportedCodecs.end(), id) != api.supportedCodecs.end()) {
+            auto accel = std::make_unique<HardwareAccel>(id, api.name, api.format, api.swFormat, CODEC_DECODER);
+            if (accel->initDevice()) {
+                RING_DBG() << "Attempting to use hardware decoder " << accel->getCodecName() << " with " << api.name;
+                return accel;
+            }
+        }
+    }
+
+    return nullptr;
+}
+
+std::unique_ptr<HardwareAccel>
+HardwareAccel::setupEncoder(AVCodecID id, int width, int height)
+{
+    static const HardwareAPI apiList[] = {
+        { "vaapi", AV_PIX_FMT_VAAPI, AV_PIX_FMT_NV12, { AV_CODEC_ID_H264, AV_CODEC_ID_MJPEG, AV_CODEC_ID_VP8 } },
+        { "videotoolbox", AV_PIX_FMT_VIDEOTOOLBOX, AV_PIX_FMT_NV12, { AV_CODEC_ID_H264 } },
+    };
+
+    for (auto api : apiList) {
+        const auto& it = std::find(api.supportedCodecs.begin(), api.supportedCodecs.end(), id);
+        if (it != api.supportedCodecs.end()) {
+            auto accel = std::make_unique<HardwareAccel>(id, api.name, api.format, api.swFormat, CODEC_ENCODER);
+            const auto& codecName = accel->getCodecName();
+            if (avcodec_find_encoder_by_name(codecName.c_str())) {
+                if (accel->initDevice() && accel->initFrame(width, height)) {
+                    RING_DBG() << "Attempting to use hardware encoder " << codecName;
+                    return accel;
+                }
             }
         }
     }
 
+    RING_WARN() << "Not using hardware encoding";
     return nullptr;
 }
 
diff --git a/src/media/video/accel.h b/src/media/video/accel.h
index e0f06855ac3e1883deea47b84398bcaaa5b964a5..85fdbe88c620ee37bf6e8b7e60f280512c4609b5 100644
--- a/src/media/video/accel.h
+++ b/src/media/video/accel.h
@@ -21,6 +21,7 @@
 #pragma once
 
 #include "libav_deps.h"
+#include "media_codec.h"
 
 #include <memory>
 #include <string>
@@ -36,7 +37,12 @@ public:
     /**
      * Static factory method for hardware decoding.
      */
-    static std::unique_ptr<HardwareAccel> setupDecoder(AVCodecContext* codecCtx);
+    static std::unique_ptr<HardwareAccel> setupDecoder(AVCodecID id);
+
+    /**
+     * Static factory method for hardware encoding.
+     */
+    static std::unique_ptr<HardwareAccel> setupEncoder(AVCodecID id, int width, int height);
 
     /**
      * Transfers a hardware decoded frame back to main memory. Should be called after
@@ -50,25 +56,72 @@ public:
     /**
      * Made public so std::unique_ptr can access it. Should not be called.
      */
-    HardwareAccel(AVCodecID id, const std::string& name, AVPixelFormat format);
+    HardwareAccel(AVCodecID id, const std::string& name, AVPixelFormat format, AVPixelFormat swFormat, CodecType type);
 
+    /**
+     * Dereferences hardware contexts.
+     */
+    ~HardwareAccel();
+
+    /**
+     * Codec that is being accelerated.
+     */
     AVCodecID getCodecId() const { return id_; };
+
+    /**
+     * Name of the hardware layer/API being used.
+     */
     std::string getName() const { return name_; };
+
+    /**
+     * Hardware format.
+     */
     AVPixelFormat getFormat() const { return format_; };
 
+    /**
+     * Software format. For encoding it is the format expected by the hardware. For decoding
+     * it is the format output by the hardware.
+     */
+    AVPixelFormat getSoftwareFormat() const { return swFormat_; }
+
+    /**
+     * Gets the name of the codec.
+     * Decoding: equivalent to avcodec_get_name(id_)
+     * Encoding: avcodec_get_name(id_) + '_' + name_
+     */
+    std::string getCodecName() const;
+
+    /**
+     * Set some extra details in the codec context. Should be called after a successful
+     * setup (setupDecoder or setupEncoder).
+     * For decoding, sets the hw_device_ctx and get_format callback. For encoding, sets
+     * hw_device_ctx and hw_frames_ctx, and may set some hardware specific options in
+     * the dictionary.
+     */
+    void setDetails(AVCodecContext* codecCtx, AVDictionary** d);
+
     /**
      * Transfers a hardware decoded frame back to main memory. Should be called after
-     * the frame is decoded using avcodec_send_packet/avcodec_receive_frame.
+     * the frame is decoded using avcodec_send_packet/avcodec_receive_frame or before
+     * the frame is encoded using avcodec_send_frame/avcodec_receive_packet.
      *
-     * @frame: Refrerence to the decoded hardware frame.
-     * @returns: Software frame.
+     * @frame: Hardware frame when decoding, software frame when encoding.
+     * @returns: Software frame when decoding, hardware frame when encoding.
      */
     std::unique_ptr<VideoFrame> transfer(const VideoFrame& frame);
 
 private:
-    AVCodecID id_;
+    bool initDevice();
+    bool initFrame(int width, int height);
+
+    AVCodecID id_ {AV_CODEC_ID_NONE};
     std::string name_;
-    AVPixelFormat format_;
+    AVPixelFormat format_ {AV_PIX_FMT_NONE};
+    AVPixelFormat swFormat_ {AV_PIX_FMT_NONE};
+    CodecType type_ {CODEC_NONE};
+
+    AVBufferRef* deviceCtx_ {nullptr};
+    AVBufferRef* framesCtx_ {nullptr};
 };
 
 }} // namespace ring::video
diff --git a/src/preferences.cpp b/src/preferences.cpp
index b421996f6c886ec1d54ae9f5d0ef9311e194ad2d..30349a2962b7c688897fb3018a17b81b9562cda8 100644
--- a/src/preferences.cpp
+++ b/src/preferences.cpp
@@ -138,6 +138,7 @@ static const char * const TOGGLE_PICKUP_HANGUP_SHORT_KEY = "togglePickupHangup";
 // video preferences
 constexpr const char * const VideoPreferences::CONFIG_LABEL;
 static const char * const DECODING_ACCELERATED_KEY = "decodingAccelerated";
+static const char * const ENCODING_ACCELERATED_KEY = "encodingAccelerated";
 #endif
 
 static const char * const DFT_PULSE_LENGTH_STR = "250"; /** Default DTMF length */
@@ -562,6 +563,7 @@ void ShortcutPreferences::unserialize(const YAML::Node &in)
 #ifdef RING_VIDEO
 VideoPreferences::VideoPreferences()
     : decodingAccelerated_(true)
+    , encodingAccelerated_(false)
 {
 }
 
@@ -570,6 +572,7 @@ void VideoPreferences::serialize(YAML::Emitter &out)
     out << YAML::Key << CONFIG_LABEL << YAML::Value << YAML::BeginMap;
 #ifdef RING_ACCEL
     out << YAML::Key << DECODING_ACCELERATED_KEY << YAML::Value << decodingAccelerated_;
+    out << YAML::Key << ENCODING_ACCELERATED_KEY << YAML::Value << encodingAccelerated_;
 #endif
     getVideoDeviceMonitor().serialize(out);
     out << YAML::EndMap;
@@ -582,7 +585,11 @@ void VideoPreferences::unserialize(const YAML::Node &in)
     // value may or may not be present
     try {
         parseValue(node, DECODING_ACCELERATED_KEY, decodingAccelerated_);
-    } catch (...) { decodingAccelerated_ = true; }
+        parseValue(node, ENCODING_ACCELERATED_KEY, encodingAccelerated_);
+    } catch (...) {
+        decodingAccelerated_ = true;
+        encodingAccelerated_ = false;
+    }
 #endif
     getVideoDeviceMonitor().unserialize(in);
 }
diff --git a/src/preferences.h b/src/preferences.h
index cf68a3bc9a52973a997b957b98e4f0f8855f0b40..4e6b170e3719cc6edce42a4abc89b60ed495fa0c 100644
--- a/src/preferences.h
+++ b/src/preferences.h
@@ -466,8 +466,18 @@ class VideoPreferences : public Serializable {
             emitSignal<DRing::ConfigurationSignal::HardwareDecodingChanged>(decodingAccelerated_);
         }
 
+        bool getEncodingAccelerated() const {
+            return encodingAccelerated_;
+        }
+
+        void setEncodingAccelerated(bool encodingAccelerated) {
+            encodingAccelerated_ = encodingAccelerated;
+            emitSignal<DRing::ConfigurationSignal::HardwareEncodingChanged>(encodingAccelerated_);
+        }
+
     private:
         bool decodingAccelerated_;
+        bool encodingAccelerated_;
         constexpr static const char* const CONFIG_LABEL = "video";
 };
 #endif // RING_VIDEO