diff --git a/src/media/media_decoder.cpp b/src/media/media_decoder.cpp
index 7e6fbf050e4c127bd327f3dddf05cb0bd7bac523..767e397ec06effb36c9dbc9f9b32d1c19471725e 100644
--- a/src/media/media_decoder.cpp
+++ b/src/media/media_decoder.cpp
@@ -2,6 +2,7 @@
  *  Copyright (C) 2013-2019 Savoir-faire Linux Inc.
  *
  *  Author: Guillaume Roguez <Guillaume.Roguez@savoirfairelinux.com>
+ *  Author: Philippe Gorley <philippe.gorley@savoirfairelinux.com>
  *
  *  This program is free software; you can redistribute it and/or modify
  *  it under the terms of the GNU General Public License as published by
@@ -228,13 +229,15 @@ MediaDecoder::setupStream(AVMediaType mediaType)
     startTime_ = av_gettime(); // used to set pts after decoding, and for rate emulation
 
 #ifdef RING_ACCEL
-    if (enableAccel_) {
-        accel_ = video::setupHardwareDecoding(decoderCtx_);
-        decoderCtx_->opaque = &accel_;
-    } else if (Manager::instance().videoPreferences.getDecodingAccelerated()) {
-        RING_WARN() << "Hardware accelerated decoding disabled because of previous failure";
-    } else {
-        RING_WARN() << "Hardware accelerated decoding disabled by user preference";
+    if (mediaType == AVMEDIA_TYPE_VIDEO) {
+        if (enableAccel_) {
+            accel_ = video::HardwareAccel::setupDecoder(decoderCtx_);
+            decoderCtx_->opaque = accel_.get();
+        } else if (Manager::instance().videoPreferences.getDecodingAccelerated()) {
+            RING_WARN() << "Hardware decoding disabled because of previous failure";
+        } else {
+            RING_WARN() << "Hardware decoding disabled by user preference";
+        }
     }
 #endif
 
@@ -293,7 +296,6 @@ MediaDecoder::decode(VideoFrame& result)
 
     if (frameFinished) {
         frame->format = (AVPixelFormat) correctPixFmt(frame->format);
-
         auto packetTimestamp = frame->pts; // in stream time base
         frame->pts = av_rescale_q_rnd(av_gettime() - startTime_,
             {1, AV_TIME_BASE}, decoderCtx_->time_base,
@@ -385,12 +387,9 @@ MediaDecoder::enableAccel(bool enableAccel)
     enableAccel_ = enableAccel;
     emitSignal<DRing::ConfigurationSignal::HardwareDecodingChanged>(enableAccel_);
     if (!enableAccel) {
-        accel_ = {};
-        if (decoderCtx_) {
-            if (decoderCtx_->hw_device_ctx)
-                av_buffer_unref(&decoderCtx_->hw_device_ctx);
+        accel_.reset();
+        if (decoderCtx_)
             decoderCtx_->opaque = nullptr;
-        }
     }
 }
 #endif
@@ -415,7 +414,7 @@ MediaDecoder::flush(VideoFrame& result)
 
     if (frameFinished) {
         av_packet_unref(&inpacket);
-        return Status::FrameFinished;
+        return Status::EOFError;
     }
 
     return Status::Success;
@@ -478,7 +477,8 @@ MediaDecoder::getStream(std::string name) const
 {
     auto ms = MediaStream(name, decoderCtx_, lastTimestamp_);
 #ifdef RING_ACCEL
-    if (decoderCtx_->codec_type == AVMEDIA_TYPE_VIDEO && enableAccel_ && !accel_.name.empty())
+    // accel_ is null if not using accelerated codecs
+    if (accel_)
         ms.format = AV_PIX_FMT_NV12; // TODO option me!
 #endif
     return ms;
diff --git a/src/media/media_decoder.h b/src/media/media_decoder.h
index 748470b1fd0a918d61914d275649a588a9a86859..6a69336e11cb625929db61a4f34205aa80299d9d 100644
--- a/src/media/media_decoder.h
+++ b/src/media/media_decoder.h
@@ -28,10 +28,6 @@
 #include "video/video_scaler.h"
 #endif // RING_VIDEO
 
-#ifdef RING_ACCEL
-#include "video/accel.h"
-#endif
-
 #include "audio/audiobuffer.h"
 
 #include "media_device.h"
@@ -63,6 +59,12 @@ class RingBuffer;
 class Resampler;
 class MediaIOHandle;
 
+#ifdef RING_ACCEL
+namespace video {
+class HardwareAccel;
+}
+#endif
+
 class MediaDecoder {
     public:
         enum class Status {
@@ -86,7 +88,7 @@ class MediaDecoder {
         int setupFromVideoData();
         Status decode(VideoFrame&);
         Status flush(VideoFrame&);
- #endif // RING_VIDEO
+#endif // RING_VIDEO
 
         int setupFromAudioData();
         Status decode(AudioFrame&);
@@ -129,7 +131,7 @@ class MediaDecoder {
 
 #ifdef RING_ACCEL
         bool enableAccel_ = true;
-        video::HardwareAccel accel_;
+        std::unique_ptr<video::HardwareAccel> accel_;
         unsigned short accelFailures_ = 0;
 #endif
 
diff --git a/src/media/media_recorder.cpp b/src/media/media_recorder.cpp
index 6b25378753a7c7e049136be9a59d950f51c6cb96..f6b02fc461a80360d38ec08c635e45337368ec65 100644
--- a/src/media/media_recorder.cpp
+++ b/src/media/media_recorder.cpp
@@ -172,7 +172,7 @@ MediaRecorder::onFrame(const std::string& name, const std::shared_ptr<MediaFrame
     const auto& ms = streams_[name]->info;
     if (ms.isVideo) {
 #ifdef RING_ACCEL
-        clone = video::transferToMainMemory(*std::static_pointer_cast<VideoFrame>(frame),
+        clone = video::HardwareAccel::transferToMainMemory(*std::static_pointer_cast<VideoFrame>(frame),
             static_cast<AVPixelFormat>(ms.format));
 #else
         clone = std::make_unique<MediaFrame>();
diff --git a/src/media/video/accel.cpp b/src/media/video/accel.cpp
index 13e14293d1027e44dfef84fe52fc823481bba1bf..f1c3a010ce0717b5f241a21b7949fc7571632235 100644
--- a/src/media/video/accel.cpp
+++ b/src/media/video/accel.cpp
@@ -33,6 +33,13 @@ extern "C" {
 
 namespace ring { namespace video {
 
+struct HardwareAPI
+{
+    std::string name;
+    AVPixelFormat format;
+    std::vector<AVCodecID> supportedCodecs;
+};
+
 static AVPixelFormat
 getFormatCb(AVCodecContext* codecCtx, const AVPixelFormat* formats)
 {
@@ -41,76 +48,48 @@ getFormatCb(AVCodecContext* codecCtx, const AVPixelFormat* formats)
     AVPixelFormat fallback = AV_PIX_FMT_NONE;
     for (int i = 0; formats[i] != AV_PIX_FMT_NONE; ++i) {
         fallback = formats[i];
-        if (accel && formats[i] == accel->format) {
+        if (accel && formats[i] == accel->getFormat()) {
+            // 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();
             return formats[i];
         }
     }
 
-    if (accel) {
-        RING_WARN("'%s' acceleration not supported, falling back to software decoding", accel->name.c_str());
-        accel->name = {}; // don't use accel
-    } else {
-        RING_WARN() << "Not using hardware decoding";
-    }
+    RING_WARN() << "Not using hardware decoding";
     return fallback;
 }
 
-int
-transferFrameData(HardwareAccel accel, AVCodecContext* /*codecCtx*/, VideoFrame& frame)
-{
-    if (accel.name.empty())
-        return -1;
-
-    auto input = frame.pointer();
-    if (input->format != accel.format) {
-        RING_ERR("Frame format mismatch: expected %s, got %s",
-                 av_get_pix_fmt_name(static_cast<AVPixelFormat>(accel.format)),
-                 av_get_pix_fmt_name(static_cast<AVPixelFormat>(input->format)));
-        return -1;
-    }
-
-    auto output = transferToMainMemory(frame, AV_PIX_FMT_NV12);
-    if (!output)
-        return -1;
-
-    frame.copyFrom(*output); // copy to input so caller receives extracted image data
-    return 0;
-}
+HardwareAccel::HardwareAccel(AVCodecID id, const std::string& name, AVPixelFormat format)
+    : id_(id)
+    , name_(name)
+    , format_(format)
+{}
 
 std::unique_ptr<VideoFrame>
-transferToMainMemory(const VideoFrame& frame, AVPixelFormat desiredFormat)
+HardwareAccel::transfer(const VideoFrame& frame)
 {
     auto input = frame.pointer();
-    auto out = std::make_unique<VideoFrame>();
-
-    auto desc = av_pix_fmt_desc_get(static_cast<AVPixelFormat>(input->format));
-    if (desc && not (desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) {
-        out->copyFrom(frame);
-        return out;
-    }
-
-    auto output = out->pointer();
-    output->format = desiredFormat;
-
-    int ret = av_hwframe_transfer_data(output, input, 0);
-    if (ret < 0) {
-        out->copyFrom(frame);
-        return out;
+    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)));
+        return nullptr;
     }
 
-    output->pts = input->pts;
-    return out;
+    return transferToMainMemory(frame, AV_PIX_FMT_NV12);
 }
 
 static int
-initDevice(HardwareAccel accel, AVCodecContext* codecCtx)
+initDevice(const HardwareAPI& api, AVCodecContext* codecCtx)
 {
     int ret = 0;
     AVBufferRef* hardwareDeviceCtx = nullptr;
-    auto hwType = av_hwdevice_find_type_by_name(accel.name.c_str());
+    auto hwType = av_hwdevice_find_type_by_name(api.name.c_str());
 #ifdef HAVE_VAAPI_ACCEL_DRM
     // default DRM device may not work on multi GPU computers, so check all possible values
-    if (accel.name == "vaapi") {
+    if (api.name == "vaapi") {
         const std::string path = "/dev/dri/";
         auto files = ring::fileutils::readDirectory(path);
         // renderD* is preferred over card*
@@ -119,7 +98,6 @@ initDevice(HardwareAccel accel, AVCodecContext* codecCtx)
             std::string deviceName = path + entry;
             if ((ret = av_hwdevice_ctx_create(&hardwareDeviceCtx, hwType, deviceName.c_str(), nullptr, 0)) >= 0) {
                 codecCtx->hw_device_ctx = hardwareDeviceCtx;
-                RING_DBG("Using '%s' hardware acceleration with device '%s'", accel.name.c_str(), deviceName.c_str());
                 return ret;
             }
         }
@@ -128,42 +106,57 @@ initDevice(HardwareAccel accel, AVCodecContext* codecCtx)
     // default device (nullptr) works for most cases
     if ((ret = av_hwdevice_ctx_create(&hardwareDeviceCtx, hwType, nullptr, nullptr, 0)) >= 0) {
         codecCtx->hw_device_ctx = hardwareDeviceCtx;
-        RING_DBG("Using '%s' hardware acceleration", accel.name.c_str());
     }
 
     return ret;
 }
 
-const HardwareAccel
-setupHardwareDecoding(AVCodecContext* codecCtx)
+std::unique_ptr<VideoFrame>
+HardwareAccel::transferToMainMemory(const VideoFrame& frame, AVPixelFormat desiredFormat)
+{
+    auto input = frame.pointer();
+    auto out = std::make_unique<VideoFrame>();
+
+    auto desc = av_pix_fmt_desc_get(static_cast<AVPixelFormat>(input->format));
+    if (desc && not (desc->flags & AV_PIX_FMT_FLAG_HWACCEL)) {
+        out->copyFrom(frame);
+        return out;
+    }
+
+    auto output = out->pointer();
+    output->format = desiredFormat;
+
+    int ret = av_hwframe_transfer_data(output, input, 0);
+    if (ret < 0) {
+        out->copyFrom(frame);
+        return out;
+    }
+
+    output->pts = input->pts;
+    return out;
+}
+
+std::unique_ptr<HardwareAccel>
+HardwareAccel::setupDecoder(AVCodecContext* codecCtx)
 {
-    /**
-     * This array represents FFmpeg's hwaccels, along with their pixel format
-     * and their potentially supported codecs. Each item contains:
-     * - Name (must match the name used in FFmpeg)
-     * - Pixel format (tells FFmpeg which hwaccel to use)
-     * - Array of AVCodecID (potential codecs that can be accelerated by the hwaccel)
-     * Note: an empty name means the video isn't accelerated
-     */
-    const HardwareAccel accels[] = {
-        { "vaapi", AV_PIX_FMT_VAAPI, { AV_CODEC_ID_H264, AV_CODEC_ID_MPEG4, AV_CODEC_ID_H263, AV_CODEC_ID_VP8, AV_CODEC_ID_MJPEG } },
-        { "vdpau", AV_PIX_FMT_VDPAU, { AV_CODEC_ID_H264, AV_CODEC_ID_MPEG4, AV_CODEC_ID_H263 } },
-        { "videotoolbox", AV_PIX_FMT_VIDEOTOOLBOX, { AV_CODEC_ID_H264, AV_CODEC_ID_MPEG4, AV_CODEC_ID_H263 } },
+    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 } },
     };
 
-    for (auto accel : accels) {
-        if (std::find(accel.supportedCodecs.begin(), accel.supportedCodecs.end(),
-                static_cast<AVCodecID>(codecCtx->codec_id)) != accel.supportedCodecs.end()) {
-            if (initDevice(accel, codecCtx) >= 0) {
+    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;
-                return accel;
+                RING_DBG() << "Attempting to use hardware accelerated decoding with " << api.name;
+                return std::make_unique<HardwareAccel>(codecCtx->codec_id, api.name, api.format);
             }
         }
     }
 
-    RING_WARN("Not using hardware accelerated decoding");
-    return {};
+    return nullptr;
 }
 
 }} // namespace ring::video
diff --git a/src/media/video/accel.h b/src/media/video/accel.h
index d90af46855de398601dc3103d65b8c13e6255a35..e0f06855ac3e1883deea47b84398bcaaa5b964a5 100644
--- a/src/media/video/accel.h
+++ b/src/media/video/accel.h
@@ -22,19 +22,53 @@
 
 #include "libav_deps.h"
 
+#include <memory>
 #include <string>
 #include <vector>
 
 namespace ring { namespace video {
 
-struct HardwareAccel {
-        std::string name;
-        AVPixelFormat format;
-        std::vector<AVCodecID> supportedCodecs;
-};
+/**
+ * Provides an abstraction layer to the hardware acceleration APIs in FFmpeg.
+ */
+class HardwareAccel {
+public:
+    /**
+     * Static factory method for hardware decoding.
+     */
+    static std::unique_ptr<HardwareAccel> setupDecoder(AVCodecContext* codecCtx);
+
+    /**
+     * Transfers a hardware decoded frame back to main memory. Should be called after
+     * the frame is decoded using avcodec_send_packet/avcodec_receive_frame.
+     *
+     * @frame: Refrerence to the decoded hardware frame.
+     * @returns: Software frame.
+     */
+    static std::unique_ptr<VideoFrame> transferToMainMemory(const VideoFrame& frame, AVPixelFormat desiredFormat);
+
+    /**
+     * Made public so std::unique_ptr can access it. Should not be called.
+     */
+    HardwareAccel(AVCodecID id, const std::string& name, AVPixelFormat format);
 
-const HardwareAccel setupHardwareDecoding(AVCodecContext* codecCtx);
-int transferFrameData(HardwareAccel accel, AVCodecContext* codecCtx, VideoFrame& frame);
-std::unique_ptr<VideoFrame> transferToMainMemory(const VideoFrame& frame, AVPixelFormat desiredFormat);
+    AVCodecID getCodecId() const { return id_; };
+    std::string getName() const { return name_; };
+    AVPixelFormat getFormat() const { return format_; };
+
+    /**
+     * Transfers a hardware decoded frame back to main memory. Should be called after
+     * the frame is decoded using avcodec_send_packet/avcodec_receive_frame.
+     *
+     * @frame: Refrerence to the decoded hardware frame.
+     * @returns: Software frame.
+     */
+    std::unique_ptr<VideoFrame> transfer(const VideoFrame& frame);
+
+private:
+    AVCodecID id_;
+    std::string name_;
+    AVPixelFormat format_;
+};
 
 }} // namespace ring::video
diff --git a/src/media/video/sinkclient.cpp b/src/media/video/sinkclient.cpp
index 9d84983d6c534fa2c17bd3d44b1cf7768d69c999..09b0cf8c2cc7438d46494ca0a8df50af5910ca77 100644
--- a/src/media/video/sinkclient.cpp
+++ b/src/media/video/sinkclient.cpp
@@ -349,7 +349,7 @@ SinkClient::update(Observable<std::shared_ptr<MediaFrame>>* /*obs*/,
 
     if (doTransfer) {
 #ifdef RING_ACCEL
-        auto framePtr = transferToMainMemory(f, AV_PIX_FMT_NV12);
+        auto framePtr = HardwareAccel::transferToMainMemory(f, AV_PIX_FMT_NV12);
         const auto& swFrame = *framePtr;
 #else
         const auto& swFrame = f;
diff --git a/src/media/video/video_mixer.cpp b/src/media/video/video_mixer.cpp
index f4e434d2be38ae03b7325635ef635bea0e03a303..84e9ce81007a68e1573c75d146c35633b183103b 100644
--- a/src/media/video/video_mixer.cpp
+++ b/src/media/video/video_mixer.cpp
@@ -173,7 +173,7 @@ VideoMixer::render_frame(VideoFrame& output, const VideoFrame& input, int index)
         return;
 
 #ifdef RING_ACCEL
-    auto framePtr = transferToMainMemory(input, AV_PIX_FMT_NV12);
+    auto framePtr = HardwareAccel::transferToMainMemory(input, AV_PIX_FMT_NV12);
     const auto& swFrame = *framePtr;
 #else
     const auto& swFrame = input;
diff --git a/src/media/video/video_sender.cpp b/src/media/video/video_sender.cpp
index 1ffa256c2bf395df265713a330b3c0d3cdac87fb..f9a9237b83e8c83b8d059171f1f9c8596b8e63fb 100644
--- a/src/media/video/video_sender.cpp
+++ b/src/media/video/video_sender.cpp
@@ -89,7 +89,7 @@ VideoSender::encodeAndSendVideo(VideoFrame& input_frame)
             --forceKeyFrame_;
 
 #ifdef RING_ACCEL
-        auto framePtr = transferToMainMemory(input_frame, AV_PIX_FMT_NV12);
+        auto framePtr = HardwareAccel::transferToMainMemory(input_frame, AV_PIX_FMT_NV12);
         auto& swFrame = *framePtr;
 #else
         auto& swFrame = input_frame;