Spice-server源碼簡要分析


下載地址: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幀以上滿足條件,就視爲流媒體。

待續~

發佈了55 篇原創文章 · 獲贊 7 · 訪問量 5萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章