Commit 918c5ca2aa for ffmpeg
commit 918c5ca2aa41e3c90d2207f7a8fa04458802a47e
Author: Michael Niedermayer <michael@niedermayer.cc>
Date: Wed Oct 14 01:02:31 2026 +0200
ffplay: render the list with libass directly
The strips were rendered by the ass filter over a color source in a
filter graph, whose generic blending took most of the time. libass now
renders each strip and its glyphs are composited over the strip
background where they cover it; a row strip at 1080p takes 0.47 instead
of 1.29 ms. ffplay suggests libass instead of the ass filter and no
longer selects the color filter.
Assisted-by: Claude
diff --git a/configure b/configure
index c97ddb186f..0ee3135b53 100755
--- a/configure
+++ b/configure
@@ -4367,8 +4367,8 @@ ffmpeg_select="aformat_filter anull_filter atrim_filter crop_filter
transpose_filter trim_filter vflip_filter"
ffmpeg_suggest="ole32 psapi shell32"
ffplay_deps="avcodec avformat avfilter swscale swresample sdl2"
-ffplay_select="crop_filter transpose_filter hflip_filter vflip_filter rotate_filter lcevc_merge_bsf color_filter"
-ffplay_suggest="shell32 libplacebo vulkan ass_filter"
+ffplay_select="crop_filter transpose_filter hflip_filter vflip_filter rotate_filter lcevc_merge_bsf"
+ffplay_suggest="shell32 libplacebo vulkan libass"
ffprobe_deps="avcodec avformat"
ffprobe_suggest="shell32"
diff --git a/doc/ffplay.texi b/doc/ffplay.texi
index f56b1e8a2e..606a1236b1 100644
--- a/doc/ffplay.texi
+++ b/doc/ffplay.texi
@@ -267,7 +267,7 @@ Seek backward/forward 1 minute.
@item page down/page up
Seek to previous/next chapter or backward/forward 10 minutes if no chapters.
The chapter list is shown briefly, marking the chapter being played at its
-left edge. It needs the ass filter and the SDL renderer, so it is unavailable
+left edge. It needs libass and the SDL renderer, so it is unavailable
in builds without libass and with the Vulkan renderer.
@item l
diff --git a/fftools/ffplay_list.c b/fftools/ffplay_list.c
index b39c81ae7c..4869cf3d49 100644
--- a/fftools/ffplay_list.c
+++ b/fftools/ffplay_list.c
@@ -16,23 +16,25 @@
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
*/
+#include "config.h"
+
#include <limits.h>
#include <string.h>
+#if CONFIG_LIBASS
+#include <ass/ass.h>
+#endif
+
#include "libavutil/avstring.h"
+#include "libavutil/avutil.h"
#include "libavutil/bprint.h"
#include "libavutil/common.h"
#include "libavutil/error.h"
-#include "libavutil/frame.h"
#include "libavutil/log.h"
#include "libavutil/mathematics.h"
#include "libavutil/mem.h"
-#include "libavutil/opt.h"
-#include "libavutil/pixdesc.h"
#include "libavutil/qsort.h"
#include "libavutil/time.h"
-#include "libavfilter/avfilter.h"
-#include "libavfilter/buffersink.h"
#include "ffplay_list.h"
@@ -65,7 +67,7 @@ typedef struct ListHit {
/* a strip rendered once, kept while it may be shown again */
typedef struct ListCacheEntry {
char *script;
- const char *background;
+ uint32_t background;
int x, w, h;
uint8_t *pixels;
unsigned last_used;
@@ -83,8 +85,10 @@ struct ListView {
SDL_Rect dirty;
uint8_t *bitmap;
SDL_Rect rect;
- AVFilterGraph *graph;
- AVFilterContext *sink;
+#if CONFIG_LIBASS
+ ASS_Library *library;
+ ASS_Renderer *ass;
+#endif
ListStrip *strips;
int nb_strips;
ListCacheEntry **cache;
@@ -111,7 +115,7 @@ struct ListView {
/* the columns the list is sorted by, the most recently clicked first */
ListSortKey sort[LIST_SORT_NB];
int shuffle_seed;
- int filter_missing_reported;
+ int libass_missing_reported;
};
static double list_alpha(const ListView *lv, int64_t now)
@@ -312,10 +316,10 @@ static void bprint_ring(AVBPrint *script, int cx, int cy, int r)
r - k, 2 * r, r + k, r);
}
-/* strip backgrounds, with premultiplied alpha as the ass filter composites */
-static const char panel_color[] = "0x000000BF";
-static const char hover_color[] = "0x373737CD";
-static const char popup_color[] = "0x000000DF";
+/* strip backgrounds as ARGB */
+#define PANEL_COLOR 0xBF000000
+#define HOVER_COLOR 0xCD444444
+#define POPUP_COLOR 0xDF000000
static void bprint_styles(AVBPrint *script, const ListLayout *l)
{
@@ -412,7 +416,7 @@ static void cache_make_room(ListView *lv, size_t bytes)
static int strip_render(ListView *lv, ListCacheEntry *entry);
/* the cached strip for the script, background and place, rendered when there is none */
-static int cache_get(ListView *lv, const SDL_Rect *rect, const char *background, const AVBPrint *script, ListCacheEntry **entry)
+static int cache_get(ListView *lv, const SDL_Rect *rect, uint32_t background, const AVBPrint *script, ListCacheEntry **entry)
{
int ret;
@@ -448,7 +452,7 @@ static int cache_get(ListView *lv, const SDL_Rect *rect, const char *background,
return 0;
}
-static int strip_add(ListView *lv, ListStrip **strips, int *nb, SDL_Rect rect, const char *background, const AVBPrint *script)
+static int strip_add(ListView *lv, ListStrip **strips, int *nb, SDL_Rect rect, uint32_t background, const AVBPrint *script)
{
int ret;
@@ -491,7 +495,7 @@ static int list_strips(ListView *lv, const ListLayout *l, ListStrip **strips, in
bprint_header_strip(lv, &script, l);
else if (row >= 0)
bprint_row_strip(lv, &script, l, row);
- ret = strip_add(lv, strips, nb, rect, row >= 0 && row == lv->hover.row ? hover_color : panel_color, &script);
+ ret = strip_add(lv, strips, nb, rect, row >= 0 && row == lv->hover.row ? HOVER_COLOR : PANEL_COLOR, &script);
}
if (ret >= 0 && lv->hover.detail) {
const AVDictionary *metadata = lv->entries[lv->rows[lv->hover.row]].metadata;
@@ -517,7 +521,7 @@ static int list_strips(ListView *lv, const ListLayout *l, ListStrip **strips, in
bprint_cell(&script, "Detail", x + l->font * 15 / 2, l->font / 4 + (i - 1) * l->font, right - l->font / 4,
rect.h, lines[i]->value);
}
- ret = strip_add(lv, strips, nb, rect, popup_color, &script);
+ ret = strip_add(lv, strips, nb, rect, POPUP_COLOR, &script);
above = tag;
tag = tag ? av_dict_iterate(metadata, tag) : NULL;
}
@@ -528,7 +532,6 @@ static int list_strips(ListView *lv, const ListLayout *l, ListStrip **strips, in
static void list_discard(ListView *lv)
{
- avfilter_graph_free(&lv->graph);
av_freep(&lv->bitmap);
av_freep(&lv->strips);
lv->nb_strips = 0;
@@ -542,75 +545,106 @@ static void list_discard(ListView *lv)
lv->texture = NULL;
}
-/* a graph rendering one strip of the bitmap at a time, the bitmap itself and the texture it is uploaded to */
-static int list_configure(ListView *lv, const ListLayout *l, const SDL_Rect *canvas)
+#if CONFIG_LIBASS
+static void libass_log(int libass_level, const char *fmt, va_list args, void *opaque)
{
- const AVFilter *ass_filter = avfilter_get_by_name("ass");
- AVFilterContext *source, *ass;
- char source_args[64];
- int ret;
+ static const int levels[] = { AV_LOG_FATAL, AV_LOG_ERROR, AV_LOG_WARNING, AV_LOG_WARNING,
+ AV_LOG_VERBOSE, AV_LOG_VERBOSE, AV_LOG_DEBUG, AV_LOG_TRACE };
+ int level = levels[av_clip(libass_level, 0, FF_ARRAY_ELEMS(levels) - 1)];
- if (!ass_filter) {
- if (!lv->filter_missing_reported++)
- av_log(NULL, AV_LOG_ERROR, "The chapter list needs the ass filter, which this build lacks\n");
- return AVERROR_FILTER_NOT_FOUND;
- }
+ av_vlog(NULL, level, fmt, args);
+ av_log(NULL, level, "\n");
+}
+#endif
+
+/* the bitmap, the texture it is uploaded to, and libass rendering a canvas wide strip at a time */
+static int list_configure(ListView *lv, const ListLayout *l, const SDL_Rect *canvas)
+{
+#if CONFIG_LIBASS
list_discard(lv);
lv->rect = *canvas;
if (l->canvas_w <= 0 || l->canvas_h <= 0 || l->canvas_w > INT_MAX / 4 / l->canvas_h)
return AVERROR(EINVAL);
- if (!(lv->graph = avfilter_graph_alloc()) ||
- !(lv->bitmap = av_mallocz(l->canvas_w * l->canvas_h * 4)))
+ if (!(lv->bitmap = av_mallocz(l->canvas_w * l->canvas_h * 4)))
return AVERROR(ENOMEM);
if (!(lv->texture = SDL_CreateTexture(lv->renderer, SDL_PIXELFORMAT_ARGB8888, SDL_TEXTUREACCESS_STREAMING,
l->canvas_w, l->canvas_h)) ||
SDL_SetTextureBlendMode(lv->texture, SDL_BLENDMODE_BLEND) < 0)
return AVERROR_EXTERNAL;
lv->dirty = (SDL_Rect){ 0, 0, l->canvas_w, l->canvas_h };
- snprintf(source_args, sizeof(source_args), "color=black@0:rate=1:size=%dx%d", l->canvas_w, l->strip_h);
- ass = avfilter_graph_alloc_filter(lv->graph, ass_filter, "ass");
- lv->sink = avfilter_graph_alloc_filter(lv->graph, avfilter_get_by_name("buffersink"), "sink");
- if (!ass || !lv->sink)
- return AVERROR(ENOMEM);
- if ((ret = avfilter_graph_create_filter(&source, avfilter_get_by_name("color"), "source", source_args, NULL, lv->graph)) < 0 ||
- (ret = av_opt_set(ass, "script", "[Script Info]\nScriptType: v4.00+\n", AV_OPT_SEARCH_CHILDREN)) < 0 ||
- (ret = av_opt_set_int(ass, "alpha", 1, AV_OPT_SEARCH_CHILDREN)) < 0 ||
- (ret = avfilter_init_str(ass, NULL)) < 0 ||
- (ret = av_opt_set_array(lv->sink, "pixel_formats", AV_OPT_SEARCH_CHILDREN, 0, 1, AV_OPT_TYPE_PIXEL_FMT,
- &(enum AVPixelFormat){ AV_PIX_FMT_RGB32 })) < 0 ||
- (ret = avfilter_init_str(lv->sink, NULL)) < 0 ||
- (ret = avfilter_link(source, 0, ass, 0)) < 0 ||
- (ret = avfilter_link(ass, 0, lv->sink, 0)) < 0)
- return ret;
- return avfilter_graph_config(lv->graph, NULL);
+ if (!lv->library) {
+ if (!(lv->library = ass_library_init()))
+ return AVERROR(ENOMEM);
+ ass_set_message_cb(lv->library, libass_log, NULL);
+ ass_set_extract_fonts(lv->library, 1);
+ if (!(lv->ass = ass_renderer_init(lv->library))) {
+ ass_library_done(lv->library);
+ lv->library = NULL;
+ return AVERROR(ENOMEM);
+ }
+ ass_set_fonts(lv->ass, NULL, NULL, 1, NULL, 1);
+ }
+ ass_set_frame_size(lv->ass, l->canvas_w, l->strip_h);
+ ass_set_storage_size(lv->ass, l->canvas_w, l->strip_h);
+ return 0;
+#else
+ if (!lv->libass_missing_reported++)
+ av_log(NULL, AV_LOG_ERROR, "The chapter list needs libass, which this build lacks\n");
+ return AVERROR(ENOSYS);
+#endif
+}
+
+#if CONFIG_LIBASS
+/* the color with opacity a over the pixel, both ARGB, using the 16.16 reciprocals of the alpha values */
+static uint32_t blend_over(uint32_t dst, uint32_t rgb, int a, const uint32_t *reciprocal)
+{
+ int dst_a = dst >> 24, out_a = a + (dst_a * (255 - a) + 127) / 255;
+ uint32_t out = (uint32_t)out_a << 24;
+
+ for (int shift = 0; shift < 24; shift += 8) {
+ int premultiplied = ((rgb >> shift & 0xFF) * a * 255 + (dst >> shift & 0xFF) * dst_a * (255 - a) + 255 * 127) / (255 * 255);
+
+ out |= FFMIN(premultiplied * reciprocal[out_a] >> 16, 255) << shift;
+ }
+ return out;
}
+#endif
-/* renders the strip, the ass filter composing it over its background */
+/* renders the strip over its background */
static int strip_render(ListView *lv, ListCacheEntry *entry)
{
- AVFrame *frame = av_frame_alloc();
- int alpha = av_pix_fmt_desc_get(AV_PIX_FMT_RGB32)->comp[3].offset;
- uint32_t scale[256] = { 1 << 16 };
- int ret;
+#if CONFIG_LIBASS
+ ASS_Track *track = ass_read_memory(lv->library, entry->script, strlen(entry->script), NULL);
+ uint32_t *pixels = (uint32_t *)entry->pixels, reciprocal[256] = { 1 << 16 };
+ int w = entry->w, h = entry->h;
- if (!frame)
- return AVERROR(ENOMEM);
+ if (!track)
+ return AVERROR(EINVAL);
for (int a = 1; a < 256; a++)
- scale[a] = ((255 << 16) + a - 1) / a;
- if ((ret = avfilter_graph_send_command(lv->graph, "source", "color", entry->background, NULL, 0, 0)) >= 0 &&
- (ret = avfilter_graph_send_command(lv->graph, "ass", "script", entry->script, NULL, 0, 0)) >= 0)
- ret = av_buffersink_get_frame(lv->sink, frame);
- /* the ass filter composites with premultiplied alpha, SDL blends straight alpha */
- for (int y = 0; ret >= 0 && y < entry->h; y++) {
- const uint8_t *src = frame->data[0] + y * frame->linesize[0] + entry->x * 4;
- uint8_t *dst = entry->pixels + y * entry->w * 4;
-
- for (int x = 0; x < entry->w * 4; x += 4)
- for (int c = 0; c < 4; c++)
- dst[x + c] = c == alpha ? src[x + c] : FFMIN(src[x + c] * scale[src[x + alpha]] >> 16, 255);
+ reciprocal[a] = ((255 << 16) + a - 1) / a;
+ for (int i = 0; i < w * h; i++)
+ pixels[i] = entry->background;
+ for (const ASS_Image *image = ass_render_frame(lv->ass, track, 0, NULL); image; image = image->next) {
+ int opacity = 255 - (image->color & 0xFF);
+ int x0 = FFMAX(image->dst_x, entry->x), x1 = FFMIN(image->dst_x + image->w, entry->x + w);
+
+ for (int y = FFMAX(image->dst_y, 0); x0 < x1 && y < FFMIN(image->dst_y + image->h, h); y++) {
+ const uint8_t *coverage = image->bitmap + (y - image->dst_y) * image->stride + x0 - image->dst_x;
+ uint32_t *dst = pixels + y * w + x0 - entry->x;
+
+ for (int x = 0; x < x1 - x0; x++) {
+ int a = (opacity * coverage[x] + 127) / 255;
+
+ if (a)
+ dst[x] = blend_over(dst[x], image->color >> 8, a, reciprocal);
+ }
+ }
}
- av_frame_free(&frame);
- return ret;
+ ass_free_track(track);
+ return 0;
+#else
+ return AVERROR(ENOSYS);
+#endif
}
static void bitmap_touched(ListView *lv, const SDL_Rect *rect)
@@ -634,7 +668,7 @@ static int list_render(ListView *lv)
lv->frame++;
for (int i = 0; i < lv->nb_strips; i++)
lv->strips[i].entry->last_used = lv->frame;
- if (!lv->graph || !SDL_RectEquals(&lv->rect, &canvas))
+ if (!lv->bitmap || !SDL_RectEquals(&lv->rect, &canvas))
ret = list_configure(lv, &l, &canvas);
if (ret >= 0)
ret = list_strips(lv, &l, &strips, &nb);
@@ -664,7 +698,7 @@ static int list_render(ListView *lv)
bitmap_touched(lv, &dirty);
}
if (ret < 0) {
- if (ret != AVERROR_FILTER_NOT_FOUND)
+ if (ret != AVERROR(ENOSYS))
av_log(NULL, AV_LOG_ERROR, "Could not render the chapter list: %s\n", av_err2str(ret));
list_discard(lv);
av_freep(&strips);
@@ -728,6 +762,12 @@ void list_view_free(ListView **plv)
if (!lv)
return;
list_discard(lv);
+#if CONFIG_LIBASS
+ if (lv->ass)
+ ass_renderer_done(lv->ass);
+ if (lv->library)
+ ass_library_done(lv->library);
+#endif
av_freep(&lv->entries);
av_freep(&lv->rows);
av_freep(&lv->sort_tmp);