mirror of
https://gitlab.freedesktop.org/gstreamer/gstreamer.git
synced 2024-10-03 17:22:29 +00:00
a6409525ef
The libwebp API doesn't match very well with the GstVideoEncoder API, as it only delivers an unframed bitstream once all pictures have been processed, which means we can only push a single buffer manually on our srcpad on finish(). Supporting animated webp is still valuable, and the feature is behind an opt-in property. Part-of: <https://gitlab.freedesktop.org/gstreamer/gstreamer/-/merge_requests/5994>
486 lines
14 KiB
C
486 lines
14 KiB
C
/* GStreamer
|
|
* Copyright (C) <2014> Sreerenj Balachandran <sreerenjb@gnome.org>
|
|
*
|
|
* This library is free software; you can redistribute it and/or
|
|
* modify it under the terms of the GNU Library General Public
|
|
* License as published by the Free Software Foundation; either
|
|
* version 2 of the License, or (at your option) any later version.
|
|
*
|
|
* This library is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
* Library General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU Library General Public
|
|
* License along with this library; if not, write to the
|
|
* Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
|
|
* Boston, MA 02110-1301, USA.
|
|
*/
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#include <string.h>
|
|
#include <stdlib.h> /* free */
|
|
|
|
#include <gst/video/video.h>
|
|
#include <gst/video/gstvideometa.h>
|
|
|
|
#include "gstwebpenc.h"
|
|
|
|
#define GST_CAT_DEFAULT webpenc_debug
|
|
GST_DEBUG_CATEGORY_STATIC (GST_CAT_DEFAULT);
|
|
|
|
enum
|
|
{
|
|
PROP_0,
|
|
PROP_LOSSLESS,
|
|
PROP_QUALITY,
|
|
PROP_SPEED,
|
|
PROP_PRESET,
|
|
PROP_ANIMATED,
|
|
};
|
|
|
|
#define DEFAULT_LOSSLESS FALSE
|
|
#define DEFAULT_QUALITY 90
|
|
#define DEFAULT_SPEED 4
|
|
#define DEFAULT_PRESET WEBP_PRESET_PHOTO
|
|
#define DEFAULT_ANIMATED FALSE
|
|
|
|
static void gst_webp_enc_set_property (GObject * object, guint prop_id,
|
|
const GValue * value, GParamSpec * pspec);
|
|
static void gst_webp_enc_get_property (GObject * object, guint prop_id,
|
|
GValue * value, GParamSpec * pspec);
|
|
static gboolean gst_webp_enc_start (GstVideoEncoder * benc);
|
|
static gboolean gst_webp_enc_stop (GstVideoEncoder * benc);
|
|
static gboolean gst_webp_enc_set_format (GstVideoEncoder * encoder,
|
|
GstVideoCodecState * state);
|
|
static GstFlowReturn gst_webp_enc_handle_frame (GstVideoEncoder * encoder,
|
|
GstVideoCodecFrame * frame);
|
|
static gboolean gst_webp_enc_propose_allocation (GstVideoEncoder * encoder,
|
|
GstQuery * query);
|
|
static GstFlowReturn gst_webp_enc_finish (GstVideoEncoder * benc);
|
|
|
|
static GstStaticPadTemplate webp_enc_sink_factory =
|
|
GST_STATIC_PAD_TEMPLATE ("sink",
|
|
GST_PAD_SINK,
|
|
GST_PAD_ALWAYS,
|
|
GST_STATIC_CAPS (GST_VIDEO_CAPS_MAKE ("{ I420, YV12, RGB, RGBA}"))
|
|
);
|
|
static GstStaticPadTemplate webp_enc_src_factory =
|
|
GST_STATIC_PAD_TEMPLATE ("src",
|
|
GST_PAD_SRC,
|
|
GST_PAD_ALWAYS,
|
|
GST_STATIC_CAPS ("image/webp, "
|
|
"framerate = (fraction) [0/1, MAX], "
|
|
"width = (int) [ 16, 16383 ], " "height = (int) [ 16, 16383 ]")
|
|
);
|
|
|
|
enum
|
|
{
|
|
GST_WEBP_PRESET_DEFAULT,
|
|
GST_WEBP_PRESET_PICTURE,
|
|
GST_WEBP_PRESET_PHOTO,
|
|
GST_WEBP_PRESET_DRAWING,
|
|
GST_WEBP_PRESET_ICON,
|
|
GST_WEBP_PREET_TEXT
|
|
};
|
|
|
|
static const GEnumValue preset_values[] = {
|
|
{GST_WEBP_PRESET_DEFAULT, "Default", "none"},
|
|
{GST_WEBP_PRESET_PICTURE, "Digital picture,inner shot", "picture"},
|
|
{GST_WEBP_PRESET_PHOTO, "Outdoor photo, natural lighting", "photo"},
|
|
{GST_WEBP_PRESET_DRAWING, "Hand or Line drawing", "drawing"},
|
|
{GST_WEBP_PRESET_ICON, "Small-sized colorful images", "icon"},
|
|
{GST_WEBP_PREET_TEXT, "text-like", "text"},
|
|
{0, NULL, NULL},
|
|
};
|
|
|
|
#define GST_WEBP_ENC_PRESET_TYPE (gst_webp_enc_preset_get_type())
|
|
static GType
|
|
gst_webp_enc_preset_get_type (void)
|
|
{
|
|
static GType preset_type = 0;
|
|
|
|
if (!preset_type) {
|
|
preset_type = g_enum_register_static ("GstWebpEncPreset", preset_values);
|
|
}
|
|
return preset_type;
|
|
}
|
|
|
|
#define gst_webp_enc_parent_class parent_class
|
|
G_DEFINE_TYPE (GstWebpEnc, gst_webp_enc, GST_TYPE_VIDEO_ENCODER);
|
|
GST_ELEMENT_REGISTER_DEFINE (webpenc, "webpenc",
|
|
GST_RANK_PRIMARY, GST_TYPE_WEBP_ENC);
|
|
|
|
static void
|
|
gst_webp_enc_class_init (GstWebpEncClass * klass)
|
|
{
|
|
GObjectClass *gobject_class;
|
|
GstElementClass *element_class;
|
|
GstVideoEncoderClass *venc_class;
|
|
|
|
gobject_class = (GObjectClass *) klass;
|
|
element_class = (GstElementClass *) klass;
|
|
venc_class = (GstVideoEncoderClass *) klass;
|
|
|
|
parent_class = g_type_class_peek_parent (klass);
|
|
|
|
gobject_class->set_property = gst_webp_enc_set_property;
|
|
gobject_class->get_property = gst_webp_enc_get_property;
|
|
gst_element_class_add_static_pad_template (element_class,
|
|
&webp_enc_sink_factory);
|
|
gst_element_class_add_static_pad_template (element_class,
|
|
&webp_enc_src_factory);
|
|
gst_element_class_set_static_metadata (element_class, "WEBP image encoder",
|
|
"Codec/Encoder/Image", "Encode images in WEBP format",
|
|
"Sreerenj Balachandran <sreerenjb@gnome.org>");
|
|
|
|
venc_class->start = gst_webp_enc_start;
|
|
venc_class->finish = gst_webp_enc_finish;
|
|
venc_class->stop = gst_webp_enc_stop;
|
|
venc_class->set_format = gst_webp_enc_set_format;
|
|
venc_class->handle_frame = gst_webp_enc_handle_frame;
|
|
venc_class->propose_allocation = gst_webp_enc_propose_allocation;
|
|
|
|
g_object_class_install_property (gobject_class, PROP_LOSSLESS,
|
|
g_param_spec_boolean ("lossless", "Lossless",
|
|
"Enable lossless encoding",
|
|
DEFAULT_LOSSLESS, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
g_object_class_install_property (gobject_class, PROP_QUALITY,
|
|
g_param_spec_float ("quality", "quality-level",
|
|
"quality level, between 0 (smallest file) and 100 (biggest)",
|
|
0, 100, DEFAULT_QUALITY, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
g_object_class_install_property (gobject_class, PROP_SPEED,
|
|
g_param_spec_uint ("speed", "Compression Method",
|
|
"quality/speed trade-off (0=fast, 6=slower-better)",
|
|
0, 6, DEFAULT_SPEED, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
g_object_class_install_property (gobject_class, PROP_PRESET,
|
|
g_param_spec_enum ("preset", "preset tuning",
|
|
"Preset name for visual tuning",
|
|
GST_WEBP_ENC_PRESET_TYPE, DEFAULT_PRESET,
|
|
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
g_object_class_install_property (gobject_class, PROP_ANIMATED,
|
|
g_param_spec_boolean ("animated", "Animated",
|
|
"Encode an animated webp, instead of several pictures",
|
|
DEFAULT_ANIMATED, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
|
|
|
|
GST_DEBUG_CATEGORY_INIT (webpenc_debug, "webpenc", 0,
|
|
"WEBP encoding element");
|
|
|
|
gst_type_mark_as_plugin_api (GST_WEBP_ENC_PRESET_TYPE, 0);
|
|
}
|
|
|
|
static void
|
|
gst_webp_enc_init (GstWebpEnc * webpenc)
|
|
{
|
|
GST_PAD_SET_ACCEPT_TEMPLATE (GST_VIDEO_ENCODER_SINK_PAD (webpenc));
|
|
|
|
webpenc->lossless = DEFAULT_LOSSLESS;
|
|
webpenc->quality = DEFAULT_QUALITY;
|
|
webpenc->speed = DEFAULT_SPEED;
|
|
webpenc->preset = DEFAULT_PRESET;
|
|
webpenc->animated = DEFAULT_ANIMATED;
|
|
|
|
webpenc->use_argb = FALSE;
|
|
webpenc->rgb_format = GST_VIDEO_FORMAT_UNKNOWN;
|
|
}
|
|
|
|
static gboolean
|
|
gst_webp_enc_set_format (GstVideoEncoder * encoder, GstVideoCodecState * state)
|
|
{
|
|
GstWebpEnc *enc = GST_WEBP_ENC (encoder);
|
|
GstVideoCodecState *output_state;
|
|
GstVideoInfo *info;
|
|
GstVideoFormat format;
|
|
|
|
info = &state->info;
|
|
format = GST_VIDEO_INFO_FORMAT (info);
|
|
|
|
if (GST_VIDEO_INFO_IS_YUV (info)) {
|
|
switch (format) {
|
|
case GST_VIDEO_FORMAT_I420:
|
|
case GST_VIDEO_FORMAT_YV12:
|
|
enc->webp_color_space = WEBP_YUV420;
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
} else {
|
|
if (GST_VIDEO_INFO_IS_RGB (info)) {
|
|
enc->rgb_format = format;
|
|
enc->use_argb = 1;
|
|
}
|
|
}
|
|
|
|
if (enc->input_state)
|
|
gst_video_codec_state_unref (enc->input_state);
|
|
enc->input_state = gst_video_codec_state_ref (state);
|
|
|
|
if (enc->anim_enc) {
|
|
WebPAnimEncoderDelete (enc->anim_enc);
|
|
enc->anim_enc = NULL;
|
|
}
|
|
|
|
if (enc->animated) {
|
|
WebPAnimEncoderOptions enc_options = { {0}
|
|
};
|
|
WebPAnimEncoderOptionsInit (&enc_options);
|
|
enc->anim_enc =
|
|
WebPAnimEncoderNew (info->width, info->height, &enc_options);
|
|
}
|
|
|
|
output_state =
|
|
gst_video_encoder_set_output_state (GST_VIDEO_ENCODER (enc),
|
|
gst_caps_new_empty_simple ("image/webp"), enc->input_state);
|
|
gst_video_codec_state_unref (output_state);
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static gboolean
|
|
gst_webp_set_picture_params (GstWebpEnc * enc)
|
|
{
|
|
GstVideoInfo *info;
|
|
gboolean ret = TRUE;
|
|
|
|
info = &enc->input_state->info;
|
|
|
|
if (!WebPPictureInit (&enc->webp_picture)) {
|
|
ret = FALSE;
|
|
goto failed_pic_init;
|
|
}
|
|
|
|
enc->webp_picture.use_argb = enc->use_argb;
|
|
if (!enc->use_argb)
|
|
enc->webp_picture.colorspace = enc->webp_color_space;
|
|
|
|
enc->webp_picture.width = GST_VIDEO_INFO_WIDTH (info);
|
|
enc->webp_picture.height = GST_VIDEO_INFO_HEIGHT (info);
|
|
|
|
WebPMemoryWriterInit (&enc->webp_writer);
|
|
enc->webp_picture.writer = WebPMemoryWrite;
|
|
enc->webp_picture.custom_ptr = &enc->webp_writer;
|
|
|
|
return ret;
|
|
|
|
failed_pic_init:
|
|
{
|
|
GST_ERROR_OBJECT (enc, "Failed to Initialize WebPPicture !");
|
|
return ret;
|
|
}
|
|
}
|
|
|
|
static GstFlowReturn
|
|
gst_webp_enc_handle_frame (GstVideoEncoder * encoder,
|
|
GstVideoCodecFrame * frame)
|
|
{
|
|
GstWebpEnc *enc = GST_WEBP_ENC (encoder);
|
|
GstVideoFrame vframe;
|
|
|
|
GST_LOG_OBJECT (enc, "got new frame");
|
|
|
|
gst_webp_set_picture_params (enc);
|
|
|
|
if (!gst_video_frame_map (&vframe, &enc->input_state->info,
|
|
frame->input_buffer, GST_MAP_READ))
|
|
return GST_FLOW_ERROR;
|
|
|
|
if (!enc->use_argb) {
|
|
enc->webp_picture.y = GST_VIDEO_FRAME_COMP_DATA (&vframe, 0);
|
|
enc->webp_picture.u = GST_VIDEO_FRAME_COMP_DATA (&vframe, 1);
|
|
enc->webp_picture.v = GST_VIDEO_FRAME_COMP_DATA (&vframe, 2);
|
|
|
|
enc->webp_picture.y_stride = GST_VIDEO_FRAME_COMP_STRIDE (&vframe, 0);
|
|
enc->webp_picture.uv_stride = GST_VIDEO_FRAME_COMP_STRIDE (&vframe, 1);
|
|
|
|
} else {
|
|
switch (enc->rgb_format) {
|
|
case GST_VIDEO_FORMAT_RGB:
|
|
WebPPictureImportRGB (&enc->webp_picture,
|
|
GST_VIDEO_FRAME_COMP_DATA (&vframe, 0),
|
|
GST_VIDEO_FRAME_COMP_STRIDE (&vframe, 0));
|
|
break;
|
|
case GST_VIDEO_FORMAT_RGBA:
|
|
WebPPictureImportRGBA (&enc->webp_picture,
|
|
GST_VIDEO_FRAME_COMP_DATA (&vframe, 0),
|
|
GST_VIDEO_FRAME_COMP_STRIDE (&vframe, 0));
|
|
break;
|
|
default:
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (enc->animated) {
|
|
/* in milliseconds */
|
|
int timestamp = frame->pts / 1000000;
|
|
|
|
enc->next_timestamp = (frame->pts + frame->duration) / 1000000;
|
|
|
|
if (!WebPAnimEncoderAdd (enc->anim_enc, &enc->webp_picture,
|
|
timestamp, &enc->webp_config)) {
|
|
GST_ERROR_OBJECT (enc, "Failed to add WebPPicture: %d (%s)",
|
|
enc->webp_picture.error_code,
|
|
WebPAnimEncoderGetError (enc->anim_enc));
|
|
gst_video_frame_unmap (&vframe);
|
|
return GST_FLOW_ERROR;
|
|
}
|
|
WebPPictureFree (&enc->webp_picture);
|
|
} else if (WebPEncode (&enc->webp_config, &enc->webp_picture)) {
|
|
GstBuffer *out_buffer;
|
|
|
|
WebPPictureFree (&enc->webp_picture);
|
|
|
|
out_buffer = gst_buffer_new_allocate (NULL, enc->webp_writer.size, NULL);
|
|
if (!out_buffer) {
|
|
GST_ERROR_OBJECT (enc, "Failed to create output buffer");
|
|
gst_video_frame_unmap (&vframe);
|
|
return GST_FLOW_ERROR;
|
|
}
|
|
gst_buffer_fill (out_buffer, 0, enc->webp_writer.mem,
|
|
enc->webp_writer.size);
|
|
free (enc->webp_writer.mem);
|
|
frame->output_buffer = out_buffer;
|
|
} else {
|
|
GST_ERROR_OBJECT (enc, "Failed to encode WebPPicture");
|
|
gst_video_frame_unmap (&vframe);
|
|
return GST_FLOW_ERROR;
|
|
}
|
|
|
|
gst_video_frame_unmap (&vframe);
|
|
|
|
return gst_video_encoder_finish_frame (encoder, frame);
|
|
}
|
|
|
|
static gboolean
|
|
gst_webp_enc_propose_allocation (GstVideoEncoder * encoder, GstQuery * query)
|
|
{
|
|
gst_query_add_allocation_meta (query, GST_VIDEO_META_API_TYPE, NULL);
|
|
return
|
|
GST_VIDEO_ENCODER_CLASS (parent_class)->propose_allocation (encoder,
|
|
query);
|
|
}
|
|
|
|
static void
|
|
gst_webp_enc_set_property (GObject * object, guint prop_id,
|
|
const GValue * value, GParamSpec * pspec)
|
|
{
|
|
GstWebpEnc *webpenc = GST_WEBP_ENC (object);
|
|
|
|
switch (prop_id) {
|
|
case PROP_LOSSLESS:
|
|
webpenc->lossless = g_value_get_boolean (value);
|
|
break;
|
|
case PROP_QUALITY:
|
|
webpenc->quality = g_value_get_float (value);
|
|
break;
|
|
case PROP_SPEED:
|
|
webpenc->speed = g_value_get_uint (value);
|
|
break;
|
|
case PROP_PRESET:
|
|
webpenc->preset = g_value_get_enum (value);
|
|
break;
|
|
case PROP_ANIMATED:
|
|
webpenc->animated = g_value_get_boolean (value);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
break;
|
|
}
|
|
|
|
}
|
|
|
|
static void
|
|
gst_webp_enc_get_property (GObject * object, guint prop_id, GValue * value,
|
|
GParamSpec * pspec)
|
|
{
|
|
GstWebpEnc *webpenc = GST_WEBP_ENC (object);
|
|
|
|
switch (prop_id) {
|
|
case PROP_LOSSLESS:
|
|
g_value_set_boolean (value, webpenc->lossless);
|
|
break;
|
|
case PROP_QUALITY:
|
|
g_value_set_float (value, webpenc->quality);
|
|
break;
|
|
case PROP_SPEED:
|
|
g_value_set_uint (value, webpenc->speed);
|
|
break;
|
|
case PROP_PRESET:
|
|
g_value_set_enum (value, webpenc->preset);
|
|
break;
|
|
case PROP_ANIMATED:
|
|
g_value_set_boolean (value, webpenc->animated);
|
|
break;
|
|
default:
|
|
G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
|
|
break;
|
|
}
|
|
}
|
|
|
|
static gboolean
|
|
gst_webp_enc_start (GstVideoEncoder * benc)
|
|
{
|
|
GstWebpEnc *enc = (GstWebpEnc *) benc;
|
|
|
|
if (!WebPConfigPreset (&enc->webp_config, enc->preset, enc->quality)) {
|
|
GST_ERROR_OBJECT (enc, "Failed to Initialize WebPConfig ");
|
|
return FALSE;
|
|
}
|
|
|
|
enc->webp_config.lossless = enc->lossless;
|
|
enc->webp_config.method = enc->speed;
|
|
if (!WebPValidateConfig (&enc->webp_config)) {
|
|
GST_ERROR_OBJECT (enc, "Failed to Validate the WebPConfig");
|
|
return FALSE;
|
|
}
|
|
|
|
return TRUE;
|
|
}
|
|
|
|
static GstFlowReturn
|
|
gst_webp_enc_finish (GstVideoEncoder * benc)
|
|
{
|
|
GstWebpEnc *enc = (GstWebpEnc *) benc;
|
|
WebPData data = { 0 };
|
|
GstFlowReturn ret = GST_FLOW_OK;
|
|
|
|
if (enc->animated) {
|
|
if (!WebPAnimEncoderAdd (enc->anim_enc, NULL, enc->next_timestamp,
|
|
&enc->webp_config)) {
|
|
GST_ERROR_OBJECT (enc, "Failed to flush animation encoder");
|
|
ret = GST_FLOW_ERROR;
|
|
goto done;
|
|
}
|
|
|
|
if (WebPAnimEncoderAssemble (enc->anim_enc, &data)) {
|
|
GstBuffer *out = gst_buffer_new_allocate (NULL, data.size, NULL);
|
|
|
|
gst_buffer_fill (out, 0, data.bytes, data.size);
|
|
|
|
WebPDataClear (&data);
|
|
|
|
ret = gst_pad_push (benc->srcpad, out);
|
|
} else {
|
|
GST_ERROR_OBJECT (enc, "Failed to assemble output animation");
|
|
ret = GST_FLOW_ERROR;
|
|
goto done;
|
|
}
|
|
}
|
|
|
|
done:
|
|
return ret;
|
|
}
|
|
|
|
static gboolean
|
|
gst_webp_enc_stop (GstVideoEncoder * benc)
|
|
{
|
|
GstWebpEnc *enc = GST_WEBP_ENC (benc);
|
|
if (enc->input_state)
|
|
gst_video_codec_state_unref (enc->input_state);
|
|
|
|
if (enc->anim_enc)
|
|
WebPAnimEncoderDelete (enc->anim_enc);
|
|
|
|
return TRUE;
|
|
}
|