====== SPICE 视频重定向的传输路径选择 ======
===== 1 借鉴 =====
==== 1.1 spice-stream-agent ====
* {{:public:it:spice:spice-stream-agent.png |}}
* 数据传输通过 StreamDev,StreamDev 是 char_device 的一种,虚拟化上是 **virtio** spiceport类型设备;
* guest 与 server 之间的协议(stream-device.h)是额外自定添加的;不影响 server 与 client 之间的协议
* 在 server 与 client 之间没有增加新类型通道,stream-channel 实现的通道类型是 DisplayChannel
StreamChannel*
stream_channel_new(RedsState *server, uint32_t id)
{
return g_object_new(TYPE_STREAM_CHANNEL,
"spice-server", server,
"core-interface", reds_get_core_interface(server),
"channel-type", SPICE_CHANNEL_DISPLAY,
// TODO this id should be after all qxl devices
"id", id,
"migration-flags", 0,
"handle-acks", TRUE, // TODO sure ??
NULL);
}
* 在 client 端,收到通道列表时就会额外创建一个屏幕来对应这个 DisplayChannel, 表现为有两个屏幕显示
* 发送创建 SPICE_MSG_DISPLAY_SURFACE_CREATE 消息时,标识 SPICE_SURFACE_FLAGS_STREAMING_MODE 来表示全屏流显示
// give an hint to client that we are sending just streaming
// see spice.proto for capability check here
if (red_channel_client_test_remote_cap(rcc, SPICE_DISPLAY_CAP_MULTI_CODEC)) {
surface_create.flags |= SPICE_SURFACE_FLAGS_STREAMING_MODE;
}
* server 与 client 之间传输视频流, SPICE_MSG_DISPLAY_STREAM_DATA
* 视频重定向如果直接借用这种方式来传输流,需要修改客户端对新增 DisplayChannel 的操作,魔改协议,得不偿失
* 问题:是否可以直接把数据插入到主 DisplayChannel 的 视频流里? 有点复杂
==== 1.2 webdav ====
* {{:public:it:spice:webdav.png |}}
* guest 与 server 之间的传输方式是相同的,都是 spiceport virtio 设备
* server 与 client 之间传输新增了一个通道类型 WebDAVChannel,但 spice-protocol 里 只新增了通道类型标识 SPICE_CHANNEL_WEBDAV,没有新增其它消息 ;再看 WebDAVChannel 的声明如下:
channel PortChannel : SpicevmcChannel {
client:
message {
uint8 event;
} @declare event = 201;
server:
message {
uint32 name_size;
uint8 *name[name_size] @zero_terminated @marshall @nonnull;
uint8 opened;
} @declare init = 201;
message {
uint8 event;
} @declare event;
};
channel WebDAVChannel : PortChannel {
};
可以看到 WebDAVChannel 协议是继承 PortChannel 通道协议,而且完全没有新增元素
* 查看 server 代码,是能自动识别创建任意名称的 PortChannel 通道:
else if (strcmp(char_device->subtype, SUBTYPE_PORT) == 0) {
if (strcmp(char_device->portname, "org.spice-space.webdav.0") == 0) {
dev_state = spicevmc_device_connect(reds, char_device, SPICE_CHANNEL_WEBDAV);
} else if (strcmp(char_device->portname, "org.spice-space.stream.0") == 0) {
dev_state = RED_CHAR_DEVICE(stream_device_connect(reds, char_device));
} else {
dev_state = spicevmc_device_connect(reds, char_device, SPICE_CHANNEL_PORT);
}
}
* 那么传输路径与其复用WebDAVChannel 不如直接创建新名称的 PortChannel
===== 2 传输方案 =====
==== 2.1 方案一 ====
直接使用 PortChannel 协议进行视频重定向流传输路径
* {{:public:it:spice:video-redirect-path.png |}}
* 优点:只借用已有通道类型,不需要修改新增 SPICE 协议, guest与client之间的传输数据内容即可自行随意定义
* 优点:不需要修改 spice-server 代码,
* 缺点:spice-gtk 客户端需要和 之前的 demo codec-agent 一样自行解码收到的视频数据,并根据坐标自行画在屏幕,并处理遮盖问题
==== 2.2 方案二 ====
直接把数据插入到主 DisplayChannel 的 视频流里传输
* 优点:不需要动客户端代码
* 缺点:需要修改 spice-server 代码,介入到主 DisplayChannel 对视频流的处理(涉及到播放区域与视频流编号),复杂度未能估算
* 缺点:相比方案一,遮盖计算从client 转移到了 server
* 缺点:如果不修改 server 与 client 之间的协议,只能传输符合 SPICE 协议格式的数据, 比如 视频流编码格式必须一致
==== 2.3 问题 ====
* virtio 的传输速率是否足够快? 足够
* virtio 是否足够稳定?
* portchannel 是否有足够稳定的用例?
===== 3 PortChannel 详解 =====
==== 3.1 验证 ====
在试验时发现 spice-gtk 里的工具 spicy.c 已经有对特定名称为 org.spice.spicy 的 spiceport 的 传输测试代码,所以测试 portchannel 传输就比较简单了:
* guest Windows10虚拟机添加名为 org.spice.spicy 的 spiceport 通道设备:
* 用 VS 编译个测试串口程序, 编译为 TestPortChannel.exe, 对应串口名为 ''\\.\Global\org.spice.spicy''
#include
#include
//#define SPICE_PORT_NAME L"\\\\.\\Global\\com.troila.newbee.0"
#define SPICE_PORT_NAME_SPICY L"\\\\.\\Global\\org.spice.spicy"
int main()
{
HANDLE port_handle = ::CreateFile(SPICE_PORT_NAME_SPICY,
GENERIC_WRITE | GENERIC_READ,
0,
NULL,
OPEN_EXISTING,
0,//FILE_FLAG_OVERLAPPED,
NULL);
if (port_handle == INVALID_HANDLE_VALUE) {
std::cout << "open spice port failed! err:" << ::GetLastError() << std::endl;
}else {
std::cout << "open spice port successfully" << std::endl;
char write_buf[] = "hello troila!\n";
const DWORD buf_size = sizeof(write_buf);
char read_buf[buf_size] = {0};
DWORD size = 0;
DWORD totalsize = 0;
BOOL ret = ::WriteFile(port_handle, (LPCVOID)write_buf, buf_size, &size, NULL);
if (ret) {
std::cout << "write successfully" << std::endl;
}
else {
std::cout << "write failed! err:" << ::GetLastError() << std::endl;
goto end;
}
size = 0;
while (totalsize < buf_size) {
ret = ::ReadFile(port_handle, read_buf+totalsize, buf_size-totalsize, &size, NULL);
if (!ret)
{
std::cout << "write failed! err:" << ::GetLastError() << std::endl;
goto end;
}
totalsize += size;
std::cout << "read " << size << std::endl;
}
std::cout << "read content: " << read_buf << std::endl;
end:
ret = ::CloseHandle(port_handle);
std::cout << "close handle ret:" << ret << std::endl;
}
}
* 在 client 端(Ubuntu 18.04) 安装 ''sudo apt install spice-client-gtk'', 启动 ''spicy'' 并连接
* 在 guest 使用管理员权限执行 TestPortChannel.exe, 此时 spicy 日志显示对应 portchannel 打开并收到了 hello troila!
* 在 spicy 随意输入十几个字母,guest 端 TestPortChannel.exe 日志显示收到对应字母,并关闭串口
open spice port successfully
write successfully
read 1
...
read content: hello guest!!!
close handle ret:1
* spicy 显示 对应 portchannel 已关闭
** Message: 10:53:14.947: main channel: opened
port 0x5561c6fcd270 org.spice.spicy: opened
hello troila!
port 0x5561c6fcd270 org.spice.spicy: closed
双向传输验证完毕
==== 3.2 流程详解 ====
=== 3.2.1 spice-gtk 对 port-channel 的处理流程 ===
@startuml spice-gtk-port-channel
skinparam sequenceMessageAlign center
skinparam shadowing false
header spice-gtk 中 port-channel 的处理流程 V0.1.0 by weiyongjiu
hide footbox
participant MainChannel
participant PortChannel
participant Spicy
==port通道创建==
[-> MainChannel: SPICE_MSG_MAIN_CHANNELS_LIST
activate MainChannel
create PortChannel
MainChannel -> PortChannel: g_object_new() \n SPICE_CHANNEL_PORT
deactivate MainChannel
PortChannel -> Spicy: signal "channel-new"
activate PortChannel
activate Spicy
Spicy -> Spicy: channel_new() 进行初始化
deactivate Spicy
deactivate PortChannel
==数据传输==
[-> PortChannel: SPICE_MSG_SPICEVMC_DATA
activate PortChannel
PortChannel -> PortChannel: port_handle_msg()
PortChannel -> Spicy++: signal "port-data"
Spicy -> Spicy --: port_data() 输出数据到 stdin
deactivate PortChannel
@enduml
PortChannel 对象构造过程:
* PortChannel 继承 SpiceChannel, 首先构造 spice_channel_class_init()
* spice_channel_constructed()
* spice_session_channel_new()
* g_signal_emit(session, signals[SPICE_SESSION_CHANNEL_NEW], 0, channel);
* SPICE_SESSION_CHANNEL_NEW "channel-new"
* PortChannel 自身构造函数 spice_port_channel_class_init()
* channel_set_handlers() 设置以下消息对应的处理函数
* SPICE_MSG_PORT_INIT
* SPICE_MSG_PORT_EVENT
* SPICE_MSG_SPICEVMC_DATA
* 消息对应的处理函数 port_handle_msg()
* g_coroutine_signal_emit() SPICE_PORT_DATA "port-data"
=== 3.3 client 端 port-channel 的使用方式 ===
* spice-gtk 实现了一个 SpicePortChannel, 详细文档 https://www.spice-space.org/api/spice-gtk/SpicePortChannel.html
* 增加对 glib 信号 ''SPICE_SESSION_CHANNEL_NEW'' ''channel-new''的处理函数,按类型与通道名来获知特定名字 portchannel 的建立,并进行初始化
* 初始化时增加对该通道信号 ''port-event'',''port-data'' 的处理函数,
* 响应 ''port-event'' 用于获取通道打开关闭事件
* 响应 ''port-data'' 来接收数据
* 使用函数 ''spice_port_write_async'',''spice_port_write_finish'' 来发送数据