From 497fcbb0a315b034bae4a565ec550a7cba058e3c Mon Sep 17 00:00:00 2001 From: Francois Gouget Date: Mon, 11 May 2015 16:51:35 +0200 Subject: [PATCH] streaming: Let the administrator pick the video encoder and codec 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 --- 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 | 162 ++++++++++++++++++++++++++++++++----- 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, 254 insertions(+), 32 deletions(-) diff --git a/server/dcc-send.c b/server/dcc-send.c index 9c5d970e..f8087eaa 100644 --- a/server/dcc-send.c +++ b/server/dcc-send.c @@ -2166,7 +2166,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 76d22f95..6dce8316 100644 --- a/server/display-channel.c +++ b/server/display-channel.c @@ -140,6 +140,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; @@ -1890,6 +1898,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; @@ -1935,6 +1944,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 0baad62e..7984c85d 100644 --- a/server/display-channel.h +++ b/server/display-channel.h @@ -183,6 +183,7 @@ struct DisplayChannel { _Drawable *free_drawables; int stream_video; + GArray *video_codecs; uint32_t stream_count; Stream streams_buf[NUM_STREAMS]; Stream *free_streams; @@ -237,6 +238,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, @@ -258,6 +260,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 6f748fb3..0e4dc8dd 100644 --- a/server/gstreamer-encoder.c +++ b/server/gstreamer-encoder.c @@ -524,9 +524,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); @@ -541,6 +544,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 57708cd6..5c143a6c 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 47fafab5..b84ae697 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 c9b6b363..00c5486f 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 aba3e6f0..b38d7c78 100644 --- a/server/red-worker.c +++ b/server/red-worker.c @@ -1059,6 +1059,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; @@ -1331,6 +1340,11 @@ static void register_callbacks(Dispatcher *dispatcher) handle_dev_set_streaming_video, 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, @@ -1516,7 +1530,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 27a5957d..4c8826ee 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); @@ -3489,6 +3492,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) @@ -3510,6 +3514,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; @@ -3521,39 +3526,131 @@ 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) { - const RendererInfo *inf = renderers_info; - while (inf->name) { - if (strcmp(name, inf->name) == 0) { - return inf; - } - inf++; - } - return NULL; -} + uint32_t index; -static int reds_add_renderer(RedsState *reds, const char *name) -{ - const RendererInfo *inf; - - if (reds->config->renderers->len == RED_RENDERER_LAST || !(inf = find_renderer(name))) { + if (reds->config->renderers->len == RED_RENDERER_LAST || + !get_name_index(renderer_names, name, &index)) { return FALSE; } - g_array_append_val(reds->config->renderers, inf->id); + 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) +{ + 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++; + } + return codecs; + } + return codecs + n; +} + +static void reds_set_video_codecs(RedsState *reds, const char *codecs) +{ + char *encoder_name, *codec_name; + + 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; + } +} + SPICE_GNUC_VISIBLE int spice_server_init(SpiceServer *reds, SpiceCoreInterface *core) { int ret; @@ -3562,12 +3659,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); @@ -3869,6 +3970,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; @@ -4261,6 +4374,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 1f050816..cd62fc10 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 87c5c59d..6eb1b1d0 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 5c3e53c8..edf04a42 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 617e33a0..3d3e6276 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 708432b7..7a04bc6c 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