mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-12-21 15:56:42 +00:00
74d75e481c
See https://gitlab.freedesktop.org/gstreamer/gst-docs/-/issues/108 Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/7859>
533 lines
25 KiB
Markdown
533 lines
25 KiB
Markdown
# Subtitle Overlays and Hardware-Accelerated Playback
|
|
|
|
This document describes some of the considerations and requirements that
|
|
led to the current `GstVideoOverlayCompositionMeta` API which allows
|
|
attaching of subtitle bitmaps or logos to video buffers.
|
|
|
|
## Background
|
|
|
|
Subtitles can be muxed in containers or come from an external source.
|
|
|
|
Subtitles come in many shapes and colours. Usually they are either
|
|
text-based (incl. 'pango markup'), or bitmap-based (e.g. DVD subtitles
|
|
and the most common form of DVB subs). Bitmap based subtitles are
|
|
usually compressed in some way, like some form of run-length encoding.
|
|
|
|
Subtitles are currently decoded and rendered in subtitle-format-specific
|
|
overlay elements. These elements have two sink pads (one for raw video
|
|
and one for the subtitle format in question) and one raw video source
|
|
pad.
|
|
|
|
They will take care of synchronising the two input streams, and of
|
|
decoding and rendering the subtitles on top of the raw video stream.
|
|
|
|
Digression: one could theoretically have dedicated decoder/render
|
|
elements that output an AYUV or ARGB image, and then let a videomixer
|
|
element do the actual overlaying, but this is not very efficient,
|
|
because it requires us to allocate and blend whole pictures (1920x1080
|
|
AYUV = 8MB, 1280x720 AYUV = 3.6MB, 720x576 AYUV = 1.6MB) even if the
|
|
overlay region is only a small rectangle at the bottom. This wastes
|
|
memory and CPU. We could do something better by introducing a new format
|
|
that only encodes the region(s) of interest, but we don't have such a
|
|
format yet, and are not necessarily keen to rewrite this part of the
|
|
logic in playbin at this point - and we can't change existing elements'
|
|
behaviour, so would need to introduce new elements for this.
|
|
|
|
Playbin supports outputting compressed formats, i.e. it does not force
|
|
decoding to a raw format, but is happy to output to a non-raw format as
|
|
long as the sink supports that as well.
|
|
|
|
In case of certain hardware-accelerated decoding APIs, we will make use
|
|
of that functionality. However, the decoder will not output a raw video
|
|
format then, but some kind of hardware/API-specific format (in the caps)
|
|
and the buffers will reference hardware/API-specific objects that the
|
|
hardware/API-specific sink will know how to handle.
|
|
|
|
## The Problem
|
|
|
|
In the case of such hardware-accelerated decoding, the decoder will not
|
|
output raw pixels that can easily be manipulated. Instead, it will
|
|
output hardware/API-specific objects that can later be used to render a
|
|
frame using the same API.
|
|
|
|
Even if we could transform such a buffer into raw pixels, we most likely
|
|
would want to avoid that, in order to avoid the need to map the data
|
|
back into system memory (and then later back to the GPU). It's much
|
|
better to upload the much smaller encoded data to the GPU/DSP and then
|
|
leave it there until rendered.
|
|
|
|
Before `GstVideoOverlayComposition` playbin only supported subtitles on
|
|
top of raw decoded video. It would try to find a suitable overlay element
|
|
from the plugin registry based on the input subtitle caps and the rank.
|
|
(It is assumed that we will be able to convert any raw video format into
|
|
any format required by the overlay using a converter such as videoconvert.)
|
|
|
|
It would not render subtitles if the video sent to the sink is not raw
|
|
YUV or RGB or if conversions had been disabled by setting the
|
|
native-video flag on playbin.
|
|
|
|
Subtitle rendering is considered an important feature. Enabling
|
|
hardware-accelerated decoding by default should not lead to a major
|
|
feature regression in this area.
|
|
|
|
This means that we need to support subtitle rendering on top of non-raw
|
|
video.
|
|
|
|
## Possible Solutions
|
|
|
|
The goal is to keep knowledge of the subtitle format within the
|
|
format-specific GStreamer plugins, and knowledge of any specific video
|
|
acceleration API to the GStreamer plugins implementing that API. We do
|
|
not want to make the pango/dvbsuboverlay/dvdspu/kate plugins link to
|
|
libva/libvdpau/etc. and we do not want to make the vaapi/vdpau plugins
|
|
link to all of libpango/libkate/libass etc.
|
|
|
|
Multiple possible solutions come to mind:
|
|
|
|
1) backend-specific overlay elements
|
|
|
|
e.g. vaapitextoverlay, vdpautextoverlay, vaapidvdspu, vdpaudvdspu,
|
|
vaapidvbsuboverlay, vdpaudvbsuboverlay, etc.
|
|
|
|
This assumes the overlay can be done directly on the
|
|
backend-specific object passed around.
|
|
|
|
The main drawback with this solution is that it leads to a lot of
|
|
code duplication and may also lead to uncertainty about distributing
|
|
certain duplicated pieces of code. The code duplication is pretty
|
|
much unavoidable, since making textoverlay, dvbsuboverlay, dvdspu,
|
|
kate, assrender, etc. available in form of base classes to derive
|
|
from is not really an option. Similarly, one would not really want
|
|
the vaapi/vdpau plugin to depend on a bunch of other libraries such
|
|
as libpango, libkate, libtiger, libass, etc.
|
|
|
|
One could add some new kind of overlay plugin feature though in
|
|
combination with a generic base class of some sort, but in order to
|
|
accommodate all the different cases and formats one would end up
|
|
with quite convoluted/tricky API.
|
|
|
|
(Of course there could also be a `GstFancyVideoBuffer` that provides
|
|
an abstraction for such video accelerated objects and that could
|
|
provide an API to add overlays to it in a generic way, but in the
|
|
end this is just a less generic variant of (c), and it is not clear
|
|
that there are real benefits to a specialised solution vs. a more
|
|
generic one).
|
|
|
|
2) convert backend-specific object to raw pixels and then overlay
|
|
|
|
Even where possible technically, this is most likely very
|
|
inefficient.
|
|
|
|
3) attach the overlay data to the backend-specific video frame buffers
|
|
in a generic way and do the actual overlaying/blitting later in
|
|
backend-specific code such as the video sink (or an accelerated
|
|
encoder/transcoder)
|
|
|
|
In this case, the actual overlay rendering (i.e. the actual text
|
|
rendering or decoding DVD/DVB data into pixels) is done in the
|
|
subtitle-format-specific GStreamer plugin. All knowledge about the
|
|
subtitle format is contained in the overlay plugin then, and all
|
|
knowledge about the video backend in the video backend specific
|
|
plugin.
|
|
|
|
The main question then is how to get the overlay pixels (and we will
|
|
only deal with pixels here) from the overlay element to the video
|
|
sink.
|
|
|
|
This could be done in multiple ways: One could send custom events
|
|
downstream with the overlay data, or one could attach the overlay
|
|
data directly to the video buffers in some way.
|
|
|
|
Sending inline events has the advantage that is fairly
|
|
transparent to any elements between the overlay element and the
|
|
video sink: if an effects plugin creates a new video buffer for the
|
|
output, nothing special needs to be done to maintain the subtitle
|
|
overlay information, since the overlay data is not attached to the
|
|
buffer. However, it slightly complicates things at the sink, since
|
|
it would also need to look for the new event in question instead of
|
|
just processing everything in its buffer render function.
|
|
|
|
If one attaches the overlay data to the buffer directly, any element
|
|
between overlay and video sink that creates a new video buffer would
|
|
need to be aware of the overlay data attached to it and copy it over
|
|
to the newly-created buffer.
|
|
|
|
One would have to do implement a special kind of new query (e.g.
|
|
FEATURE query) that is not passed on automatically by
|
|
`gst_pad_query_default()` in order to make sure that all elements
|
|
downstream will handle the attached overlay data. (This is only a
|
|
problem if we want to also attach overlay data to raw video pixel
|
|
buffers; for new non-raw types we can just make it mandatory and
|
|
assume support and be done with it; for existing non-raw types
|
|
nothing changes anyway if subtitles don't work) (we need to maintain
|
|
backwards compatibility for existing raw video pipelines like e.g.:
|
|
`..decoder ! suboverlay ! encoder..`)
|
|
|
|
Even though slightly more work, attaching the overlay information to
|
|
buffers seems more intuitive than sending it interleaved as events.
|
|
And buffers stored or passed around (e.g. via the "last-buffer"
|
|
property in the sink when doing screenshots via playbin) always
|
|
contain all the information needed.
|
|
|
|
4) create a video/x-raw-delta format and use a backend-specific
|
|
videomixer
|
|
|
|
This possibility was hinted at already in the digression in section
|
|
1. It would satisfy the goal of keeping subtitle format knowledge in
|
|
the subtitle plugins and video backend knowledge in the video
|
|
backend plugin. It would also add a concept that might be generally
|
|
useful (think ximagesrc capture with xdamage). However, it would
|
|
require adding foorender variants of all the existing overlay
|
|
elements, and changing playbin to that new design, which is somewhat
|
|
intrusive. And given the general nature of such a new format/API, we
|
|
would need to take a lot of care to be able to accommodate all
|
|
possible use cases when designing the API, which makes it
|
|
considerably more ambitious. Lastly, we would need to write
|
|
videomixer variants for the various accelerated video backends as
|
|
well.
|
|
|
|
Overall (c) appears to be the most promising solution. It is the least
|
|
intrusive and should be fairly straight-forward to implement with
|
|
reasonable effort, requiring only small changes to existing elements and
|
|
requiring no new elements.
|
|
|
|
Doing the final overlaying in the sink as opposed to a videomixer or
|
|
overlay in the middle of the pipeline has other advantages:
|
|
|
|
- if video frames need to be dropped, e.g. for QoS reasons, we could
|
|
also skip the actual subtitle overlaying and possibly the
|
|
decoding/rendering as well, if the implementation and API allows for
|
|
that to be delayed.
|
|
|
|
- the sink often knows the actual size of the window/surface/screen
|
|
the output video is rendered to. This *may* make it possible to
|
|
render the overlay image in a higher resolution than the input
|
|
video, solving a long standing issue with pixelated subtitles on top
|
|
of low-resolution videos that are then scaled up in the sink. This
|
|
would require for the rendering to be delayed of course instead of
|
|
just attaching an AYUV/ARGB/RGBA blog of pixels to the video buffer
|
|
in the overlay, but that could all be supported.
|
|
|
|
- if the video backend / sink has support for high-quality text
|
|
rendering (clutter?) we could just pass the text or pango markup to
|
|
the sink and let it do the rest (this is unlikely to be supported in
|
|
the general case - text and glyph rendering is hard; also, we don't
|
|
really want to make up our own text markup system, and pango markup
|
|
is probably too limited for complex karaoke stuff).
|
|
|
|
## API needed
|
|
|
|
1) Representation of subtitle overlays to be rendered
|
|
|
|
We need to pass the overlay pixels from the overlay element to the
|
|
sink somehow. Whatever the exact mechanism, let's assume we pass a
|
|
refcounted `GstVideoOverlayComposition` struct or object.
|
|
|
|
A composition is made up of one or more overlays/rectangles.
|
|
|
|
In the simplest case an overlay rectangle is just a blob of
|
|
RGBA/ABGR \[FIXME?\] or AYUV pixels with positioning info and other
|
|
metadata, and there is only one rectangle to render.
|
|
|
|
We're keeping the naming generic ("OverlayFoo" rather than
|
|
"SubtitleFoo") here, since this might also be handy for other use
|
|
cases such as e.g. logo overlays or so. It is not designed for
|
|
full-fledged video stream mixing
|
|
though.
|
|
|
|
```
|
|
// Note: don't mind the exact implementation details, they'll be hidden
|
|
|
|
// FIXME: might be confusing in 0.11 though since GstXOverlay was
|
|
// renamed to GstVideoOverlay in 0.11, but not much we can do,
|
|
// maybe we can rename GstVideoOverlay to something better
|
|
|
|
struct GstVideoOverlayComposition
|
|
{
|
|
guint num_rectangles;
|
|
GstVideoOverlayRectangle ** rectangles;
|
|
|
|
/* lowest rectangle sequence number still used by the upstream
|
|
* overlay element. This way a renderer maintaining some kind of
|
|
* rectangles <-> surface cache can know when to free cached
|
|
* surfaces/rectangles. */
|
|
guint min_seq_num_used;
|
|
|
|
/* sequence number for the composition (same series as rectangles) */
|
|
guint seq_num;
|
|
}
|
|
|
|
struct GstVideoOverlayRectangle
|
|
{
|
|
/* Position on video frame and dimension of output rectangle in
|
|
* output frame terms (already adjusted for the PAR of the output
|
|
* frame). x/y can be negative (overlay will be clipped then) */
|
|
gint x, y;
|
|
guint render_width, render_height;
|
|
|
|
/* Dimensions of overlay pixels */
|
|
guint width, height, stride;
|
|
|
|
/* This is the PAR of the overlay pixels */
|
|
guint par_n, par_d;
|
|
|
|
/* Format of pixels, GST_VIDEO_FORMAT_ARGB on big-endian systems,
|
|
* and BGRA on little-endian systems (i.e. pixels are treated as
|
|
* 32-bit values and alpha is always in the most-significant byte,
|
|
* and blue is in the least-significant byte).
|
|
*
|
|
* FIXME: does anyone actually use AYUV in practice? (we do
|
|
* in our utility function to blend on top of raw video)
|
|
* What about AYUV and endianness? Do we always have [A][Y][U][V]
|
|
* in memory? */
|
|
/* FIXME: maybe use our own enum? */
|
|
GstVideoFormat format;
|
|
|
|
/* Refcounted blob of memory, no caps or timestamps */
|
|
GstBuffer *pixels;
|
|
|
|
// FIXME: how to express source like text or pango markup?
|
|
// (just add source type enum + source buffer with data)
|
|
//
|
|
// FOR 0.10: always send pixel blobs, but attach source data in
|
|
// addition (reason: if downstream changes, we can't renegotiate
|
|
// that properly, if we just do a query of supported formats from
|
|
// the start). Sink will just ignore pixels and use pango markup
|
|
// from source data if it supports that.
|
|
//
|
|
// FOR 0.11: overlay should query formats (pango markup, pixels)
|
|
// supported by downstream and then only send that. We can
|
|
// renegotiate via the reconfigure event.
|
|
//
|
|
|
|
/* sequence number: useful for backends/renderers/sinks that want
|
|
* to maintain a cache of rectangles <-> surfaces. The value of
|
|
* the min_seq_num_used in the composition tells the renderer which
|
|
* rectangles have expired. */
|
|
guint seq_num;
|
|
|
|
/* FIXME: we also need a (private) way to cache converted/scaled
|
|
* pixel blobs */
|
|
}
|
|
|
|
(a1) Overlay consumer
|
|
API:
|
|
|
|
How would this work in a video sink that supports scaling of textures:
|
|
|
|
gst_foo_sink_render () {
|
|
/* assume only one for now */
|
|
if video_buffer has composition:
|
|
composition = video_buffer.get_composition()
|
|
|
|
for each rectangle in composition:
|
|
if rectangle.source_data_type == PANGO_MARKUP
|
|
actor = text_from_pango_markup (rectangle.get_source_data())
|
|
else
|
|
pixels = rectangle.get_pixels_unscaled (FORMAT_RGBA, ...)
|
|
actor = texture_from_rgba (pixels, ...)
|
|
|
|
.. position + scale on top of video surface ...
|
|
}
|
|
|
|
(a2) Overlay producer
|
|
API:
|
|
|
|
e.g. logo or subpicture overlay: got pixels, stuff into rectangle:
|
|
|
|
if (logoverlay->cached_composition == NULL) {
|
|
comp = composition_new ();
|
|
|
|
rect = rectangle_new (format, pixels_buf,
|
|
width, height, stride, par_n, par_d,
|
|
x, y, render_width, render_height);
|
|
|
|
/* composition adds its own ref for the rectangle */
|
|
composition_add_rectangle (comp, rect);
|
|
rectangle_unref (rect);
|
|
|
|
/* buffer adds its own ref for the composition */
|
|
video_buffer_attach_composition (comp);
|
|
|
|
/* we take ownership of the composition and save it for later */
|
|
logoverlay->cached_composition = comp;
|
|
} else {
|
|
video_buffer_attach_composition (logoverlay->cached_composition);
|
|
}
|
|
```
|
|
|
|
FIXME: also add some API to modify render position/dimensions of a
|
|
rectangle (probably requires creation of new rectangle, unless we
|
|
handle writability like with other mini objects).
|
|
|
|
2) Fallback overlay rendering/blitting on top of raw video
|
|
|
|
Eventually we want to use this overlay mechanism not only for
|
|
hardware-accelerated video, but also for plain old raw video, either
|
|
at the sink or in the overlay element directly.
|
|
|
|
Apart from the advantages listed earlier in section 3, this allows
|
|
us to consolidate a lot of overlaying/blitting code that is
|
|
currently repeated in every single overlay element in one location.
|
|
This makes it considerably easier to support a whole range of raw
|
|
video formats out of the box, add SIMD-optimised rendering using
|
|
ORC, or handle corner cases correctly.
|
|
|
|
(Note: side-effect of overlaying raw video at the video sink is that
|
|
if e.g. a screnshotter gets the last buffer via the last-buffer
|
|
property of basesink, it would get an image without the subtitles on
|
|
top. This could probably be fixed by re-implementing the property in
|
|
`GstVideoSink` though. Playbin2 could handle this internally as well).
|
|
|
|
```
|
|
void
|
|
gst_video_overlay_composition_blend (GstVideoOverlayComposition * comp
|
|
GstBuffer * video_buf)
|
|
{
|
|
guint n;
|
|
|
|
g_return_if_fail (gst_buffer_is_writable (video_buf));
|
|
g_return_if_fail (GST_BUFFER_CAPS (video_buf) != NULL);
|
|
|
|
... parse video_buffer caps into BlendVideoFormatInfo ...
|
|
|
|
for each rectangle in the composition: {
|
|
|
|
if (gst_video_format_is_yuv (video_buf_format)) {
|
|
overlay_format = FORMAT_AYUV;
|
|
} else if (gst_video_format_is_rgb (video_buf_format)) {
|
|
overlay_format = FORMAT_ARGB;
|
|
} else {
|
|
/* FIXME: grayscale? */
|
|
return;
|
|
}
|
|
|
|
/* this will scale and convert AYUV<->ARGB if needed */
|
|
pixels = rectangle_get_pixels_scaled (rectangle, overlay_format);
|
|
|
|
... clip output rectangle ...
|
|
|
|
__do_blend (video_buf_format, video_buf->data,
|
|
overlay_format, pixels->data,
|
|
x, y, width, height, stride);
|
|
|
|
gst_buffer_unref (pixels);
|
|
}
|
|
}
|
|
```
|
|
|
|
3) Flatten all rectangles in a composition
|
|
|
|
We cannot assume that the video backend API can handle any number of
|
|
rectangle overlays, it's possible that it only supports one single
|
|
overlay, in which case we need to squash all rectangles into one.
|
|
|
|
However, we'll just declare this a corner case for now, and
|
|
implement it only if someone actually needs it. It's easy to add
|
|
later API-wise. Might be a bit tricky if we have rectangles with
|
|
different PARs/formats (e.g. subs and a logo), though we could
|
|
probably always just use the code from (b) with a fully transparent
|
|
video buffer to create a flattened overlay buffer.
|
|
|
|
4) query support for the new video composition mechanism
|
|
|
|
This is handled via `GstMeta` and an ALLOCATION query - we can simply
|
|
query whether downstream supports the `GstVideoOverlayComposition` meta.
|
|
|
|
There appears to be no issue with downstream possibly not being
|
|
linked yet at the time when an overlay would want to do such a
|
|
query, but we would just have to default to something and update
|
|
ourselves later on a reconfigure event then.
|
|
|
|
Other considerations:
|
|
|
|
- renderers (overlays or sinks) may be able to handle only ARGB or
|
|
only AYUV (for most graphics/hw-API it's likely ARGB of some sort,
|
|
while our blending utility functions will likely want the same
|
|
colour space as the underlying raw video format, which is usually
|
|
YUV of some sort). We need to convert where required, and should
|
|
cache the conversion.
|
|
|
|
- renderers may or may not be able to scale the overlay. We need to do
|
|
the scaling internally if not (simple case: just horizontal scaling
|
|
to adjust for PAR differences; complex case: both horizontal and
|
|
vertical scaling, e.g. if subs come from a different source than the
|
|
video or the video has been rescaled or cropped between overlay
|
|
element and sink).
|
|
|
|
- renderers may be able to generate (possibly scaled) pixels on demand
|
|
from the original data (e.g. a string or RLE-encoded data). We will
|
|
ignore this for now, since this functionality can still be added
|
|
later via API additions. The most interesting case would be to pass
|
|
a pango markup string, since e.g. clutter can handle that natively.
|
|
|
|
- renderers may be able to write data directly on top of the video
|
|
pixels (instead of creating an intermediary buffer with the overlay
|
|
which is then blended on top of the actual video frame), e.g.
|
|
dvdspu, dvbsuboverlay
|
|
|
|
However, in the interest of simplicity, we should probably ignore the
|
|
fact that some elements can blend their overlays directly on top of the
|
|
video (decoding/uncompressing them on the fly), even more so as it's not
|
|
obvious that it's actually faster to decode the same overlay 70-90 times
|
|
(say) (ie. ca. 3 seconds of video frames) and then blend it 70-90 times
|
|
instead of decoding it once into a temporary buffer and then blending it
|
|
directly from there, possibly SIMD-accelerated. Also, this is only
|
|
relevant if the video is raw video and not some hardware-acceleration
|
|
backend object.
|
|
|
|
And ultimately it is the overlay element that decides whether to do the
|
|
overlay right there and then or have the sink do it (if supported). It
|
|
could decide to keep doing the overlay itself for raw video and only use
|
|
our new API for non-raw video.
|
|
|
|
- renderers may want to make sure they only upload the overlay pixels
|
|
once per rectangle if that rectangle recurs in subsequent frames (as
|
|
part of the same composition or a different composition), as is
|
|
likely. This caching of e.g. surfaces needs to be done renderer-side
|
|
and can be accomplished based on the sequence numbers. The
|
|
composition contains the lowest sequence number still in use
|
|
upstream (an overlay element may want to cache created
|
|
compositions+rectangles as well after all to re-use them for
|
|
multiple frames), based on that the renderer can expire cached
|
|
objects. The caching needs to be done renderer-side because
|
|
attaching renderer-specific objects to the rectangles won't work
|
|
well given the refcounted nature of rectangles and compositions,
|
|
making it unpredictable when a rectangle or composition will be
|
|
freed or from which thread context it will be freed. The
|
|
renderer-specific objects are likely bound to other types of
|
|
renderer-specific contexts, and need to be managed in connection
|
|
with those.
|
|
|
|
- composition/rectangles should internally provide a certain degree of
|
|
thread-safety. Multiple elements (sinks, overlay element) might
|
|
access or use the same objects from multiple threads at the same
|
|
time, and it is expected that elements will keep a ref to
|
|
compositions and rectangles they push downstream for a while, e.g.
|
|
until the current subtitle composition expires.
|
|
|
|
## Future considerations
|
|
|
|
- alternatives: there may be multiple versions/variants of the same
|
|
subtitle stream. On DVDs, there may be a 4:3 version and a 16:9
|
|
version of the same subtitles. We could attach both variants and let
|
|
the renderer pick the best one for the situation (currently we just
|
|
use the 16:9 version). With totem, it's ultimately totem that adds
|
|
the 'black bars' at the top/bottom, so totem also knows if it's got
|
|
a 4:3 display and can/wants to fit 4:3 subs (which may render on top
|
|
of the bars) or not, for example.
|
|
|
|
## Misc. FIXMEs
|
|
|
|
TEST: should these look (roughly) alike (note text distortion) - needs
|
|
fixing in textoverlay
|
|
|
|
```
|
|
gst-launch-1.0 \
|
|
videotestsrc ! video/x-raw,width=640,height=480,pixel-aspect-ratio=1/1 \
|
|
! textoverlay text=Hello font-desc=72 ! xvimagesink \
|
|
videotestsrc ! video/x-raw,width=320,height=480,pixel-aspect-ratio=2/1 \
|
|
! textoverlay text=Hello font-desc=72 ! xvimagesink \
|
|
videotestsrc ! video/x-raw,width=640,height=240,pixel-aspect-ratio=1/2 \
|
|
! textoverlay text=Hello font-desc=72 ! xvimagesink
|
|
```
|