首页
/ OBS Studio屏幕标注:实时绘图与标注工具

OBS Studio屏幕标注:实时绘图与标注工具

2026-02-05 05:10:42作者:翟江哲Frasier

痛点直击:直播教学与演示的标注困境

你是否在直播教学时,需要反复切换窗口来强调重点?是否在远程会议中,因无法实时标注屏幕内容导致沟通效率低下?OBS Studio作为开源直播软件的标杆,虽提供强大的音视频处理能力,但原生缺乏屏幕标注功能。本文将从零构建一个高性能标注插件,解决实时绘图、动态标注与直播流无缝融合的核心痛点。

读完本文你将获得:

  • 掌握OBS插件开发的核心框架与生命周期管理
  • 实现支持多种画笔工具的实时标注系统
  • 学会GPU加速绘制技术提升性能
  • 理解帧数据处理与直播流融合的底层原理
  • 获取完整可复用的开源代码与集成指南

OBS插件开发基础架构

插件生命周期管理

OBS Studio采用模块化架构,所有功能通过插件实现。一个标注插件需要实现obs_source_info结构体,定义插件的元数据与回调函数:

struct obs_source_info annotation_filter = {
    .id = "annotation_filter",
    .type = OBS_SOURCE_TYPE_FILTER,
    .output_flags = OBS_SOURCE_ASYNC_VIDEO,
    .get_name = annotation_get_name,
    .create = annotation_create,
    .destroy = annotation_destroy,
    .activate = annotation_activate,
    .deactivate = annotation_deactivate,
    .get_properties = annotation_get_properties,
    .update = annotation_update,
    .video_render = annotation_video_render,
    .video_tick = annotation_video_tick,
};

关键生命周期函数说明:

函数名 作用 调用时机
create 初始化标注数据结构 添加滤镜时
destroy 释放资源 移除滤镜时
activate 启动标注会话 滤镜激活时
deactivate 结束标注会话 滤镜停用或切换场景时
video_render 执行绘制逻辑 每帧渲染前
video_tick 处理时间相关逻辑 每帧更新时

帧数据处理流程

OBS视频处理采用流水线架构,标注插件作为滤镜插入该流水线。核心数据流向如下:

flowchart LR
    A[输入源] --> B[前置滤镜]
    B --> C[标注滤镜]
    C --> D[后置滤镜]
    D --> E[编码器/显示器]
    
    subgraph 标注滤镜内部
        C1[接收原始帧] --> C2[GPU绘制标注]
        C3[用户输入] --> C4[路径记录]
        C4 --> C2
    end

标注系统核心实现

数据结构设计

设计高效的路径存储结构是实现流畅标注的基础:

typedef struct {
    float x;          // 规范化X坐标 (0-1)
    float y;          // 规范化Y坐标 (0-1)
    float pressure;   // 压力值 (0-1)
    uint64_t timestamp; // 时间戳(毫秒)
} AnnotationPoint;

typedef struct {
    AnnotationPoint *points;
    size_t point_count;
    size_t point_capacity;
    uint32_t color;    // ARGB格式
    float width;       // 画笔宽度(像素)
    enum BrushType type; // 画笔类型
} AnnotationStroke;

typedef struct {
    AnnotationStroke *strokes;
    size_t stroke_count;
    bool is_drawing;
    // GPU资源
    GLuint vao;
    GLuint vbo;
    GLuint program;
    // 配置参数
    float default_width;
    uint32_t default_color;
} AnnotationData;

采用规范化坐标(0-1范围)而非像素坐标,确保在不同分辨率下保持一致的绘制效果。

画笔引擎实现

实现支持多种画笔类型的渲染系统,利用GPU加速提升性能:

static void render_strokes(AnnotationData *data, obs_source_t *source) {
    if (!data->stroke_count) return;
    
    // 绑定着色器程序
    glUseProgram(data->program);
    
    // 设置MVP矩阵
    struct obs_source_frame_info frame_info;
    obs_source_get_frame_info(source, &frame_info);
    GLfloat mvp[4][4];
    matrix4_from_camera(mvp, &frame_info.camera);
    glUniformMatrix4fv(glGetUniformLocation(data->program, "mvp"), 
                      1, GL_FALSE, (const GLfloat*)mvp);
    
    // 渲染每个笔画
    for (size_t i = 0; i < data->stroke_count; i++) {
        AnnotationStroke *stroke = &data->strokes[i];
        if (stroke->point_count < 2) continue;
        
        // 更新顶点缓冲区
        glBindBuffer(GL_ARRAY_BUFFER, data->vbo);
        glBufferData(GL_ARRAY_BUFFER, 
                    stroke->point_count * sizeof(AnnotationPoint),
                    stroke->points, GL_STREAM_DRAW);
        
        // 设置颜色和宽度
        glUniform4f(glGetUniformLocation(data->program, "color"),
                   (stroke->color >> 16) / 255.0f,  // R
                   (stroke->color >> 8) / 255.0f,   // G
                   (stroke->color) / 255.0f,        // B
                   (stroke->color >> 24) / 255.0f); // A
        glUniform1f(glGetUniformLocation(data->program, "width"), 
                   stroke->width);
        
        // 绘制线段
        glDrawArrays(GL_LINE_STRIP, 0, stroke->point_count);
    }
    
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    glUseProgram(0);
}

输入处理系统

实现跨平台的鼠标/触摸输入处理:

static void handle_mouse_event(AnnotationData *data, 
                              struct obs_mouse_event *event) {
    if (event->button == MOUSE_LEFT && event->action == MOUSE_DOWN) {
        // 开始新笔画
        data->is_drawing = true;
        data->strokes = realloc(data->strokes, 
                               (data->stroke_count + 1) * sizeof(AnnotationStroke));
        AnnotationStroke *new_stroke = &data->strokes[data->stroke_count++];
        new_stroke->points = NULL;
        new_stroke->point_count = 0;
        new_stroke->point_capacity = 0;
        new_stroke->color = data->default_color;
        new_stroke->width = data->default_width;
        new_stroke->type = data->current_brush;
    } else if (event->button == MOUSE_LEFT && event->action == MOUSE_UP) {
        // 结束笔画
        data->is_drawing = false;
    } else if (data->is_drawing && event->action == MOUSE_MOVE) {
        // 添加笔画点
        AnnotationStroke *current = &data->strokes[data->stroke_count - 1];
        if (current->point_count >= current->point_capacity) {
            current->point_capacity = MAX(current->point_capacity * 2, 32);
            current->points = realloc(current->points, 
                                     current->point_capacity * sizeof(AnnotationPoint));
        }
        
        AnnotationPoint *p = &current->points[current->point_count++];
        p->x = event->x / (float)event->width;  // 规范化X坐标
        p->y = event->y / (float)event->height; // 规范化Y坐标
        p->pressure = event->pressure;
        p->timestamp = os_gettime_ns() / 1000000; // 毫秒级时间戳
    }
}

高级功能实现

多种画笔效果

通过片段着色器实现不同画笔风格:

// 圆形画笔
float circleBrush(vec2 uv, vec2 center, float radius) {
    float d = distance(uv, center);
    return 1.0 - smoothstep(radius - 0.5, radius + 0.5, d);
}

// 喷枪效果
float sprayBrush(vec2 uv, vec2 center, float radius) {
    float d = distance(uv, center);
    if (d > radius) return 0.0;
    
    // 随机散布点
    float count = 0.0;
    for (int i = 0; i < 20; i++) {
        vec2 offset = vec2(random(uv + vec2(i)*0.1), random(uv - vec2(i)*0.1)) * radius;
        if (distance(uv, center + offset) < radius * 0.5) {
            count += 0.05;
        }
    }
    return count;
}

// 纹理画笔
float texturedBrush(vec2 uv, vec2 center, float radius, sampler2D texture) {
    float d = distance(uv, center);
    if (d > radius) return 0.0;
    
    vec2 texUV = (uv - center) / radius * 0.5 + 0.5;
    return texture2D(texture, texUV).r * (1.0 - d/radius);
}

撤销/重做系统

实现基于命令模式的操作历史记录:

typedef enum {
    ANNOTATION_CMD_DRAW_STROKE,
    ANNOTATION_CMD_CLEAR_ALL,
} AnnotationCommandType;

typedef struct {
    AnnotationCommandType type;
    union {
        struct {
            size_t start_index;
            size_t count;
            AnnotationStroke *strokes;
        } draw;
    } data;
} AnnotationCommand;

// 记录绘制操作
static void record_draw_command(AnnotationData *data) {
    if (data->stroke_count == 0) return;
    
    AnnotationCommand cmd = {
        .type = ANNOTATION_CMD_DRAW_STROKE,
        .data.draw.start_index = data->stroke_count - 1,
        .data.draw.count = 1,
        .data.draw.strokes = malloc(sizeof(AnnotationStroke)),
    };
    memcpy(cmd.data.draw.strokes, 
          &data->strokes[data->stroke_count - 1], 
          sizeof(AnnotationStroke));
    
    // 添加到命令历史
    data->undo_stack = realloc(data->undo_stack, 
                              (data->undo_count + 1) * sizeof(AnnotationCommand));
    data->undo_stack[data->undo_count++] = cmd;
    
    // 清空重做栈
    clear_redo_stack(data);
}

// 撤销操作实现
static void annotation_undo(AnnotationData *data) {
    if (data->undo_count == 0) return;
    
    AnnotationCommand cmd = data->undo_stack[--data->undo_count];
    switch (cmd.type) {
        case ANNOTATION_CMD_DRAW_STROKE:
            // 将撤销的笔画移到重做栈
            data->redo_stack = realloc(data->redo_stack, 
                                      (data->redo_count + 1) * sizeof(AnnotationCommand));
            data->redo_stack[data->redo_count++] = cmd;
            
            // 从当前笔画列表移除
            data->stroke_count -= cmd.data.draw.count;
            break;
        // 其他命令类型处理...
    }
}

性能优化策略

GPU加速渲染

利用OBS的图形API抽象层实现跨平台GPU加速:

static bool init_gpu_resources(AnnotationData *data) {
    // 创建着色器程序
    const char *vs = 
        "attribute vec2 position;\n"
        "uniform mat4 mvp;\n"
        "void main() {\n"
        "   gl_Position = mvp * vec4(position, 0.0, 1.0);\n"
        "}\n";
    
    const char *fs = 
        "uniform vec4 color;\n"
        "uniform float width;\n"
        "void main() {\n"
        "   gl_FragColor = color;\n"
        "}\n";
    
    data->program = gl_create_program(vs, fs);
    if (!data->program) return false;
    
    // 创建顶点数组对象
    glGenVertexArrays(1, &data->vao);
    glBindVertexArray(data->vao);
    
    // 创建顶点缓冲对象
    glGenBuffers(1, &data->vbo);
    glBindBuffer(GL_ARRAY_BUFFER, data->vbo);
    
    // 设置顶点属性指针
    GLint pos_attr = glGetAttribLocation(data->program, "position");
    glVertexAttribPointer(pos_attr, 2, GL_FLOAT, GL_FALSE, 
                         sizeof(AnnotationPoint), (void*)offsetof(AnnotationPoint, x));
    glEnableVertexAttribArray(pos_attr);
    
    glBindVertexArray(0);
    glBindBuffer(GL_ARRAY_BUFFER, 0);
    
    return true;
}

帧数据处理优化

采用双缓冲技术减少绘制延迟:

static void annotation_video_render(void *data, gs_effect_t *effect) {
    AnnotationData *annotation = data;
    if (!annotation->is_active || !annotation->stroke_count) {
        // 无标注内容时直接传递原始帧
        obs_source_skip_video_filter(data);
        return;
    }
    
    // 获取输入纹理
    gs_texture_t *input = obs_filter_get_video_texture(data);
    if (!input) return;
    
    // 获取纹理尺寸
    uint32_t width = gs_texture_get_width(input);
    uint32_t height = gs_texture_get_height(input);
    
    // 创建临时渲染目标
    gs_texture_t *target = gs_texrender_get_texture(annotation->texrender);
    if (!gs_texrender_begin(annotation->texrender, width, height)) {
        gs_texrender_reset(annotation->texrender);
        return;
    }
    
    // 复制原始帧到目标
    gs_effect_t *copy_effect = obs_get_base_effect(OBS_EFFECT_DEFAULT);
    gs_effect_set_texture(gs_effect_get_param_by_name(copy_effect, "image"), input);
    gs_draw_sprite(input, 0, width, height);
    
    // 渲染标注内容
    render_strokes(annotation, obs_filter_get_parent(data));
    
    // 完成渲染并输出结果
    gs_texrender_end(annotation->texrender);
    gs_effect_set_texture(gs_effect_get_param_by_name(effect, "image"), target);
    
    // 释放资源
    gs_texture_release(input);
}

插件集成与使用指南

编译配置

在插件目录创建CMakeLists.txt

cmake_minimum_required(VERSION 3.14)
project(obs-annotation-filter)

set(CMAKE_PREFIX_PATH "${QTDIR}")
set(CMAKE_INCLUDE_CURRENT_DIR ON)
set(CMAKE_AUTOMOC ON)
set(CMAKE_AUTOUIC ON)

find_package(LibObs REQUIRED)
find_package(Qt5 COMPONENTS Widgets REQUIRED)

set(SOURCES
    annotation-filter.c
    annotation-gui.cpp
    annotation-render.c
)

set(HEADERS
    annotation-filter.h
    annotation-gui.h
    annotation-render.h
)

add_library(obs-annotation-filter MODULE
    ${SOURCES}
    ${HEADERS}
)

target_link_libraries(obs-annotation-filter
    libobs
    Qt5::Widgets
)

set_target_properties(obs-annotation-filter PROPERTIES
    CXX_STANDARD 17
    CXX_STANDARD_REQUIRED YES
    CXX_EXTENSIONS NO
)

install(TARGETS obs-annotation-filter
    LIBRARY DESTINATION "${OBS_PLUGIN_DESTINATION}"
)

使用流程

  1. 安装插件

    • 将编译好的插件文件复制到OBS插件目录
    • Windows: C:\Program Files\obs-studio\obs-plugins\64bit\
    • macOS: ~/Library/Application Support/obs-studio/plugins/
    • Linux: ~/.config/obs-studio/plugins/
  2. 添加标注滤镜

    • 在来源列表中选择需要标注的场景或媒体源
    • 右键选择"滤镜"→"添加"→"视频滤镜"→"屏幕标注工具"
  3. 使用标注功能

    • 在"属性"面板调整画笔颜色、大小和类型
    • 启用"允许标注"开始绘图
    • 使用鼠标拖动进行绘制
    • 通过控制面板执行撤销/重做/清除操作

高级应用场景

多机位切换标注

通过场景记忆功能实现多机位标注内容的独立管理:

static void annotation_scene_changed(void *data, calldata_t *params) {
    AnnotationData *annotation = data;
    obs_source_t *source = calldata_ptr(params, "source");
    if (!source) return;
    
    // 保存当前场景的标注数据
    const char *scene_name = obs_source_get_name(source);
    save_annotation_state(annotation, scene_name);
    
    // 加载新场景的标注数据
    load_annotation_state(annotation, scene_name);
}

触控设备支持

添加对Wacom等压感设备的支持:

static void handle_tablet_event(AnnotationData *data, struct obs_tablet_event *event) {
    // 压感影响画笔宽度
    float adjusted_width = data->default_width * event->pressure;
    
    // 倾斜角度影响画笔形状
    float tilt_x = event->tilt_x;
    float tilt_y = event->tilt_y;
    
    // 在当前笔画中应用压感数据
    AnnotationStroke *current = &data->strokes[data->stroke_count - 1];
    current->width = adjusted_width;
    
    // 添加带压感信息的点
    AnnotationPoint *p = &current->points[current->point_count - 1];
    p->pressure = event->pressure;
    p->tilt_x = tilt_x;
    p->tilt_y = tilt_y;
}

问题排查与解决方案

常见问题处理

问题 原因 解决方案
标注内容闪烁 帧率不匹配 启用垂直同步或缓冲绘制命令
高分辨率下卡顿 CPU绘制瓶颈 迁移至GPU渲染路径
插件加载失败 依赖缺失 静态链接Qt库或提示安装依赖包
标注内容错位 坐标转换错误 使用规范化坐标系统

性能优化检查清单

  • [ ] 已启用GPU加速渲染
  • [ ] 实现顶点缓冲对象(VBO)复用
  • [ ] 采用增量绘制而非重绘全部内容
  • [ ] 限制最大笔画点数(如每笔画1000点)
  • [ ] 使用纹理压缩存储笔刷纹理
  • [ ] 禁用时释放GPU资源
  • [ ] 实现绘制命令批处理

开源与扩展

本插件采用GPLv2许可证开源,完整代码可通过以下方式获取:

git clone https://gitcode.com/GitHub_Trending/ob/obs-studio
cd obs-studio/plugins
git clone https://gitcode.com/yourusername/obs-annotation-filter

功能扩展建议

  1. 协作标注系统

    • 添加WebSocket API实现多用户远程标注
    • 实现标注操作的网络同步
  2. AI辅助标注

    • 集成文本识别自动生成标注
    • 添加目标检测辅助重点标记
  3. 标注内容导出

    • 支持将标注内容导出为SVG/PNG格式
    • 添加标注轨迹的视频录制功能

总结与展望

本文详细介绍了OBS Studio标注插件的设计原理与实现细节,从基础架构到高级功能,全面覆盖插件开发的各个方面。通过学习本文,你不仅能掌握OBS插件开发的核心技术,还能理解实时图形处理、用户输入管理和GPU加速等关键技术点。

随着远程协作和在线教育需求的增长,屏幕标注功能将成为直播软件的标配。未来版本可进一步优化触控体验、增强AI辅助功能,并探索AR标注等创新应用场景。我们欢迎开发者参与贡献,共同完善这一工具生态。

请收藏本文以备开发参考,关注项目仓库获取更新,如有问题可提交issue或参与社区讨论。

附录:API速查手册

核心结构体

结构体 作用 关键成员
obs_source_info 插件元数据 id, type, 生命周期回调函数
AnnotationData 标注状态管理 笔画列表、GPU资源、配置参数
AnnotationStroke 单笔画数据 点数组、颜色、宽度、画笔类型
AnnotationPoint 笔画采样点 坐标、压力、时间戳

关键函数

函数 功能 参数
obs_register_source 注册插件 const struct obs_source_info *info
obs_source_create 创建源实例 const char *id, const char *name, obs_data_t *settings, obs_data_t *hotkey_data
gs_effect_create 创建着色器程序 const char *effect_string, const char *name
gs_texrender_begin 开始纹理渲染 gs_texrender_t *texrender, uint32_t width, uint32_t height
登录后查看全文
热门项目推荐
相关项目推荐