# OBS的插件-画板实现（基于Qt)

## 简介

OBS基于OpenGL和D3D11实现了一个通用的图形库，可以利用这个图形库来实现简单的画板，至于更复杂的画板实现的原理也是一样的。实现图如下。

<figure><img src="/files/DgbwdqLAY5wFhZ3jfV3R" alt=""><figcaption><p>缩略图</p></figcaption></figure>

## 原理

说明一下，因为OBS自带的做图非常麻烦且无法满足要求，这里使用Qt自带的QPainter进行内存做画，然后输出RGBA像素数据。

在使用插件之前我们要对插件进行注册

```cpp
static void RegisterInkCanvasSource()
{
    struct obs_source_info info = {};

    info.id = "ink_canvas_source";
    info.type = OBS_SOURCE_TYPE_INPUT;
    info.output_flags = OBS_SOURCE_ASYNC_VIDEO | 
        OBS_SOURCE_INTERACTION |
        OBS_SOURCE_DO_NOT_DUPLICATE | OBS_SOURCE_SRGB;
    ...

    info.mouse_click = [](void *data, const struct obs_mouse_event *ev,
        int32_t type, bool mouse_up, uint32_t click_count) {
        ...
    };
    info.mouse_move = [](void *data, const struct obs_mouse_event *ev,
        bool mouse_leave) {
        ...
    };

    obs_register_source(&info);
}

bool obs_module_load(void)
{
    RegisterInkCanvasSource();
    return true;
}
```

### 实现方式一：`obs_source_output_video`

使用异步视频源，注意插件的`output_flags`使用`OBS_SOURCE_ASYNC_VIDEO`这个标志，当画面需要更新时，使用`obs_source_output_video`将帧输出源中。

大致代码如下：

```cpp
QPoint last = QPoint(), moved = QPoint();

void mousePress(QPoint p) 
{// 起笔
    last = p;
}

void mouseMove(QPoint p)
{// 移动绘制
    QPainter p(&image);

    moved = p;

    p->drawLine(last, moved);
    updateRect(QRect(last, moved); 

    last = moved;
    moved = QPoint();
}

void mouseRelease()
{// 收笔
    last = QPoint();
    last = QPoint();
}

// 当需要更新时输出视频帧
void updateRect(QRect rt) {
    obs_source_frame frame;
    memset(&frame, 0, sizeof(frame);

    frame.width = image.width();
    frame.height = image.height();
    frame.format = VIDEO_FORMAT_RGBA;

    m_frame.linesize[0] = image.width() * 4;
    m_frame.data[0] = (uint8_t *)m_backImage.constBits();

    obs_source_output_video(m_source, &m_frame);
}
```

上述代码很好地展示了绘制后立即输出到源上，但上面的代码有一定的性能问题。

问题：当鼠标事件瞬时非常多时，会出现fps过高导致CPU和内存暴涨，那么这时候，我们可以加一个定时器解决，比如我们限定最多30fps。

```cpp
QTimer updateTimer;
bool timerInited = false;
QRect dirtyRect;

// 当需要更新时输出视频帧
void updateRect(QRect rt) {
    dirtyRect = dirtyRect.united(rt); // 求并集
    if (updateTimer.isActive()) {
        return;
    }

    if (!timerInited) {
        updateTimer.setSingleShot(true); // 只触发一次

        connnect(&updateTimer, &QTimer::timeout, this, [](){
            obs_source_frame frame;
            memset(&frame, 0, sizeof(frame);

            frame.width = image.width();
            frame.height = image.height();
            frame.format = VIDEO_FORMAT_RGBA;

            m_frame.linesize[0] = image.width() * 4;
            m_frame.data[0] = (uint8_t *)m_backImage.constBits();

            obs_source_output_video(m_source, &m_frame);
            dirtyRect = QRect();
        });

        timerInited = true;
    }

    updateTimer.start(33);  // 33ms
}
```

我们修改了updateRect代码，使得将更新的频率固定住，这样不管鼠标事件多么的多，我们最多只会33ms更新一次(fps=30)。

上面的代码看上去比之前还要好了，但还是有优化的空间，因为每一次更新的是局部图片，但是输出却是完整的像素，会重复拷贝内存像素数据到显存，分辨率较小时体现不出来，一是分辨率稍微大一点也会有严重的内存和CPU性能问题，此时这种方法已经无法满足要求了。

### 实现方式二：使用纹理

第一种方式使用了异步视频源，这种方式在需要的时候输出帧就可以了，但是有一定的性能问题，那么可以可以使用纹理贴图的方法。如果要使用纹理贴图我们首先要指定插件的渲染回调函数。

```c
static void RegisterInkCanvasSource()
{
    struct obs_source_info info = {};
    ....
    info.video_render = [](void *data, gs_effect_t *effect) {
        VideoRender(data,effect);
    };

    obs_register_source(&info);
}

bool obs_module_load(void)
{
    RegisterInkCanvasSource();
    return true;
}
```

接下来，我们将将内存数据贴到纹理上。

```cpp
void InkCavasSource::VideoRender(gs_effect *effect)
{
    char *buffer;

    // 如果纹理为nullptr,那么进行初始化
    if (texure == nullptr) {
        obs_enter_graphics();
        buffer = (char *)m_image.constBits();
        m_texure = gs_texture_create(width, height, GS_RGBA, 1,
           (const uint8_t **)&buffer, GS_DYNAMIC);
        obs_leave_graphics();
    }

    buffer = (char *)m_image.constBits();
    if (!dirtyRect.isEmpty()) {
        gs_texture_set_image(texure, (const uint8_t *)buffer, width * 4, false);
        dirtyRect = QRectF();
    }
    ....
    gs_eparam_t *const param = gs_effect_get_param_by_name(effect, "image");
    gs_effect_set_texture_srgb(param, texure);
    gs_draw_sprite(texure, 0, width, height);
    ...
}
```

这样就实现了对纹理的贴图，但是上面的方法还有一个性能问题。问题在于，只要渲染就将所有像素数据拷贝到显存，显然是没有必要的。我们应该使用局部更新，在需要更新的时候将那些需要更新的像素拷贝到显卡。也就是dirtyRect那个部分。很遗憾的事OBS没有这个接口，所以我们自己实现一个。

在实现之前我们看一下全局更新的代码，`gs_texture_set_image`如下。

```c
void gs_texture_set_image(gs_texture_t *tex, const uint8_t *data,
              uint32_t linesize, bool flip)
{
    ...
    if (!gs_texture_map(tex, &ptr, &linesize_out))
        return;
    row_copy = (linesize < linesize_out) ? linesize : linesize_out;
    height = gs_texture_get_height(tex);
    if (flip) {// 是否翻转拷贝
        uint8_t *const end = ptr + height * linesize_out;
        data += (height - 1) * linesize;
        while (ptr < end) {
            memcpy(ptr, data, row_copy);
            ptr += linesize_out;
            data -= linesize;
        }
    } else if (linesize == linesize_out) { // 行大小是否一样
        memcpy(ptr, data, row_copy * height);
    } else { // 按行拷贝
        uint8_t *const end = ptr + height * linesize_out;
        while (ptr < end) {
            memcpy(ptr, data, row_copy);
            ptr += linesize_out;
            data += linesize;
        }
    }
    gs_texture_unmap(tex);
}
```

然后在调用的时间传入dirtyRect就可以了。

```c
void InkCavasSource::VideoRender(gs_effect *effect) 
{
    .... 
    buffer = (char *)m_image.constBits();
    if (!dirtyRect.isEmpty()) {
        int x = dirtyRect.x();
        int 
        gs_texture_set_sub_image(m_texure, (const uint8_t *)buffer,
                dirtyRect.x(), dirtyRect.y(), 
                dirtyRect.width(), dirtyRect.height(), 
                width * 4, false);
        dirtyRect = QRectF();
    }
    ....
}
```

到此，所以优化就结束啦。

## 实现

代码整理中，即将扩充各种图形，联系<nie950@gmail.com>


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://app.xesoa.com/untitled/obs-de-cha-jian-hua-ban-shi-xian-ji-yu-qt.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
