Remember tune=iq option across frames (#2997)
diff --git a/src/codec_aom.c b/src/codec_aom.c
index 7741f8d..51b59c0 100644
--- a/src/codec_aom.c
+++ b/src/codec_aom.c
@@ -69,6 +69,7 @@
     aom_img_fmt_t aomFormat;
     uint32_t currentLayer;
     int qualityFirstLayer;
+    avifBool previousFrameUsedTuneIq;
 #endif
 };
 
@@ -403,16 +404,21 @@
     return AVIF_TRUE;
 }
 
-static avifBool avifImageUsesTuneIq(const avifCodec * codec, avifBool alpha)
+static avifBool avifImageUsesTuneIq(const avifCodec * codec, avifBool alpha, avifBool useLibavifDefaultTuneMetric, aom_tune_metric libavifDefaultTuneMetric)
 {
-    avifBool ret = AVIF_FALSE;
-
 #if !defined(AOM_HAVE_TUNE_IQ)
 // Define the tune IQ value here if libaom doesn't define it. The enum value is guaranteed to never change
 // in libaom, so this definition won't ever get out of sync.
 #define AOM_TUNE_IQ 10
 #endif
 
+    if (useLibavifDefaultTuneMetric) {
+        return libavifDefaultTuneMetric == AOM_TUNE_IQ;
+    }
+
+    avifBool useTuneIq = AVIF_FALSE;
+    avifBool isAnyTuneDefined = AVIF_FALSE;
+
     // Tune IQ string -> enum mapping
     static const struct aomOptionEnumList tuneIqEnum[] = { { "iq", AOM_TUNE_IQ }, // Image Quality (IQ) mode
                                                            { NULL, 0 } };
@@ -423,16 +429,24 @@
         // If there are multiple "tune" options specified, honor the last one.
         // For consistent behavior, handle both cases where tune IQ was either specified as a string (tune=iq),
         // or as an enum value (tune=10).
-        if (avifKeyEqualsName(entry->key, "tune", alpha) && aomOptionParseEnum(entry->value, tuneIqEnum, &val)) {
-            ret = (val == AOM_TUNE_IQ);
+        if (avifKeyEqualsName(entry->key, "tune", alpha)) {
+            isAnyTuneDefined = AVIF_TRUE;
+
+            if (aomOptionParseEnum(entry->value, tuneIqEnum, &val)) {
+                useTuneIq = (val == AOM_TUNE_IQ);
+            }
         }
     }
 
-    // In practice this function should also return true if avifEncoderSetCodecSpecificOption("tune", "iq")
-    // was called for a previous frame and not called (or called with NULL) for this frame, because the tune
-    // option persists across frames in libaom. However AOM_TUNE_IQ is only supported with still images in
-    // libavif and libaom as of today, so there is no need to remember this option across frames.
-    return ret;
+    if (!isAnyTuneDefined && codec->internal->previousFrameUsedTuneIq) {
+        // Handle the case where the encoder was called with avifEncoderSetCodecSpecificOption("tune", "iq")
+        // for a previous frame and not called (or called with NULL) for this frame, because the tune
+        // option persists across frames in libaom.
+        // In this case, we know libaom will also use tune=iq for this frame.
+        return AVIF_TRUE;
+    }
+
+    return useTuneIq;
 }
 
 #if !defined(HAVE_AOM_CODEC_SET_OPTION)
@@ -725,23 +739,33 @@
 
     avifBool useLibavifDefaultTuneMetric = AVIF_FALSE;        // If true, override libaom's default tune option.
     aom_tune_metric libavifDefaultTuneMetric = AOM_TUNE_PSNR; // Meaningless unless useLibavifDefaultTuneMetric.
-    if (quality != AVIF_QUALITY_LOSSLESS && !avifAOMOptionsContainExplicitTuning(codec, alpha)) {
-        useLibavifDefaultTuneMetric = AVIF_TRUE;
-        if (alpha) {
-            // Minimize ringing for alpha.
-            libavifDefaultTuneMetric = AOM_TUNE_PSNR;
-        } else {
-            libavifDefaultTuneMetric = AOM_TUNE_SSIM;
+
+    // libavif only needs to set the default tune metric for the first frame,
+    // because libaom will persist that setting until explicitly changed.
+    if (!codec->internal->encoderInitialized) {
+        if (quality != AVIF_QUALITY_LOSSLESS && !avifAOMOptionsContainExplicitTuning(codec, alpha)) {
+            useLibavifDefaultTuneMetric = AVIF_TRUE;
+            if (alpha) {
+                // Minimize ringing for alpha.
+                libavifDefaultTuneMetric = AOM_TUNE_PSNR;
+            } else {
+                libavifDefaultTuneMetric = AOM_TUNE_SSIM;
+            }
         }
+        AVIF_ASSERT_OR_RETURN(!codec->internal->previousFrameUsedTuneIq);
     }
 
     struct aom_codec_enc_cfg * cfg = &codec->internal->cfg;
     avifBool quantizerUpdated = AVIF_FALSE;
     // True if libavif knows that tune=iq is used, either by default by libavif, or explicitly set by the user.
     // False otherwise (including if libaom uses tune=iq by default, which is not the case as of v3.13.1 and earlier versions).
-    const avifBool useTuneIq = useLibavifDefaultTuneMetric ? libavifDefaultTuneMetric == AOM_TUNE_IQ : avifImageUsesTuneIq(codec, alpha);
+    const avifBool useTuneIq = avifImageUsesTuneIq(codec, alpha, useLibavifDefaultTuneMetric, libavifDefaultTuneMetric);
     const int quantizer = aomQualityToQuantizer(quality, useTuneIq);
 
+    // libavif needs to know whether the current frame uses tune=iq for the next frame, as libaom persists
+    // tuning modes across frames
+    codec->internal->previousFrameUsedTuneIq = useTuneIq;
+
     // For encoder->scalingMode.horizontal and encoder->scalingMode.vertical to take effect in AOM
     // encoder, config should be applied for each frame, so we don't care about changes on these
     // two fields.
diff --git a/tests/gtest/avifprogressivetest.cc b/tests/gtest/avifprogressivetest.cc
index ceb34c4..d9e211a 100644
--- a/tests/gtest/avifprogressivetest.cc
+++ b/tests/gtest/avifprogressivetest.cc
@@ -87,6 +87,37 @@
   TestDecode(kImageSize, kImageSize);
 }
 
+// NOTE: This test requires libaom v3.12.0 or later, as this was the first
+// version where tune IQ was available
+TEST_F(ProgressiveTest, TuneIq) {
+  encoder_->extraLayerCount = 1;
+  // Tune IQ requires all-intra mode, which libavif determines when the first
+  // layer is encoded at a very low quality (e.g. quality 10)
+  encoder_->quality = 10;
+  encoder_->codecChoice = AVIF_CODEC_CHOICE_AOM;
+
+  ASSERT_EQ(avifEncoderSetCodecSpecificOption(encoder_.get(), "tune", "iq"),
+            AVIF_RESULT_OK);
+  avifResult result = avifEncoderAddImage(encoder_.get(), image_.get(), 1,
+                                          AVIF_ADD_IMAGE_FLAG_NONE);
+
+  if (result == AVIF_RESULT_INVALID_CODEC_SPECIFIC_OPTION) {
+    // The aom version that libavif was built with likely does not support
+    // AOM_TUNE_IQ.
+    return;
+  }
+
+  ASSERT_EQ(result, AVIF_RESULT_OK);
+  encoder_->quality = 50;
+  ASSERT_EQ(avifEncoderAddImage(encoder_.get(), image_.get(), 1,
+                                AVIF_ADD_IMAGE_FLAG_NONE),
+            AVIF_RESULT_OK);
+
+  ASSERT_EQ(avifEncoderFinish(encoder_.get(), &encoded_avif_), AVIF_RESULT_OK);
+
+  TestDecode(kImageSize, kImageSize);
+}
+
 // NOTE: This test requires libaom v3.6.0 or later, otherwise the following
 // assertion in libaom fails:
 //   av1/encoder/mcomp.c:1717: av1_full_pixel_search: Assertion