spice stream多屏技术方案

目前现状

目前spice对qxl接口的云视频方案支持最好,对于扩展多屏的方案在qemu的启动配置参数可以完成,libvirt 的多屏参数配置如下:
<video>   
    <model type='qxl' vram='9126' heads='2'/>  
	</video>

通过指定heads 的数量就可以确定spice云桌面的扩展屏的数量。

spice的流方案是利用虚拟机上的spice-streaming-agent推流转发到spice server,然后通过display channel 发送到客户端,流方案的libvirt配置如下:
    <channel type='spiceport'>
      <source channel='org.spice-space.stream.0'/>
      <target type='virtio' name='org.spice-space.stream.0'/>
      <address type='virtio-serial' controller='0' bus='0' port='2'/>
    </channel>

配置一个spiceport 的虚拟串口可以产生一个云桌面窗口,但是对于扩展多屏,目前不支持这种配置。需要进一步探索流模式下的多屏方案。

spice stream 多屏方案探索

多stream-channel 方案

方案

stream 的流程如下图所示:

guest::stream-agent
server::StreamDevice
server::StreamChannel
client::window

一个spiceport就对应一个streamdevice, 一个streamdevice就对应一个streamChannel, 相对应的客户端创建一个显示窗口。基于以上思路,可以创建两个spiceport,stream-agent 底层抓屏后,拆分成两个屏幕, 再通过两个spiceport 推出去,这样客户端就会产生两个窗口,形成多屏。

guest::stream-agent
server::StreamDevice1
server::StreamDevice2
server::StreamChannel1
server::StreamChannel2
client::window1
client::window2

实现上是通过修改libvirt的配置如下:

    <channel type='spiceport'>
      <source channel='org.spice-space.stream.0'/>
      <target type='virtio' name='org.spice-space.stream.0'/>
      <address type='virtio-serial' controller='0' bus='0' port='2'/>
    </channel>
    <channel type='spiceport'>
      <source channel='org.spice-space.stream.1'/>
      <target type='virtio' name='org.spice-space.stream.1'/>
      <address type='virtio-serial' controller='0' bus='0' port='3'/>
    </channel>

问题

目前这种方案实现上最容易,不需要修改任何代码,最终客户端可以出现双屏,但是在客户端上鼠标只能在主屏上生效,在扩展屏上鼠标不生效。 产生这种问题的原因是因为,两个流的显示id 都是0
,这样没办法区分两个屏幕,底层的vdagent只会处理display id 为0 的消息,这样第一个流的窗口会生效,第二个不生效。

单spice stream 多monitor的方案

多个流的方案会导致在inputchannel(鼠标输入出现覆盖),可以创建单个流多个monitor的方案,一个monitor 在客户端产生一个窗口对对应一个显示id, 这样各个窗口的显示id不会重复。方案流程如下所示:

guest::stream-agent
server::StreamDevice
server::StreamChannel
client::display_channel
client::monitor_config
client::widow1
client::widow2
server::monitor_config

依据moitor_config 就可以产生多个屏幕,monitor_config的配置参数如下:

typedef struct SpiceHead {
    uint32_t monitor_id;    //标识monitor 
    uint32_t surface_id;    //对于扩展屏的方式只有一个surface 为0
    uint32_t width;         //该monitor对应显示图像的宽
    uint32_t height;	      //该monitor 对应显示图像的高
    uint32_t x;            //在surface 中的起始位置  
    uint32_t y;            //在surface位置
    uint32_t flags;
} SpiceHead;
typedef struct SpiceMsgDisplayMonitorsConfig {
    uint16_t count;
    uint16_t max_allowed;
    SpiceHead heads[0];
} SpiceMsgDisplayMonitorsConfig;

qxl模式下通过读取libvirt多屏配置文件后就会在spice-server 中生成monitor_config, 所以qxl 方式下多屏可以直接生成。但是sream 方式需要额外生成, 目前spice-server 系统中的moitor_config 是直接写成了固定的方式,参考spice_server 项目stream-channel.cpp的marshall_monitors_config 函数:

void
StreamChannelClient::marshall_monitors_config(StreamChannel *channel, SpiceMarshaller *m)
{
    struct {
        SpiceMsgDisplayMonitorsConfig config;
        SpiceHead head;
    } msg = {
        { 1, 1, },
        {
            // monitor ID. These IDs are allocated per channel starting from 0
            0,
            PRIMARY_SURFACE_ID,
            channel->width, channel->height,
            0, 0,
            0 // flags
        }
    };

    init_send_data(SPICE_MSG_DISPLAY_MONITORS_CONFIG);
    spice_marshall_msg_display_monitors_config(m, &msg.config);
}

以上配置中monitor_config 配置固定成了单屏方式, 显示的是整个屏幕,通过发送SPICE_MSG_DISPLAY_MONITORS_CONFIG信号通知client monitor_config 配置。

spice-server 代码中修改支持多屏

在marshall_monitors_config 直接修改成两屏模式(水平方式),代码如下:

void
StreamChannelClient::marshall_monitors_config(StreamChannel *channel, SpiceMarshaller *m)
{
    struct {
        SpiceMsgDisplayMonitorsConfig config;
        SpiceHead head[2];
    } msg = {
        { 2, 2, },
        {
            // monitor ID. These IDs are allocated per channel starting from 0
            0,
            PRIMARY_SURFACE_ID,
            channel->width/2, channel->height,
            0, 0,
            0 // flags
        },
                {
            // monitor ID. These IDs are allocated per channel starting from 0
            0,
            PRIMARY_SURFACE_ID,
            channel->width、2, channel->height,
            channel->width/2, 0,
            0 // flags
        }
    };

    init_send_data(SPICE_MSG_DISPLAY_MONITORS_CONFIG);
    spice_marshall_msg_display_monitors_config(m, &msg.config);
}

通过以上的方式修改后,在spicy 客户端显示中出现出了两个屏幕,但是第二个屏幕屏幕是黑的,通过日志分析问题出现在客户端。

客户端创建流模式流程

以gsteamer的模式为例,调用关系如下:

channel-display::display_handle_stream_create
channel-display::display_stream_create
channel-display-gst::create_gstreamer_decoder
channel-display-gst::create_pipeline
channel-display::hand_pipeline_to_widget
  • display_handle_stream_create 是客户端接收到服务端信号SPICE_MSG_DISPLAY_STREAM_CREATE完成创建。
  • hand_pipeline_to_widget 的左右是确定显示方式,代码如下:
G_GNUC_INTERNAL
gboolean hand_pipeline_to_widget(display_stream *st, GstPipeline *pipeline)
{
    gboolean res = false;

    if (st->surface->streaming_mode) {
        g_signal_emit(st->channel, signals[SPICE_DISPLAY_OVERLAY], 0,
                      pipeline, &res);
    }
    return res;
}

目前流模式默认通过发送widget窗口发送SPICE_DISPLAY_OVERLAY采用GstVideoOverlay interface进行显示,这种显示方式是直接讲pipeline和窗口绑定进行显示。这样的方式在多屏的模式下,流直接绑定到了0号窗口显示。

  • 修改的方式不采用GstVideoOverlay interface进行显示,而是直接采用gtstream 解码出图片后,将图片的不同区域显示在不同的窗口下。修改方式如下:
G_GNUC_INTERNAL
gboolean hand_pipeline_to_widget(display_stream *st, GstPipeline *pipeline)
{
    gboolean res = false;
    return res;
}

修改之后出现了双屏显示正常。

双屏流显示流程

客户端调用关系如图所示:

channel-display::display_handle_stream_data
channel-display-gst::spice_gst_decoder_queue_frame
channel-display-gst::new_sample
channel-display-gst::schedule_frame
channel-display-gst::display_frame
channel-display::stream_display_frame

这里是两个线程, 工作刘晨如下:

  • display_channel 接收到SPICE_MSG_DISPLAY_STREAM_DATA 之后就调用流数据处理函数
    display_handle_stream_data, 然后将frame 送入解码队列中。
  • 解码线程从队列中得到数据frame 后进行解码,然后定时调用display_frame 进行显示画图
  • stream_display_frame 显示画图后就发信号通知widget进行显示,这部分的代码如下:
G_GNUC_INTERNAL
void stream_display_frame(display_stream *st, SpiceFrame *frame,
                          uint32_t width, uint32_t height, int stride, uint8_t *data)
{
    if (stride == SPICE_UNKNOWN_STRIDE) {
        stride = width * sizeof(uint32_t);
    }
    if (!(st->flags & SPICE_STREAM_FLAGS_TOP_DOWN)) {
        data += stride * (height - 1);
        stride = -stride;
    }
    st->surface->canvas->ops->put_image(st->surface->canvas,
                                        &frame->dest, data,
                                        width, height, stride,
                                        st->have_region ? &st->region : NULL);

    

    if (st->surface->primary) {
        g_signal_emit(st->channel, signals[SPICE_DISPLAY_INVALIDATE], 0,
                      frame->dest.left, frame->dest.top,
                      frame->dest.right - frame->dest.left,
                      frame->dest.bottom - frame->dest.top);
    }
}

鼠标显示异常的问题

通过上面的修改双屏显示问题解决,第二个屏幕上的鼠标可以操作(有cursor显示,可以点击鼠标), 但是第二个屏幕上的鼠标操作,控制的还是第一个屏幕上的内容。

检查客户端鼠标操作

检查调试客户端的代码,在鼠标操作的时候,原生获取屏幕显示id的函数如下:

static gint get_display_id(SpiceDisplay *display)
{
    SpiceDisplayPrivate *d = display->priv;

    /* supported monitor_id only with display channel #0 */


    if (d->channel_id == 0 && d->monitor_id >= 0)
        return d->monitor_id;

    g_return_val_if_fail(d->monitor_id <= 0, -1);

    return d->channel_id;
}

从上面的代码可以分析得到, 该多屏模式只针对channel_id 为0的场景,而对于channel_id > 0 的场景会存在问题,修改
为如下代码:

static gint get_display_id(SpiceDisplay *display)
{
    SpiceDisplayPrivate *d = display->priv;

    /* supported monitor_id only with display channel #0 */

    if (d->monitor_id >= 0)
            return d->monitor_id;

    g_return_val_if_fail(d->monitor_id <= 0, -1);

    return d->channel_id;
}

修改完成之后,日志分析中disply_id = 0 和 display_id 的坐标位置(widget中的坐标位置)都发送到了server, 但是鼠标问题还是存在。

服务端鼠标位置的处理

检查服务端的鼠标位置的处理流程,参考spice-server端inputs-channel 处理代码:

    case SPICE_MSGC_INPUTS_MOUSE_POSITION: {
        auto pos = static_cast<SpiceMsgcMousePosition *>(message);
        SpiceTabletInstance *tablet = inputs_channel->tablet;

        on_mouse_motion();
        if (reds_get_mouse_mode(reds) != SPICE_MOUSE_MODE_CLIENT) {
            break;
        }
        spice_assert((reds_config_get_agent_mouse(reds) && reds_has_vdagent(reds)) || tablet);
        if (!reds_config_get_agent_mouse(reds) || !reds_has_vdagent(reds)) {
            SpiceTabletInterface *sif;
            sif = SPICE_UPCAST(SpiceTabletInterface, tablet->base.sif);
            sif->position(tablet, pos->x, pos->y, RED_MOUSE_STATE_TO_LOCAL(pos->buttons_state)); #服务端模式
            break;
        }
        //采用vdagent 辅助,完成客户端模式,客户端模式下客户窗口传送的是鼠标的相对位置和形状
        VDAgentMouseState *mouse_state = &inputs_channel->mouse_state;   
        mouse_state->x = pos->x;
        mouse_state->y = pos->y;
        mouse_state->buttons = RED_MOUSE_BUTTON_STATE_TO_AGENT(pos->buttons_state);
        mouse_state->display_id = pos->display_id;
        reds_handle_agent_mouse_event(reds, mouse_state);
        break;
    }

从以上代码可以分析出当设置为客户端模式下的时候,鼠标地址会通过虚拟串口下发到vdagent 底层进行处理,需要进一步分析vdagent 端的代码。

vdagent 鼠标位置处理

vdagent 中鼠标处理的调用关系如下:

vdagentd::do_client_mouse
uinput::vdagentd_uinput_do_mouse
uinput::lookup_screen_info
mouse_postion_module

mouse_postion_module 主要是完成了坐标点的转换,代码如下:

        mouse->x += screen_info->x;
        mouse->y += screen_info->y;

screen_info 是通过lookup_screen_info获取的当前的屏幕的信息, screen_info 的信息内容如下:

struct vdagentd_guest_xorg_resolution {
    int width;           //屏幕显示的宽度
    int height;          //屏幕显示的高度
    int x;               //屏幕的起始x
    int y;				//屏幕的起始y
    int display_id;      //屏幕显示的id
};

从打印的日志分析目前display_id =1时候screen_info 的x, y 都是0, 这样导致了坐标转换的时候没有映射成真实的坐标地址,x=0, y=0 时候计算的坐标会位置会映射到display_id = 0 的屏幕上,导致坐标不对。

lookup_screen_info 的代码内容如下:

static struct vdagentd_guest_xorg_resolution* lookup_screen_info(struct vdagentd_uinput *uinput, int display_id)
{
    int i;
    for (i = 0; i < uinput->screen_count; i++) {
        if (uinput->screen_info[i].display_id == display_id) {
            return &uinput->screen_info[i];
        }
    }
    syslog(LOG_WARNING, "Unable to find output index for display id %d", display_id);
    return NULL;
}

从以上代码可以分析得到,屏幕信息都存储在uinput 中, 而uinput 是一个全局的缓存,接下来需要分析整个全局的写入的位置。

uinput屏幕信息写入

阅读代码,最终定位信息的写入是在do_agent_xorg_resolution 中完成,核心代码如下:

    agent_data->screen_info = g_memdup2(data, header->size);
    agent_data->width  = header->arg1;
    agent_data->height = header->arg2;
    agent_data->screen_count = n;

梳理整个协议调用关系如下:

spice_stream_agent spice_server spice_vagentd spice_vagent STREAM_TYPE_DEVICE_DISPLAY_INFO(协议命令) StreamDevice::handle_msg_device_display_info (处理函数) VD_AGENT_GRAPHICS_DEVICE_INFO(协议命令) VDAGENTD_GRAPHICS_DEVICE_INFO(协议命令) vdagent_display_handle_graphics_device_info(处理函数) vdagent_display_send_daemon_guest_res(处理函数) VDAGENTD_GUEST_XORG_RESOLUTION(协议命令) do_agent_xorg_resolution(处理函数) spice_stream_agent spice_server spice_vagentd spice_vagent

通过调试整个协议流程通路是正常的,而通过分析协议和代码,spice_vagent 模块负责读取本地系统x11的多屏信息,
定位出最终是在vdagent_x11_get_resolutions 这个函数实现了屏幕信息关系的读取,该函数生成多屏信息有两种模式

  • xrandr 模式
  • xinerama 模式
    在vdagent 初始化时候两种模式都支持,但是实际的代码逻辑是存在xrandr模式就不会执行xinerama, 而xinerama方式对于
    多屏信息处理是正常的,xrandr 处理的只能去读系统存在几个虚拟显示设备,没有多屏关系, 代码示意如下:
GArray *vdagent_x11_get_resolutions(struct vdagent_x11 *x11, gboolean update,
                                    int *width, int *height, int *system_screen_count)
{
    GArray *res_array = g_array_new(FALSE, FALSE, sizeof(struct vdagentd_guest_xorg_resolution));
    int i, screen_count = 0;

    *width = *height = 0;
    if (x11->has_xrandr) {
        .......
    } else if (x11->has_xinerama) {
       .......
    } 

所以修改以下代码逻辑,先执行xinerama 模式,如果xinerama模式不支持,再运行xrandr模式。

GArray *vdagent_x11_get_resolutions(struct vdagent_x11 *x11, gboolean update,
                                    int *width, int *height, int *system_screen_count)
{
    GArray *res_array = g_array_new(FALSE, FALSE, sizeof(struct vdagentd_guest_xorg_resolution));
    int i, screen_count = 0;

    *width = *height = 0;
    if (x11->has_xinerama) {
        .......
    } else if (x11->has_xrandr) {
       .......
    } 

xinerama获取屏幕信息的代码如下:

 XineramaScreenInfo *screen_info = NULL;

screen_info = XineramaQueryScreens(x11->display, &screen_count);

通过以上的代码可以获取一个执行screen 信息表的指针,信息表中每个元素的信息如下:

typedef struct {
   int   screen_number;   //屏幕索引
   short x_org;           //起始位置x   
   short y_org;           //起始位置y  
   short width;           //宽度
   short height;          //高度
} XineramaScreenInfo;

通过上面的修改解决了双屏显示,并且双屏的坐标都正常,但是目前存在的问题有:

  • 双屏配置被写死在了server代码中
  • 没有办法通过配置的方式,配置几个屏幕,屏幕之间的关系(目前代码上是配置固定的左右屏)

需要进一步完善方案,要动态配置有以下两种方案:

  • 通过配置文件动态配置,在启动生效
  • 通过stream_agent 发送配置信息

第一种方案,当前的配置文件,没有该配置项,二次开发需要修改qemu,代价高。
第二种方案,只需要修改stream_agent 和 spice_server 即可。

分析spice_steam_agent的整个通信协议过程如下:

spice_stream_agent spice_server STREAM_TYPE_START_STOP STREAM_TYPE_CAPABILITIES STREAM_TYPE_DEVICE_DISPLAY_INFO STREAM_TYPE_FORMAT STREAM_TYPE_DATA spice_stream_agent spice_server

以上是当前版本spice_stream_agent 和 spice_server 之间通信的整个协议流程。

spice 多屏上报处理

stream_agent修改

在不改变协议的条件下,可以在STREAM_TYPE_DEVICE_DISPLAY_INFO 中加入,屏幕信息, 添加的代码如下所示

static std::vector<uint8_t> getXrandrMonitorConfig()
{
    Display *display = XOpenDisplay(NULL);
    Window window = DefaultRootWindow(display);
    int monitors = 0;
    XRRMonitorInfo* info = XRRGetMonitors(display, window, True, &monitors);


    std::vector<uint8_t> out(sizeof(MonitorConfigsInfo) + monitors * sizeof(MonitorHead));

    uint8_t *pBase = out.data();
    MonitorConfigsInfo  *monitor_info = (MonitorConfigsInfo  *)(pBase  );
    monitor_info->monitor_count = monitors;

    for (int i = 0; i < monitors; i++)
    {
        monitor_info->head[i].x = info[i].x;
        monitor_info->head[i].y = info[i].y;
        monitor_info->head[i].width = info[i].width;
        monitor_info->head[i].height = info[i].height;
        monitor_info->head[i].monitor_id = i;
        monitor_info->head[i].primary = info[i].primary;
    }

    XRRFreeMonitors(info);
    XFree(display);

    return out;
}

修改DeviceDisplayInfoMessage消息的发送的数据格式,参考如下代码:

class DeviceDisplayInfoMessage : public OutboundMessage<StreamMsgDeviceDisplayInfo, DeviceDisplayInfoMessage, STREAM_TYPE_DEVICE_DISPLAY_INFO>
{
public:
    DeviceDisplayInfoMessage(const DeviceDisplayInfo &info) : OutboundMessage(info) {} 
    DeviceDisplayInfoMessage(const std::vector<uint8_t> &info) : OutboundMessage(info) {}  // 新增
    static size_t size(const DeviceDisplayInfo &info)
    {
        return sizeof(PayloadType) +
               std::min(info.device_address.length(), static_cast<size_t>(max_device_address_len)) +
               1;
    }

    static size_t size(const std::vector<uint8_t> &info)   // 新增
    {
        return sizeof(PayloadType) + info.size();
    }

    void write_message_body(StreamPort &stream_port, const DeviceDisplayInfo &info)
    {
        std::string device_address = info.device_address;
        if (device_address.length() > max_device_address_len) {
            syslog(LOG_WARNING,
                   "device address of stream id %u is longer than %u bytes, trimming.",
                   info.stream_id, max_device_address_len);
            device_address = device_address.substr(0, max_device_address_len);
        }
        StreamMsgDeviceDisplayInfo strm_msg_info{};
        strm_msg_info.stream_id = info.stream_id;
        strm_msg_info.device_display_id = info.device_display_id;
        strm_msg_info.device_address_len = device_address.length() + 1;
        stream_port.write(&strm_msg_info, sizeof(strm_msg_info));
        stream_port.write(device_address.c_str(), device_address.length() + 1);
    }

    void write_message_body(StreamPort &stream_port, const std::vector<uint8_t> &info_vec)// 新增
    {
        if (info_vec.size() > max_device_address_len )
        {
            syslog(LOG_WARNING, "system monitor info message size %d is more than the setting capacity %d", info_vec.size(), max_device_address_len);
        }

        StreamMsgDeviceDisplayInfo strm_msg_info{};
        strm_msg_info.stream_id = 17;
        strm_msg_info.device_address_len = info_vec.size() ;
        strm_msg_info.device_display_id = strm_msg_info.stream_id  +  strm_msg_info.device_address_len;
        stream_port.write(&strm_msg_info, sizeof(strm_msg_info));
        stream_port.write(info_vec.data(), info_vec.size());
    }
}

然后在stream agent 在读取STREAM_TYPE_START_STOP之前调用如下函数:

auto config_vec = getXrandrMonitorConfig();
stream_port.send<DeviceDisplayInfoMessage>(config_vec);

spice_server修改

在StreamChannel类中增加存储变量stream_monitors_config_vec用来保存保存上报的信息,代码如下:

    inline void set_config_stream_monitor_head(std::vector<StreamMonitorHead> &heads)
    {
        stream_monitors_config_vec = heads;
    }

    inline std::vector<StreamMonitorHead> get_config_stream_monitor_head()
    {
        return stream_monitors_config_vec;
    }

在red-stream-device.cpp 的handle_msg_device_display_info 函数中增加,解析配置的模块

MonitorConfigsInfo * config_info = (MonitorConfigsInfo * )(display_info_msg->device_address);        
uint32_t moinitor_count = GUINT32_FROM_LE(config_info->monitor_count);
g_info("report monitor num is %d", moinitor_count);

std::vector<StreamMonitorHead> heads(moinitor_count);
for(int i = 0; i < moinitor_count; i++)
{
    heads[i].monitor_id = config_info->head[i].monitor_id;
    heads[i].primary = config_info->head[i].primary;             
    heads[i].x = GUINT32_FROM_LE(config_info->head[i].x);
    heads[i].y = GUINT32_FROM_LE(config_info->head[i].y);
    heads[i].width = GUINT32_FROM_LE(config_info->head[i].width);
    heads[i].height = GUINT32_FROM_LE(config_info->head[i].height);   
}

if (stream_channel)
{
    stream_channel->set_config_stream_monitor_head(heads);   //保存配置
}

在stream-channel.cpp 中的marshall_monitors_config 模块修改,增加读取缓存配置上报的功能,代码如下:

    std::vector<StreamMonitorHead> stream_monitors_config_vec = channel->get_config_stream_monitor_head();
    if (stream_monitors_config_vec.size() == 0)
    {
        spice_warning("stream_monitors_config_vec size is 0, use default config");
        stream_monitors_config_vec  ={{0, 0, 0, 0, channel->width, channel->height}};
    }

    int heads_size = sizeof(SpiceHead) *stream_monitors_config_vec.size();
    auto msg = static_cast<SpiceMsgDisplayMonitorsConfig *>(
    g_malloc0(sizeof(SpiceMsgDisplayMonitorsConfig) + heads_size));
    msg->count = stream_monitors_config_vec.size();
    msg->max_allowed  = stream_monitors_config_vec.size();
  
    for (int i = 0; i < msg->count; i++)
    {
       msg->heads[i].monitor_id = stream_monitors_config_vec[i].monitor_id;
       msg->heads[i].surface_id = 0;
       msg->heads[i].width = stream_monitors_config_vec[i].width;
       msg->heads[i].height = stream_monitors_config_vec[i].height;
       msg->heads[i].x =  stream_monitors_config_vec[i].x;
       msg->heads[i].y = stream_monitors_config_vec[i].y;
       msg->heads[i].flags = 0;
    }

完成以上功能后,就可以通过在虚拟机上配置xrandr 完成上报配置到客户端。

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值