下載地址:https://www.spice-space.org/download/releases/spice-server/spice-0.14.1.tar.bz2
也可以在gitlab下載。https://gitlab.com/spice
spcie-server主要是以一個lib的形勢被qemu調用。研究spice的代碼,可以先從qemu入手。
閱讀qemu的main函數代碼,發現關於libspice的調用有兩個地方:
qemu_spice_init();
qemu_spice_display_init();
簡單看一下這兩個函數的實現:
#include "ui/qemu-spice.h"
ui\spice-core.c
實際上呢是qemu把libspice裏面的代碼做了封裝。
void qemu_spice_init(void)
{
QemuOpts *opts = QTAILQ_FIRST(&qemu_spice_opts.head);
const char *password, *str, *x509_dir, *addr,
*x509_key_password = NULL,
*x509_dh_file = NULL,
*tls_ciphers = NULL;
char *x509_key_file = NULL,
*x509_cert_file = NULL,
*x509_cacert_file = NULL;
int port, tls_port, addr_flags;
spice_image_compression_t compression;
spice_wan_compression_t wan_compr;
bool seamless_migration;
qemu_thread_get_self(&me);
if (!opts) {
return;
}
port = qemu_opt_get_number(opts, "port", 0);
tls_port = qemu_opt_get_number(opts, "tls-port", 0);
if (port < 0 || port > 65535) {
error_report("spice port is out of range");
exit(1);
}
if (tls_port < 0 || tls_port > 65535) {
error_report("spice tls-port is out of range");
exit(1);
}
password = qemu_opt_get(opts, "password");
if (tls_port) {
x509_dir = qemu_opt_get(opts, "x509-dir");
if (!x509_dir) {
x509_dir = ".";
}
str = qemu_opt_get(opts, "x509-key-file");
if (str) {
x509_key_file = g_strdup(str);
} else {
x509_key_file = g_strdup_printf("%s/%s", x509_dir,
X509_SERVER_KEY_FILE);
}
str = qemu_opt_get(opts, "x509-cert-file");
if (str) {
x509_cert_file = g_strdup(str);
} else {
x509_cert_file = g_strdup_printf("%s/%s", x509_dir,
X509_SERVER_CERT_FILE);
}
str = qemu_opt_get(opts, "x509-cacert-file");
if (str) {
x509_cacert_file = g_strdup(str);
} else {
x509_cacert_file = g_strdup_printf("%s/%s", x509_dir,
X509_CA_CERT_FILE);
}
x509_key_password = qemu_opt_get(opts, "x509-key-password");
x509_dh_file = qemu_opt_get(opts, "x509-dh-key-file");
tls_ciphers = qemu_opt_get(opts, "tls-ciphers");
}
addr = qemu_opt_get(opts, "addr");
addr_flags = 0;
if (qemu_opt_get_bool(opts, "ipv4", 0)) {
addr_flags |= SPICE_ADDR_FLAG_IPV4_ONLY;
} else if (qemu_opt_get_bool(opts, "ipv6", 0)) {
addr_flags |= SPICE_ADDR_FLAG_IPV6_ONLY;
#ifdef SPICE_ADDR_FLAG_UNIX_ONLY
} else if (qemu_opt_get_bool(opts, "unix", 0)) {
addr_flags |= SPICE_ADDR_FLAG_UNIX_ONLY;
#endif
}
spice_server = spice_server_new();
spice_server_set_addr(spice_server, addr ? addr : "", addr_flags);
if (port) {
spice_server_set_port(spice_server, port);
}
if (tls_port) {
spice_server_set_tls(spice_server, tls_port,
x509_cacert_file,
x509_cert_file,
x509_key_file,
x509_key_password,
x509_dh_file,
tls_ciphers);
}
if (password) {
qemu_spice_set_passwd(password, false, false);
}
if (qemu_opt_get_bool(opts, "sasl", 0)) {
if (spice_server_set_sasl(spice_server, 1) == -1) {
error_report("spice: failed to enable sasl");
exit(1);
}
auth = "sasl";
}
if (qemu_opt_get_bool(opts, "disable-ticketing", 0)) {
auth = "none";
spice_server_set_noauth(spice_server);
}
if (qemu_opt_get_bool(opts, "disable-copy-paste", 0)) {
spice_server_set_agent_copypaste(spice_server, false);
}
if (qemu_opt_get_bool(opts, "disable-agent-file-xfer", 0)) {
#if SPICE_SERVER_VERSION >= 0x000c04
spice_server_set_agent_file_xfer(spice_server, false);
#else
error_report("this qemu build does not support the "
"\"disable-agent-file-xfer\" option");
exit(1);
#endif
}
compression = SPICE_IMAGE_COMPRESS_AUTO_GLZ;
str = qemu_opt_get(opts, "image-compression");
if (str) {
compression = parse_compression(str);
}
spice_server_set_image_compression(spice_server, compression);
wan_compr = SPICE_WAN_COMPRESSION_AUTO;
str = qemu_opt_get(opts, "jpeg-wan-compression");
if (str) {
wan_compr = parse_wan_compression(str);
}
spice_server_set_jpeg_compression(spice_server, wan_compr);
wan_compr = SPICE_WAN_COMPRESSION_AUTO;
str = qemu_opt_get(opts, "zlib-glz-wan-compression");
if (str) {
wan_compr = parse_wan_compression(str);
}
spice_server_set_zlib_glz_compression(spice_server, wan_compr);
str = qemu_opt_get(opts, "streaming-video");
if (str) {
int streaming_video = parse_stream_video(str);
spice_server_set_streaming_video(spice_server, streaming_video);
} else {
spice_server_set_streaming_video(spice_server, SPICE_STREAM_VIDEO_OFF);
}
str = qemu_opt_get(opts, "video-codecs");
if (str) {
#if SPICE_SERVER_VERSION >= 0x000c06
if (spice_server_set_video_codecs(spice_server, str)) {
error_report("Invalid video codecs");
exit(1);
}
#else
error_report("this qemu build does not support the "\"video-codecs\"option");
exit(1);
#endif
}
spice_server_set_agent_mouse
(spice_server, qemu_opt_get_bool(opts, "agent-mouse", 1));
spice_server_set_playback_compression
(spice_server, qemu_opt_get_bool(opts, "playback-compression", 1));
qemu_opt_foreach(opts, add_channel, &tls_port, NULL);
spice_server_set_name(spice_server, qemu_name);
spice_server_set_uuid(spice_server, (unsigned char *)&qemu_uuid);
seamless_migration = qemu_opt_get_bool(opts, "seamless-migration", 0);
spice_server_set_seamless_migration(spice_server, seamless_migration);
spice_server_set_sasl_appname(spice_server, "qemu");
if (spice_server_init(spice_server, &core_interface) != 0) {
error_report("failed to initialize spice server");
exit(1);
};
using_spice = 1;
migration_state.notify = migration_state_notifier;
add_migration_state_change_notifier(&migration_state);
spice_migrate.base.sif = &migrate_interface.base;
qemu_spice_add_interface(&spice_migrate.base);
qemu_spice_input_init();
qemu_spice_audio_init();
qemu_add_vm_change_state_handler(vm_change_state_handler, NULL);
qemu_spice_display_stop();
g_free(x509_key_file);
g_free(x509_cert_file);
g_free(x509_cacert_file);
#if SPICE_SERVER_VERSION >= 0x000c02
qemu_spice_register_ports();
#endif
#ifdef HAVE_SPICE_GL
if (qemu_opt_get_bool(opts, "gl", 0)) {
if ((port != 0) || (tls_port != 0)) {
error_report("SPICE GL support is local-only for now and "
"incompatible with -spice port/tls-port");
exit(1);
}
if (egl_rendernode_init(qemu_opt_get(opts, "rendernode")) != 0) {
error_report("Failed to initialize EGL render node for SPICE GL");
exit(1);
}
display_opengl = 1;
spice_opengl = 1;
}
#endif
}
qemu_spice_init()裏面,主要解析了一堆關於spice的參數,比如說port、password、壓縮參數等等。主要對spice-server的使用做初始化,設定參數。
qemu_spice_display_init()這部分主要是爲圖形顯示做準備,註冊listener和回調。
spice server的源碼裏面,很重要的一部分就是關於流媒體的處理。
目前默認的流媒體格式是mjpeg,主要實現是在server/mjpeg-encoder.c裏面。
另外也支持使用gstreamer,主要實現是在server/gstreamer-encoder.c裏面。
二者的主要內容大都是涉及到流媒體的幀處理、碼率控制、壓縮編碼。這裏面有個值得注意的地方就是spice會探測網絡狀況來自適應調整流媒體的質量。從而保證用戶的基本流暢使用。
下面基於spice-0.14.1的代碼做個簡要分析。
在mjpeg-encoder.c的源碼裏面,開頭有這麼一段:
#define MJPEG_MAX_FPS 25
#define MJPEG_MIN_FPS 1
#define MJPEG_QUALITY_SAMPLE_NUM 7
static const int mjpeg_quality_samples[MJPEG_QUALITY_SAMPLE_NUM] = {20, 30, 40, 50, 60, 70, 80};
#define MJPEG_IMPROVE_QUALITY_FPS_STRICT_TH 10
#define MJPEG_IMPROVE_QUALITY_FPS_PERMISSIVE_TH 5
#define MJPEG_AVERAGE_SIZE_WINDOW 3
#define MJPEG_BIT_RATE_EVAL_MIN_NUM_FRAMES 3
#define MJPEG_LOW_FPS_RATE_TH 3
#define MJPEG_SERVER_STATUS_EVAL_FPS_INTERVAL 1
#define MJPEG_SERVER_STATUS_DOWNGRADE_DROP_FACTOR_TH 0.1
上面的代碼大致能看出,spice-server在mjpeg格式的流媒體下,最大幀率控制爲 25 FPS,最小爲 1,圖像質量分爲7個檔次,20–80%。
再往下看看,能看到一些函數,獲取源fps,計算fps幀時間間隔等,
一直往下看,有個函數mjpeg_encoder_start_frame(),這個就是spice-server開始壓縮流媒體幀的函數,
一直拉到底,能看到一個函數mjpeg_encoder_new(),
VideoEncoder *mjpeg_encoder_new(SpiceVideoCodecType codec_type,
uint64_t starting_bit_rate,
VideoEncoderRateControlCbs *cbs,
bitmap_ref_t bitmap_ref,
bitmap_unref_t bitmap_unref)
{
MJpegEncoder *encoder;
spice_return_val_if_fail(codec_type == SPICE_VIDEO_CODEC_TYPE_MJPEG, NULL);
encoder = g_new0(MJpegEncoder, 1);
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;
encoder->cbs = *cbs;
mjpeg_encoder_reset_quality(encoder, MJPEG_QUALITY_SAMPLE_NUM / 2, 5, 0);
encoder->rate_control.during_quality_eval = TRUE;
encoder->rate_control.quality_eval_data.type = MJPEG_QUALITY_EVAL_TYPE_SET;
encoder->rate_control.quality_eval_data.reason = MJPEG_QUALITY_EVAL_REASON_RATE_CHANGE;
encoder->rate_control.warmup_start_time = spice_get_monotonic_time_ns();
encoder->cinfo.err = jpeg_std_error(&encoder->jerr);
jpeg_create_compress(&encoder->cinfo);
return (VideoEncoder*)encoder;
}
這裏面能看到實現了許多基本的功能,也定義了一些流媒體質量控制的東西。這玩意兒由誰在用呢?server/reds.c裏面有這麼一段:
static const new_video_encoder_t video_encoder_procs[] = {
&mjpeg_encoder_new,
#if defined(HAVE_GSTREAMER_1_0) || defined(HAVE_GSTREAMER_0_10)
&gstreamer_encoder_new,
#else
NULL,
#endif
};
它會根據你給定的參數不同,來決定用內置的mjpeg-encoder或者gstreamer。
在mjpeg_encoder_encode_frame()這個函數的實現裏面,我們能看到,這是通過調用libjpeg的函數來實現的,也從側面證明了spice-server中對於圖像的處理是先通過qxl在host翻譯圖形渲染指令,然後渲染成yuv,再將yuv轉爲argb32,再將argb32壓縮爲一張張的jpeg。理論上來講,x86的處理器裏面有對於圖形處理的優化指令集,比如說MMX、SSE、AVX等等。純粹的libjpeg裏面是沒有這些優化的,這個時候就需要用到libjpeg-turbo,接口與libjpeg一致,但是在這些指令集的優化之下,JPEG的編解碼速度有了成倍的提升,建議使用。
在gstreamer-encoder.c的相關代碼裏面,其實個mjpeg-encoder.c的內容差不多,核心還是視頻的壓縮、以及基於網絡和丟幀的碼率控制。裏面有關於gstreamer不同編碼器的調用,幀率控制等等。
server端關於視頻判斷條件的代碼在哪兒呢?
spice-0.14.1\server\video-stream.h
#define RED_STREAM_FRAMES_START_CONDITION 20
#define RED_STREAM_GRADUAL_FRAMES_START_CONDITION 0.2
#define RED_STREAM_MIN_SIZE (96 * 96)
spice-0.14.1\server\video-stream.c
static int is_stream_start(Drawable *drawable)
{
return ((drawable->frames_count >= RED_STREAM_FRAMES_START_CONDITION) &&
(drawable->gradual_frames_count >=
(RED_STREAM_GRADUAL_FRAMES_START_CONDITION * drawable->frames_count)));
}
簡單來說,就是20幀裏面,20%的幀數,即4幀以上滿足條件,就視爲流媒體。
待續~