mirror of
https://github.com/mpv-player/mpv
synced 2025-03-11 08:37:59 +00:00
video/image_writer: add avif screenshot support
Notes: - converts the (image) write() api to filenames, because using avio with FILE* is a pain. - adds more debug logs for screenshots. build: rename av1 dependency to avif_muxer wscript: unify lavf dependency with meson
This commit is contained in:
parent
0bfafd2451
commit
de7f4fb1ee
@ -4317,6 +4317,7 @@ Screenshot
|
||||
:jpeg: JPEG (alias for jpg)
|
||||
:webp: WebP
|
||||
:jxl: JPEG XL
|
||||
:avif: AVIF
|
||||
|
||||
``--screenshot-tag-colorspace=<yes|no>``
|
||||
Tag screenshots with the appropriate colorspace.
|
||||
@ -4472,6 +4473,33 @@ Screenshot
|
||||
Set the JPEG XL compression effort. Higher effort (usually) means better
|
||||
compression, but takes more CPU time. The default is 4.
|
||||
|
||||
``--screenshot-avif-encoder=<encoder>``
|
||||
Specify the AV1 encoder to be used by libavcodec for encoding avif
|
||||
screenshots.
|
||||
|
||||
Default: ``libaom-av1``
|
||||
|
||||
``--screenshot-avif-pixfmt=<format>``
|
||||
Specify the pixel format to the libavcodec encoder.
|
||||
|
||||
Default: ``yuv420p``
|
||||
|
||||
``--screenshot-avif-opts=key1=value1,key2=value2,...``
|
||||
Specifies libavcodec options for selected encoder. For more information,
|
||||
consult the FFmpeg documentation.
|
||||
|
||||
Default: ``usage=allintra,crf=32,cpu-used=8,tune=ssim``
|
||||
|
||||
Note: the default is only guaranteed to work with the libaom-av1 encoder.
|
||||
Above options may not be valid and or optimal for other encoders.
|
||||
|
||||
This is a key/value list option. See `List Options`_ for details.
|
||||
|
||||
.. admonition:: Example
|
||||
|
||||
"``--screenshot-avif-opts=crf=32,aq-mode=complexity``"
|
||||
sets the crf to 32 and quantization (aq-mode) to complexity based.
|
||||
|
||||
``--screenshot-sw=<yes|no>``
|
||||
Whether to use software rendering for screenshots (default: no).
|
||||
|
||||
|
@ -43,6 +43,7 @@ features = {
|
||||
'ffmpeg': true,
|
||||
'gpl': get_option('gpl'),
|
||||
'jpegxl': libavformat.version().version_compare('>= 59.27.100'),
|
||||
'avif_muxer': libavformat.version().version_compare('>= 59.24.100'),
|
||||
'libass': true,
|
||||
'threads': true,
|
||||
}
|
||||
|
@ -20,9 +20,12 @@
|
||||
#include <string.h>
|
||||
|
||||
#include <libavcodec/avcodec.h>
|
||||
#include <libavformat/avformat.h>
|
||||
#include <libavutil/mem.h>
|
||||
#include <libavutil/opt.h>
|
||||
#include <libavutil/pixdesc.h>
|
||||
|
||||
#include "common/msg.h"
|
||||
#include "config.h"
|
||||
|
||||
#if HAVE_JPEG
|
||||
@ -32,11 +35,13 @@
|
||||
|
||||
#include "osdep/io.h"
|
||||
|
||||
#include "common/av_common.h"
|
||||
#include "common/msg.h"
|
||||
#include "image_writer.h"
|
||||
#include "mpv_talloc.h"
|
||||
#include "video/fmt-conversion.h"
|
||||
#include "video/img_format.h"
|
||||
#include "video/mp_image.h"
|
||||
#include "video/fmt-conversion.h"
|
||||
#include "video/sws_utils.h"
|
||||
|
||||
#include "options/m_option.h"
|
||||
@ -52,6 +57,15 @@ const struct image_writer_opts image_writer_opts_defaults = {
|
||||
.webp_compression = 4,
|
||||
.jxl_distance = 1.0,
|
||||
.jxl_effort = 4,
|
||||
.avif_encoder = "libaom-av1",
|
||||
.avif_pixfmt = "yuv420p",
|
||||
.avif_opts = (char*[]){
|
||||
"usage", "allintra",
|
||||
"crf", "32",
|
||||
"cpu-used", "8",
|
||||
"tune", "ssim",
|
||||
NULL
|
||||
},
|
||||
.tag_csp = true,
|
||||
};
|
||||
|
||||
@ -62,6 +76,9 @@ const struct m_opt_choice_alternatives mp_image_writer_formats[] = {
|
||||
{"webp", AV_CODEC_ID_WEBP},
|
||||
#if HAVE_JPEGXL
|
||||
{"jxl", AV_CODEC_ID_JPEGXL},
|
||||
#endif
|
||||
#if HAVE_AVIF_MUXER
|
||||
{"avif", AV_CODEC_ID_AV1},
|
||||
#endif
|
||||
{0}
|
||||
};
|
||||
@ -80,6 +97,11 @@ const struct m_option image_writer_opts[] = {
|
||||
#if HAVE_JPEGXL
|
||||
{"jxl-distance", OPT_DOUBLE(jxl_distance), M_RANGE(0.0, 15.0)},
|
||||
{"jxl-effort", OPT_INT(jxl_effort), M_RANGE(1, 9)},
|
||||
#endif
|
||||
#if HAVE_AVIF_MUXER
|
||||
{"avif-encoder", OPT_STRING(avif_encoder)},
|
||||
{"avif-opts", OPT_KEYVALUELIST(avif_opts)},
|
||||
{"avif-pixfmt", OPT_STRING(avif_pixfmt)},
|
||||
#endif
|
||||
{"high-bit-depth", OPT_BOOL(high_bit_depth)},
|
||||
{"tag-colorspace", OPT_BOOL(tag_csp)},
|
||||
@ -102,8 +124,15 @@ static enum AVPixelFormat replace_j_format(enum AVPixelFormat fmt)
|
||||
return fmt;
|
||||
}
|
||||
|
||||
static bool write_lavc(struct image_writer_ctx *ctx, mp_image_t *image, FILE *fp)
|
||||
|
||||
static bool write_lavc(struct image_writer_ctx *ctx, mp_image_t *image, const char *filename)
|
||||
{
|
||||
FILE* fp = fopen(filename, "wb");
|
||||
if (!fp) {
|
||||
MP_ERR(ctx, "Error opening '%s' for writing!\n", filename);
|
||||
return false;
|
||||
}
|
||||
|
||||
bool success = false;
|
||||
AVFrame *pic = NULL;
|
||||
AVPacket *pkt = NULL;
|
||||
@ -208,7 +237,7 @@ error_exit:
|
||||
avcodec_free_context(&avctx);
|
||||
av_frame_free(&pic);
|
||||
av_packet_free(&pkt);
|
||||
return success;
|
||||
return !fclose(fp) && success;
|
||||
}
|
||||
|
||||
#if HAVE_JPEG
|
||||
@ -222,8 +251,15 @@ static void write_jpeg_error_exit(j_common_ptr cinfo)
|
||||
longjmp(*(jmp_buf*)cinfo->client_data, 1);
|
||||
}
|
||||
|
||||
static bool write_jpeg(struct image_writer_ctx *ctx, mp_image_t *image, FILE *fp)
|
||||
static bool write_jpeg(struct image_writer_ctx *ctx, mp_image_t *image,
|
||||
const char *filename)
|
||||
{
|
||||
FILE *fp = fopen(filename, "wb");
|
||||
if (!fp) {
|
||||
MP_ERR(ctx, "Error opening '%s' for writing!\n", filename);
|
||||
return false;
|
||||
}
|
||||
|
||||
struct jpeg_compress_struct cinfo;
|
||||
struct jpeg_error_mgr jerr;
|
||||
|
||||
@ -270,7 +306,242 @@ static bool write_jpeg(struct image_writer_ctx *ctx, mp_image_t *image, FILE *fp
|
||||
|
||||
jpeg_destroy_compress(&cinfo);
|
||||
|
||||
return true;
|
||||
return !fclose(fp);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
#if HAVE_AVIF_MUXER
|
||||
|
||||
static void log_side_data(struct image_writer_ctx *ctx, AVPacketSideData *data,
|
||||
size_t size)
|
||||
{
|
||||
if (!mp_msg_test(ctx->log, MSGL_DEBUG))
|
||||
return;
|
||||
char dbgbuff[129];
|
||||
if (size)
|
||||
MP_DBG(ctx, "write_avif() packet side data:\n");
|
||||
for (int i = 0; i < size; i++) {
|
||||
AVPacketSideData *sd = &data[i];
|
||||
int k = 0;
|
||||
for (; k < MPMIN(sd->size, 64); k++)
|
||||
sprintf(dbgbuff + k*2, "%02x", sd->data[k]);
|
||||
dbgbuff[k] = '\0';
|
||||
MP_DBG(ctx, " [%d] = {[%s], '%s'}\n",
|
||||
i, av_packet_side_data_name(sd->type), dbgbuff);
|
||||
}
|
||||
}
|
||||
|
||||
static bool write_avif(struct image_writer_ctx *ctx, mp_image_t *image,
|
||||
const char *filename)
|
||||
{
|
||||
const AVCodec *codec = NULL;
|
||||
const AVOutputFormat *ofmt = NULL;
|
||||
AVCodecContext *avctx = NULL;
|
||||
AVIOContext *avioctx = NULL;
|
||||
AVFormatContext *fmtctx = NULL;
|
||||
AVStream *stream = NULL;
|
||||
AVFrame *pic = NULL;
|
||||
AVPacket *pkt = NULL;
|
||||
int ret;
|
||||
bool success = false;
|
||||
|
||||
codec = avcodec_find_encoder_by_name(ctx->opts->avif_encoder);
|
||||
if (!codec) {
|
||||
MP_ERR(ctx, "Could not find encoder '%s', for saving images\n",
|
||||
ctx->opts->avif_encoder);
|
||||
goto free_data;
|
||||
}
|
||||
|
||||
ofmt = av_guess_format("avif", NULL, NULL);
|
||||
if (!ofmt) {
|
||||
MP_ERR(ctx, "Could not guess output format 'avif'\n");
|
||||
goto free_data;
|
||||
}
|
||||
|
||||
avctx = avcodec_alloc_context3(codec);
|
||||
if (!avctx) {
|
||||
MP_ERR(ctx, "Failed to allocate AVContext.\n");
|
||||
goto free_data;
|
||||
}
|
||||
|
||||
avctx->width = image->w;
|
||||
avctx->height = image->h;
|
||||
avctx->time_base = (AVRational){1, 30};
|
||||
avctx->pkt_timebase = (AVRational){1, 30};
|
||||
avctx->codec_type = AVMEDIA_TYPE_VIDEO;
|
||||
avctx->pix_fmt = imgfmt2pixfmt(image->imgfmt);
|
||||
|
||||
av_opt_set_int(avctx, "still-image", 1, AV_OPT_SEARCH_CHILDREN);
|
||||
|
||||
AVDictionary *avd = NULL;
|
||||
mp_set_avdict(&avd, ctx->opts->avif_opts);
|
||||
av_opt_set_dict2(avctx, &avd, AV_OPT_SEARCH_CHILDREN);
|
||||
av_dict_free(&avd);
|
||||
|
||||
pic = av_frame_alloc();
|
||||
if (!pic) {
|
||||
MP_ERR(ctx, "Could not allocate AVFrame\n");
|
||||
goto free_data;
|
||||
}
|
||||
|
||||
for (int n = 0; n < 4; n++) {
|
||||
pic->data[n] = image->planes[n];
|
||||
pic->linesize[n] = image->stride[n];
|
||||
}
|
||||
pic->format = avctx->pix_fmt;
|
||||
pic->width = avctx->width;
|
||||
pic->height = avctx->height;
|
||||
// Not setting this flag caused ffmpeg to output avif that was not passing
|
||||
// standard checks but ffmpeg would still read and not complain...
|
||||
avctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER;
|
||||
avctx->color_range = pic->color_range =
|
||||
mp_csp_levels_to_avcol_range(image->params.color.levels);
|
||||
avctx->color_primaries = pic->color_primaries =
|
||||
mp_csp_prim_to_avcol_pri(image->params.color.primaries);
|
||||
avctx->color_trc = pic->color_trc =
|
||||
mp_csp_trc_to_avcol_trc(image->params.color.gamma);
|
||||
avctx->colorspace = pic->colorspace =
|
||||
mp_csp_to_avcol_spc(image->params.color.space);
|
||||
avctx->chroma_sample_location = pic->chroma_location =
|
||||
mp_chroma_location_to_av(image->params.chroma_location);
|
||||
|
||||
ret = avcodec_open2(avctx, codec, NULL);
|
||||
if (ret < 0) {
|
||||
MP_ERR(ctx, "Could not open libavcodec encoder for saving images\n");
|
||||
goto free_data;
|
||||
}
|
||||
|
||||
ret = avio_open(&avioctx, filename, AVIO_FLAG_WRITE);
|
||||
if (ret < 0) {
|
||||
MP_ERR(ctx, "Could not open file '%s' for saving images\n", filename);
|
||||
goto free_data;
|
||||
}
|
||||
|
||||
fmtctx = avformat_alloc_context();
|
||||
if (!fmtctx) {
|
||||
MP_ERR(ctx, "Could not allocate format context\n");
|
||||
goto free_data;
|
||||
}
|
||||
fmtctx->pb = avioctx;
|
||||
fmtctx->oformat = ofmt;
|
||||
|
||||
stream = avformat_new_stream(fmtctx, codec);
|
||||
if (!stream) {
|
||||
MP_ERR(ctx, "Could not allocate stream\n");
|
||||
goto free_data;
|
||||
}
|
||||
|
||||
ret = avcodec_parameters_from_context(stream->codecpar, avctx);
|
||||
if (ret < 0) {
|
||||
MP_ERR(ctx, "Could not copy parameters from context\n");
|
||||
goto free_data;
|
||||
}
|
||||
|
||||
MP_DBG(ctx, "write_avif() Codec Context:\n"
|
||||
" color_trc = %s\n"
|
||||
" color_primaries = %s\n"
|
||||
" color_range = %s\n"
|
||||
" colorspace = %s\n"
|
||||
" chroma_sample_location = %s\n",
|
||||
av_color_transfer_name(avctx->color_trc),
|
||||
av_color_primaries_name(avctx->color_primaries),
|
||||
av_color_range_name(avctx->color_range),
|
||||
av_color_space_name(avctx->colorspace),
|
||||
av_chroma_location_name(avctx->chroma_sample_location)
|
||||
);
|
||||
|
||||
ret = avformat_init_output(fmtctx, NULL);
|
||||
if (ret < 0) {
|
||||
MP_ERR(ctx, "Could not initialize output\n");
|
||||
goto free_data;
|
||||
}
|
||||
|
||||
ret = avformat_write_header(fmtctx, NULL);
|
||||
if (ret < 0) {
|
||||
MP_ERR(ctx, "Could not write format header\n");
|
||||
goto free_data;
|
||||
}
|
||||
|
||||
pkt = av_packet_alloc();
|
||||
if (!pkt) {
|
||||
MP_ERR(ctx, "Could not allocate packet\n");
|
||||
goto free_data;
|
||||
}
|
||||
|
||||
ret = avcodec_send_frame(avctx, pic);
|
||||
if (ret < 0) {
|
||||
MP_ERR(ctx, "Error sending frame\n");
|
||||
goto free_data;
|
||||
}
|
||||
|
||||
int pts = 0;
|
||||
log_side_data(ctx, avctx->coded_side_data, avctx->nb_coded_side_data);
|
||||
while (ret >= 0) {
|
||||
ret = avcodec_receive_packet(avctx, pkt);
|
||||
if (ret == AVERROR(EAGAIN) || ret == AVERROR_EOF)
|
||||
break;
|
||||
if (ret < 0) {
|
||||
MP_ERR(ctx, "Error receiving packet\n");
|
||||
goto free_data;
|
||||
}
|
||||
pkt->pts = ++pts;
|
||||
pkt->dts = pts;
|
||||
pkt->stream_index = stream->index;
|
||||
log_side_data(ctx, pkt->side_data, pkt->side_data_elems);
|
||||
|
||||
ret = av_write_frame(fmtctx, pkt);
|
||||
if (ret < 0) {
|
||||
MP_ERR(ctx, "Error writing frame\n");
|
||||
goto free_data;
|
||||
}
|
||||
av_packet_unref(pkt);
|
||||
}
|
||||
|
||||
ret = avcodec_send_frame(avctx, NULL);
|
||||
if (ret < 0) {
|
||||
MP_ERR(ctx, "Error sending flushing frame\n");
|
||||
goto free_data;
|
||||
}
|
||||
|
||||
while (ret >= 0) {
|
||||
ret = avcodec_receive_packet(avctx, pkt);
|
||||
if (ret == AVERROR_EOF)
|
||||
break;
|
||||
if (ret != 0) {
|
||||
MP_ERR(ctx, "Error receiving packet\n");
|
||||
goto free_data;
|
||||
}
|
||||
pkt->pts = ++pts;
|
||||
pkt->dts = pts;
|
||||
pkt->stream_index = stream->index;
|
||||
log_side_data(ctx, pkt->side_data, pkt->side_data_elems);
|
||||
|
||||
ret = av_write_frame(fmtctx, pkt);
|
||||
if (ret < 0) {
|
||||
MP_ERR(ctx, "Error writing frame\n");
|
||||
goto free_data;
|
||||
}
|
||||
av_packet_unref(pkt);
|
||||
}
|
||||
|
||||
ret = av_write_trailer(fmtctx);
|
||||
if (ret < 0) {
|
||||
MP_ERR(ctx, "Could not write trailer\n");
|
||||
goto free_data;
|
||||
}
|
||||
MP_DBG(ctx, "write_avif(): avio_size() = %ld\n", avio_size(avioctx));
|
||||
|
||||
success = true;
|
||||
|
||||
free_data:
|
||||
success = !avio_closep(&avioctx) && success;
|
||||
avformat_free_context(fmtctx);
|
||||
avcodec_free_context(&avctx);
|
||||
av_packet_free(&pkt);
|
||||
av_frame_free(&pic);
|
||||
|
||||
return success;
|
||||
}
|
||||
|
||||
#endif
|
||||
@ -335,6 +606,9 @@ bool image_writer_high_depth(const struct image_writer_opts *opts)
|
||||
return opts->format == AV_CODEC_ID_PNG
|
||||
#if HAVE_JPEGXL
|
||||
|| opts->format == AV_CODEC_ID_JPEGXL
|
||||
#endif
|
||||
#if HAVE_AVIF_MUXER
|
||||
|| opts->format == AV_CODEC_ID_AV1
|
||||
#endif
|
||||
;
|
||||
}
|
||||
@ -345,6 +619,9 @@ bool image_writer_flexible_csp(const struct image_writer_opts *opts)
|
||||
#if HAVE_JPEGXL
|
||||
|| opts->format == AV_CODEC_ID_JPEGXL
|
||||
#endif
|
||||
#if HAVE_AVIF_MUXER
|
||||
|| opts->format == AV_CODEC_ID_AV1
|
||||
#endif
|
||||
#if LIBAVCODEC_VERSION_INT >= AV_VERSION_INT(59, 58, 100)
|
||||
// This version added support for cICP tag writing
|
||||
|| opts->format == AV_CODEC_ID_PNG
|
||||
@ -423,7 +700,7 @@ static struct mp_image *convert_image(struct mp_image *image, int destfmt,
|
||||
}
|
||||
|
||||
bool write_image(struct mp_image *image, const struct image_writer_opts *opts,
|
||||
const char *filename, struct mpv_global *global,
|
||||
const char *filename, struct mpv_global *global,
|
||||
struct mp_log *log)
|
||||
{
|
||||
struct image_writer_opts defs = image_writer_opts_defaults;
|
||||
@ -431,7 +708,7 @@ bool write_image(struct mp_image *image, const struct image_writer_opts *opts,
|
||||
opts = &defs;
|
||||
|
||||
struct image_writer_ctx ctx = { log, opts, image->fmt };
|
||||
bool (*write)(struct image_writer_ctx *, mp_image_t *, FILE *) = write_lavc;
|
||||
bool (*write)(struct image_writer_ctx *, mp_image_t *, const char *) = write_lavc;
|
||||
int destfmt = 0;
|
||||
|
||||
#if HAVE_JPEG
|
||||
@ -439,6 +716,12 @@ bool write_image(struct mp_image *image, const struct image_writer_opts *opts,
|
||||
write = write_jpeg;
|
||||
destfmt = IMGFMT_RGB24;
|
||||
}
|
||||
#endif
|
||||
#if HAVE_AVIF_MUXER
|
||||
if (opts->format == AV_CODEC_ID_AV1) {
|
||||
write = write_avif;
|
||||
destfmt = mp_imgfmt_from_name(bstr0(opts->avif_pixfmt));
|
||||
}
|
||||
#endif
|
||||
if (opts->format == AV_CODEC_ID_WEBP && !opts->webp_lossless) {
|
||||
// For lossy images, libwebp has its own RGB->YUV conversion.
|
||||
@ -461,16 +744,9 @@ bool write_image(struct mp_image *image, const struct image_writer_opts *opts,
|
||||
if (!dst)
|
||||
return false;
|
||||
|
||||
FILE *fp = fopen(filename, "wb");
|
||||
bool success = false;
|
||||
if (fp == NULL) {
|
||||
mp_err(log, "Error opening '%s' for writing!\n", filename);
|
||||
} else {
|
||||
success = write(&ctx, dst, fp);
|
||||
success = !fclose(fp) && success;
|
||||
if (!success)
|
||||
mp_err(log, "Error writing file '%s'!\n", filename);
|
||||
}
|
||||
bool success = write(&ctx, dst, filename);
|
||||
if (!success)
|
||||
mp_err(log, "Error writing file '%s'!\n", filename);
|
||||
|
||||
talloc_free(dst);
|
||||
return success;
|
||||
|
@ -32,6 +32,9 @@ struct image_writer_opts {
|
||||
int webp_compression;
|
||||
double jxl_distance;
|
||||
int jxl_effort;
|
||||
char *avif_encoder;
|
||||
char *avif_pixfmt;
|
||||
char **avif_opts;
|
||||
bool tag_csp;
|
||||
};
|
||||
|
||||
|
4
wscript
4
wscript
@ -411,6 +411,10 @@ iconv support use --disable-iconv.",
|
||||
'name': 'jpegxl',
|
||||
'desc': 'JPEG XL support via libavcodec',
|
||||
'func': check_pkg_config('libavcodec >= 59.27.100'),
|
||||
}, {
|
||||
'name': 'avif_muxer',
|
||||
'desc': 'avif support via libavcodec',
|
||||
'func': check_pkg_config('libavformat >= 59.24.100'),
|
||||
}, {
|
||||
'name': 'rubberband-3',
|
||||
'desc': 'new engine support for librubberband',
|
||||
|
Loading…
Reference in New Issue
Block a user