[Spice-devel,v16,07/23] streaming: Let the administrator pick the video encoder and codec

Submitted by Francois Gouget on June 7, 2016, 1:59 p.m.

Details

Message ID c1a823aa263325e0c6fd77f5222d32e2f93eb2e7.1465306176.git.fgouget@free.fr
State New
Headers show
Series "Add GStreamer support for video streaming" ( rev: 22 21 20 19 18 ) in Spice

Not browsing as part of any series.

Commit Message

Francois Gouget June 7, 2016, 1:59 p.m.
The Spice server administrator can specify the encoder and codec
preferences to optimize for CPU or bandwidth usage. Preferences are
described in a semi-colon separated list of encoder:codec pairs.
The server has a default preference list which can explicitly be
selected by specifying 'auto'.

Signed-off-by: Francois Gouget <fgouget@codeweavers.com>
---
 server/dcc-send.c          |   2 +-
 server/display-channel.c   |  10 +++
 server/display-channel.h   |   4 ++
 server/gstreamer-encoder.c |   6 +-
 server/mjpeg-encoder.c     |   6 +-
 server/red-qxl.c           |   9 +++
 server/red-qxl.h           |   6 ++
 server/red-worker.c        |  18 +++++-
 server/reds.c              | 156 ++++++++++++++++++++++++++++++++++++++++-----
 server/reds.h              |   1 +
 server/spice-server.h      |   8 +++
 server/spice-server.syms   |   5 ++
 server/stream.c            |  29 +++++++--
 server/video-encoder.h     |  20 +++++-
 14 files changed, 251 insertions(+), 29 deletions(-)

Patch hide | download patch | download mbox

diff --git a/server/dcc-send.c b/server/dcc-send.c
index a0443f0..497f879 100644
--- a/server/dcc-send.c
+++ b/server/dcc-send.c
@@ -2146,7 +2146,7 @@  static void marshall_stream_start(RedChannelClient *rcc,
     stream_create.surface_id = 0;
     stream_create.id = get_stream_id(DCC_TO_DC(dcc), stream);
     stream_create.flags = stream->top_down ? SPICE_STREAM_FLAGS_TOP_DOWN : 0;
-    stream_create.codec_type = SPICE_VIDEO_CODEC_TYPE_MJPEG;
+    stream_create.codec_type = agent->video_encoder->codec_type;
 
     stream_create.src_width = stream->width;
     stream_create.src_height = stream->height;
diff --git a/server/display-channel.c b/server/display-channel.c
index 0ba4064..00c0bfd 100644
--- a/server/display-channel.c
+++ b/server/display-channel.c
@@ -199,6 +199,14 @@  void display_channel_set_stream_video(DisplayChannel *display, int stream_video)
     display->stream_video = stream_video;
 }
 
+void display_channel_set_video_codecs(DisplayChannel *display, GArray *video_codecs)
+{
+    spice_return_if_fail(display);
+
+    g_array_unref(display->video_codecs);
+    display->video_codecs = g_array_ref(video_codecs);
+}
+
 static void stop_streams(DisplayChannel *display)
 {
     Ring *ring = &display->streams;
@@ -1949,6 +1957,7 @@  static SpiceCanvas *image_surfaces_get(SpiceImageSurfaces *surfaces, uint32_t su
 
 DisplayChannel* display_channel_new(SpiceServer *reds, RedWorker *worker, 
                                     int migrate, int stream_video,
+                                    GArray *video_codecs,
                                     uint32_t n_surfaces)
 {
     DisplayChannel *display;
@@ -2000,6 +2009,7 @@  DisplayChannel* display_channel_new(SpiceServer *reds, RedWorker *worker,
     drawables_init(display);
     image_cache_init(&display->image_cache);
     display->stream_video = stream_video;
+    display->video_codecs = g_array_ref(video_codecs);
     display_channel_init_streams(display);
 
     return display;
diff --git a/server/display-channel.h b/server/display-channel.h
index 5891e94..7a4067c 100644
--- a/server/display-channel.h
+++ b/server/display-channel.h
@@ -185,6 +185,7 @@  struct DisplayChannel {
     uint32_t glz_drawable_count;
 
     int stream_video;
+    GArray *video_codecs;
     uint32_t stream_count;
     Stream streams_buf[NUM_STREAMS];
     Stream *free_streams;
@@ -246,6 +247,7 @@  DisplayChannel*            display_channel_new                       (SpiceServe
                                                                       RedWorker *worker,
                                                                       int migrate,
                                                                       int stream_video,
+                                                                      GArray *video_codecs,
                                                                       uint32_t n_surfaces);
 void                       display_channel_create_surface            (DisplayChannel *display, uint32_t surface_id,
                                                                       uint32_t width, uint32_t height,
@@ -267,6 +269,8 @@  void                       display_channel_update                    (DisplayCha
 void                       display_channel_free_some                 (DisplayChannel *display);
 void                       display_channel_set_stream_video          (DisplayChannel *display,
                                                                       int stream_video);
+void                       display_channel_set_video_codecs          (DisplayChannel *display,
+                                                                      GArray *video_codecs);
 int                        display_channel_get_streams_timeout       (DisplayChannel *display);
 void                       display_channel_compress_stats_print      (const DisplayChannel *display);
 void                       display_channel_compress_stats_reset      (DisplayChannel *display);
diff --git a/server/gstreamer-encoder.c b/server/gstreamer-encoder.c
index c3db28f..67a757f 100644
--- a/server/gstreamer-encoder.c
+++ b/server/gstreamer-encoder.c
@@ -525,9 +525,12 @@  static void spice_gst_encoder_get_stats(VideoEncoder *video_encoder,
     }
 }
 
-VideoEncoder *gstreamer_encoder_new(uint64_t starting_bit_rate,
+VideoEncoder *gstreamer_encoder_new(SpiceVideoCodecType codec_type,
+                                    uint64_t starting_bit_rate,
                                     VideoEncoderRateControlCbs *cbs)
 {
+    spice_return_val_if_fail(codec_type == SPICE_VIDEO_CODEC_TYPE_MJPEG, NULL);
+
     GError *err = NULL;
     if (!gst_init_check(NULL, NULL, &err)) {
         spice_warning("GStreamer error: %s", err->message);
@@ -542,6 +545,7 @@  VideoEncoder *gstreamer_encoder_new(uint64_t starting_bit_rate,
     encoder->base.notify_server_frame_drop = spice_gst_encoder_notify_server_frame_drop;
     encoder->base.get_bit_rate = spice_gst_encoder_get_bit_rate;
     encoder->base.get_stats = spice_gst_encoder_get_stats;
+    encoder->base.codec_type = codec_type;
 
     if (cbs) {
         encoder->cbs = *cbs;
diff --git a/server/mjpeg-encoder.c b/server/mjpeg-encoder.c
index 57708cd..5c143a6 100644
--- a/server/mjpeg-encoder.c
+++ b/server/mjpeg-encoder.c
@@ -1341,17 +1341,21 @@  static void mjpeg_encoder_get_stats(VideoEncoder *video_encoder,
     stats->avg_quality = (double)encoder->avg_quality / encoder->num_frames;
 }
 
-VideoEncoder *mjpeg_encoder_new(uint64_t starting_bit_rate,
+VideoEncoder *mjpeg_encoder_new(SpiceVideoCodecType codec_type,
+                                uint64_t starting_bit_rate,
                                 VideoEncoderRateControlCbs *cbs)
 {
     MJpegEncoder *encoder = spice_new0(MJpegEncoder, 1);
 
+    spice_return_val_if_fail(codec_type == SPICE_VIDEO_CODEC_TYPE_MJPEG, NULL);
+
     encoder->base.destroy = mjpeg_encoder_destroy;
     encoder->base.encode_frame = mjpeg_encoder_encode_frame;
     encoder->base.client_stream_report = mjpeg_encoder_client_stream_report;
     encoder->base.notify_server_frame_drop = mjpeg_encoder_notify_server_frame_drop;
     encoder->base.get_bit_rate = mjpeg_encoder_get_bit_rate;
     encoder->base.get_stats = mjpeg_encoder_get_stats;
+    encoder->base.codec_type = codec_type;
     encoder->first_frame = TRUE;
     encoder->rate_control.byte_rate = starting_bit_rate / 8;
     encoder->starting_bit_rate = starting_bit_rate;
diff --git a/server/red-qxl.c b/server/red-qxl.c
index 47fafab..b84ae69 100644
--- a/server/red-qxl.c
+++ b/server/red-qxl.c
@@ -1045,6 +1045,15 @@  void red_qxl_on_sv_change(QXLInstance *qxl, int sv)
                             &payload);
 }
 
+void red_qxl_on_vc_change(QXLInstance *qxl, GArray *video_codecs)
+{
+    RedWorkerMessageSetVideoCodecs payload;
+    payload.video_codecs = g_array_ref(video_codecs);
+    dispatcher_send_message(qxl->st->dispatcher,
+                            RED_WORKER_MESSAGE_SET_VIDEO_CODECS,
+                            &payload);
+}
+
 void red_qxl_set_mouse_mode(QXLInstance *qxl, uint32_t mode)
 {
     RedWorkerMessageSetMouseMode payload;
diff --git a/server/red-qxl.h b/server/red-qxl.h
index c9b6b36..00c5486 100644
--- a/server/red-qxl.h
+++ b/server/red-qxl.h
@@ -27,6 +27,7 @@  void red_qxl_init(SpiceServer *reds, QXLInstance *qxl);
 
 void red_qxl_on_ic_change(QXLInstance *qxl, SpiceImageCompression ic);
 void red_qxl_on_sv_change(QXLInstance *qxl, int sv);
+void red_qxl_on_vc_change(QXLInstance *qxl, GArray* video_codecs);
 void red_qxl_set_mouse_mode(QXLInstance *qxl, uint32_t mode);
 void red_qxl_attach_worker(QXLInstance *qxl);
 void red_qxl_set_compression_level(QXLInstance *qxl, int level);
@@ -113,6 +114,7 @@  enum {
     RED_WORKER_MESSAGE_DRIVER_UNLOAD,
     RED_WORKER_MESSAGE_GL_SCANOUT,
     RED_WORKER_MESSAGE_GL_DRAW_ASYNC,
+    RED_WORKER_MESSAGE_SET_VIDEO_CODECS,
 
     RED_WORKER_MESSAGE_COUNT // LAST
 };
@@ -250,6 +252,10 @@  typedef struct RedWorkerMessageSetStreamingVideo {
     uint32_t streaming_video;
 } RedWorkerMessageSetStreamingVideo;
 
+typedef struct RedWorkerMessageSetVideoCodecs {
+    GArray* video_codecs;
+} RedWorkerMessageSetVideoCodecs;
+
 typedef struct RedWorkerMessageSetMouseMode {
     uint32_t mode;
 } RedWorkerMessageSetMouseMode;
diff --git a/server/red-worker.c b/server/red-worker.c
index e754bd2..55dd171 100644
--- a/server/red-worker.c
+++ b/server/red-worker.c
@@ -1052,6 +1052,15 @@  static void handle_dev_set_streaming_video(void *opaque, void *payload)
     display_channel_set_stream_video(worker->display_channel, msg->streaming_video);
 }
 
+void handle_dev_set_video_codecs(void *opaque, void *payload)
+{
+    RedWorkerMessageSetVideoCodecs *msg = payload;
+    RedWorker *worker = opaque;
+
+    display_channel_set_video_codecs(worker->display_channel, msg->video_codecs);
+    g_array_unref(msg->video_codecs);
+}
+
 static void handle_dev_set_mouse_mode(void *opaque, void *payload)
 {
     RedWorkerMessageSetMouseMode *msg = payload;
@@ -1325,6 +1334,11 @@  static void register_callbacks(Dispatcher *dispatcher)
                                 sizeof(RedWorkerMessageSetStreamingVideo),
                                 DISPATCHER_NONE);
     dispatcher_register_handler(dispatcher,
+                                RED_WORKER_MESSAGE_SET_VIDEO_CODECS,
+                                handle_dev_set_video_codecs,
+                                sizeof(RedWorkerMessageSetVideoCodecs),
+                                DISPATCHER_NONE);
+    dispatcher_register_handler(dispatcher,
                                 RED_WORKER_MESSAGE_SET_MOUSE_MODE,
                                 handle_dev_set_mouse_mode,
                                 sizeof(RedWorkerMessageSetMouseMode),
@@ -1509,7 +1523,9 @@  RedWorker* red_worker_new(QXLInstance *qxl,
     reds_register_channel(reds, channel);
 
     // TODO: handle seemless migration. Temp, setting migrate to FALSE
-    worker->display_channel = display_channel_new(reds, worker, FALSE, reds_get_streaming_video(reds),
+    worker->display_channel = display_channel_new(reds, worker, FALSE,
+                                                  reds_get_streaming_video(reds),
+                                                  reds_get_video_codecs(reds),
                                                   init_info.n_surfaces);
 
     channel = RED_CHANNEL(worker->display_channel);
diff --git a/server/reds.c b/server/reds.c
index 4fd1d35..43fd5c3 100644
--- a/server/reds.c
+++ b/server/reds.c
@@ -71,6 +71,7 @@ 
 #include "utils.h"
 
 #include "reds-private.h"
+#include "video-encoder.h"
 
 static void reds_client_monitors_config(RedsState *reds, VDAgentMonitorsConfig *monitors_config);
 static gboolean reds_use_client_monitors_config(RedsState *reds);
@@ -178,6 +179,7 @@  struct RedServerConfig {
 
     gboolean ticketing_enabled;
     uint32_t streaming_video;
+    GArray* video_codecs;
     SpiceImageCompression image_compression;
     uint32_t playback_compression;
     spice_wan_compression_t jpeg_state;
@@ -298,6 +300,7 @@  static void reds_add_char_device(RedsState *reds, RedCharDevice *dev);
 static void reds_send_mm_time(RedsState *reds);
 static void reds_on_ic_change(RedsState *reds);
 static void reds_on_sv_change(RedsState *reds);
+static void reds_on_vc_change(RedsState *reds);
 static void reds_on_vm_stop(RedsState *reds);
 static void reds_on_vm_start(RedsState *reds);
 static void reds_set_mouse_mode(RedsState *reds, uint32_t mode);
@@ -3496,6 +3499,7 @@  err:
 }
 
 static const char default_renderer[] = "sw";
+static const char default_video_codecs[] = "spice:mjpeg;gstreamer:mjpeg";
 
 /* new interface */
 SPICE_GNUC_VISIBLE SpiceServer *spice_server_new(void)
@@ -3517,6 +3521,7 @@  SPICE_GNUC_VISIBLE SpiceServer *spice_server_new(void)
     memset(reds->config->spice_uuid, 0, sizeof(reds->config->spice_uuid));
     reds->config->ticketing_enabled = TRUE; /* ticketing enabled by default */
     reds->config->streaming_video = SPICE_STREAM_VIDEO_FILTER;
+    reds->config->video_codecs = g_array_new(FALSE, FALSE, sizeof(RedVideoCodec));
     reds->config->image_compression = SPICE_IMAGE_COMPRESSION_AUTO_GLZ;
     reds->config->playback_compression = TRUE;
     reds->config->jpeg_state = SPICE_WAN_COMPRESSION_AUTO;
@@ -3528,37 +3533,129 @@  SPICE_GNUC_VISIBLE SpiceServer *spice_server_new(void)
     return reds;
 }
 
-typedef struct RendererInfo {
-    int id;
+typedef struct {
+    uint32_t id;
     const char *name;
-} RendererInfo;
+} EnumNames;
 
-static const RendererInfo renderers_info[] = {
+static gboolean get_name_index(const EnumNames names[], const char *name, uint32_t *index)
+{
+    if (name) {
+        int i;
+        for (i = 0; names[i].name; i++) {
+            if (strcmp(name, names[i].name) == 0) {
+                *index = i;
+                return TRUE;
+            }
+        }
+    }
+    return FALSE;
+}
+
+static const EnumNames renderer_names[] = {
     {RED_RENDERER_SW, "sw"},
     {RED_RENDERER_INVALID, NULL},
 };
 
-static const RendererInfo *find_renderer(const char *name)
+static gboolean reds_add_renderer(RedsState *reds, const char *name)
+{
+    uint32_t index;
+
+    if (reds->config->renderers->len == RED_RENDERER_LAST ||
+        !get_name_index(renderer_names, name, &index)) {
+        return FALSE;
+    }
+    g_array_append_val(reds->config->renderers, renderer_names[index].id);
+    return TRUE;
+}
+
+static const EnumNames video_encoder_names[] = {
+    {0, "spice"},
+    {1, "gstreamer"},
+    {0, NULL},
+};
+
+static new_video_encoder_t video_encoder_procs[] = {
+    &mjpeg_encoder_new,
+#ifdef HAVE_GSTREAMER_1_0
+    &gstreamer_encoder_new,
+#else
+    NULL,
+#endif
+};
+
+static const EnumNames video_codec_names[] = {
+    {SPICE_VIDEO_CODEC_TYPE_MJPEG, "mjpeg"},
+    {0, NULL},
+};
+
+static int video_codec_caps[] = {
+    SPICE_DISPLAY_CAP_CODEC_MJPEG,
+};
+
+
+/* Expected string:  encoder:codec;encoder:codec */
+static const char* parse_video_codecs(const char *codecs, char **encoder,
+                                      char **codec)
 {
-    const RendererInfo *inf = renderers_info;
-    while (inf->name) {
-        if (strcmp(name, inf->name) == 0) {
-            return inf;
+    if (!codecs) {
+        return NULL;
+    }
+    while (*codecs == ';') {
+        codecs++;
+    }
+    if (!*codecs) {
+        return NULL;
+    }
+    int n;
+    *encoder = *codec = NULL;
+    if (sscanf(codecs, "%m[0-9a-zA-Z_]:%m[0-9a-zA-Z_]%n", encoder, codec, &n) != 2) {
+        while (*codecs != '\0' && *codecs != ';') {
+            codecs++;
         }
-        inf++;
+        return codecs;
     }
-    return NULL;
+    return codecs + n;
 }
 
-static int reds_add_renderer(RedsState *reds, const char *name)
+static void reds_set_video_codecs(RedsState *reds, const char *codecs)
 {
-    const RendererInfo *inf;
+    char *encoder_name, *codec_name;
 
-    if (reds->config->renderers->len == RED_RENDERER_LAST || !(inf = find_renderer(name))) {
-        return FALSE;
+    if (strcmp(codecs, "auto") == 0) {
+        codecs = default_video_codecs;
+    }
+
+    /* The video_codecs array is immutable */
+    g_array_unref(reds->config->video_codecs);
+    reds->config->video_codecs = g_array_new(FALSE, FALSE, sizeof(RedVideoCodec));
+    const char *c = codecs;
+    while ( (c = parse_video_codecs(c, &encoder_name, &codec_name)) ) {
+        uint32_t encoder_index, codec_index;
+        if (!encoder_name || !codec_name) {
+            spice_warning("spice: invalid encoder:codec value at %s", codecs);
+
+        } else if (!get_name_index(video_encoder_names, encoder_name, &encoder_index)){
+            spice_warning("spice: unknown video encoder %s", encoder_name);
+
+        } else if (!get_name_index(video_codec_names, codec_name, &codec_index)) {
+            spice_warning("spice: unknown video codec %s", codec_name);
+
+        } else if (!video_encoder_procs[encoder_index]) {
+            spice_warning("spice: unsupported video encoder %s", encoder_name);
+
+        } else {
+            RedVideoCodec new_codec;
+            new_codec.create = video_encoder_procs[encoder_index];
+            new_codec.type = video_codec_names[codec_index].id;
+            new_codec.cap = video_codec_caps[codec_index];
+            g_array_append_val(reds->config->video_codecs, new_codec);
+        }
+
+        free(encoder_name);
+        free(codec_name);
+        codecs = c;
     }
-    g_array_append_val(reds->config->renderers, inf->id);
-    return TRUE;
 }
 
 SPICE_GNUC_VISIBLE int spice_server_init(SpiceServer *reds, SpiceCoreInterface *core)
@@ -3569,12 +3666,16 @@  SPICE_GNUC_VISIBLE int spice_server_init(SpiceServer *reds, SpiceCoreInterface *
     if (reds->config->renderers->len == 0) {
         reds_add_renderer(reds, default_renderer);
     }
+    if (reds->config->video_codecs->len == 0) {
+        reds_set_video_codecs(reds, default_video_codecs);
+    }
     return ret;
 }
 
 SPICE_GNUC_VISIBLE void spice_server_destroy(SpiceServer *reds)
 {
     g_array_unref(reds->config->renderers);
+    g_array_unref(reds->config->video_codecs);
     free(reds->config);
     if (reds->main_channel) {
         main_channel_close(reds->main_channel);
@@ -3876,6 +3977,18 @@  uint32_t reds_get_streaming_video(const RedsState *reds)
     return reds->config->streaming_video;
 }
 
+SPICE_GNUC_VISIBLE int spice_server_set_video_codecs(SpiceServer *reds, const char *video_codecs)
+{
+    reds_set_video_codecs(reds, video_codecs);
+    reds_on_vc_change(reds);
+    return 0;
+}
+
+GArray* reds_get_video_codecs(const RedsState *reds)
+{
+    return reds->config->video_codecs;
+}
+
 SPICE_GNUC_VISIBLE int spice_server_set_playback_compression(SpiceServer *reds, int enable)
 {
     reds->config->playback_compression = !!enable;
@@ -4268,6 +4381,15 @@  void reds_on_sv_change(RedsState *reds)
     }
 }
 
+void reds_on_vc_change(RedsState *reds)
+{
+    GList *l;
+
+    for (l = reds->qxl_instances; l != NULL; l = l->next) {
+        red_qxl_on_vc_change(l->data, reds_get_video_codecs(reds));
+    }
+}
+
 void reds_on_vm_stop(RedsState *reds)
 {
     GList *l;
diff --git a/server/reds.h b/server/reds.h
index 1f05081..cd62fc1 100644
--- a/server/reds.h
+++ b/server/reds.h
@@ -93,6 +93,7 @@  void reds_on_main_channel_migrate(RedsState *reds, MainChannelClient *mcc);
 
 void reds_set_client_mm_time_latency(RedsState *reds, RedClient *client, uint32_t latency);
 uint32_t reds_get_streaming_video(const RedsState *reds);
+GArray* reds_get_video_codecs(const RedsState *reds);
 spice_wan_compression_t reds_get_jpeg_state(const RedsState *reds);
 spice_wan_compression_t reds_get_zlib_glz_state(const RedsState *reds);
 SpiceCoreInterfaceInternal* reds_get_core_interface(RedsState *reds);
diff --git a/server/spice-server.h b/server/spice-server.h
index 87c5c59..6eb1b1d 100644
--- a/server/spice-server.h
+++ b/server/spice-server.h
@@ -114,6 +114,14 @@  enum {
 };
 
 int spice_server_set_streaming_video(SpiceServer *s, int value);
+
+enum {
+    SPICE_STREAMING_INVALID,
+    SPICE_STREAMING_SPICE,
+    SPICE_STREAMING_GSTREAMER
+};
+
+int spice_server_set_video_codecs(SpiceServer *s, const char* video_codecs);
 int spice_server_set_playback_compression(SpiceServer *s, int enable);
 int spice_server_set_agent_mouse(SpiceServer *s, int enable);
 int spice_server_set_agent_copypaste(SpiceServer *s, int enable);
diff --git a/server/spice-server.syms b/server/spice-server.syms
index 5c3e53c..edf04a4 100644
--- a/server/spice-server.syms
+++ b/server/spice-server.syms
@@ -168,3 +168,8 @@  global:
     spice_qxl_gl_scanout;
     spice_qxl_gl_draw_async;
 } SPICE_SERVER_0.12.6;
+
+SPICE_SERVER_0.13.2 {
+global:
+    spice_server_set_video_codecs;
+} SPICE_SERVER_0.13.1;
diff --git a/server/stream.c b/server/stream.c
index f16beb8..9f74a6b 100644
--- a/server/stream.c
+++ b/server/stream.c
@@ -711,17 +711,34 @@  static VideoEncoder* dcc_create_video_encoder(DisplayChannelClient *dcc,
                                               uint64_t starting_bit_rate,
                                               VideoEncoderRateControlCbs *cbs)
 {
+    DisplayChannel *display = DCC_TO_DC(dcc);
     RedChannelClient *rcc = RED_CHANNEL_CLIENT(dcc);
     int client_has_multi_codec = red_channel_client_test_remote_cap(rcc, SPICE_DISPLAY_CAP_MULTI_CODEC);
-    if (!client_has_multi_codec || red_channel_client_test_remote_cap(rcc, SPICE_DISPLAY_CAP_CODEC_MJPEG)) {
-#ifdef HAVE_GSTREAMER_1_0
-        VideoEncoder* video_encoder = gstreamer_encoder_new(starting_bit_rate, cbs);
+    int i;
+
+    for (i = 0; i < display->video_codecs->len; i++) {
+        RedVideoCodec* video_codec = &g_array_index (display->video_codecs, RedVideoCodec, i);
+
+        if (!client_has_multi_codec &&
+            video_codec->type != SPICE_VIDEO_CODEC_TYPE_MJPEG) {
+            /* Old clients only support MJPEG */
+            continue;
+        }
+        if (client_has_multi_codec &&
+            !red_channel_client_test_remote_cap(rcc, video_codec->cap)) {
+            /* The client is recent but does not support this codec */
+            continue;
+        }
+
+        VideoEncoder* video_encoder = video_codec->create(video_codec->type, starting_bit_rate, cbs);
         if (video_encoder) {
             return video_encoder;
         }
-#endif
-        /* Use the builtin MJPEG video encoder as a fallback */
-        return mjpeg_encoder_new(starting_bit_rate, cbs);
+    }
+
+    /* Try to use the builtin MJPEG video encoder as a fallback */
+    if (!client_has_multi_codec || red_channel_client_test_remote_cap(rcc, SPICE_DISPLAY_CAP_CODEC_MJPEG)) {
+        return mjpeg_encoder_new(SPICE_VIDEO_CODEC_TYPE_MJPEG, starting_bit_rate, cbs);
     }
 
     return NULL;
diff --git a/server/video-encoder.h b/server/video-encoder.h
index 708432b..7a04bc6 100644
--- a/server/video-encoder.h
+++ b/server/video-encoder.h
@@ -120,6 +120,9 @@  struct VideoEncoder {
      *              statistics.
      */
     void (*get_stats)(VideoEncoder *encoder, VideoEncoderStats *stats);
+
+    /* The codec being used by the video encoder */
+    SpiceVideoCodecType codec_type;
 };
 
 
@@ -152,17 +155,30 @@  typedef struct VideoEncoderRateControlCbs {
 
 /* Instantiates the video encoder.
  *
+ * @codec_type:        The codec to use.
  * @starting_bit_rate: An initial estimate of the available stream bit rate
  *                     or zero if the client does not support rate control.
  * @cbs:               A set of callback methods to be used for rate control.
  * @return:            A pointer to a structure implementing the VideoEncoder
  *                     methods.
  */
-VideoEncoder* mjpeg_encoder_new(uint64_t starting_bit_rate,
+typedef VideoEncoder* (*new_video_encoder_t)(SpiceVideoCodecType codec_type,
+                                             uint64_t starting_bit_rate,
+                                             VideoEncoderRateControlCbs *cbs);
+
+VideoEncoder* mjpeg_encoder_new(SpiceVideoCodecType codec_type,
+                                uint64_t starting_bit_rate,
                                 VideoEncoderRateControlCbs *cbs);
 #ifdef HAVE_GSTREAMER_1_0
-VideoEncoder* gstreamer_encoder_new(uint64_t starting_bit_rate,
+VideoEncoder* gstreamer_encoder_new(SpiceVideoCodecType codec_type,
+                                    uint64_t starting_bit_rate,
                                     VideoEncoderRateControlCbs *cbs);
 #endif
 
+typedef struct RedVideoCodec {
+    new_video_encoder_t create;
+    SpiceVideoCodecType type;
+    uint32_t cap;
+} RedVideoCodec;
+
 #endif