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