diff --git a/src/media/media_encoder.cpp b/src/media/media_encoder.cpp
index 193869e8cba1414d181315b5aece0face4380215..94c6e4c59a9cedd1bdeb37fcb1b9b88c704a8da5 100644
--- a/src/media/media_encoder.cpp
+++ b/src/media/media_encoder.cpp
@@ -27,6 +27,7 @@
 #include "media_io_handle.h"
 
 #include "audio/audiobuffer.h"
+#include "fileutils.h"
 #include "string_utils.h"
 #include "logger.h"
 
@@ -34,9 +35,11 @@ extern "C" {
 #include <libavutil/parseutils.h>
 }
 
+#include <algorithm>
+#include <fstream>
 #include <iostream>
+#include <json/json.h>
 #include <sstream>
-#include <algorithm>
 #include <thread> // hardware_concurrency
 
 // Define following line if you need to debug libav SDP
@@ -274,7 +277,8 @@ MediaEncoder::addStream(const SystemCodecInfo& systemCodecInfo, std::string para
 
     currentStreamIdx_ = stream->index;
 
-    if (avcodec_open2(encoderCtx, outputCodec, nullptr) < 0)
+    readConfig(&options_, outputCodec->name);
+    if (avcodec_open2(encoderCtx, outputCodec, &options_) < 0)
         throw MediaEncoderException("Could not open encoder");
 
 #ifndef _WIN32
@@ -657,4 +661,49 @@ MediaEncoder::getStream(const std::string& name, int streamIdx) const
     return MediaStream(name, enc);
 }
 
+void
+MediaEncoder::readConfig(AVDictionary** dict, const std::string& encoder)
+{
+    std::string path = fileutils::get_config_dir() + DIR_SEPARATOR_STR + "encoder.json";
+    if (fileutils::isFile(path)) {
+        try {
+            Json::Value root;
+            std::ifstream file(path);
+            file >> root;
+            if (!root.isObject()) {
+                RING_ERR() << "Invalid encoder configuration: root is not an object";
+                return;
+            }
+            const auto& config = root[encoder];
+            if (config.isNull()) {
+                RING_WARN() << "Encoder '" << encoder << "' not found in configuration file";
+                return;
+            }
+            if (!config.isObject()) {
+                RING_ERR() << "Invalid encoder configuration: '" << encoder << "' is not an object";
+                return;
+            }
+            // If users want to change these, they should use the settings page.
+            std::vector<std::string> ignoredKeys = { "width", "height", "framerate", "sample_rate", "channels", "frame_size", "parameters" };
+            for (Json::Value::const_iterator it = config.begin(); it != config.end(); ++it) {
+                Json::Value v = *it;
+                if (!it.key().isConvertibleTo(Json::ValueType::stringValue)
+                    || !v.isConvertibleTo(Json::ValueType::stringValue)) {
+                    RING_ERR() << "Invalid configuration for '" << encoder << "'";
+                    return;
+                }
+                const auto& key = it.key().asString();
+                const auto& value = v.asString();
+                // TODO treat some keys specially, such as profile, level, bit_rate, rate control options, qmin, qmax, as these are AVCodecContext fields
+                if (std::find(ignoredKeys.cbegin(), ignoredKeys.cend(), key) != ignoredKeys.cend())
+                    continue;
+                else
+                    libav_utils::setDictValue(dict, key, value);
+            }
+        } catch (const Json::Exception& e) {
+            RING_ERR() << "Failed to load encoder configuration file: " << e.what();
+        }
+    }
+}
+
 } // namespace ring
diff --git a/src/media/media_encoder.h b/src/media/media_encoder.h
index 8791ed035ec53f82d137a7939715317ff86a9f54..6a878dbbde721a3039adcb647506a9158f5e3846 100644
--- a/src/media/media_encoder.h
+++ b/src/media/media_encoder.h
@@ -124,6 +124,7 @@ private:
     bool is_muted = false;
 
 protected:
+    void readConfig(AVDictionary** dict, const std::string& encoder);
     AVDictionary *options_ = nullptr;
     DeviceParams device_;
     std::shared_ptr<const AccountCodecInfo> codec_;