Support update encoder settings during encoding

Detect changes made to avifEncoder between avifEncoderAddImage() calls.
diff --git a/include/avif/avif.h b/include/avif/avif.h
index acc6671..5800b33 100644
--- a/include/avif/avif.h
+++ b/include/avif/avif.h
@@ -149,7 +149,9 @@
     AVIF_RESULT_WAITING_ON_IO, // similar to EAGAIN/EWOULDBLOCK, this means the avifIO doesn't have necessary data available yet
     AVIF_RESULT_INVALID_ARGUMENT, // an argument passed into this function is invalid
     AVIF_RESULT_NOT_IMPLEMENTED,  // a requested code path is not (yet) implemented
-    AVIF_RESULT_OUT_OF_MEMORY
+    AVIF_RESULT_OUT_OF_MEMORY,
+    AVIF_RESULT_CANNOT_CHANGE_SETTING, // a setting that can't change is changed during encoding
+    AVIF_RESULT_INCOMPATIBLE_IMAGE     // given image is not compatible with already encoded image
 } avifResult;
 
 AVIF_API const char * avifResultToString(avifResult result);
@@ -1056,12 +1058,17 @@
 //   image in less bytes. AVIF_SPEED_DEFAULT means "Leave the AV1 codec to its default speed settings"./
 //   If avifEncoder uses rav1e, the speed value is directly passed through (0-10). If libaom is used,
 //   a combination of settings are tweaked to simulate this speed range.
+// * AV1 encoder settings and codec specific options set by avifEncoderSetCodecSpecificOption()
+//   will be applied / updated to AV1 encoder before each call to avifEncoderAddImage().
 typedef struct avifEncoder
 {
     // Defaults to AVIF_CODEC_CHOICE_AUTO: Preference determined by order in availableCodecs table (avif.c)
     avifCodecChoice codecChoice;
 
     // settings (see Notes above)
+    int keyframeInterval; // How many frames between automatic forced keyframes; 0 to disable (default).
+    uint64_t timescale;   // timescale of the media (Hz)
+    // AV1 encoder settings.
     int maxThreads;
     int minQuantizer;
     int maxQuantizer;
@@ -1070,8 +1077,6 @@
     int tileRowsLog2;
     int tileColsLog2;
     int speed;
-    int keyframeInterval; // How many frames between automatic forced keyframes; 0 to disable (default).
-    uint64_t timescale;   // timescale of the media (Hz)
 
     // stats from the most recent write
     avifIOStats ioStats;
@@ -1123,9 +1128,7 @@
                                             avifAddImageFlags addImageFlags);
 AVIF_API avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output);
 
-// Codec-specific, optional "advanced" tuning settings, in the form of string key/value pairs. These
-// should be set as early as possible, preferably just after creating avifEncoder but before
-// performing any other actions.
+// Codec-specific, optional "advanced" tuning settings, in the form of string key/value pairs.
 // key must be non-NULL, but passing a NULL value will delete that key, if it exists.
 // Setting an incorrect or unknown option for the current codec will cause errors of type
 // AVIF_RESULT_INVALID_CODEC_SPECIFIC_OPTION from avifEncoderWrite() or avifEncoderAddImage().
diff --git a/include/avif/internal.h b/include/avif/internal.h
index f17ee38..87bb374 100644
--- a/include/avif/internal.h
+++ b/include/avif/internal.h
@@ -267,6 +267,7 @@
                                                avifEncoder * encoder,
                                                const avifImage * image,
                                                avifBool alpha,
+                                               avifBool updateConfig,
                                                avifAddImageFlags addImageFlags,
                                                avifCodecEncodeOutput * output);
 typedef avifBool (*avifCodecEncodeFinishFunc)(struct avifCodec * codec, avifCodecEncodeOutput * output);
diff --git a/src/avif.c b/src/avif.c
index 41e4748..81f2889 100644
--- a/src/avif.c
+++ b/src/avif.c
@@ -98,6 +98,8 @@
         case AVIF_RESULT_INVALID_ARGUMENT:              return "Invalid argument";
         case AVIF_RESULT_NOT_IMPLEMENTED:               return "Not implemented";
         case AVIF_RESULT_OUT_OF_MEMORY:                 return "Out of memory";
+        case AVIF_RESULT_CANNOT_CHANGE_SETTING:         return "Can not change some settings during encoding";
+        case AVIF_RESULT_INCOMPATIBLE_IMAGE:            return "This image is incompatible with already encoded image";
         case AVIF_RESULT_UNKNOWN_ERROR:
         default:
             break;
diff --git a/src/codec_aom.c b/src/codec_aom.c
index a3fba99..6f64e59 100644
--- a/src/codec_aom.c
+++ b/src/codec_aom.c
@@ -60,6 +60,7 @@
 #if defined(AVIF_CODEC_AOM_ENCODE)
     avifBool encoderInitialized;
     aom_codec_ctx_t encoder;
+    struct aom_codec_enc_cfg cfg;
     avifPixelFormatInfo formatInfo;
     aom_img_fmt_t aomFormat;
     avifBool monochromeEnabled;
@@ -526,10 +527,11 @@
                                       avifEncoder * encoder,
                                       const avifImage * image,
                                       avifBool alpha,
+                                      avifBool updateConfig,
                                       avifAddImageFlags addImageFlags,
                                       avifCodecEncodeOutput * output)
 {
-    if (!codec->internal->encoderInitialized) {
+    if (!codec->internal->encoderInitialized || updateConfig) {
         // Map encoder speed to AOM usage + CpuUsed:
         // Speed  0: GoodQuality CpuUsed 0
         // Speed  1: GoodQuality CpuUsed 1
@@ -587,37 +589,41 @@
             }
         }
 
-        codec->internal->aomFormat = avifImageCalcAOMFmt(image, alpha);
-        if (codec->internal->aomFormat == AOM_IMG_FMT_NONE) {
-            return AVIF_RESULT_UNKNOWN_ERROR;
+        struct aom_codec_enc_cfg * cfg = &codec->internal->cfg;
+
+        aom_codec_iface_t * encoderInterface = NULL;
+        if (!codec->internal->encoderInitialized) {
+            codec->internal->aomFormat = avifImageCalcAOMFmt(image, alpha);
+            if (codec->internal->aomFormat == AOM_IMG_FMT_NONE) {
+                return AVIF_RESULT_UNKNOWN_ERROR;
+            }
+
+            avifGetPixelFormatInfo(image->yuvFormat, &codec->internal->formatInfo);
+
+            encoderInterface = aom_codec_av1_cx();
+            aom_codec_err_t err = aom_codec_enc_config_default(encoderInterface, cfg, aomUsage);
+            if (err != AOM_CODEC_OK) {
+                avifDiagnosticsPrintf(codec->diag, "aom_codec_enc_config_default() failed: %s", aom_codec_err_to_string(err));
+                return AVIF_RESULT_UNKNOWN_ERROR;
+            }
         }
 
-        avifGetPixelFormatInfo(image->yuvFormat, &codec->internal->formatInfo);
-
-        aom_codec_iface_t * encoderInterface = aom_codec_av1_cx();
-        struct aom_codec_enc_cfg cfg;
-        aom_codec_err_t err = aom_codec_enc_config_default(encoderInterface, &cfg, aomUsage);
-        if (err != AOM_CODEC_OK) {
-            avifDiagnosticsPrintf(codec->diag, "aom_codec_enc_config_default() failed: %s", aom_codec_err_to_string(err));
-            return AVIF_RESULT_UNKNOWN_ERROR;
-        }
-
-        // Set our own default cfg.rc_end_usage value, which may differ from libaom's default.
+        // Set our own default cfg->rc_end_usage value, which may differ from libaom's default.
         switch (aomUsage) {
             case AOM_USAGE_GOOD_QUALITY:
                 // libaom's default is AOM_VBR. Change the default to AOM_Q since we don't need to
                 // hit a certain target bit rate. It's easier to control the worst quality in Q
                 // mode.
-                cfg.rc_end_usage = AOM_Q;
+                cfg->rc_end_usage = AOM_Q;
                 break;
             case AOM_USAGE_REALTIME:
                 // For real-time mode we need to use CBR rate control mode. AOM_Q doesn't fit the
                 // rate control requirements for real-time mode. CBR does.
-                cfg.rc_end_usage = AOM_CBR;
+                cfg->rc_end_usage = AOM_CBR;
                 break;
 #if defined(AOM_USAGE_ALL_INTRA)
             case AOM_USAGE_ALL_INTRA:
-                cfg.rc_end_usage = AOM_Q;
+                cfg->rc_end_usage = AOM_Q;
                 break;
 #endif
         }
@@ -657,16 +663,16 @@
             }
         }
 
-        cfg.g_profile = seqProfile;
-        cfg.g_bit_depth = image->depth;
-        cfg.g_input_bit_depth = image->depth;
-        cfg.g_w = image->width;
-        cfg.g_h = image->height;
+        cfg->g_profile = seqProfile;
+        cfg->g_bit_depth = image->depth;
+        cfg->g_input_bit_depth = image->depth;
+        cfg->g_w = image->width;
+        cfg->g_h = image->height;
         if (addImageFlags & AVIF_ADD_IMAGE_FLAG_SINGLE) {
             // Set the maximum number of frames to encode to 1. This instructs
             // libaom to set still_picture and reduced_still_picture_header to
             // 1 in AV1 sequence headers.
-            cfg.g_limit = 1;
+            cfg->g_limit = 1;
 
             // Use the default settings of the new AOM_USAGE_ALL_INTRA (added in
             // https://crbug.com/aomedia/2959).
@@ -674,14 +680,14 @@
             // Set g_lag_in_frames to 0 to reduce the number of frame buffers
             // (from 20 to 2) in libaom's lookahead structure. This reduces
             // memory consumption when encoding a single image.
-            cfg.g_lag_in_frames = 0;
+            cfg->g_lag_in_frames = 0;
             // Disable automatic placement of key frames by the encoder.
-            cfg.kf_mode = AOM_KF_DISABLED;
+            cfg->kf_mode = AOM_KF_DISABLED;
             // Tell libaom that all frames will be key frames.
-            cfg.kf_max_dist = 0;
+            cfg->kf_max_dist = 0;
         }
         if (encoder->maxThreads > 1) {
-            cfg.g_threads = encoder->maxThreads;
+            cfg->g_threads = encoder->maxThreads;
         }
 
         int minQuantizer = AVIF_CLAMP(encoder->minQuantizer, 0, 63);
@@ -691,8 +697,8 @@
             maxQuantizer = AVIF_CLAMP(encoder->maxQuantizerAlpha, 0, 63);
         }
         avifBool lossless = ((minQuantizer == AVIF_QUANTIZER_LOSSLESS) && (maxQuantizer == AVIF_QUANTIZER_LOSSLESS));
-        cfg.rc_min_quantizer = minQuantizer;
-        cfg.rc_max_quantizer = maxQuantizer;
+        cfg->rc_min_quantizer = minQuantizer;
+        cfg->rc_max_quantizer = maxQuantizer;
 
         codec->internal->monochromeEnabled = AVIF_FALSE;
         if (aomVersion > aomVersion_2_0_0) {
@@ -700,32 +706,43 @@
             // access nonexistent UV planes when encoding monochrome at faster libavif "speeds". It
             // was fixed shortly after the 2.0.0 libaom release, and the fix exists in both the
             // master and applejack branches. This ensures that the next version *after* 2.0.0 will
-            // have the fix, and we must avoid cfg.monochrome until then.
+            // have the fix, and we must avoid cfg->monochrome until then.
             //
             // Bugfix Change-Id: https://aomedia-review.googlesource.com/q/I26a39791f820b4d4e1d63ff7141f594c3c7181f5
 
             if (alpha || (image->yuvFormat == AVIF_PIXEL_FORMAT_YUV400)) {
                 codec->internal->monochromeEnabled = AVIF_TRUE;
-                cfg.monochrome = 1;
+                cfg->monochrome = 1;
             }
         }
 
-        if (!avifProcessAOMOptionsPreInit(codec, alpha, &cfg)) {
+        if (!avifProcessAOMOptionsPreInit(codec, alpha, cfg)) {
             return AVIF_RESULT_INVALID_CODEC_SPECIFIC_OPTION;
         }
 
-        aom_codec_flags_t encoderFlags = 0;
-        if (image->depth > 8) {
-            encoderFlags |= AOM_CODEC_USE_HIGHBITDEPTH;
+        if (!codec->internal->encoderInitialized) {
+            aom_codec_flags_t encoderFlags = 0;
+            if (image->depth > 8) {
+                encoderFlags |= AOM_CODEC_USE_HIGHBITDEPTH;
+            }
+
+            if (aom_codec_enc_init(&codec->internal->encoder, encoderInterface, cfg, encoderFlags) != AOM_CODEC_OK) {
+                avifDiagnosticsPrintf(codec->diag,
+                                      "aom_codec_enc_init() failed: %s: %s",
+                                      aom_codec_error(&codec->internal->encoder),
+                                      aom_codec_error_detail(&codec->internal->encoder));
+                return AVIF_RESULT_UNKNOWN_ERROR;
+            }
+            codec->internal->encoderInitialized = AVIF_TRUE;
+        } else {
+            if (aom_codec_enc_config_set(&codec->internal->encoder, cfg) != AOM_CODEC_OK) {
+                avifDiagnosticsPrintf(codec->diag,
+                                      "aom_codec_enc_config_set() failed: %s: %s",
+                                      aom_codec_error(&codec->internal->encoder),
+                                      aom_codec_error_detail(&codec->internal->encoder));
+                return AVIF_RESULT_UNKNOWN_ERROR;
+            }
         }
-        if (aom_codec_enc_init(&codec->internal->encoder, encoderInterface, &cfg, encoderFlags) != AOM_CODEC_OK) {
-            avifDiagnosticsPrintf(codec->diag,
-                                  "aom_codec_enc_init() failed: %s: %s",
-                                  aom_codec_error(&codec->internal->encoder),
-                                  aom_codec_error_detail(&codec->internal->encoder));
-            return AVIF_RESULT_UNKNOWN_ERROR;
-        }
-        codec->internal->encoderInitialized = AVIF_TRUE;
 
         if (lossless) {
             aom_codec_control(&codec->internal->encoder, AV1E_SET_LOSSLESS, 1);
@@ -756,8 +773,8 @@
             // set the min and max quantizers in the avifEncoder struct. If this is the case, set
             // cq-level to a reasonable value for the user, otherwise the default cq-level
             // (currently 10) will be unknowingly used.
-            assert(cfg.rc_end_usage == AOM_Q);
-            unsigned int cqLevel = (cfg.rc_min_quantizer + cfg.rc_max_quantizer) / 2;
+            assert(cfg->rc_end_usage == AOM_Q);
+            unsigned int cqLevel = (cfg->rc_min_quantizer + cfg->rc_max_quantizer) / 2;
             aom_codec_control(&codec->internal->encoder, AOME_SET_CQ_LEVEL, cqLevel);
         }
 #endif
diff --git a/src/codec_rav1e.c b/src/codec_rav1e.c
index e9ed443..f8b301a 100644
--- a/src/codec_rav1e.c
+++ b/src/codec_rav1e.c
@@ -51,9 +51,14 @@
                                         avifEncoder * encoder,
                                         const avifImage * image,
                                         avifBool alpha,
+                                        avifBool updateConfig,
                                         uint32_t addImageFlags,
                                         avifCodecEncodeOutput * output)
 {
+    if (updateConfig) {
+        return AVIF_RESULT_NOT_IMPLEMENTED;
+    }
+
     avifResult result = AVIF_RESULT_UNKNOWN_ERROR;
 
     RaConfig * rav1eConfig = NULL;
diff --git a/src/codec_svt.c b/src/codec_svt.c
index f6aefde..f45ccf1 100644
--- a/src/codec_svt.c
+++ b/src/codec_svt.c
@@ -46,9 +46,14 @@
                                       avifEncoder * encoder,
                                       const avifImage * image,
                                       avifBool alpha,
+                                      avifBool updateConfig,
                                       uint32_t addImageFlags,
                                       avifCodecEncodeOutput * output)
 {
+    if (updateConfig) {
+        return AVIF_RESULT_NOT_IMPLEMENTED;
+    }
+
     avifResult result = AVIF_RESULT_UNKNOWN_ERROR;
     EbColorFormat color_format = EB_YUV420;
     EbBufferHeaderType * input_buffer = NULL;
diff --git a/src/write.c b/src/write.c
index 2585678..0083dcf 100644
--- a/src/write.c
+++ b/src/write.c
@@ -122,11 +122,13 @@
 {
     avifEncoderItemArray items;
     avifEncoderFrameArray frames;
+    avifEncoder lastEncoder;
     avifImage * imageMetadata;
     uint16_t lastItemID;
     uint16_t primaryItemID;
     avifBool singleImage; // if true, the AVIF_ADD_IMAGE_FLAG_SINGLE flag was set on the first call to avifEncoderAddImage()
     avifBool alphaPresent;
+    avifBool csOptionsUpdated;
 } avifEncoderData;
 
 static void avifEncoderDataDestroy(avifEncoderData * data);
@@ -317,6 +319,54 @@
 void avifEncoderSetCodecSpecificOption(avifEncoder * encoder, const char * key, const char * value)
 {
     avifCodecSpecificOptionsSet(encoder->csOptions, key, value);
+    encoder->data->csOptionsUpdated = AVIF_TRUE; // False positive is possible but not important.
+}
+
+static void avifBackupSettings(avifEncoder * encoder)
+{
+    avifEncoder * lastEncoder = &encoder->data->lastEncoder;
+
+    // lastEncoder->data is used to mark that lastEncoder is initialized.
+    lastEncoder->data = encoder->data;
+    lastEncoder->codecChoice = encoder->codecChoice;
+    lastEncoder->keyframeInterval = encoder->keyframeInterval;
+    lastEncoder->timescale = encoder->timescale;
+    lastEncoder->maxThreads = encoder->maxThreads;
+    lastEncoder->minQuantizer = encoder->minQuantizer;
+    lastEncoder->maxQuantizer = encoder->maxQuantizer;
+    lastEncoder->minQuantizerAlpha = encoder->minQuantizerAlpha;
+    lastEncoder->maxQuantizerAlpha = encoder->maxQuantizerAlpha;
+    lastEncoder->tileRowsLog2 = encoder->tileRowsLog2;
+    lastEncoder->tileColsLog2 = encoder->tileColsLog2;
+    lastEncoder->speed = encoder->speed;
+    encoder->data->csOptionsUpdated = AVIF_FALSE;
+}
+
+// This function detect changes made on avifEncoder.
+// It reports if the change is valid, i.e. if any setting that can't change was changed.
+// It also sets needUpdate to true if valid changes are detected.
+static avifBool avifEncoderSettingsChanged(const avifEncoder * encoder, avifBool * needUpdate)
+{
+    const avifEncoder * lastEncoder = &encoder->data->lastEncoder;
+
+    if (!lastEncoder->data) {
+        return AVIF_TRUE;
+    }
+
+    if ((lastEncoder->codecChoice != encoder->codecChoice) || (lastEncoder->keyframeInterval != encoder->keyframeInterval) ||
+        (lastEncoder->timescale != encoder->timescale)) {
+        return AVIF_FALSE;
+    }
+
+    if ((lastEncoder->maxThreads != encoder->maxThreads) || (lastEncoder->minQuantizer != encoder->minQuantizer) ||
+        (lastEncoder->maxQuantizer != encoder->maxQuantizer) || (lastEncoder->minQuantizerAlpha != encoder->minQuantizerAlpha) ||
+        (lastEncoder->maxQuantizerAlpha != encoder->maxQuantizerAlpha) || (lastEncoder->tileRowsLog2 != encoder->tileRowsLog2) ||
+        (lastEncoder->tileColsLog2 != encoder->tileColsLog2) || (lastEncoder->speed != encoder->speed) ||
+        (encoder->data->csOptionsUpdated)) {
+        *needUpdate = AVIF_TRUE;
+    }
+
+    return AVIF_TRUE;
 }
 
 // This function is used in two codepaths:
@@ -606,6 +656,12 @@
         return AVIF_RESULT_NO_CODEC_AVAILABLE;
     }
 
+    avifBool updateConfig = AVIF_FALSE;
+    if (!avifEncoderSettingsChanged(encoder, &updateConfig)) {
+        return AVIF_RESULT_CANNOT_CHANGE_SETTING;
+    }
+    avifBackupSettings(encoder);
+
     // -----------------------------------------------------------------------
     // Validate images
 
@@ -806,10 +862,23 @@
     } else {
         // Another frame in an image sequence
 
-        if (encoder->data->alphaPresent && !firstCell->alphaPlane) {
-            // If the first image in the sequence had an alpha plane (even if fully opaque), all
-            // subsequence images must have alpha as well.
-            return AVIF_RESULT_ENCODE_ALPHA_FAILED;
+        const avifImage * imageMetadata = encoder->data->imageMetadata;
+        // HEIF (ISO 23008-12:2017), Section 6.6.2.3.1:
+        //   All input images shall have exactly the same width and height; call those tile_width and tile_height.
+        // MIAF (ISO 23000-22:2019), Section 7.3.11.4.1:
+        //   All input images of a grid image item shall use the same coding format, chroma sampling format, and the
+        //   same decoder configuration (see 7.3.6.2).
+        // If the first image in the sequence had an alpha plane (even if fully opaque), all
+        // subsequence images must have alpha as well.
+        if ((imageMetadata->width != firstCell->width) || (imageMetadata->height != firstCell->height) ||
+            (imageMetadata->depth != firstCell->depth) || (imageMetadata->yuvFormat != firstCell->yuvFormat) ||
+            (imageMetadata->yuvRange != firstCell->yuvRange) || (imageMetadata->colorPrimaries != firstCell->colorPrimaries) ||
+            (imageMetadata->transferCharacteristics != firstCell->transferCharacteristics) ||
+            (imageMetadata->matrixCoefficients != firstCell->matrixCoefficients) ||
+            (!!imageMetadata->alphaPlane != !!firstCell->alphaPlane) ||
+            (imageMetadata->alphaPremultiplied != firstCell->alphaPremultiplied) ||
+            (encoder->data->alphaPresent && !firstCell->alphaPlane)) {
+            return AVIF_RESULT_INCOMPATIBLE_IMAGE;
         }
     }
 
@@ -825,7 +894,7 @@
         if (item->codec) {
             const avifImage * cellImage = cellImages[item->cellIndex];
             avifResult encodeResult =
-                item->codec->encodeImage(item->codec, encoder, cellImage, item->alpha, addImageFlags, item->encodeOutput);
+                item->codec->encodeImage(item->codec, encoder, cellImage, item->alpha, updateConfig, addImageFlags, item->encodeOutput);
             if (encodeResult == AVIF_RESULT_UNKNOWN_ERROR) {
                 encodeResult = item->alpha ? AVIF_RESULT_ENCODE_ALPHA_FAILED : AVIF_RESULT_ENCODE_COLOR_FAILED;
             }
diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt
index 2c8bbdb..8edf20a 100644
--- a/tests/CMakeLists.txt
+++ b/tests/CMakeLists.txt
@@ -100,6 +100,11 @@
     target_link_libraries(avify4mtest aviftest_helpers avif_apps ${GTEST_BOTH_LIBRARIES})
     target_include_directories(avify4mtest PRIVATE ${GTEST_INCLUDE_DIRS})
     add_test(NAME avify4mtest COMMAND avify4mtest)
+
+    add_executable(avifchangesettingtest gtest/avifchangesettingtest.cc)
+    target_link_libraries(avifchangesettingtest aviftest_helpers avif_apps ${GTEST_BOTH_LIBRARIES})
+    target_include_directories(avifchangesettingtest PRIVATE ${GTEST_INCLUDE_DIRS})
+    add_test(NAME avifchangesettingtest COMMAND avifchangesettingtest)
 else()
     message(STATUS "Most tests are disabled because AVIF_ENABLE_GTEST is OFF.")
 endif()
diff --git a/tests/gtest/avifchangesettingtest.cc b/tests/gtest/avifchangesettingtest.cc
new file mode 100644
index 0000000..13d3b36
--- /dev/null
+++ b/tests/gtest/avifchangesettingtest.cc
@@ -0,0 +1,147 @@
+// Copyright 2022 Yuan Tong. All rights reserved.
+// SPDX-License-Identifier: BSD-2-Clause
+
+#include <map>
+#include <string>
+
+#include "avif/avif.h"
+#include "aviftest_helpers.h"
+#include "gtest/gtest.h"
+
+namespace libavif {
+namespace {
+
+void TestEncodeDecode(avifCodecChoice codec,
+                      const std::map<std::string, std::string>& init_cs_options,
+                      bool can_encode, bool use_cq) {
+  if (avifCodecName(codec, AVIF_CODEC_FLAG_CAN_ENCODE) == nullptr) {
+    GTEST_SKIP() << "Codec unavailable, skip test.";
+  }
+
+  const uint32_t image_size = 512;
+  testutil::AvifImagePtr image =
+      testutil::CreateImage(image_size, image_size, 8, AVIF_PIXEL_FORMAT_YUV420,
+                            AVIF_PLANES_YUV, AVIF_RANGE_FULL);
+  ASSERT_NE(image, nullptr);
+  testutil::FillImageGradient(image.get());
+
+  // Encode
+  testutil::AvifEncoderPtr encoder(avifEncoderCreate(), avifEncoderDestroy);
+  ASSERT_NE(encoder, nullptr);
+  encoder->codecChoice = codec;
+  encoder->speed = AVIF_SPEED_FASTEST;
+  encoder->timescale = 1;
+
+  for (const auto& option : init_cs_options) {
+    avifEncoderSetCodecSpecificOption(encoder.get(), option.first.c_str(),
+                                      option.second.c_str());
+  }
+
+  if (use_cq) {
+    encoder->minQuantizer = 0;
+    encoder->maxQuantizer = 63;
+    avifEncoderSetCodecSpecificOption(encoder.get(), "end-usage", "q");
+    avifEncoderSetCodecSpecificOption(encoder.get(), "cq-level", "63");
+  } else {
+    encoder->minQuantizer = 63;
+    encoder->maxQuantizer = 63;
+  }
+
+  ASSERT_EQ(avifEncoderAddImage(encoder.get(), image.get(), 1,
+                                AVIF_ADD_IMAGE_FLAG_FORCE_KEYFRAME),
+            AVIF_RESULT_OK);
+
+  if (use_cq) {
+    avifEncoderSetCodecSpecificOption(encoder.get(), "cq-level", "0");
+  } else {
+    encoder->minQuantizer = 0;
+    encoder->maxQuantizer = 0;
+  }
+
+  if (!can_encode) {
+    ASSERT_EQ(avifEncoderAddImage(encoder.get(), image.get(), 1,
+                                  AVIF_ADD_IMAGE_FLAG_FORCE_KEYFRAME),
+              AVIF_RESULT_NOT_IMPLEMENTED);
+
+    return;
+  }
+
+  ASSERT_EQ(avifEncoderAddImage(encoder.get(), image.get(), 1,
+                                AVIF_ADD_IMAGE_FLAG_FORCE_KEYFRAME),
+            AVIF_RESULT_OK);
+
+  testutil::AvifRwData encodedAvif;
+  ASSERT_EQ(avifEncoderFinish(encoder.get(), &encodedAvif), AVIF_RESULT_OK);
+
+  // Decode
+  testutil::AvifDecoderPtr decoder(avifDecoderCreate(), avifDecoderDestroy);
+  ASSERT_NE(decoder, nullptr);
+
+  // The second frame is set to have far better quality,
+  // and should be much bigger, so small amount of data at beginning
+  // should be enough to decode the first frame.
+  avifIO* io = testutil::AvifIOCreateLimitedReader(
+      avifIOCreateMemoryReader(encodedAvif.data, encodedAvif.size),
+      encodedAvif.size / 10);
+  ASSERT_NE(io, nullptr);
+  avifDecoderSetIO(decoder.get(), io);
+  ASSERT_EQ(avifDecoderParse(decoder.get()), AVIF_RESULT_OK);
+  ASSERT_EQ(avifDecoderNextImage(decoder.get()), AVIF_RESULT_OK);
+  ASSERT_EQ(avifDecoderNextImage(decoder.get()), AVIF_RESULT_WAITING_ON_IO);
+  ((testutil::AvifIOLimitedReader*)io)->clamp =
+      testutil::AvifIOLimitedReader::kNoClamp;
+  ASSERT_EQ(avifDecoderNextImage(decoder.get()), AVIF_RESULT_OK);
+  ASSERT_EQ(avifDecoderNextImage(decoder.get()),
+            AVIF_RESULT_NO_IMAGES_REMAINING);
+}
+
+TEST(ChangeSettingTest, AOM) {
+  // Test if changes to AV1 encode settings are detected.
+  TestEncodeDecode(AVIF_CODEC_CHOICE_AOM, {{"end-usage", "cbr"}}, true, false);
+
+  // Test if changes to codec specific options are detected.
+  TestEncodeDecode(AVIF_CODEC_CHOICE_AOM, {}, true, true);
+}
+
+TEST(ChangeSettingTest, RAV1E) {
+  TestEncodeDecode(AVIF_CODEC_CHOICE_RAV1E, {}, false, false);
+}
+
+TEST(ChangeSettingTest, SVT) {
+  TestEncodeDecode(AVIF_CODEC_CHOICE_SVT, {}, false, false);
+}
+
+TEST(ChangeSettingTest, UnchangeableSetting) {
+  if (avifCodecName(AVIF_CODEC_CHOICE_AOM, AVIF_CODEC_FLAG_CAN_ENCODE) ==
+      nullptr) {
+    GTEST_SKIP() << "Codec unavailable, skip test.";
+  }
+
+  const uint32_t image_size = 512;
+  testutil::AvifImagePtr image =
+      testutil::CreateImage(image_size, image_size, 8, AVIF_PIXEL_FORMAT_YUV420,
+                            AVIF_PLANES_YUV, AVIF_RANGE_FULL);
+  ASSERT_NE(image, nullptr);
+  testutil::FillImageGradient(image.get());
+
+  // Encode
+  testutil::AvifEncoderPtr encoder(avifEncoderCreate(), avifEncoderDestroy);
+  ASSERT_NE(encoder, nullptr);
+  encoder->codecChoice = AVIF_CODEC_CHOICE_AOM;
+  encoder->speed = AVIF_SPEED_FASTEST;
+  encoder->timescale = 1;
+  encoder->minQuantizer = 63;
+  encoder->maxQuantizer = 63;
+
+  ASSERT_EQ(avifEncoderAddImage(encoder.get(), image.get(), 1,
+                                AVIF_ADD_IMAGE_FLAG_FORCE_KEYFRAME),
+            AVIF_RESULT_OK);
+
+  encoder->timescale = 2;
+  ASSERT_EQ(avifEncoderAddImage(encoder.get(), image.get(), 1,
+                                AVIF_ADD_IMAGE_FLAG_FORCE_KEYFRAME),
+            AVIF_RESULT_CANNOT_CHANGE_SETTING);
+}
+
+}  // namespace
+}  // namespace libavif
diff --git a/tests/gtest/aviftest_helpers.cc b/tests/gtest/aviftest_helpers.cc
index 3c4e1dc..d981a50 100644
--- a/tests/gtest/aviftest_helpers.cc
+++ b/tests/gtest/aviftest_helpers.cc
@@ -214,7 +214,39 @@
   return true;
 }
 
-//------------------------------------------------------------------------------
+static avifResult avifIOLimitedReaderRead(struct avifIO* io, uint32_t readFlags,
+                                          uint64_t offset, size_t size,
+                                          avifROData* out) {
+  auto reader = reinterpret_cast<AvifIOLimitedReader*>(io);
 
+  if (offset + size > reader->clamp) {
+    return AVIF_RESULT_WAITING_ON_IO;
+  }
+
+  return reader->underlayIO->read(reader->underlayIO, readFlags, offset, size,
+                                  out);
+}
+
+static void avifIOLimitedReaderDestroy(struct avifIO* io) {
+  auto reader = reinterpret_cast<AvifIOLimitedReader*>(io);
+  reader->underlayIO->destroy(reader->underlayIO);
+  delete reader;
+}
+
+avifIO* AvifIOCreateLimitedReader(avifIO* underlayIO, uint64_t clamp) {
+  return reinterpret_cast<avifIO*>(
+      new AvifIOLimitedReader{{
+                                  avifIOLimitedReaderDestroy,
+                                  avifIOLimitedReaderRead,
+                                  nullptr,
+                                  underlayIO->sizeHint,
+                                  underlayIO->persistent,
+                                  nullptr,
+                              },
+                              underlayIO,
+                              clamp});
+}
+
+//------------------------------------------------------------------------------
 }  // namespace testutil
 }  // namespace libavif
diff --git a/tests/gtest/aviftest_helpers.h b/tests/gtest/aviftest_helpers.h
index b7c3138..ccca8a9 100644
--- a/tests/gtest/aviftest_helpers.h
+++ b/tests/gtest/aviftest_helpers.h
@@ -4,6 +4,7 @@
 #ifndef LIBAVIF_TESTS_AVIFTEST_HELPERS_H_
 #define LIBAVIF_TESTS_AVIFTEST_HELPERS_H_
 
+#include <limits>
 #include <memory>
 
 #include "avif/avif.h"
@@ -61,6 +62,16 @@
 
 //------------------------------------------------------------------------------
 
+struct AvifIOLimitedReader {
+  static constexpr uint64_t kNoClamp = std::numeric_limits<uint64_t>::max();
+
+  avifIO io;
+  avifIO* underlayIO;
+  uint64_t clamp;
+};
+
+avifIO* AvifIOCreateLimitedReader(avifIO* underlayIO, uint64_t clamp);
+
 }  // namespace testutil
 }  // namespace libavif