PR #22869 opened by padenot
URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22869
Patch URL: https://code.ffmpeg.org/FFmpeg/FFmpeg/pulls/22869.patch
SVC (Scalable Video Coding) plumbing for libaomenc, modeled after the
libvpx code doing the same thing.
We've been shipping this code for some time in Firefox, to implement
bits of WebRTC and WebCodecs.
Add a new svc-parameters AVOption (a dictionary) to configure SVC for
the libaom AV1 encoder when in CBR mode. The option accepts the
following keys:
number_spatial_layers, number_temporal_layers, max_quantizers,
min_quantizers, scaling_factor_num, scaling_factor_den,
layer_target_bitrate, framerate_factor
Per-frame temporal layer IDs can be set via the "temporal_id" metadata
key on the input AVFrame.
Example encoding a 2-temporal-layer stream at 500kbps:
ffmpeg -i input.mp4 -c:v libaom-av1 -b:v 500k \
-minrate 500k -maxrate 500k \
-svc-parameters number_spatial_layers=1:number_temporal_layers=2:\
framerate_factor=1,2:layer_target_bitrate=200,500 \
output.ivf
Some minimal FATE coverage added, but this has received (and continues to
receive)
heavy testing and fuzzing in Firefox.
It's my first time adding to FATE, lmk if I missed anything.
Signed-off-by: Paul Adenot <[email protected]>
>From 2181f3c8efc9b6eb4e18a7101572b1f9216a1d8c Mon Sep 17 00:00:00 2001
From: Paul Adenot <[email protected]>
Date: Mon, 20 Apr 2026 16:31:47 +0000
Subject: [PATCH] avcodec/libaomenc: add SVC configuration support
SVC (Scalable Video Coding) plumbing for libaomenc, modeled after the
libvpx code.
We've been shipping this code for some time in Firefox, to implement
bits of WebRTC and WebCodecs.
Add a new svc-parameters AVOption (a dictionary) to configure SVC for
the libaom AV1 encoder when in CBR mode. The option accepts the
following keys:
number_spatial_layers, number_temporal_layers, max_quantizers,
min_quantizers, scaling_factor_num, scaling_factor_den,
layer_target_bitrate, framerate_factor
Per-frame temporal layer IDs can be set via the "temporal_id" metadata
key on the input AVFrame.
Example encoding a 2-temporal-layer stream at 500kbps:
ffmpeg -i input.mp4 -c:v libaom-av1 -b:v 500k \
-minrate 500k -maxrate 500k \
-svc-parameters number_spatial_layers=1:number_temporal_layers=2:\
framerate_factor=1,2:layer_target_bitrate=200,500 \
output.ivf
Some minimal FATE coverage added, but this has received heavy testing
and fuzzing in Firefox.
Signed-off-by: Paul Adenot <[email protected]>
---
libavcodec/libaomenc.c | 101 ++++++++++++++++++++++
tests/fate/av1.mak | 15 ++++
tests/ref/fate/libaom-av1-svc | 3 +
tests/ref/fate/libaom-av1-svc-temporal-id | 3 +
4 files changed, 122 insertions(+)
create mode 100644 tests/ref/fate/libaom-av1-svc
create mode 100644 tests/ref/fate/libaom-av1-svc-temporal-id
diff --git a/libavcodec/libaomenc.c b/libavcodec/libaomenc.c
index 20e97e5d43..7fd05dc847 100644
--- a/libavcodec/libaomenc.c
+++ b/libavcodec/libaomenc.c
@@ -30,6 +30,7 @@
#include <aom/aomcx.h>
#include "libavutil/avassert.h"
+#include "libavutil/avstring.h"
#include "libavutil/base64.h"
#include "libavutil/cpu.h"
#include "libavutil/hdr_dynamic_metadata.h"
@@ -138,6 +139,7 @@ typedef struct AOMEncoderContext {
int enable_diff_wtd_comp;
int enable_dist_wtd_comp;
int enable_dual_filter;
+ AVDictionary *svc_parameters;
AVDictionary *aom_params;
} AOMContext;
@@ -204,6 +206,7 @@ static const char *const ctlidstr[] = {
[AV1E_GET_TARGET_SEQ_LEVEL_IDX] = "AV1E_GET_TARGET_SEQ_LEVEL_IDX",
#endif
[AV1_GET_NEW_FRAME_IMAGE] = "AV1_GET_NEW_FRAME_IMAGE",
+ [AV1E_SET_SVC_PARAMS] = "AV1E_SET_SVC_PARAMS",
};
static av_cold void log_encoder_error(AVCodecContext *avctx, const char *desc)
@@ -420,6 +423,31 @@ static av_cold int codecctl_imgp(AVCodecContext *avctx,
return 0;
}
+static av_cold int codecctl_svcp(AVCodecContext *avctx,
+#ifdef UENUM1BYTE
+ aome_enc_control_id id,
+#else
+ enum aome_enc_control_id id,
+#endif
+ aom_svc_params_t *svc_params)
+{
+ AOMContext *ctx = avctx->priv_data;
+ char buf[80];
+ int res;
+
+ snprintf(buf, sizeof(buf), "%s:", ctlidstr[id]);
+
+ res = aom_codec_control(&ctx->encoder, id, svc_params);
+ if (res != AOM_CODEC_OK) {
+ snprintf(buf, sizeof(buf), "Failed to get %s codec control",
+ ctlidstr[id]);
+ log_encoder_error(avctx, buf);
+ return AVERROR(EINVAL);
+ }
+
+ return 0;
+}
+
static av_cold int aom_free(AVCodecContext *avctx)
{
AOMContext *ctx = avctx->priv_data;
@@ -466,6 +494,24 @@ static av_cold int aom_free(AVCodecContext *avctx)
return 0;
}
+static av_cold void aom_svc_parse_int_array(int *dest, const char *value, int
max_entries)
+{
+ char *buf = av_strdup(value);
+ char *saveptr = NULL;
+ char *token;
+ int dest_idx = 0;
+
+ if (!buf)
+ return;
+
+ token = av_strtok(buf, ",", &saveptr);
+ while (token && dest_idx < max_entries) {
+ dest[dest_idx++] = strtol(token, NULL, 10);
+ token = av_strtok(NULL, ",", &saveptr);
+ }
+ av_free(buf);
+}
+
static int set_pix_fmt(AVCodecContext *avctx, aom_codec_caps_t codec_caps,
struct aom_codec_enc_cfg *enccfg, aom_codec_flags_t
*flags,
aom_img_fmt_t *img_fmt)
@@ -983,6 +1029,41 @@ static av_cold int aom_init(AVCodecContext *avctx,
if (ctx->enable_intrabc >= 0)
codecctl_int(avctx, AV1E_SET_ENABLE_INTRABC, ctx->enable_intrabc);
+ if (enccfg.rc_end_usage == AOM_CBR && ctx->svc_parameters) {
+ aom_svc_params_t svc_params = { 0 };
+ AVDictionaryEntry *en = NULL;
+
+ svc_params.framerate_factor[0] = 1;
+ svc_params.number_spatial_layers = 1;
+ svc_params.number_temporal_layers = 1;
+
+ while ((en = av_dict_get(ctx->svc_parameters, "", en,
AV_DICT_IGNORE_SUFFIX))) {
+ if (!strlen(en->value))
+ return AVERROR(EINVAL);
+
+ if (!strcmp(en->key, "number_spatial_layers"))
+ svc_params.number_spatial_layers = strtol(en->value, NULL, 10);
+ else if (!strcmp(en->key, "number_temporal_layers"))
+ svc_params.number_temporal_layers = strtol(en->value, NULL,
10);
+ else if (!strcmp(en->key, "max_quantizers"))
+ aom_svc_parse_int_array(svc_params.max_quantizers, en->value,
AOM_MAX_LAYERS);
+ else if (!strcmp(en->key, "min_quantizers"))
+ aom_svc_parse_int_array(svc_params.min_quantizers, en->value,
AOM_MAX_LAYERS);
+ else if (!strcmp(en->key, "scaling_factor_num"))
+ aom_svc_parse_int_array(svc_params.scaling_factor_num,
en->value, AOM_MAX_SS_LAYERS);
+ else if (!strcmp(en->key, "scaling_factor_den"))
+ aom_svc_parse_int_array(svc_params.scaling_factor_den,
en->value, AOM_MAX_SS_LAYERS);
+ else if (!strcmp(en->key, "layer_target_bitrate"))
+ aom_svc_parse_int_array(svc_params.layer_target_bitrate,
en->value, AOM_MAX_LAYERS);
+ else if (!strcmp(en->key, "framerate_factor"))
+ aom_svc_parse_int_array(svc_params.framerate_factor,
en->value, AOM_MAX_TS_LAYERS);
+ }
+
+ res = codecctl_svcp(avctx, AV1E_SET_SVC_PARAMS, &svc_params);
+ if (res < 0)
+ return res;
+ }
+
#if AOM_ENCODER_ABI_VERSION >= 23
{
const AVDictionaryEntry *en = NULL;
@@ -1321,6 +1402,25 @@ static int aom_encode(AVCodecContext *avctx, AVPacket
*pkt,
if (frame->pict_type == AV_PICTURE_TYPE_I)
flags |= AOM_EFLAG_FORCE_KF;
+ AVDictionaryEntry *en = av_dict_get(frame->metadata, "temporal_id",
NULL, 0);
+ if (en) {
+ aom_svc_layer_id_t layer_id = { 0 };
+ layer_id.temporal_layer_id = strtol(en->value, NULL, 10);
+ av_log(avctx, AV_LOG_DEBUG, "Frame pts %" PRId64 " temporal layer
id %d\n",
+ frame->pts, layer_id.temporal_layer_id);
+ if (!ctx->svc_parameters) {
+ av_log(avctx, AV_LOG_WARNING,
+ "Temporal SVC not enabled, but temporal layer id
received.\n");
+ }
+ res = aom_codec_control(&ctx->encoder, AV1E_SET_SVC_LAYER_ID,
&layer_id);
+ if (res != AOM_CODEC_OK) {
+ av_log(avctx, AV_LOG_ERROR,
+ "Error setting temporal layer id %d on frame pts %"
PRId64 "\n",
+ layer_id.temporal_layer_id, frame->pts);
+ return AVERROR_EXTERNAL;
+ }
+ }
+
res = add_hdr_plus(avctx, rawimg, frame);
if (res < 0)
return res;
@@ -1551,6 +1651,7 @@ static const AVOption options[] = {
{ "enable-masked-comp", "Enable masked compound",
OFFSET(enable_masked_comp), AV_OPT_TYPE_BOOL, {.i64 =
-1}, -1, 1, VE},
{ "enable-interintra-comp", "Enable interintra compound",
OFFSET(enable_interintra_comp), AV_OPT_TYPE_BOOL, {.i64 =
-1}, -1, 1, VE},
{ "enable-smooth-interintra", "Enable smooth interintra mode",
OFFSET(enable_smooth_interintra), AV_OPT_TYPE_BOOL, {.i64 =
-1}, -1, 1, VE},
+ { "svc-parameters", "SVC configuration using a :-separated list of
key=value parameters (only applied in CBR mode)", OFFSET(svc_parameters),
AV_OPT_TYPE_DICT, { 0 }, 0, 0, VE},
#if AOM_ENCODER_ABI_VERSION >= 23
{ "aom-params", "Set libaom options using a :-separated
list of key=value pairs", OFFSET(aom_params), AV_OPT_TYPE_DICT, { 0 }, 0, 0, VE
},
#endif
diff --git a/tests/fate/av1.mak b/tests/fate/av1.mak
index 81072be5b8..28798e709d 100644
--- a/tests/fate/av1.mak
+++ b/tests/fate/av1.mak
@@ -2,6 +2,21 @@
FATE_AV1_FFMPEG_FFPROBE-$(call ENCDEC, LIBAOM_AV1 VP9, IVF MATROSKA) +=
fate-libaom-hdr10-plus
fate-libaom-hdr10-plus: CMD = enc_external
$(TARGET_SAMPLES)/mkv/hdr10_plus_vp9_sample.webm ivf "-map 0 -c:v libaom-av1
-cpu-used 8" "-show_frames -show_entries frame=side_data_list -codec:v
libaom-av1"
+# Tests that libaom-av1 SVC temporal layer configuration initialises without
error in CBR mode.
+FATE_AV1_FFMPEG_FFPROBE-$(call ENCDEC, LIBAOM_AV1 VP9, IVF MATROSKA) +=
fate-libaom-av1-svc
+fate-libaom-av1-svc: CMD = enc_external
$(TARGET_SAMPLES)/mkv/hdr10_plus_vp9_sample.webm ivf \
+ "-map 0:v -c:v libaom-av1 -b:v 500k -minrate 500k -maxrate 500k -cpu-used
8 \
+ -svc-parameters
number_spatial_layers=1:number_temporal_layers=2:framerate_factor=1,2:layer_target_bitrate=200,500"
\
+ "-show_entries stream=codec_name,width,height -of flat"
+
+# Tests that per-frame temporal layer IDs are passed through to the encoder
via frame metadata.
+FATE_AV1_FFMPEG_FFPROBE-$(call ENCDEC, LIBAOM_AV1 VP9, IVF MATROSKA) +=
fate-libaom-av1-svc-temporal-id
+fate-libaom-av1-svc-temporal-id: CMD = enc_external
$(TARGET_SAMPLES)/mkv/hdr10_plus_vp9_sample.webm ivf \
+ "-map 0:v -c:v libaom-av1 -b:v 500k -minrate 500k -maxrate 500k -cpu-used
8 \
+ -svc-parameters
number_spatial_layers=1:number_temporal_layers=2:framerate_factor=1,2:layer_target_bitrate=200,500
\
+ -vf metadata=mode=add:key=temporal_id:value=1" \
+ "-show_entries stream=codec_name,width,height -of flat"
+
FATE_SAMPLES_FFMPEG_FFPROBE += $(FATE_AV1_FFMPEG_FFPROBE-yes)
fate-av1: $(FATE_AV1_FFMPEG_FFPROBE-yes)
diff --git a/tests/ref/fate/libaom-av1-svc b/tests/ref/fate/libaom-av1-svc
new file mode 100644
index 0000000000..2c587b1f27
--- /dev/null
+++ b/tests/ref/fate/libaom-av1-svc
@@ -0,0 +1,3 @@
+streams.stream.0.codec_name="av1"
+streams.stream.0.width=1280
+streams.stream.0.height=720
diff --git a/tests/ref/fate/libaom-av1-svc-temporal-id
b/tests/ref/fate/libaom-av1-svc-temporal-id
new file mode 100644
index 0000000000..2c587b1f27
--- /dev/null
+++ b/tests/ref/fate/libaom-av1-svc-temporal-id
@@ -0,0 +1,3 @@
+streams.stream.0.codec_name="av1"
+streams.stream.0.width=1280
+streams.stream.0.height=720
--
2.52.0
_______________________________________________
ffmpeg-devel mailing list -- [email protected]
To unsubscribe send an email to [email protected]