Roll "singleImage" and keyframe forcing into a flags value, add keyframe interval feature with a sensible default in avifenc
diff --git a/apps/avifenc.c b/apps/avifenc.c
index 1a2d9c1..3c61135 100644
--- a/apps/avifenc.c
+++ b/apps/avifenc.c
@@ -12,6 +12,8 @@
 #include <stdlib.h>
 #include <string.h>
 
+#define KEYFRAME_INTERVAL_DEFAULT 8 // A reasonable (but arbitrary) default
+
 #define NEXTARG()                                                     \
     if (((argIndex + 1) == argc) || (argv[argIndex + 1][0] == '-')) { \
         fprintf(stderr, "%s requires an argument.", arg);             \
@@ -56,6 +58,8 @@
            AVIF_SPEED_FASTEST);
     printf("    -c,--codec C                      : AV1 codec to use (choose from versions list below)\n");
     printf("    --timescale,--fps V               : Set the timescale to V. If all frames are 1 timescale in length, this is equivalent to frames per second\n");
+    printf("    -k,--keyframe INTERVAL            : Set the keyframe interval (how many frames between keyframes). Set to 0 to disable. Default: %d\n",
+           KEYFRAME_INTERVAL_DEFAULT);
     printf("    --ignore-icc                      : If the input file contains an embedded ICC profile, ignore it (no-op if absent)\n");
     printf("    --pasp H,V                        : Add pasp property (aspect ratio). H=horizontal spacing, V=vertical spacing\n");
     printf("    --clap WN,WD,HN,HD,HON,HOD,VON,VOD: Add clap property (clean aperture). Width, Height, HOffset, VOffset (in num/denom pairs)\n");
@@ -165,6 +169,7 @@
     avifImage * nextImage = NULL;
     avifRWData raw = AVIF_DATA_EMPTY;
     int timescale = 1; // 1 fps by default
+    int keyframeInterval = KEYFRAME_INTERVAL_DEFAULT;
 
     // By default, the color profile itself is unspecified, so CP/TC are set (to 2) accordingly.
     // However, if the end-user doesn't specify any CICP, we will convert to YUV using BT709
@@ -212,6 +217,9 @@
                 returnCode = 1;
                 goto cleanup;
             }
+        } else if (!strcmp(arg, "-k") || !strcmp(arg, "--keyframe")) {
+            NEXTARG();
+            keyframeInterval = atoi(arg);
         } else if (!strcmp(arg, "--min")) {
             NEXTARG();
             minQuantizer = atoi(arg);
@@ -538,13 +546,18 @@
     encoder->codecChoice = codecChoice;
     encoder->speed = speed;
     encoder->timescale = (uint64_t)timescale;
-    encoder->singleImage = (inputFilenamesCount == 1);
+    encoder->keyframeInterval = keyframeInterval;
+
+    uint32_t addImageFlags = AVIF_ADD_IMAGE_FLAG_NONE;
+    if (inputFilenamesCount == 1) {
+        addImageFlags |= AVIF_ADD_IMAGE_FLAG_SINGLE;
+    }
 
     uint32_t firstDurationInTimescales = 1; // TODO: allow arbitrary per-frame durations
     if (inputFilenamesCount > 1) {
         printf(" * Encoding frame 1 [%u/%d ts]: %s\n", firstDurationInTimescales, timescale, inputFilenames[0]);
     }
-    avifResult addImageResult = avifEncoderAddImage(encoder, image, 1);
+    avifResult addImageResult = avifEncoderAddImage(encoder, image, 1, addImageFlags);
     if (addImageResult != AVIF_RESULT_OK) {
         fprintf(stderr, "ERROR: Failed to encode image: %s\n", avifResultToString(addImageResult));
         goto cleanup;
@@ -630,7 +643,7 @@
                 goto cleanup;
             }
 
-            avifResult nextImageResult = avifEncoderAddImage(encoder, nextImage, nextDurationInTimescales);
+            avifResult nextImageResult = avifEncoderAddImage(encoder, nextImage, nextDurationInTimescales, AVIF_ADD_IMAGE_FLAG_NONE);
             if (nextImageResult != AVIF_RESULT_OK) {
                 fprintf(stderr, "ERROR: Failed to encode image: %s\n", avifResultToString(nextImageResult));
                 goto cleanup;
diff --git a/include/avif/avif.h b/include/avif/avif.h
index 3f83efc..89b889c 100644
--- a/include/avif/avif.h
+++ b/include/avif/avif.h
@@ -651,10 +651,8 @@
     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)
-    avifBool singleImage; // Set to true when encoding a single image. Signals "still_picture" to AV1 encoders, which
-                          // tweaks various compression rules. This is enabled automatically when using the
-                          // avifEncoderWrite() single-image encode path.
 
     // stats from the most recent write
     avifIOStats ioStats;
@@ -667,6 +665,19 @@
 avifResult avifEncoderWrite(avifEncoder * encoder, const avifImage * image, avifRWData * output);
 void avifEncoderDestroy(avifEncoder * encoder);
 
+enum avifAddImageFlags
+{
+    AVIF_ADD_IMAGE_FLAG_NONE = 0,
+
+    // Force this frame to be a keyframe (sync frame).
+    AVIF_ADD_IMAGE_FLAG_FORCE_KEYFRAME = (1 << 0),
+
+    // Use this flag when encoding a single image. Signals "still_picture" to AV1 encoders, which
+    // tweaks various compression rules. This is enabled automatically when using the
+    // avifEncoderWrite() single-image encode path.
+    AVIF_ADD_IMAGE_FLAG_SINGLE = (1 << 1)
+};
+
 // Multi-function alternative to avifEncoderWrite() for image sequences.
 //
 // Usage / function call order is:
@@ -676,7 +687,7 @@
 // * avifEncoderFinish()
 // * avifEncoderDestroy()
 //
-avifResult avifEncoderAddImage(avifEncoder * encoder, const avifImage * image, uint64_t durationInTimescales);
+avifResult avifEncoderAddImage(avifEncoder * encoder, const avifImage * image, uint64_t durationInTimescales, uint32_t addImageFlags);
 avifResult avifEncoderFinish(avifEncoder * encoder, avifRWData * output);
 
 // Helpers
diff --git a/include/avif/internal.h b/include/avif/internal.h
index 325de86..5ff0fd4 100644
--- a/include/avif/internal.h
+++ b/include/avif/internal.h
@@ -136,7 +136,7 @@
                                              const avifImage * image,
                                              avifEncoder * encoder,
                                              avifBool alpha,
-                                             avifBool forceKeyframe,
+                                             uint32_t addImageFlags,
                                              avifCodecEncodeOutput * output);
 typedef avifBool (*avifCodecEncodeFinishFunc)(struct avifCodec * codec, avifCodecEncodeOutput * output);
 typedef void (*avifCodecDestroyInternalFunc)(struct avifCodec * codec);
diff --git a/src/codec_aom.c b/src/codec_aom.c
index 7802d6d..a23e0cc 100644
--- a/src/codec_aom.c
+++ b/src/codec_aom.c
@@ -239,7 +239,7 @@
                                     const avifImage * image,
                                     avifEncoder * encoder,
                                     avifBool alpha,
-                                    avifBool forceKeyframe,
+                                    uint32_t addImageFlags,
                                     avifCodecEncodeOutput * output)
 {
     if (!codec->internal->encoderInitialized) {
@@ -398,7 +398,7 @@
     }
 
     aom_enc_frame_flags_t encodeFlags = 0;
-    if (forceKeyframe) {
+    if (addImageFlags & AVIF_ADD_IMAGE_FLAG_FORCE_KEYFRAME) {
         encodeFlags |= AOM_EFLAG_FORCE_KF;
     }
     aom_codec_encode(&codec->internal->encoder, aomImage, 0, 1, encodeFlags);
diff --git a/src/codec_rav1e.c b/src/codec_rav1e.c
index 9cdf133..b765159 100644
--- a/src/codec_rav1e.c
+++ b/src/codec_rav1e.c
@@ -35,7 +35,7 @@
                                       const avifImage * image,
                                       avifEncoder * encoder,
                                       avifBool alpha,
-                                      avifBool forceKeyframe,
+                                      uint32_t addImageFlags,
                                       avifCodecEncodeOutput * output)
 {
     avifBool success = AVIF_FALSE;
@@ -79,7 +79,7 @@
             goto cleanup;
         }
 
-        if (encoder->singleImage) {
+        if (addImageFlags & AVIF_ADD_IMAGE_FLAG_SINGLE) {
             if (rav1e_config_parse(rav1eConfig, "still_picture", "true") == -1) {
                 goto cleanup;
             }
@@ -153,7 +153,7 @@
     }
 
     RaFrameTypeOverride frameType = RA_FRAME_TYPE_OVERRIDE_NO;
-    if (forceKeyframe) {
+    if (addImageFlags & AVIF_ADD_IMAGE_FLAG_FORCE_KEYFRAME) {
         frameType = RA_FRAME_TYPE_OVERRIDE_KEY;
     }
     rav1e_frame_set_type(rav1eFrame, frameType);
diff --git a/src/write.c b/src/write.c
index b666d65..1c94e65 100644
--- a/src/write.c
+++ b/src/write.c
@@ -177,7 +177,7 @@
     encoder->speed = AVIF_SPEED_DEFAULT;
     encoder->data = avifEncoderDataCreate();
     encoder->timescale = 1;
-    encoder->singleImage = AVIF_FALSE;
+    encoder->keyframeInterval = 0;
     return encoder;
 }
 
@@ -332,7 +332,7 @@
     avifRWStreamFinishBox(s, meta);
 }
 
-avifResult avifEncoderAddImage(avifEncoder * encoder, const avifImage * image, uint64_t durationInTimescales)
+avifResult avifEncoderAddImage(avifEncoder * encoder, const avifImage * image, uint64_t durationInTimescales, uint32_t addImageFlags)
 {
     // -----------------------------------------------------------------------
     // Validate image
@@ -446,15 +446,14 @@
     // -----------------------------------------------------------------------
     // Encode AV1 OBUs
 
-    avifBool forceKeyframe = AVIF_FALSE;
-    if ((encoder->data->frames.count % 8) == 0) { // TODO: make this configurable
-        forceKeyframe = AVIF_TRUE;
+    if (encoder->keyframeInterval && ((encoder->data->frames.count % encoder->keyframeInterval) == 0)) {
+        addImageFlags |= AVIF_ADD_IMAGE_FLAG_FORCE_KEYFRAME;
     }
 
     for (uint32_t itemIndex = 0; itemIndex < encoder->data->items.count; ++itemIndex) {
         avifEncoderItem * item = &encoder->data->items.item[itemIndex];
         if (item->codec) {
-            if (!item->codec->encodeImage(item->codec, image, encoder, item->alpha, forceKeyframe, item->encodeOutput)) {
+            if (!item->codec->encodeImage(item->codec, image, encoder, item->alpha, addImageFlags, item->encodeOutput)) {
                 return item->alpha ? AVIF_RESULT_ENCODE_ALPHA_FAILED : AVIF_RESULT_ENCODE_COLOR_FAILED;
             }
         }
@@ -956,8 +955,7 @@
 
 avifResult avifEncoderWrite(avifEncoder * encoder, const avifImage * image, avifRWData * output)
 {
-    encoder->singleImage = AVIF_TRUE;
-    avifResult addImageResult = avifEncoderAddImage(encoder, image, 1);
+    avifResult addImageResult = avifEncoderAddImage(encoder, image, 1, AVIF_ADD_IMAGE_FLAG_SINGLE);
     if (addImageResult != AVIF_RESULT_OK) {
         return addImageResult;
     }