From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from ffbox0-bg.mplayerhq.hu (ffbox0-bg.ffmpeg.org [79.124.17.100]) by master.gitmailbox.com (Postfix) with ESMTPS id EA1E34D1F7 for ; Tue, 18 Feb 2025 13:09:34 +0000 (UTC) Received: from [127.0.1.1] (localhost [127.0.0.1]) by ffbox0-bg.mplayerhq.hu (Postfix) with ESMTP id 8680E68C1C1; Tue, 18 Feb 2025 15:08:46 +0200 (EET) Received: from mail-pl1-f171.google.com (mail-pl1-f171.google.com [209.85.214.171]) by ffbox0-bg.mplayerhq.hu (Postfix) with ESMTPS id 95A9268C14C for ; Tue, 18 Feb 2025 15:08:38 +0200 (EET) Received: by mail-pl1-f171.google.com with SMTP id d9443c01a7336-22100006bc8so55184065ad.0 for ; Tue, 18 Feb 2025 05:08:38 -0800 (PST) DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=gmail.com; s=20230601; t=1739884116; x=1740488916; darn=ffmpeg.org; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:from:to:cc:subject:date:message-id :reply-to; bh=N/ufQdIsBRHHjc8Ido7eqWeNe8Pllk3wdNjHdTb3hUg=; b=ULqbEVYnzTI6RnJ3RfyQ8bXGif/fXcwlu0kH7CYROBsNujRrx69Ci3a4qTsJmmm4Av DMQKcUgB41LECPort90wiU0EpAj/Y4QCyzGhpg2+6O1F58+bYzORSgxodUh7uSpQtxSp SNpcZ/BeuufRELYmRZcKY+EtRKGTimrR2or3ki1RWmsa3raVDAknL5mTwZQ3aWQumBBU bHJOdoCc9Xk2M3fnVfFanIsApzQg6CSCJM6ohEbZtzdubTQE4gyT+EzglR9nCYi7FN6C C/0h5M57KV5j7+YvzdP0LSNe/CXCbJDgGNeiklU8v0xVTPqet9+CnfDgJmwL3qdQkqwv vtag== X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=1e100.net; s=20230601; t=1739884116; x=1740488916; h=content-transfer-encoding:mime-version:references:in-reply-to :message-id:date:subject:to:from:x-gm-message-state:from:to:cc :subject:date:message-id:reply-to; bh=N/ufQdIsBRHHjc8Ido7eqWeNe8Pllk3wdNjHdTb3hUg=; b=hmqISy0dT9pWhsDW5PMOPrbd0hkIei2nd0GfhLmlDl3MDRtGPxUbetz+hEZ92N/XHE XEPJrSyMdWN9+SUPrZaodsiN7Xhl5Wrg6u4+eyKCB1cR0aVS8cNqIlaOXs8H2PdEQF5E VqC5d3joCrGqxBozrR8VUpFltG0V0yYbHoQ3XSYVNxwfWn3jdwsDodllseKWfUMFc2bB 8rHlDn3FeCOJ5lQvFsLH3uO9t+L3qYMolx7HILBOuV2WxK199zuqG6ZqM33rRCXsqv9I xlijwjeWetsAnoWc10NIOok2R0Lyuf0uCqnbdMAHFE5nmGuUQhkhJprECEgGOIl/2UvQ BMaA== X-Gm-Message-State: AOJu0Yykdck/46omaMifDZEEdluQ2c11PQIIvrTqxe5U5JxsaJ3adbiA kOyUJ6tKM1tP/ATEbh61TeqbqyfF0h9tkAuC9lRQoMWLjSH2sup5SzsAGwmI X-Gm-Gg: ASbGncvWMgMy8FLFCfYFHMCZV9BCCKAHZQNNcrzXLiBALkzkrnIZK8rQILT1xVBVkJ3 SytBnbUs4I1Vhz4opnPWN5kwaXllptDaAPgGWyyx2JyQJaJWuEsoKbtTs7Km35ObkqaE/rVphXy uoAvXuHnY7gOb/brU7xQCkeYUkD8uFKoaRutJc8qhMWrKsfZVsaaetsXMpvLbTkiPG4efSM10cP L8/VIsY21sjGwP24Kbnkffp/BX4ZdksYzVT8ALu4R5BfZvVinTqTn4r9zCSrtPD+O2QW/LhP7D/ AaH1XNSB6fFWMzBHeLF6Kzycjkjnng== X-Google-Smtp-Source: AGHT+IHd5rv3zJwH0JOTaywliyKdzeglanPKA7w1FScsjfRT1gzXCyjyHjJcnpl9otyZYO7CNid8hw== X-Received: by 2002:a17:903:8c5:b0:220:c63b:d945 with SMTP id d9443c01a7336-22104030d60mr213635195ad.14.1739884116321; Tue, 18 Feb 2025 05:08:36 -0800 (PST) Received: from localhost.localdomain ([2800:2121:b040:c:a0a7:974:71c7:ca89]) by smtp.gmail.com with ESMTPSA id d9443c01a7336-220d556d66fsm89178635ad.180.2025.02.18.05.08.35 for (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256); Tue, 18 Feb 2025 05:08:35 -0800 (PST) From: James Almer To: ffmpeg-devel@ffmpeg.org Date: Tue, 18 Feb 2025 10:08:07 -0300 Message-ID: <20250218130813.74-5-jamrial@gmail.com> X-Mailer: git-send-email 2.48.1 In-Reply-To: <20250218130813.74-1-jamrial@gmail.com> References: <20250218130813.74-1-jamrial@gmail.com> MIME-Version: 1.0 Subject: [FFmpeg-devel] [PATCH 05/11] avcodec/libaomenc: refactor encoder configuration functions X-BeenThere: ffmpeg-devel@ffmpeg.org X-Mailman-Version: 2.1.29 Precedence: list List-Id: FFmpeg development discussions and patches List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Reply-To: FFmpeg development discussions and patches Content-Type: text/plain; charset="us-ascii" Content-Transfer-Encoding: 7bit Errors-To: ffmpeg-devel-bounces@ffmpeg.org Sender: "ffmpeg-devel" Archived-At: List-Archive: List-Post: Will be useful in the next commit. Signed-off-by: James Almer --- libavcodec/libaomenc.c | 238 +++++++++++++++++++++++------------------ 1 file changed, 131 insertions(+), 107 deletions(-) diff --git a/libavcodec/libaomenc.c b/libavcodec/libaomenc.c index 0f7571ee7a..38bdff6a38 100644 --- a/libavcodec/libaomenc.c +++ b/libavcodec/libaomenc.c @@ -73,7 +73,10 @@ typedef struct AOMEncoderContext { AVBSFContext *bsf; DOVIContext dovi; struct aom_codec_ctx encoder; + struct aom_codec_enc_cfg enccfg; struct aom_image rawimg; + aom_codec_flags_t flags; + aom_img_fmt_t img_fmt; struct aom_fixed_buf twopass_stats; unsigned twopass_stats_size; struct FrameListData *coded_frame_list; @@ -342,9 +345,6 @@ static av_cold int codecctl_intp(AVCodecContext *avctx, int width = -30; int res; - snprintf(buf, sizeof(buf), "%s:", ctlidstr[id]); - av_log(avctx, AV_LOG_DEBUG, " %*s%d\n", width, buf, *ptr); - res = aom_codec_control(&ctx->encoder, id, ptr); if (res != AOM_CODEC_OK) { snprintf(buf, sizeof(buf), "Failed to set %s codec control", @@ -353,6 +353,9 @@ static av_cold int codecctl_intp(AVCodecContext *avctx, return AVERROR(EINVAL); } + snprintf(buf, sizeof(buf), "%s:", ctlidstr[id]); + av_log(avctx, AV_LOG_DEBUG, " %*s%d\n", width, buf, *ptr); + return 0; } #endif @@ -673,29 +676,22 @@ static int choose_tiling(AVCodecContext *avctx, return 0; } -static av_cold int aom_init(AVCodecContext *avctx, - const struct aom_codec_iface *iface) +static av_cold int aom_config(AVCodecContext *avctx, + const struct aom_codec_iface *iface) { AOMContext *ctx = avctx->priv_data; - const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(avctx->pix_fmt); - struct aom_codec_enc_cfg enccfg = { 0 }; - aom_codec_flags_t flags = - (avctx->flags & AV_CODEC_FLAG_PSNR) ? AOM_CODEC_USE_PSNR : 0; - AVCPBProperties *cpb_props; int res; - aom_img_fmt_t img_fmt; aom_codec_caps_t codec_caps = aom_codec_get_caps(iface); - av_log(avctx, AV_LOG_INFO, "%s\n", aom_codec_version_str()); - av_log(avctx, AV_LOG_VERBOSE, "%s\n", aom_codec_build_config()); + ctx->flags = (avctx->flags & AV_CODEC_FLAG_PSNR) ? AOM_CODEC_USE_PSNR : 0; - if ((res = aom_codec_enc_config_default(iface, &enccfg, ctx->usage)) != AOM_CODEC_OK) { + if ((res = aom_codec_enc_config_default(iface, &ctx->enccfg, ctx->usage)) != AOM_CODEC_OK) { av_log(avctx, AV_LOG_ERROR, "Failed to get config: %s\n", aom_codec_err_to_string(res)); return AVERROR(EINVAL); } - if (set_pix_fmt(avctx, codec_caps, &enccfg, &flags, &img_fmt)) + if (set_pix_fmt(avctx, codec_caps, &ctx->enccfg, &ctx->flags, &ctx->img_fmt)) return AVERROR(EINVAL); if(!avctx->bit_rate) @@ -704,39 +700,30 @@ static av_cold int aom_init(AVCodecContext *avctx, return AVERROR(EINVAL); } - dump_enc_cfg(avctx, &enccfg, AV_LOG_DEBUG); - - enccfg.g_w = avctx->width; - enccfg.g_h = avctx->height; - enccfg.g_timebase.num = avctx->time_base.num; - enccfg.g_timebase.den = avctx->time_base.den; - enccfg.g_threads = + ctx->enccfg.g_w = avctx->width; + ctx->enccfg.g_h = avctx->height; + ctx->enccfg.g_timebase.num = avctx->time_base.num; + ctx->enccfg.g_timebase.den = avctx->time_base.den; + ctx->enccfg.g_threads = FFMIN(avctx->thread_count ? avctx->thread_count : av_cpu_count(), 64); if (ctx->lag_in_frames >= 0) - enccfg.g_lag_in_frames = ctx->lag_in_frames; - - if (avctx->flags & AV_CODEC_FLAG_PASS1) - enccfg.g_pass = AOM_RC_FIRST_PASS; - else if (avctx->flags & AV_CODEC_FLAG_PASS2) - enccfg.g_pass = AOM_RC_LAST_PASS; - else - enccfg.g_pass = AOM_RC_ONE_PASS; + ctx->enccfg.g_lag_in_frames = ctx->lag_in_frames; if (avctx->rc_min_rate == avctx->rc_max_rate && avctx->rc_min_rate == avctx->bit_rate && avctx->bit_rate) { - enccfg.rc_end_usage = AOM_CBR; + ctx->enccfg.rc_end_usage = AOM_CBR; } else if (ctx->crf >= 0) { - enccfg.rc_end_usage = AOM_CQ; + ctx->enccfg.rc_end_usage = AOM_CQ; if (!avctx->bit_rate) - enccfg.rc_end_usage = AOM_Q; + ctx->enccfg.rc_end_usage = AOM_Q; } if (avctx->bit_rate) { - enccfg.rc_target_bitrate = av_rescale_rnd(avctx->bit_rate, 1, 1000, + ctx->enccfg.rc_target_bitrate = av_rescale_rnd(avctx->bit_rate, 1, 1000, AV_ROUND_NEAR_INF); - } else if (enccfg.rc_end_usage != AOM_Q) { - enccfg.rc_end_usage = AOM_Q; + } else if (ctx->enccfg.rc_end_usage != AOM_Q) { + ctx->enccfg.rc_end_usage = AOM_Q; ctx->crf = 32; av_log(avctx, AV_LOG_WARNING, "Neither bitrate nor constrained quality specified, using default CRF of %d\n", @@ -744,95 +731,65 @@ static av_cold int aom_init(AVCodecContext *avctx, } if (avctx->qmin >= 0) - enccfg.rc_min_quantizer = avctx->qmin; + ctx->enccfg.rc_min_quantizer = avctx->qmin; if (avctx->qmax >= 0) { - enccfg.rc_max_quantizer = avctx->qmax; + ctx->enccfg.rc_max_quantizer = avctx->qmax; } else if (!ctx->crf) { - enccfg.rc_max_quantizer = 0; + ctx->enccfg.rc_max_quantizer = 0; } - if (enccfg.rc_end_usage == AOM_CQ || enccfg.rc_end_usage == AOM_Q) { - if (ctx->crf < enccfg.rc_min_quantizer || ctx->crf > enccfg.rc_max_quantizer) { + if (ctx->enccfg.rc_end_usage == AOM_CQ || ctx->enccfg.rc_end_usage == AOM_Q) { + if (ctx->crf < ctx->enccfg.rc_min_quantizer || ctx->crf > ctx->enccfg.rc_max_quantizer) { av_log(avctx, AV_LOG_ERROR, "CQ level %d must be between minimum and maximum quantizer value (%d-%d)\n", - ctx->crf, enccfg.rc_min_quantizer, enccfg.rc_max_quantizer); + ctx->crf, ctx->enccfg.rc_min_quantizer, ctx->enccfg.rc_max_quantizer); return AVERROR(EINVAL); } } - enccfg.rc_dropframe_thresh = ctx->drop_threshold; + ctx->enccfg.rc_dropframe_thresh = ctx->drop_threshold; // 0-100 (0 => CBR, 100 => VBR) - enccfg.rc_2pass_vbr_bias_pct = round(avctx->qcompress * 100); + ctx->enccfg.rc_2pass_vbr_bias_pct = round(avctx->qcompress * 100); if (ctx->minsection_pct >= 0) - enccfg.rc_2pass_vbr_minsection_pct = ctx->minsection_pct; + ctx->enccfg.rc_2pass_vbr_minsection_pct = ctx->minsection_pct; else if (avctx->bit_rate) - enccfg.rc_2pass_vbr_minsection_pct = + ctx->enccfg.rc_2pass_vbr_minsection_pct = avctx->rc_min_rate * 100LL / avctx->bit_rate; if (ctx->maxsection_pct >= 0) - enccfg.rc_2pass_vbr_maxsection_pct = ctx->maxsection_pct; + ctx->enccfg.rc_2pass_vbr_maxsection_pct = ctx->maxsection_pct; else if (avctx->rc_max_rate) - enccfg.rc_2pass_vbr_maxsection_pct = + ctx->enccfg.rc_2pass_vbr_maxsection_pct = avctx->rc_max_rate * 100LL / avctx->bit_rate; if (avctx->rc_buffer_size) - enccfg.rc_buf_sz = + ctx->enccfg.rc_buf_sz = avctx->rc_buffer_size * 1000LL / avctx->bit_rate; if (avctx->rc_initial_buffer_occupancy) - enccfg.rc_buf_initial_sz = + ctx->enccfg.rc_buf_initial_sz = avctx->rc_initial_buffer_occupancy * 1000LL / avctx->bit_rate; - enccfg.rc_buf_optimal_sz = enccfg.rc_buf_sz * 5 / 6; + ctx->enccfg.rc_buf_optimal_sz = ctx->enccfg.rc_buf_sz * 5 / 6; if (ctx->rc_undershoot_pct >= 0) - enccfg.rc_undershoot_pct = ctx->rc_undershoot_pct; + ctx->enccfg.rc_undershoot_pct = ctx->rc_undershoot_pct; if (ctx->rc_overshoot_pct >= 0) - enccfg.rc_overshoot_pct = ctx->rc_overshoot_pct; + ctx->enccfg.rc_overshoot_pct = ctx->rc_overshoot_pct; // _enc_init() will balk if kf_min_dist differs from max w/AOM_KF_AUTO if (avctx->keyint_min >= 0 && avctx->keyint_min == avctx->gop_size) - enccfg.kf_min_dist = avctx->keyint_min; + ctx->enccfg.kf_min_dist = avctx->keyint_min; if (avctx->gop_size >= 0) - enccfg.kf_max_dist = avctx->gop_size; - - if (enccfg.g_pass == AOM_RC_FIRST_PASS) - enccfg.g_lag_in_frames = 0; - else if (enccfg.g_pass == AOM_RC_LAST_PASS) { - int decode_size, ret; - - if (!avctx->stats_in) { - av_log(avctx, AV_LOG_ERROR, "No stats file for second pass\n"); - return AVERROR_INVALIDDATA; - } - - ctx->twopass_stats.sz = strlen(avctx->stats_in) * 3 / 4; - ret = av_reallocp(&ctx->twopass_stats.buf, ctx->twopass_stats.sz); - if (ret < 0) { - av_log(avctx, AV_LOG_ERROR, - "Stat buffer alloc (%"SIZE_SPECIFIER" bytes) failed\n", - ctx->twopass_stats.sz); - ctx->twopass_stats.sz = 0; - return ret; - } - decode_size = av_base64_decode(ctx->twopass_stats.buf, avctx->stats_in, - ctx->twopass_stats.sz); - if (decode_size < 0) { - av_log(avctx, AV_LOG_ERROR, "Stat buffer decode failed\n"); - return AVERROR_INVALIDDATA; - } - - ctx->twopass_stats.sz = decode_size; - enccfg.rc_twopass_stats_in = ctx->twopass_stats; - } + ctx->enccfg.kf_max_dist = avctx->gop_size; /* 0-3: For non-zero values the encoder increasingly optimizes for reduced * complexity playback on low powered devices at the expense of encode * quality. */ if (avctx->profile != AV_PROFILE_UNKNOWN) - enccfg.g_profile = avctx->profile; + ctx->enccfg.g_profile = avctx->profile; - enccfg.g_error_resilient = ctx->error_resilient; + ctx->enccfg.g_error_resilient = ctx->error_resilient; - res = choose_tiling(avctx, &enccfg); + res = choose_tiling(avctx, &ctx->enccfg); if (res < 0) return res; @@ -840,22 +797,22 @@ static av_cold int aom_init(AVCodecContext *avctx, // Set the maximum number of frames to 1. This will let libaom set // still_picture and reduced_still_picture_header to 1 in the Sequence // Header as required by AVIF still images. - enccfg.g_limit = 1; + ctx->enccfg.g_limit = 1; // Reduce memory usage for still images. - enccfg.g_lag_in_frames = 0; + ctx->enccfg.g_lag_in_frames = 0; // All frames will be key frames. - enccfg.kf_max_dist = 0; - enccfg.kf_mode = AOM_KF_DISABLED; + ctx->enccfg.kf_max_dist = 0; + ctx->enccfg.kf_mode = AOM_KF_DISABLED; } - /* Construct Encoder Context */ - res = aom_codec_enc_init(&ctx->encoder, iface, &enccfg, flags); - if (res != AOM_CODEC_OK) { - dump_enc_cfg(avctx, &enccfg, AV_LOG_WARNING); - log_encoder_error(avctx, "Failed to initialize encoder"); - return AVERROR(EINVAL); - } - dump_enc_cfg(avctx, &enccfg, AV_LOG_DEBUG); + return 0; +} + +static av_cold int aom_codecctl(AVCodecContext *avctx) +{ + AOMContext *ctx = avctx->priv_data; + const AVPixFmtDescriptor *desc = av_pix_fmt_desc_get(avctx->pix_fmt); + aom_codec_caps_t codec_caps = aom_codec_get_caps(ctx->encoder.iface); // codec control failures are currently treated only as warnings av_log(avctx, AV_LOG_DEBUG, "aom_codec_control\n"); @@ -983,20 +940,87 @@ static av_cold int aom_init(AVCodecContext *avctx, #endif // provide dummy value to initialize wrapper, values will be updated each _encode() - aom_img_wrap(&ctx->rawimg, img_fmt, avctx->width, avctx->height, 1, + aom_img_wrap(&ctx->rawimg, ctx->img_fmt, avctx->width, avctx->height, 1, (unsigned char*)1); if (codec_caps & AOM_CODEC_CAP_HIGHBITDEPTH) - ctx->rawimg.bit_depth = enccfg.g_bit_depth; + ctx->rawimg.bit_depth = ctx->enccfg.g_bit_depth; - cpb_props = ff_encode_add_cpb_side_data(avctx); - if (!cpb_props) - return AVERROR(ENOMEM); + return 0; +} + +static av_cold int aom_init(AVCodecContext *avctx, + const struct aom_codec_iface *iface) +{ + AOMContext *ctx = avctx->priv_data; + AVCPBProperties *cpb_props; + int res; + + av_log(avctx, AV_LOG_INFO, "%s\n", aom_codec_version_str()); + av_log(avctx, AV_LOG_VERBOSE, "%s\n", aom_codec_build_config()); + + res = aom_config(avctx, iface); + if (res < 0) + return res; + + if (avctx->flags & AV_CODEC_FLAG_PASS1) + ctx->enccfg.g_pass = AOM_RC_FIRST_PASS; + else if (avctx->flags & AV_CODEC_FLAG_PASS2) + ctx->enccfg.g_pass = AOM_RC_LAST_PASS; + else + ctx->enccfg.g_pass = AOM_RC_ONE_PASS; + + if (ctx->enccfg.g_pass == AOM_RC_FIRST_PASS) + ctx->enccfg.g_lag_in_frames = 0; + else if (ctx->enccfg.g_pass == AOM_RC_LAST_PASS) { + int decode_size, ret; + + if (!avctx->stats_in) { + av_log(avctx, AV_LOG_ERROR, "No stats file for second pass\n"); + return AVERROR_INVALIDDATA; + } + + ctx->twopass_stats.sz = strlen(avctx->stats_in) * 3 / 4; + ret = av_reallocp(&ctx->twopass_stats.buf, ctx->twopass_stats.sz); + if (ret < 0) { + av_log(avctx, AV_LOG_ERROR, + "Stat buffer alloc (%"SIZE_SPECIFIER" bytes) failed\n", + ctx->twopass_stats.sz); + ctx->twopass_stats.sz = 0; + return ret; + } + decode_size = av_base64_decode(ctx->twopass_stats.buf, avctx->stats_in, + ctx->twopass_stats.sz); + if (decode_size < 0) { + av_log(avctx, AV_LOG_ERROR, "Stat buffer decode failed\n"); + return AVERROR_INVALIDDATA; + } + + ctx->twopass_stats.sz = decode_size; + ctx->enccfg.rc_twopass_stats_in = ctx->twopass_stats; + } + + /* Construct Encoder Context */ + res = aom_codec_enc_init(&ctx->encoder, iface, &ctx->enccfg, ctx->flags); + if (res != AOM_CODEC_OK) { + dump_enc_cfg(avctx, &ctx->enccfg, AV_LOG_WARNING); + log_encoder_error(avctx, "Failed to initialize encoder"); + return AVERROR(EINVAL); + } + dump_enc_cfg(avctx, &ctx->enccfg, AV_LOG_DEBUG); + + res = aom_codecctl(avctx); + if (res < 0) + return res; ctx->dovi.logctx = avctx; if ((res = ff_dovi_configure(&ctx->dovi, avctx)) < 0) return res; + cpb_props = ff_encode_add_cpb_side_data(avctx); + if (!cpb_props) + return AVERROR(ENOMEM); + if (avctx->flags & AV_CODEC_FLAG_GLOBAL_HEADER) { const AVBitStreamFilter *filter = av_bsf_get_by_name("extract_extradata"); int ret; @@ -1019,8 +1043,8 @@ static av_cold int aom_init(AVCodecContext *avctx, return ret; } - if (enccfg.rc_end_usage == AOM_CBR || - enccfg.g_pass != AOM_RC_ONE_PASS) { + if (ctx->enccfg.rc_end_usage == AOM_CBR || + ctx->enccfg.g_pass != AOM_RC_ONE_PASS) { cpb_props->max_bitrate = avctx->rc_max_rate; cpb_props->min_bitrate = avctx->rc_min_rate; cpb_props->avg_bitrate = avctx->bit_rate; -- 2.48.1 _______________________________________________ ffmpeg-devel mailing list ffmpeg-devel@ffmpeg.org https://ffmpeg.org/mailman/listinfo/ffmpeg-devel To unsubscribe, visit link above, or email ffmpeg-devel-request@ffmpeg.org with subject "unsubscribe".