> For the complete documentation index, see [llms.txt](https://app.xesoa.com/untitled/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://app.xesoa.com/untitled/obs-de-cha-jian-hua-ban-shi-xian-ji-yu-qt.md).

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

## 简介

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

<figure><img src="https://1116164914-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2Fp2w13KCEx4ewZUVGqdyS%2Fuploads%2F6HumRntZlXzixh0OopPt%2F466ef9d1-3954-41fb-9ebe-9cce1e99a620.png?alt=media&amp;token=12afe89a-ab7f-4708-94e9-8697e27848b9" 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>
