• spice-gtk的spicy命令源码分析


    1、主函数入口
    1. int main(int argc, char *argv[])
    2. {
    3.     GError *error = NULL;
    4.     GOptionContext *context;
    5.     spice_connection *conn;
    6.     gchar *conf_file, *conf;
    7.     char *host = NULL, *port = NULL, *tls_port = NULL, *unix_path = NULL;
    8.     keyfile = g_key_file_new();
    9.     int mode = S_IRWXU;
    10.     conf_file = g_build_filename(g_get_user_config_dir(), "spicy", NULL);
    11.     if (g_mkdir_with_parents(conf_file, mode) == -1)
    12.         SPICE_DEBUG("failed to create config directory");
    13.     g_free(conf_file);
    14.     conf_file = g_build_filename(g_get_user_config_dir(), "spicy", "settings", NULL);
    15.     if (!g_key_file_load_from_file(keyfile, conf_file,
    16.                                    G_KEY_FILE_KEEP_COMMENTS|G_KEY_FILE_KEEP_TRANSLATIONS, &error)) {
    17.         SPICE_DEBUG("Couldn't load configuration: %s", error->message);
    18.         g_clear_error(&error);
    19.     }
    20.     /* parse opts */
    21.     gtk_init(&argc, &argv);
    22.     context = g_option_context_new("- spice client test application");
    23.     g_option_context_set_summary(context, "Gtk+ test client to connect to Spice servers.");
    24.     g_option_context_set_description(context, "Report bugs to " PACKAGE_BUGREPORT ".");
    25.     g_option_context_add_group(context, spice_get_option_group());
    26.     g_option_context_set_main_group(context, spice_cmdline_get_option_group());
    27.     g_option_context_add_main_entries(context, cmd_entries, NULL);
    28.     g_option_context_add_group(context, gtk_get_option_group(TRUE));
    29.     g_option_context_add_group(context, gst_init_get_option_group());
    30.     if (!g_option_context_parse (context, &argc, &argv, &error)) {
    31.         g_print("option parsing failed: %s\n", error->message);
    32.         exit(1);
    33.     }
    34.     g_option_context_free(context);
    35.     if (version) {
    36.         g_print("spicy " PACKAGE_VERSION "\n");
    37.         exit(0);
    38.     }
    39.     mainloop = g_main_loop_new(NULL, false);
    40.     conn = connection_new();
    41.     spice_set_session_option(conn->session);
    42.     spice_cmdline_session_setup(conn->session);
    43.     g_object_get(conn->session,"unix-path", &unix_path,"host", &host,"port", &port, "tls-port", &tls_port,NULL);
    44.     /* If user doesn't provide hostname and port, show the dialog window instead of connecting to server automatically */
    45.     if ((host == NULL || (port == NULL && tls_port == NULL)) && unix_path == NULL) {
    46.         if (!spicy_connect_dialog(conn->session)) {
    47.             exit(0);
    48.         }
    49.     }
    50.     g_free(host);
    51.     g_free(port);
    52.     g_free(tls_port);
    53.     g_free(unix_path);
    54.     connection_connect(conn);
    55.     if (connections > 0)
    56.         g_main_loop_run(mainloop);
    57.     g_main_loop_unref(mainloop);
    58.     if ((conf = g_key_file_to_data(keyfile, NULL, &error)) == NULL ||
    59.         !g_file_set_contents(conf_file, conf, -1, &error)) {
    60.         SPICE_DEBUG("Couldn't save configuration: %s", error->message);
    61.         g_error_free(error);
    62.         error = NULL;
    63.     }
    64.     g_free(conf_file);
    65.     g_free(conf);
    66.     g_key_file_free(keyfile);
    67.     g_free(spicy_title);
    68.     setup_terminal(true);
    69.     gst_deinit();
    70.     return 0;
    71. }

    2、建立spice连接
    1. static void connection_connect(spice_connection *conn)
    2. {
    3.     conn->disconnecting = false;
    4.     spice_session_connect(conn->session);
    5. }

    3、跳转到spice-session.c中,先断开连接,创建主通道,调用主通道建立连接
    1. gboolean spice_session_connect(SpiceSession *session)
    2. {
    3.     SpiceSessionPrivate *s;
    4.     g_return_val_if_fail(SPICE_IS_SESSION(session), FALSE);
    5.     s = session->priv;
    6.     g_return_val_if_fail(!s->disconnecting, FALSE);
    7.     session_disconnect(session, TRUE);
    8.     s->client_provided_sockets = FALSE;
    9.     if (s->cmain == NULL)
    10.         s->cmain = spice_channel_new(session, SPICE_CHANNEL_MAIN, 0);
    11.     glz_decoder_window_clear(s->glz_window);
    12.     return spice_channel_connect(s->cmain);
    13. }

    4、检查是否已经建立连接,调用channel_connect函数建立连接
    1. gboolean spice_channel_connect(SpiceChannel *channel)
    2. {
    3.     g_return_val_if_fail(SPICE_IS_CHANNEL(channel), FALSE);
    4.     SpiceChannelPrivate *c = channel->priv;
    5.     if (c->state >= SPICE_CHANNEL_STATE_CONNECTING)
    6.         return TRUE;
    7.     g_return_val_if_fail(channel->priv->fd == -1, FALSE);
    8.     return channel_connect(channel, FALSE);
    9. }

    5、是否通过TLS建立连接,主线程空闲时通过connect_delayed函数去建立连接
    1. static gboolean channel_connect(SpiceChannel *channel, gboolean tls)
    2. {
    3.     SpiceChannelPrivate *c = channel->priv;
    4.     g_return_val_if_fail(c != NULL, FALSE);
    5.     if (c->session == NULL || c->channel_type == -1 || c->channel_id == -1) {
    6.         /* unset properties or unknown channel type */
    7.         g_warning("%s: channel setup incomplete", __FUNCTION__);
    8.         return false;
    9.     }
    10.     c->state = SPICE_CHANNEL_STATE_CONNECTING;
    11.     c->tls = tls;
    12.     if (spice_session_get_client_provided_socket(c->session)) {
    13.         if (c->fd == -1) {
    14.             CHANNEL_DEBUG(channel, "requesting fd");
    15.             /* FIXME: no way for client to provide fd atm. */
    16.             /* It could either chain on parent channel.. */
    17.             /* or register migration channel on parent session, or ? */
    18.             g_signal_emit(channel, signals[SPICE_CHANNEL_OPEN_FD], 0, c->tls);
    19.             return true;
    20.         }
    21.     }
    22.     c->xmit_queue_blocked = FALSE;
    23.     g_return_val_if_fail(c->sock == NULL, FALSE);
    24.     g_object_ref(G_OBJECT(channel)); /* Unref'd when co-routine exits */
    25.     /* we connect in idle, to let previous coroutine exit, if present */
    26.     c->connect_delayed_id = g_idle_add(connect_delayed, channel);
    27.     return true;
    28. }

    6、创建协程,通过spice_channel_coroutine函数建立连接
    1. static gboolean connect_delayed(gpointer data)
    2. {
    3.     SpiceChannel *channel = data;
    4.     SpiceChannelPrivate *c = channel->priv;
    5.     struct coroutine *co;
    6.     CHANNEL_DEBUG(channel, "Open coroutine starting %p", channel);
    7.     c->connect_delayed_id = 0;
    8.     co = &c->coroutine.coroutine;
    9.     co->stack_size = 16 << 20; /* 16Mb */
    10.     co->entry = spice_channel_coroutine;
    11.     coroutine_init(co);
    12.     coroutine_yieldto(co, channel);
    13.     return FALSE;
    14. }

    7、通过socket连接到服务器的session会话中,如果启动TLS就完成TLS握手通信,完成认证后通过spice_channel_iterate函数监听socket消息
    1. /* coroutine context */
    2. static void *spice_channel_coroutine(void *data)
    3. {
    4.     SpiceChannel *channel = SPICE_CHANNEL(data);
    5.     SpiceChannelPrivate *c = channel->priv;
    6.     guint verify;
    7.     int rc, delay_val = 1;
    8.     /* When some other SSL/TLS version becomes obsolete, add it to this variable. */
    9.     long ssl_options = SSL_OP_NO_SSLv2 | SSL_OP_NO_SSLv3 | SSL_OP_NO_TLSv1;
    10.     CHANNEL_DEBUG(channel, "Started background coroutine %p", &c->coroutine);
    11.     if (spice_session_get_client_provided_socket(c->session)) {
    12.         if (c->fd < 0) {
    13.             g_critical("fd not provided!");
    14.             c->event = SPICE_CHANNEL_ERROR_CONNECT;
    15.             goto cleanup;
    16.         }
    17.         if (!(c->sock = g_socket_new_from_fd(c->fd, NULL))) {
    18.                 CHANNEL_DEBUG(channel, "Failed to open socket from fd %d", c->fd);
    19.                 c->event = SPICE_CHANNEL_ERROR_CONNECT;
    20.                 goto cleanup;
    21.         }
    22.         g_socket_set_blocking(c->sock, FALSE);
    23.         g_socket_set_keepalive(c->sock, TRUE);
    24.         c->conn = g_socket_connection_factory_create_connection(c->sock);
    25.         goto connected;
    26.     }
    27. reconnect:
    28.     c->conn = spice_session_channel_open_host(c->session, channel, &c->tls, &c->error);
    29.     if (c->conn == NULL) {
    30.         if (!c->error && !c->tls) {
    31.             CHANNEL_DEBUG(channel, "trying with TLS port");
    32.             c->tls = true; /* FIXME: does that really work with provided fd */
    33.             goto reconnect;
    34.         } else {
    35.             CHANNEL_DEBUG(channel, "Connect error");
    36.             c->event = SPICE_CHANNEL_ERROR_CONNECT;
    37.             goto cleanup;
    38.         }
    39.     }
    40.     c->sock = g_object_ref(g_socket_connection_get_socket(c->conn));
    41.     if (c->tls) {
    42.         c->ctx = SSL_CTX_new(SSLv23_method());
    43.         if (c->ctx == NULL) {
    44.             g_critical("SSL_CTX_new failed");
    45.             c->event = SPICE_CHANNEL_ERROR_TLS;
    46.             goto cleanup;
    47.         }
    48.         SSL_CTX_set_options(c->ctx, ssl_options);
    49.         verify = spice_session_get_verify(c->session);
    50.         if (verify &
    51.             (SPICE_SESSION_VERIFY_SUBJECT | SPICE_SESSION_VERIFY_HOSTNAME)) {
    52.             rc = spice_channel_load_ca(channel);
    53.             if (rc == 0) {
    54.                 g_warning("no cert loaded");
    55.                 if (verify & SPICE_SESSION_VERIFY_PUBKEY) {
    56.                     g_warning("only pubkey active");
    57.                     verify = SPICE_SESSION_VERIFY_PUBKEY;
    58.                 } else {
    59.                     c->event = SPICE_CHANNEL_ERROR_TLS;
    60.                     goto cleanup;
    61.                 }
    62.             }
    63.         }
    64.         {
    65.             const gchar *ciphers = spice_session_get_ciphers(c->session);
    66.             if (ciphers != NULL) {
    67.                 rc = SSL_CTX_set_cipher_list(c->ctx, ciphers);
    68.                 if (rc != 1)
    69.                     g_warning("loading cipher list %s failed", ciphers);
    70.             }
    71.         }
    72.         c->ssl = SSL_new(c->ctx);
    73.         if (c->ssl == NULL) {
    74.             g_critical("SSL_new failed");
    75.             c->event = SPICE_CHANNEL_ERROR_TLS;
    76.             goto cleanup;
    77.         }
    78.         BIO *bio = bio_new_giostream(G_IO_STREAM(c->conn));
    79.         SSL_set_bio(c->ssl, bio, bio);
    80.         {
    81.             guint8 *pubkey;
    82.             guint pubkey_len;
    83.             spice_session_get_pubkey(c->session, &pubkey, &pubkey_len);
    84.             c->sslverify = spice_openssl_verify_new(c->ssl, verify,
    85.                 spice_session_get_host(c->session),
    86.                 (char*)pubkey, pubkey_len,
    87.                 spice_session_get_cert_subject(c->session));
    88.         }
    89. #if OPENSSL_VERSION_NUMBER >= 0x0090806fL && !defined(OPENSSL_NO_TLSEXT)
    90.         {
    91.             const char *hostname = spice_session_get_host(c->session);
    92.             // check is not an ip address
    93.             GInetAddress * ip = g_inet_address_new_from_string(hostname);
    94.             if (ip == NULL) {
    95.                 SSL_set_tlsext_host_name(c->ssl, hostname);
    96.             } else {
    97.                 g_object_unref(ip);
    98.             }
    99.         }
    100. #endif
    101. ssl_reconnect:
    102.         rc = SSL_connect(c->ssl);
    103.         if (rc <= 0) {
    104.             rc = SSL_get_error(c->ssl, rc);
    105.             if (rc == SSL_ERROR_WANT_READ || rc == SSL_ERROR_WANT_WRITE) {
    106.                 g_coroutine_socket_wait(&c->coroutine, c->sock, ssl_error_to_cond(rc));
    107.                 goto ssl_reconnect;
    108.             } else {
    109.                 g_warning("%s: SSL_connect: %s",
    110.                           c->name, ERR_error_string(rc, NULL));
    111.                 c->event = SPICE_CHANNEL_ERROR_TLS;
    112.                 goto cleanup;
    113.             }
    114.         }
    115.     }
    116. connected:
    117.     c->has_error = FALSE;
    118.     c->in = g_io_stream_get_input_stream(G_IO_STREAM(c->conn));
    119.     c->out = g_io_stream_get_output_stream(G_IO_STREAM(c->conn));
    120.     rc = setsockopt(g_socket_get_fd(c->sock), IPPROTO_TCP, TCP_NODELAY,
    121.                     (const char*)&delay_val, sizeof(delay_val));
    122.     if ((rc != 0)
    123. #ifdef ENOTSUP
    124.         && (errno != ENOTSUP)
    125. #endif
    126.         ) {
    127.         g_warning("%s: could not set sockopt TCP_NODELAY: %s", c->name,strerror(errno));
    128.     }
    129.     spice_channel_send_link(channel);
    130.     if (!spice_channel_recv_link_hdr(channel) ||
    131.         !spice_channel_recv_link_msg(channel) ||
    132.         !spice_channel_recv_auth(channel))
    133.         goto cleanup;
    134.     while (spice_channel_iterate(channel));
    135. cleanup:
    136.     CHANNEL_DEBUG(channel, "Coroutine exit %s", c->name);
    137.     spice_channel_reset(channel, FALSE);
    138.     if (c->state == SPICE_CHANNEL_STATE_RECONNECTING ||
    139.         c->state == SPICE_CHANNEL_STATE_SWITCHING) {
    140.         g_warn_if_fail(c->event == SPICE_CHANNEL_NONE);
    141.         if (channel_connect(channel, c->tls)) {
    142.             g_object_unref(channel);
    143.             return NULL;
    144.         }
    145.         c->event = SPICE_CHANNEL_ERROR_CONNECT;
    146.     }
    147.     g_idle_add(spice_channel_delayed_unref, channel);
    148.     /* Co-routine exits now - the SpiceChannel object may no longer exist,
    149.        so don't do anything else now unless you like SEGVs */
    150.     return NULL;
    151. }

    8、获取socket的读消息和者写消息,并调用相关的读函数iterate_write和写函数iterate_read
    1. /* coroutine context */
    2. static gboolean spice_channel_iterate(SpiceChannel *channel)
    3. {
    4.     SpiceChannelPrivate *c = channel->priv;
    5.     if (c->state == SPICE_CHANNEL_STATE_MIGRATING &&
    6.         !g_coroutine_condition_wait(&c->coroutine, wait_migration, channel))
    7.         CHANNEL_DEBUG(channel, "migration wait cancelled");
    8.     /* flush any pending write and read */
    9.     if (!c->has_error)
    10.         SPICE_CHANNEL_GET_CLASS(channel)->iterate_write(channel);
    11.     if (!c->has_error)
    12.         SPICE_CHANNEL_GET_CLASS(channel)->iterate_read(channel);
    13.     if (c->has_error) {
    14.         GIOCondition ret;
    15.         if (!c->sock)
    16.             return FALSE;
    17.         /* We don't want to report an error if the socket was closed gracefully on the other end (VM shutdown) */
    18.         ret = g_socket_condition_check(c->sock, G_IO_IN | G_IO_ERR);
    19.         if (ret & G_IO_ERR) {
    20.             CHANNEL_DEBUG(channel, "channel got error");
    21.             if (c->state > SPICE_CHANNEL_STATE_CONNECTING) {
    22.                 if (c->state == SPICE_CHANNEL_STATE_READY)
    23.                     c->event = SPICE_CHANNEL_ERROR_IO;
    24.                 else
    25.                     c->event = SPICE_CHANNEL_ERROR_LINK;
    26.             }
    27.         }
    28.         return FALSE;
    29.     }
    30.     return TRUE;
    31. }

    9、设置的读写回调函数
    klass->iterate_write --> spice_channel_iterate_write
    klass->iterate_read -->spice_channel_iterate_read
    klass->handle_msg  --> spice_channel_handle_msg
    1. static void spice_channel_class_init(SpiceChannelClass *klass)
    2. {
    3.     GObjectClass *gobject_class = G_OBJECT_CLASS (klass);
    4.     klass->iterate_write = spice_channel_iterate_write;
    5.     klass->iterate_read  = spice_channel_iterate_read;
    6.     klass->channel_reset = channel_reset;
    7.     gobject_class->constructed  = spice_channel_constructed;
    8.     gobject_class->dispose      = spice_channel_dispose;
    9.     gobject_class->finalize     = spice_channel_finalize;
    10.     gobject_class->get_property = spice_channel_get_property;
    11.     gobject_class->set_property = spice_channel_set_property;
    12.     klass->handle_msg           = spice_channel_handle_msg;
    13.     ...
    14. }

    10、以读函数为例子,通过g_pollable_input_stream_is_readable获取到是否有可读的消息,如果有消息,调用spice_channel_recv_msg函数处理
    注意参数是一个函数指针:SPICE_CHANNEL_GET_CLASS(channel)->handle_msg 
    1. /* coroutine context */
    2. static void spice_channel_iterate_read(SpiceChannel *channel)
    3. {
    4.     SpiceChannelPrivate *c = channel->priv;
    5.     g_coroutine_socket_wait(&c->coroutine, c->sock, G_IO_IN);
    6.     /* treat all incoming data (block on message completion) */
    7.     while (!c->has_error &&
    8.            c->state != SPICE_CHANNEL_STATE_MIGRATING &&
    9.            (g_pollable_input_stream_is_readable(G_POLLABLE_INPUT_STREAM(c->in))
    10. #ifdef HAVE_SASL
    11.             /* flush the sasl buffer too */
    12.            || c->sasl_decoded != NULL
    13. #endif
    14.            )
    15.     ) {
    16.         spice_channel_recv_msg(channel, (handler_msg_in)SPICE_CHANNEL_GET_CLASS(channel)->handle_msg, NULL);
    17.     }
    18. }

    11、通过spice_channel_read读取消息,然后通过msg_handler函数处理消息,msg_handler是一个函数指针
    1. /* coroutine context */
    2. G_GNUC_INTERNAL
    3. void spice_channel_recv_msg(SpiceChannel *channel,
    4.                             handler_msg_in msg_handler, gpointer data)
    5. {
    6.     SpiceChannelPrivate *c = channel->priv;
    7.     SpiceMsgIn *in;
    8.     int msg_size;
    9.     int msg_type;
    10.     int sub_list_offset = 0;
    11.     in = spice_msg_in_new(channel);
    12.     /* receive message */
    13.     spice_channel_read(channel, in->header,spice_header_get_header_size(c->use_mini_header));
    14.     if (c->has_error)
    15.         goto end;
    16.     msg_size = spice_header_get_msg_size(in->header, c->use_mini_header);
    17.     /* FIXME: do not allow others to take ref on in, and use realloc here? this would avoid malloc/free on each message? */
    18.     in->data = g_malloc0(msg_size);
    19.     spice_channel_read(channel, in->data, msg_size);
    20.     if (c->has_error)
    21.         goto end;
    22.     in->dpos = msg_size;
    23.     msg_type = spice_header_get_msg_type(in->header, c->use_mini_header);
    24.     sub_list_offset = spice_header_get_msg_sub_list(in->header, c->use_mini_header);
    25.     if (msg_type == SPICE_MSG_LIST || sub_list_offset) {
    26.         SpiceSubMessageList *sub_list;
    27.         SpiceSubMessage *sub;
    28.         SpiceMsgIn *sub_in;
    29.         int i;
    30.         sub_list = (SpiceSubMessageList *)(in->data + sub_list_offset);
    31.         for (i = 0; i < sub_list->size; i++) {
    32.             sub = (SpiceSubMessage *)(in->data + sub_list->sub_messages[i]);
    33.             sub_in = spice_msg_in_sub_new(channel, in, sub);
    34.             sub_in->parsed = c->parser(sub_in->data, sub_in->data + sub_in->dpos,
    35.                                        spice_header_get_msg_type(sub_in->header,c->use_mini_header),
    36.                                        c->peer_hdr.minor_version,
    37.                                        &sub_in->psize, &sub_in->pfree);
    38.             if (sub_in->parsed == NULL) {
    39.                 g_critical("failed to parse sub-message: %s type %d",
    40.                            c->name, spice_header_get_msg_type(sub_in->header, c->use_mini_header));
    41.                 goto end;
    42.             }
    43.             msg_handler(channel, sub_in, data);
    44.             spice_msg_in_unref(sub_in);
    45.         }
    46.     }
    47.     /* ack message */
    48.     if (c->message_ack_count) {
    49.         c->message_ack_count--;
    50.         if (!c->message_ack_count) {
    51.             SpiceMsgOut *out = spice_msg_out_new(channel, SPICE_MSGC_ACK);
    52.             spice_msg_out_send_internal(out);
    53.             c->message_ack_count = c->message_ack_window;
    54.         }
    55.     }
    56.     if (msg_type == SPICE_MSG_LIST) {
    57.         goto end;
    58.     }
    59.     /* parse message */
    60.     in->parsed = c->parser(in->data, in->data + msg_size, msg_type,
    61.                            c->peer_hdr.minor_version, &in->psize, &in->pfree);
    62.     if (in->parsed == NULL) {
    63.         g_critical("failed to parse message: %s type %d",
    64.                    c->name, msg_type);
    65.         goto end;
    66.     }
    67.     /* process message */
    68.     /* spice_msg_in_hexdump(in); */
    69.     msg_handler(channel, in, data);
    70. end:
    71.     /* If the server uses full header, the serial is not necessarily equal
    72.      * to c->in_serial (the server can sometimes skip serials) */
    73.     c->last_message_serial = spice_header_get_in_msg_serial(in);
    74.     c->in_serial++;
    75.     spice_msg_in_unref(in);
    76. }

    12、msg_handler调用本质就是 spice_channel_handle_msg函数,而这个函数就是每个channel定义时会设置的const spice_msg_handler handlers[]函数指针数组。
    1. /* coroutine context */
    2. static void spice_channel_handle_msg(SpiceChannel *channel, SpiceMsgIn *msg)
    3. {
    4.     SpiceChannelClass *klass = SPICE_CHANNEL_GET_CLASS(channel);
    5.     int type = spice_msg_in_type(msg);// SPICE_MSG_PLAYBACK_DATA
    6.     spice_msg_handler handler;
    7.     g_return_if_fail(type < klass->priv->handlers->len);
    8.     if (type > SPICE_MSG_BASE_LAST && channel->priv->disable_channel_msg)
    9.         return;
    10.     handler = g_array_index(klass->priv->handlers, spice_msg_handler, type); //获取回调函数
    11.     g_return_if_fail(handler != NULL);
    12.     handler(channel, msg); //调用相应的回到函数
    13. }

    13、通过spice_protocol协议定义了枚举数值来找到相应的回调函数,如下图 
    1. SPICE_MSG_PLAYBACK_DATA --> playback_handle_data
    2. enum {
    3.     SPICE_MSG_PLAYBACK_DATA = 101,
    4.     SPICE_MSG_PLAYBACK_MODE,
    5.     SPICE_MSG_PLAYBACK_START,
    6.     SPICE_MSG_PLAYBACK_STOP,
    7.     SPICE_MSG_PLAYBACK_VOLUME,
    8.     SPICE_MSG_PLAYBACK_MUTE,
    9.     SPICE_MSG_PLAYBACK_LATENCY,
    10.     SPICE_MSG_END_PLAYBACK
    11. };
    12. static void channel_set_handlers(SpiceChannelClass *klass)
    13. {
    14.     static const spice_msg_handler handlers[] = {
    15.         [ SPICE_MSG_PLAYBACK_DATA ]            = playback_handle_data,
    16.         [ SPICE_MSG_PLAYBACK_MODE ]            = playback_handle_mode,
    17.         [ SPICE_MSG_PLAYBACK_START ]           = playback_handle_start,
    18.         [ SPICE_MSG_PLAYBACK_STOP ]            = playback_handle_stop,
    19.         [ SPICE_MSG_PLAYBACK_VOLUME ]          = playback_handle_set_volume,
    20.         [ SPICE_MSG_PLAYBACK_MUTE ]            = playback_handle_set_mute,
    21.         [ SPICE_MSG_PLAYBACK_LATENCY ]         = playback_handle_set_latency,
    22.     };
    23.     spice_channel_set_handlers(klass, handlers, G_N_ELEMENTS(handlers));
    24. }
  • 相关阅读:
    05-在Idea中编写Servlet程序
    HTML5期末考核大作业——学生网页设计作业源码HTML+CSS+JavaScript 中华美德6页面带音乐文化
    java 面试题 基础部分
    Vue根据屏幕分辨率计算div可以显示的数量,dom渲染在v-if之后造成的复杂处理
    UDP/TCP协议报头详细分析
    UWB室内定位系统全套源码 高精度人员定位系统源码
    搭建nacos集群,并通过nginx实现负载均衡
    window10下安装ubuntu系统以及docker使用
    web前端网页设计期末课程大作业:企业网页主题网站设计——舞蹈培训11页HTML+CSS+JavaScript
    spring Boot使用Mybatis实践
  • 原文地址:https://blog.csdn.net/cai742925624/article/details/126379167