[Spice-commits] 2 commits - server/gstreamer-encoder.c server/tests
Frediano Ziglio
fziglio at kemper.freedesktop.org
Tue Jan 10 13:43:08 UTC 2017
server/gstreamer-encoder.c | 4
server/tests/.gitignore | 1
server/tests/Makefile.am | 9
server/tests/test-gst.c | 957 +++++++++++++++++++++++++++++++++++++++++++++
4 files changed, 970 insertions(+), 1 deletion(-)
New commits:
commit e8d078673acbb1e955f468c2aefe13dd31ab6a84
Author: Frediano Ziglio <fziglio at redhat.com>
Date: Thu Oct 20 10:45:12 2016 +0100
gstreamer: Do not warn for tested formats
These formats were tested manually using test-gst utility
in server/tests.
Signed-off-by: Frediano Ziglio <fziglio at redhat.com>
Acked-by: Pavel Grunt <pgrunt at redhat.com>
diff --git a/server/gstreamer-encoder.c b/server/gstreamer-encoder.c
index 988d193..4a0917e 100644
--- a/server/gstreamer-encoder.c
+++ b/server/gstreamer-encoder.c
@@ -759,7 +759,7 @@ static const SpiceFormatForGStreamer format_map[] = {
{SPICE_BITMAP_FMT_INVALID, "", 0, 0, 0, 0, 0, 0},
{SPICE_BITMAP_FMT_RGBA, "BGRA", 32, 24, 4321, 0xff000000, 0xff0000, 0xff00},
{SPICE_BITMAP_FMT_16BIT, "RGB15", 16, 15, 4321, 0x001f, 0x03E0, 0x7C00},
- /* TODO: Test the other formats */
+ /* TODO: Test the other formats under GStreamer 0.10*/
{SPICE_BITMAP_FMT_32BIT, "BGRx", 32, 24, 4321, 0xff000000, 0xff0000, 0xff00},
{SPICE_BITMAP_FMT_24BIT, "BGR", 24, 24, 4321, 0xff0000, 0xff00, 0xff},
};
@@ -771,9 +771,11 @@ static const SpiceFormatForGStreamer *map_format(SpiceBitmapFmt format)
int i;
for (i = 0; i < G_N_ELEMENTS(format_map); i++) {
if (format_map[i].spice_format == format) {
+#ifdef HAVE_GSTREAMER_0_10
if (i > 2) {
spice_warning("The %d format has not been tested yet", format);
}
+#endif
return &format_map[i];
}
}
commit 623e7d4a9399473ecf04b0a16ed8ddd7a2c284dd
Author: Frediano Ziglio <fziglio at redhat.com>
Date: Wed Oct 19 14:45:14 2016 +0100
Add an helper to test VideoEncoder
Add an utility to make possible to check various features of
VideoEncoder.
2 GStreamer plugins are used in a chain like this:
(1) input pipeline -> (2) video encoder -> (3) output pipeline
While converting output from (1) is compared with output of (3)
making sure the streaming is working correctly.
You can set various options:
- part of the input pipeline description to allow specifying different
video from GStreamer test ones to a video file;
- the encoder to use;
- different image properties to use for (2) input:
- different bit depth;
- top/down or down/up;
- initial bitrate.
The idea is to use this helper in combination with a shell script
and some video sources to make able to test various settings.
Also can be used to extend the current encoder list.
As an example you can use a command like
$ ./test-gst -e gstreamer:vp8 -i \
'filesrc location=bbb_sunflower_1080p_30fps_normal.mp4 \
! decodebin ! videoconvert'
to check vp8 encoding.
Currently it does not emulate bandwidth changes as stream reports
from the client are not coded.
Signed-off-by: Frediano Ziglio <fziglio at redhat.com>
Acked-by: Pavel Grunt <pgrunt at redhat.com>
diff --git a/server/tests/.gitignore b/server/tests/.gitignore
index a360297..0154f32 100644
--- a/server/tests/.gitignore
+++ b/server/tests/.gitignore
@@ -22,3 +22,4 @@ test-stat-file
test-stream
test-two-servers
test-vdagent
+test-gst
diff --git a/server/tests/Makefile.am b/server/tests/Makefile.am
index 3870b9e..f696f9f 100644
--- a/server/tests/Makefile.am
+++ b/server/tests/Makefile.am
@@ -59,6 +59,7 @@ noinst_PROGRAMS = \
test-vdagent \
test-display-width-stride \
spice-server-replay \
+ test-gst \
$(check_PROGRAMS) \
$(NULL)
@@ -107,3 +108,11 @@ libtest_stat4_a_SOURCES = stat-test.c
libtest_stat4_a_CPPFLAGS = $(AM_CPPFLAGS) -DTEST_COMPRESS_STAT=1 -DTEST_RED_WORKER_STAT=1 -DTEST_NAME=stat_test4
test_qxl_parsing_LDADD = ../libserver.la $(LDADD)
+
+test_gst_SOURCES = test-gst.c \
+ $(NULL)
+test_gst_CPPFLAGS = \
+ $(AM_CPPFLAGS) \
+ $(GSTREAMER_0_10_CFLAGS) \
+ $(GSTREAMER_1_0_CFLAGS) \
+ $(NULL)
diff --git a/server/tests/test-gst.c b/server/tests/test-gst.c
new file mode 100644
index 0000000..f88db25
--- /dev/null
+++ b/server/tests/test-gst.c
@@ -0,0 +1,957 @@
+/* -*- Mode: C; c-basic-offset: 4; indent-tabs-mode: nil -*- */
+/*
+ Copyright (C) 2016 Red Hat, Inc.
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 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
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, see <http://www.gnu.org/licenses/>.
+*/
+/* Utility to check video encoder code
+ * (see program_description below)
+ */
+#undef NDEBUG
+#include <config.h>
+#include <stdio.h>
+#include <glib.h>
+#include <string.h>
+#include <errno.h>
+#include <math.h>
+#include <pthread.h>
+#include <common/log.h>
+#include <gst/gst.h>
+#include <gst/app/gstappsrc.h>
+#include <gst/app/gstappsink.h>
+
+#include "video-encoder.h"
+#include "spice-bitmap-utils.h"
+#include "reds.h" // reds_get_mm_time
+
+static const char program_description[] =
+ "2 GStreamer plugins are used in a chain like this:\n"
+ " (1) input pipeline -> (2) video encoder -> (3) output pipeline\n"
+ "While converting output from (1) is compared with output of (3)\n"
+ "making sure the streaming is working correctly.\n"
+ "\n"
+ "As an example you can use a command like\n"
+ "\n"
+ " $ ./test-gst -e gstreamer:vp8 -i \\\n"
+ " 'filesrc location=bbb_sunflower_1080p_30fps_normal.mp4 \\\n"
+ " ! decodebin ! videoconvert'\n"
+ "\n"
+ "to check vp8 encoding.";
+
+// clipping informations passed in command line
+typedef enum {
+ COORDS_INVALID,
+ COORDS_NUMBER,
+ COORDS_PERCENT,
+} CoordsUnit;
+static struct {
+ unsigned int value;
+ CoordsUnit unit;
+} clipping_coords[4];
+enum {
+ COORDS_BOX,
+ COORDS_SIZE
+} clipping_type;
+
+typedef struct {
+ gint refs;
+ SpiceBitmap *bitmap;
+} TestFrame;
+
+typedef void (*SampleProc)(GstSample *sample, void *param);
+
+typedef struct {
+ GstAppSrc *appsrc;
+ GstAppSink *appsink;
+ GstElement *gst_pipeline;
+ SampleProc sample_proc;
+ void *sample_param;
+ gboolean got_eos;
+} TestPipeline;
+
+typedef struct {
+ const char *name;
+ new_video_encoder_t new_encoder;
+ SpiceVideoCodecType coded_type;
+ // see spice-gtk channel-display-gst.c
+ const char *caps;
+ const char *decoder;
+} EncoderInfo;
+
+// our video encoder we are testing
+static VideoEncoder *video_encoder = NULL;
+
+// image settings
+static gboolean top_down = FALSE;
+static SpiceBitmapFmt bitmap_format = SPICE_BITMAP_FMT_32BIT;
+static gint image_split_lines = 60000;
+
+static gboolean clipping_type_computed = FALSE;
+static SpiceRect clipping_rect;
+static pthread_mutex_t frame_queue_mtx = PTHREAD_MUTEX_INITIALIZER;
+static pthread_cond_t frame_queue_cond = PTHREAD_COND_INITIALIZER;
+static GQueue frame_queue = G_QUEUE_INIT;
+static TestPipeline *input_pipeline, *output_pipeline;
+static pthread_mutex_t eos_mtx = PTHREAD_MUTEX_INITIALIZER;
+static pthread_cond_t eos_cond = PTHREAD_COND_INITIALIZER;
+// minimum image different expected, depends on quality
+// and encoder
+static gdouble minimum_psnr = 25;
+static uint64_t starting_bit_rate = 3000000;
+
+static void compute_clipping_rect(GstSample *sample);
+static void parse_clipping(const char *clipping);
+static TestFrame *gst_to_spice_frame(GstSample *sample);
+static void bitmap_free(SpiceBitmap *bitmap);
+static void frame_ref(TestFrame *frame);
+static void frame_unref(TestFrame *frame);
+static void pipeline_send_raw_data(TestPipeline *pipeline, VideoBuffer *buffer);
+static void pipeline_wait_eos(TestPipeline *pipeline);
+static void create_input_pipeline(const char *input_pipeline,
+ SampleProc sample_proc, void *param);
+static void create_output_pipeline(const EncoderInfo *encoder,
+ SampleProc output_frames, void *param);
+static void create_video_encoder(const EncoderInfo *encoder);
+static const EncoderInfo *get_encoder_info(const char *encoder_name);
+static SpiceBitmapFmt get_bitmap_format(const char *format);
+static double compute_psnr(SpiceBitmap *bitmap1, int32_t x1, int32_t y1,
+ SpiceBitmap *bitmap2, int32_t x2, int32_t y2,
+ int32_t w, int32_t h);
+
+// handle output frames from input pipeline
+static void
+input_frames(GstSample *sample, void *param)
+{
+ spice_assert(video_encoder && sample);
+
+ if (SPICE_UNLIKELY(!clipping_type_computed)) {
+ compute_clipping_rect(sample);
+ }
+
+ VideoBuffer *p_outbuf = NULL;
+ // TODO correct ?? emulate another timer ??
+ uint32_t frame_mm_time = reds_get_mm_time();
+
+ // convert frame to SpiceBitmap/DRM prime
+ TestFrame *frame = gst_to_spice_frame(sample);
+
+ // send frame to our video encoder (must be from a single thread)
+ int res = video_encoder->encode_frame(video_encoder, frame_mm_time, frame->bitmap,
+ &clipping_rect, top_down, frame,
+ &p_outbuf);
+ switch (res) {
+ case VIDEO_ENCODER_FRAME_ENCODE_DONE:
+ // save frame into queue for comparison later
+ frame_ref(frame);
+ pthread_mutex_lock(&frame_queue_mtx);
+ g_queue_push_tail(&frame_queue, frame);
+ while (g_queue_get_length(&frame_queue) >= 16) {
+ pthread_cond_wait(&frame_queue_cond, &frame_queue_mtx);
+ }
+ pthread_mutex_unlock(&frame_queue_mtx);
+ spice_assert(p_outbuf);
+ pipeline_send_raw_data(output_pipeline, p_outbuf);
+ break;
+ case VIDEO_ENCODER_FRAME_UNSUPPORTED:
+ // ?? what to do ??
+ // should not happen, format passed should be supported
+ // could happen for serious problems and encoder gave up
+ spice_assert(0);
+ break;
+ case VIDEO_ENCODER_FRAME_DROP:
+ break;
+ default:
+ // invalid value returned
+ spice_assert(0);
+ }
+
+ // TODO call client_stream_report to simulate this report from the client
+
+ frame_unref(frame);
+}
+
+// handle output frames from output pipeline
+static void
+output_frames(GstSample *sample, void *param)
+{
+ TestFrame *curr_frame = gst_to_spice_frame(sample);
+
+ // get first frame queued
+ pthread_mutex_lock(&frame_queue_mtx);
+ TestFrame *expected_frame = g_queue_pop_head(&frame_queue);
+ pthread_cond_signal(&frame_queue_cond);
+ pthread_mutex_unlock(&frame_queue_mtx);
+ if (!expected_frame) {
+ g_printerr("Frame not present in the queue but arrived in output!\n");
+ exit(1);
+ }
+
+ // TODO try to understand if this is correct
+ if (!top_down) {
+ curr_frame->bitmap->flags ^= SPICE_BITMAP_FLAGS_TOP_DOWN;
+ }
+#ifdef DUMP_BITMAP
+ dump_bitmap(expected_frame->bitmap);
+ dump_bitmap(curr_frame->bitmap);
+#endif
+
+ // compute difference
+ double psnr = compute_psnr(expected_frame->bitmap, clipping_rect.left, clipping_rect.top,
+ curr_frame->bitmap, 0, 0,
+ clipping_rect.right - clipping_rect.left,
+ clipping_rect.bottom - clipping_rect.top);
+
+ // check is more or less the same
+ if (psnr < minimum_psnr) {
+ g_printerr("Frame PSNR too low, got %g minimum %g\n", psnr, minimum_psnr);
+ exit(1);
+ }
+
+ frame_unref(expected_frame);
+ frame_unref(curr_frame);
+}
+
+static const EncoderInfo encoder_infos[] = {
+ { "mjpeg", mjpeg_encoder_new, SPICE_VIDEO_CODEC_TYPE_MJPEG,
+ "caps=image/jpeg", "jpegdec" },
+ { "gstreamer:mjpeg", gstreamer_encoder_new, SPICE_VIDEO_CODEC_TYPE_MJPEG,
+ "caps=image/jpeg", "jpegdec" },
+ { "gstreamer:vp8", gstreamer_encoder_new, SPICE_VIDEO_CODEC_TYPE_VP8,
+ "caps=video/x-vp8", "vp8dec" },
+ { "gstreamer:h264", gstreamer_encoder_new, SPICE_VIDEO_CODEC_TYPE_H264,
+ "", "h264parse ! avdec_h264" },
+ { NULL, NULL, SPICE_VIDEO_CODEC_TYPE_ENUM_END, NULL, NULL }
+};
+
+int main(int argc, char *argv[])
+{
+ gchar *input_pipeline_desc = NULL;
+ const gchar *image_format = "32BIT";
+ const gchar *encoder_name = "mjpeg";
+ gboolean use_hw_encoder = FALSE; // TODO use
+ const gchar *clipping = "(0,0)x(100%,100%)";
+
+ // - input pipeline
+ // - top/down
+ // - format for video encoder input (bits, rgb/bgr)
+ // - encoder (mjpeg/vp8/h264)
+ // - use h/w acceleration (if available)
+ // - clipping (part of the source)
+ // - TODO bandwidth changes?
+ // - TODO fps ??
+ GOptionEntry entries[] = {
+ { "input-pipeline", 'i', 0, G_OPTION_ARG_STRING, &input_pipeline_desc,
+ "GStreamer input pipeline", "PIPELINE" },
+ { "top-down", 0, 0, G_OPTION_ARG_NONE, &top_down,
+ "Image encoded as top-down", NULL },
+ { "format", 'f', 0, G_OPTION_ARG_STRING, &image_format,
+ "Image format (16BIT/24BIT/32BIT/RGBA)", "FMT" },
+ { "encoder", 'e', 0, G_OPTION_ARG_STRING, &encoder_name,
+ "Encoder to use", "ENC" },
+ { "use-hw-encoder", 0, 0, G_OPTION_ARG_NONE, &use_hw_encoder,
+ "Use H/W encoders if possible", NULL },
+ { "clipping", 0, 0, G_OPTION_ARG_STRING, &clipping,
+ "Clipping region (x1,y1)-(x2,y2) or (x,y)x(w,h). "
+ "You can specify coordinates using pixel or percentage.", "STRING" },
+ { "starting-bitrate", 0, 0, G_OPTION_ARG_INT64, &starting_bit_rate,
+ "Initial bitrate", "BITRATE" },
+ { "min-psnr", 0, 0, G_OPTION_ARG_DOUBLE, &minimum_psnr,
+ "Minimum PSNR accepted", "PSNR" },
+ { "split-lines", 0, 0, G_OPTION_ARG_INT, &image_split_lines,
+ "Split image into different chunks every LINES lines", "LINES" },
+ { NULL }
+ };
+
+ GOptionContext *context = NULL;
+ GError *error = NULL;
+ context = g_option_context_new("- helper for testing VideoEncoder");
+ g_option_context_set_description(context, program_description);
+ g_option_context_add_main_entries(context, entries, NULL);
+ if (!g_option_context_parse(context, &argc, &argv, &error)) {
+ g_printerr("Option parsing failed: %s\n", error->message);
+ exit(1);
+ }
+
+ if (!input_pipeline_desc) {
+ g_printerr("Input pipeline option missing\n");
+ exit(1);
+ }
+
+ if (!encoder_name) {
+ g_printerr("Encoder name option missing\n");
+ exit(1);
+ }
+
+ const EncoderInfo *encoder = get_encoder_info(encoder_name);
+ if (!encoder) {
+ g_printerr("Encoder name unsupported: %s\n", encoder_name);
+ exit(1);
+ }
+
+ bitmap_format = get_bitmap_format(image_format);
+ if (bitmap_format == SPICE_BITMAP_FMT_INVALID) {
+ g_printerr("Invalid image format: %s\n", image_format);
+ exit(1);
+ }
+
+ parse_clipping(clipping);
+
+ if (minimum_psnr < 0) {
+ g_printerr("Invalid PSNR specified %f\n", minimum_psnr);
+ exit(1);
+ }
+
+ if (image_split_lines < 1) {
+ g_printerr("Invalid --split-lines option: %d\n", image_split_lines);
+ exit(1);
+ }
+
+ gst_init(&argc, &argv);
+
+ // TODO give particular error if pipeline fails to be created
+
+ create_output_pipeline(encoder, output_frames, NULL);
+
+ create_video_encoder(encoder);
+
+ create_input_pipeline(input_pipeline_desc, input_frames, NULL);
+
+ // run all input streaming
+ pipeline_wait_eos(input_pipeline);
+
+ video_encoder->destroy(video_encoder);
+
+ // send EOS to output and wait
+ // this assure we processed all frames sent from input pipeline
+ if (gst_app_src_end_of_stream(output_pipeline->appsrc) != GST_FLOW_OK) {
+ g_printerr("gst_app_src_end_of_stream failed\n");
+ exit(1);
+ }
+ pipeline_wait_eos(output_pipeline);
+
+ // check queue is now empty
+ pthread_mutex_lock(&frame_queue_mtx);
+ TestFrame *frame = g_queue_pop_head(&frame_queue);
+ pthread_mutex_unlock(&frame_queue_mtx);
+ if (frame) {
+ g_printerr("Queue not empty at the end\n");
+ exit(1);
+ }
+
+ return 0;
+}
+
+static void
+parse_clipping(const char *clipping)
+{
+ spice_assert(clipping);
+
+#define NUM_FMT "%31[^,)]"
+#define NUM(n) coords[n]
+ char coords[4][32];
+ char clipping_type_sign[2];
+ int i;
+
+ if (sscanf(clipping, "(" NUM_FMT "," NUM_FMT ")%1[x-](" NUM_FMT "," NUM_FMT ")",
+ NUM(0), NUM(1), clipping_type_sign, NUM(2), NUM(3)) < 5) {
+ goto format_error;
+ }
+ for (i = 0; i < 4; ++i) {
+ char *end = NULL;
+ errno = 0;
+ clipping_coords[i].unit = COORDS_NUMBER;
+ clipping_coords[i].value = strtoul(coords[i], &end, 10);
+ if (errno || !end || (strcmp(end, "") != 0 && strcmp(end, "%") != 0)) {
+ goto format_error;
+ }
+ if (strcmp(end, "%") == 0) {
+ clipping_coords[i].unit = COORDS_PERCENT;
+ if (clipping_coords[i].value > 100) {
+ goto format_error;
+ }
+ }
+ }
+ if (clipping_type_sign[0] == 'x') {
+ clipping_type = COORDS_SIZE;
+ } else {
+ clipping_type = COORDS_BOX;
+ }
+ return;
+
+format_error:
+ g_printerr("Invalid clipping format: %s\n", clipping);
+ exit(1);
+
+}
+
+static void
+compute_clipping_rect(GstSample *sample)
+{
+ GstCaps *caps = gst_sample_get_caps(sample);
+ spice_assert(caps);
+
+ GstStructure *s = gst_caps_get_structure(caps, 0);
+ spice_assert(s);
+
+ gint width, height;
+ spice_assert(gst_structure_get_int(s, "width", &width) &&
+ gst_structure_get_int(s, "height", &height));
+
+ // transform from percent to pixel values
+ int i;
+ unsigned int coords[4];
+ for (i = 0; i < 4; ++i) {
+ unsigned int coord = coords[i] = clipping_coords[i].value;
+ if (clipping_coords[i].unit != COORDS_PERCENT) {
+ spice_assert(clipping_coords[i].unit == COORDS_NUMBER);
+ continue;
+ }
+ coords[i] = coord * ((i&1) ? height : width) / 100;
+ }
+
+ // transform from sized to box
+ if (clipping_type == COORDS_SIZE) {
+ coords[2] += coords[0];
+ coords[3] += coords[1];
+ }
+
+ // clip to sample
+ coords[0] = MIN(coords[0], width);
+ coords[1] = MIN(coords[1], height);
+ coords[2] = MIN(coords[2], width);
+ coords[3] = MIN(coords[3], height);
+
+ // check coordinated are valid
+ spice_assert(coords[0] < coords[2]);
+ spice_assert(coords[1] < coords[3]);
+
+ // set
+ clipping_rect.left = coords[0];
+ clipping_rect.top = coords[1];
+ clipping_rect.right = coords[2];
+ clipping_rect.bottom = coords[3];
+ clipping_type_computed = TRUE;
+}
+
+static const EncoderInfo *
+get_encoder_info(const char *encoder_name)
+{
+ const EncoderInfo *info;
+ for (info = encoder_infos; info->name; ++info) {
+ if (strcmp(info->name, encoder_name) == 0) {
+ return info;
+ }
+ }
+ return NULL;
+}
+
+static GstFlowReturn
+new_sample(GstAppSink *gstappsink, gpointer test_pipeline)
+{
+ TestPipeline *pipeline = test_pipeline;
+
+ GstSample *sample = gst_app_sink_pull_sample(pipeline->appsink);
+ if (sample) {
+ pipeline->sample_proc(sample, pipeline->sample_param);
+ gst_sample_unref(sample);
+ }
+ return GST_FLOW_OK;
+}
+
+static GstBusSyncReply
+handle_pipeline_message(GstBus *bus, GstMessage *msg, gpointer test_pipeline)
+{
+ TestPipeline *pipeline = (TestPipeline *) test_pipeline;
+
+ if (GST_MESSAGE_TYPE(msg) == GST_MESSAGE_EOS) {
+ pthread_mutex_lock(&eos_mtx);
+ pipeline->got_eos = TRUE;
+ pthread_cond_signal(&eos_cond);
+ pthread_mutex_unlock(&eos_mtx);
+ }
+ return GST_BUS_PASS;
+}
+
+static TestPipeline*
+create_pipeline(const char *desc, SampleProc sample_proc, void *param)
+{
+ TestPipeline *pipeline = spice_new0(TestPipeline, 1);
+
+ pipeline->sample_proc = sample_proc;
+ pipeline->sample_param = param;
+
+ GError *err = NULL;
+ pipeline->gst_pipeline = gst_parse_launch_full(desc, NULL, GST_PARSE_FLAG_FATAL_ERRORS, &err);
+ if (!pipeline->gst_pipeline) {
+ g_printerr("GStreamer error: %s\n", err->message);
+ return NULL;
+ }
+
+ pipeline->appsrc = GST_APP_SRC(gst_bin_get_by_name(GST_BIN(pipeline->gst_pipeline), "src"));
+ pipeline->appsink = GST_APP_SINK(gst_bin_get_by_name(GST_BIN(pipeline->gst_pipeline), "sink"));
+ if (!pipeline->appsink) {
+ g_printerr("Appsync not found in pipeline: %s\n", desc);
+ return NULL;
+ }
+
+ GstAppSinkCallbacks appsink_cbs = { NULL, NULL, new_sample, {NULL} };
+ gst_app_sink_set_callbacks(pipeline->appsink, &appsink_cbs, pipeline, NULL);
+
+ GstBus *bus = gst_element_get_bus(pipeline->gst_pipeline);
+ gst_bus_set_sync_handler(bus, handle_pipeline_message, pipeline, NULL);
+ gst_object_unref(bus);
+
+ if (gst_element_set_state(pipeline->gst_pipeline, GST_STATE_PLAYING) ==
+ GST_STATE_CHANGE_FAILURE) {
+ g_printerr("GStreamer error: Unable to set the pipeline to the playing state.\n");
+ exit(1);
+ }
+
+ return pipeline;
+}
+
+static void
+create_output_pipeline(const EncoderInfo *encoder, SampleProc sample_proc, void *param)
+{
+ gchar *desc =
+ g_strdup_printf("appsrc name=src is-live=true format=time max-bytes=0 block=true "
+ "%s ! %s ! videoconvert ! appsink name=sink caps=video/x-raw,format=BGRx"
+ " sync=false drop=false", encoder->caps, encoder->decoder);
+
+ TestPipeline *pipeline = create_pipeline(desc, sample_proc, param);
+ if (!pipeline) {
+ g_printerr("Error creating output pipeline: %s\n", desc);
+ exit(1);
+ }
+ g_free(desc);
+
+ output_pipeline = pipeline;
+}
+
+static void
+create_input_pipeline(const char *input_pipeline_desc, SampleProc sample_proc, void *param)
+{
+ gchar *desc =
+ g_strdup_printf("%s ! appsink name=sink caps=video/x-raw,format=BGRx"
+ " sync=false drop=false", input_pipeline_desc);
+
+ TestPipeline *pipeline = create_pipeline(desc, sample_proc, param);
+ g_free(desc);
+ if (!pipeline) {
+ // TODO specific error
+ g_printerr("Error creating input pipeline\n");
+ exit(1);
+ }
+
+ input_pipeline = pipeline;
+}
+
+static void
+video_buffer_release(VideoBuffer *video_buffer)
+{
+ video_buffer->free(video_buffer);
+}
+
+static void
+pipeline_send_raw_data(TestPipeline *pipeline, VideoBuffer *video_buffer)
+{
+ GstBuffer *buffer =
+ gst_buffer_new_wrapped_full(GST_MEMORY_FLAG_PHYSICALLY_CONTIGUOUS,
+ video_buffer->data, video_buffer->size,
+ 0, video_buffer->size,
+ video_buffer, (void (*)(void*)) video_buffer_release);
+
+ GST_BUFFER_DURATION(buffer) = GST_CLOCK_TIME_NONE;
+ GST_BUFFER_DTS(buffer) = GST_CLOCK_TIME_NONE;
+
+ if (gst_app_src_push_buffer(pipeline->appsrc, buffer) != GST_FLOW_OK) {
+ g_printerr("GStreamer error: unable to push frame of size %u\n", video_buffer->size);
+ exit(1);
+ }
+}
+
+static void
+pipeline_wait_eos(TestPipeline *pipeline)
+{
+ pthread_mutex_lock(&eos_mtx);
+ while (!pipeline->got_eos) {
+ pthread_cond_wait(&eos_cond, &eos_mtx);
+ }
+ pthread_mutex_unlock(&eos_mtx);
+}
+
+static uint32_t
+mock_get_roundtrip_ms(void *opaque)
+{
+ // TODO
+ return 20;
+}
+
+static uint32_t
+mock_get_source_fps(void *opaque)
+{
+ // TODO
+ return 10;
+}
+
+static void
+mock_update_client_playback_delay(void *opaque, uint32_t delay_ms)
+{
+ // TODO
+}
+
+static VideoEncoderRateControlCbs rate_control_cbs = {
+ .get_roundtrip_ms = mock_get_roundtrip_ms,
+ .get_source_fps = mock_get_source_fps,
+ .update_client_playback_delay = mock_update_client_playback_delay,
+};
+
+static void
+create_video_encoder(const EncoderInfo *encoder)
+{
+ spice_assert(encoder);
+
+ video_encoder = encoder->new_encoder(encoder->coded_type, starting_bit_rate, &rate_control_cbs,
+ (bitmap_ref_t) frame_ref, (bitmap_unref_t) frame_unref);
+ // TODO return not supported error
+ spice_assert(video_encoder);
+}
+
+static void
+frame_ref(TestFrame *frame)
+{
+ g_atomic_int_inc(&frame->refs);
+}
+
+static void
+frame_unref(TestFrame *frame)
+{
+ if (!g_atomic_int_dec_and_test(&frame->refs)) {
+ return;
+ }
+ bitmap_free(frame->bitmap);
+ free(frame);
+}
+
+static void
+bitmap_free(SpiceBitmap *bitmap)
+{
+ if (!bitmap) {
+ return;
+ }
+ spice_assert(!bitmap->palette);
+ spice_assert(bitmap->data);
+ spice_chunks_destroy(bitmap->data);
+ free(bitmap);
+}
+
+static SpiceChunks* chunks_alloc(uint32_t stride, uint32_t height, uint32_t split);
+static uint8_t *bitmap_get_line(SpiceBitmap *bitmap, int y);
+static uint32_t compute_stride(int width, SpiceBitmapFmt format);
+typedef void convert_line_t(uint8_t *dest, const uint8_t *src, uint32_t width);
+static convert_line_t convert_line16;
+static convert_line_t convert_line24;
+static convert_line_t convert_line32;
+static convert_line_t *get_convert_line(SpiceBitmapFmt format);
+
+static SpiceBitmap *
+gst_to_spice_bitmap(GstSample *sample)
+{
+ GstCaps *caps = gst_sample_get_caps(sample);
+ spice_assert(caps);
+
+ GstStructure *s = gst_caps_get_structure(caps, 0);
+ spice_assert(s);
+
+ gint width, height;
+ spice_assert(gst_structure_get_int(s, "width", &width) &&
+ gst_structure_get_int(s, "height", &height));
+
+ SpiceBitmap *bitmap = spice_new0(SpiceBitmap, 1);
+ bitmap->format = bitmap_format;
+ bitmap->flags = top_down ? SPICE_BITMAP_FLAGS_TOP_DOWN : 0;
+ bitmap->x = width;
+ bitmap->y = height;
+ bitmap->stride = compute_stride(width, bitmap->format);
+ bitmap->data = chunks_alloc(bitmap->stride, height, image_split_lines);
+
+ GstBuffer *buffer = gst_sample_get_buffer(sample);
+ GstMapInfo mapinfo;
+ if (!gst_buffer_map(buffer, &mapinfo, GST_MAP_READ)) {
+ spice_error("GStreamer error: could not map the buffer");
+ }
+
+ // convert image
+ gint y;
+ convert_line_t *convert_line = get_convert_line(bitmap->format);
+ for (y = 0; y < height; ++y) {
+ convert_line(bitmap_get_line(bitmap, y),
+ mapinfo.data + y * width * 4,
+ width);
+ }
+ gst_buffer_unmap(buffer, &mapinfo);
+ // TODO should we unref buffer ??
+
+ return bitmap;
+}
+
+static uint32_t
+compute_stride(int width, SpiceBitmapFmt format)
+{
+ spice_assert(width > 0);
+
+ switch (format) {
+ case SPICE_BITMAP_FMT_16BIT:
+ return width * 2;
+ case SPICE_BITMAP_FMT_24BIT:
+ return width * 3;
+ case SPICE_BITMAP_FMT_32BIT:
+ case SPICE_BITMAP_FMT_RGBA:
+ return width * 4;
+ default:
+ break;
+ }
+ spice_assert(0);
+ return 0;
+}
+
+static SpiceChunks*
+chunks_alloc(uint32_t stride, uint32_t height, uint32_t split)
+{
+ spice_assert(stride && height && split);
+ const uint32_t num_chunks = (height + split - 1u) / split;
+ SpiceChunks *chunks = spice_malloc0(sizeof(SpiceChunks) + sizeof(SpiceChunk) * num_chunks);
+
+ chunks->data_size = stride * height;
+ chunks->num_chunks = num_chunks;
+ chunks->flags = SPICE_CHUNKS_FLAGS_FREE;
+ unsigned n;
+ uint32_t allocated = 0;
+ for (n = 0; n < num_chunks; ++n) {
+ SpiceChunk *chunk = &chunks->chunk[n];
+ uint32_t len = stride * split;
+ spice_assert(chunks->data_size > allocated);
+ len = MIN(len, chunks->data_size - allocated);
+ chunk->data = spice_malloc0(len);
+ chunk->len = len;
+ allocated += len;
+ }
+ spice_assert(chunks->data_size == allocated);
+ return chunks;
+}
+
+static uint8_t *
+bitmap_get_line(SpiceBitmap *bitmap, int y)
+{
+ spice_assert(bitmap && y >= 0 && y < bitmap->y);
+ if (!(bitmap->flags & SPICE_BITMAP_FLAGS_TOP_DOWN)) {
+ y = bitmap->y - y - 1;
+ }
+ const uint32_t stride = bitmap->stride;
+ uint32_t pos = stride * y;
+ SpiceChunk *chunk = &bitmap->data->chunk[0];
+ while (pos >= chunk->len) {
+ pos -= chunk->len;
+ ++chunk;
+ spice_assert(chunk < &bitmap->data->chunk[bitmap->data->num_chunks]);
+ }
+ spice_assert(pos + stride <= chunk->len);
+ return chunk->data + pos;
+}
+
+static convert_line_t *
+get_convert_line(SpiceBitmapFmt format)
+{
+ switch (format) {
+ case SPICE_BITMAP_FMT_16BIT:
+ return convert_line16;
+ case SPICE_BITMAP_FMT_24BIT:
+ return convert_line24;
+ case SPICE_BITMAP_FMT_32BIT:
+ case SPICE_BITMAP_FMT_RGBA:
+ return convert_line32;
+ default:
+ break;
+ }
+ spice_assert(0);
+ return 0;
+}
+
+static void
+convert_line16(uint8_t *dest, const uint8_t *src, uint32_t width)
+{
+ uint16_t *dest16 = (uint16_t *) dest;
+ for (; width; --width) {
+ *dest16++ = (src[0] >> 3) | ((src[1] & 0xf8) << 2) | ((src[2] & 0xf8) << 7);
+ src += 4;
+ }
+}
+
+static void
+convert_line24(uint8_t *dest, const uint8_t *src, uint32_t width)
+{
+ for (; width; --width) {
+ *dest++ = *src++;
+ *dest++ = *src++;
+ *dest++ = *src++;
+ ++src;
+ }
+}
+
+static void
+convert_line32(uint8_t *dest, const uint8_t *src, uint32_t width)
+{
+ for (; width; --width) {
+ *dest++ = *src++;
+ *dest++ = *src++;
+ *dest++ = *src++;
+ *dest++ = 0;
+ ++src;
+ }
+}
+
+static SpiceBitmapFmt
+get_bitmap_format(const char *format)
+{
+ if (strcmp(format, "32BIT") == 0) {
+ return SPICE_BITMAP_FMT_32BIT;
+ } else if (strcmp(format, "24BIT") == 0) {
+ return SPICE_BITMAP_FMT_24BIT;
+ } else if (strcmp(format, "16BIT") == 0) {
+ return SPICE_BITMAP_FMT_16BIT;
+ } else if (strcmp(format, "RGBA") == 0) {
+ return SPICE_BITMAP_FMT_RGBA;
+ }
+ return SPICE_BITMAP_FMT_INVALID;
+}
+
+static TestFrame *
+gst_to_spice_frame(GstSample *sample)
+{
+ TestFrame *frame = spice_new0(TestFrame, 1);
+ frame->refs = 1;
+ frame->bitmap = gst_to_spice_bitmap(sample);
+ return frame;
+}
+
+static uint32_t
+line_diff_rgb(const uint8_t *pixel1, const uint8_t *pixel2, uint32_t w)
+{
+ uint32_t diff_sum = 0;
+ for (w *= 3; w; --w) {
+ int diff = *pixel1 - *pixel2;
+ diff_sum += diff * diff;
+ ++pixel1;
+ ++pixel2;
+ }
+ return diff_sum;
+}
+
+typedef uint8_t *bitmap_extract_rgb_line_t(SpiceBitmap *bitmap, uint8_t *buf,
+ int32_t x, int32_t y, int32_t w);
+static bitmap_extract_rgb_line_t *get_bitmap_extract(SpiceBitmapFmt format);
+static bitmap_extract_rgb_line_t bitmap_extract16;
+static bitmap_extract_rgb_line_t bitmap_extract24;
+static bitmap_extract_rgb_line_t bitmap_extract32;
+
+// compute PSNR
+// see https://en.wikipedia.org/wiki/Peak_signal-to-noise_ratio
+// higher is better (less data loosed)
+// typical are 30-50
+static double
+compute_psnr(SpiceBitmap *bitmap1, int32_t x1, int32_t y1,
+ SpiceBitmap *bitmap2, int32_t x2, int32_t y2,
+ int32_t w, int32_t h)
+{
+ spice_assert(x1 >= 0 && y1 >= 0);
+ spice_assert(x2 >= 0 && y2 >= 0);
+ spice_assert(w > 0 && h > 0);
+ spice_assert(x1 + w <= bitmap1->x);
+ spice_assert(y1 + h <= bitmap1->y);
+ spice_assert(x2 + w <= bitmap2->x);
+ spice_assert(y2 + h <= bitmap2->y);
+
+ int y;
+ uint64_t diff_sum = 0;
+ uint8_t pixels[2][w*3];
+ bitmap_extract_rgb_line_t *extract1 = get_bitmap_extract(bitmap1->format);
+ bitmap_extract_rgb_line_t *extract2 = get_bitmap_extract(bitmap2->format);
+ for (y = 0; y < h; ++y) {
+ uint8_t *line1 = extract1(bitmap1, pixels[0], x1, y1 + y, w);
+ uint8_t *line2 = extract2(bitmap2, pixels[1], x2, y2 + y, w);
+ diff_sum += line_diff_rgb(line1, line2, w);
+ }
+
+ double mse = (double) diff_sum / (w*h*3);
+ double psnr = 10 * log10(255*255/mse);
+
+ return psnr;
+}
+
+static bitmap_extract_rgb_line_t *
+get_bitmap_extract(SpiceBitmapFmt format)
+{
+ switch (format) {
+ case SPICE_BITMAP_FMT_16BIT:
+ return bitmap_extract16;
+ case SPICE_BITMAP_FMT_24BIT:
+ return bitmap_extract24;
+ case SPICE_BITMAP_FMT_32BIT:
+ case SPICE_BITMAP_FMT_RGBA:
+ return bitmap_extract32;
+ default:
+ break;
+ }
+ spice_assert(0);
+ return 0;
+}
+
+static uint8_t *
+bitmap_extract24(SpiceBitmap *bitmap, uint8_t *buf, int32_t x, int32_t y, int32_t w)
+{
+ uint8_t *line = bitmap_get_line(bitmap, y) + x * 3;
+ return line;
+}
+
+static uint8_t *
+bitmap_extract32(SpiceBitmap *bitmap, uint8_t *buf, int32_t x, int32_t y, int32_t w)
+{
+ const uint8_t *line = bitmap_get_line(bitmap, y) + x * 4;
+ uint8_t *dest = buf;
+ for (; w; --w) {
+ *dest++ = *line++;
+ *dest++ = *line++;
+ *dest++ = *line++;
+ ++line;
+ }
+ return buf;
+}
+
+static uint8_t *
+bitmap_extract16(SpiceBitmap *bitmap, uint8_t *buf, int32_t x, int32_t y, int32_t w)
+{
+ const uint16_t *line = (const uint16_t *)(bitmap_get_line(bitmap, y) + x * 2);
+ uint8_t *dest = buf;
+ for (; w; --w) {
+ uint16_t pixel = *line++;
+ uint8_t comp;
+ comp = (pixel >> 0) & 0x1f;
+ *dest++ = (comp << 3) | (comp >> 2);
+ comp = (pixel >> 5) & 0x1f;
+ *dest++ = (comp << 3) | (comp >> 2);
+ comp = (pixel >> 10) & 0x1f;
+ *dest++ = (comp << 3) | (comp >> 2);
+ }
+ return buf;
+}
More information about the Spice-commits
mailing list