我要提问
ARTICLE DETAIL

资讯详情

前沿编程新知与开发实战干货的深度解读。

OBS插件开发实战:实现Excel数据动态读取与直播展示

OBS插件开发实战:实现Excel数据动态读取与直播展示 在实际直播、录屏或内容创作过程中我们经常需要将数据实时展示在屏幕上。无论是直播间的实时数据看板、游戏内的排行榜还是教学演示中的动态图表手动更新这些信息不仅繁琐还容易出错。OBS Studio 作为一款强大的开源直播和录屏软件其核心功能在于捕获和混合多种来源的视频、音频但它本身并不直接支持读取和解析像 Excel 这样的结构化数据文件。这时一个能够连接 OBS 与 Excel 的插件就显得至关重要它能让你的直播内容“活”起来实现数据驱动的动态展示。本文将带你深入理解如何让 OBS 读取 Excel 数据并实现动态更新。我们将从 OBS 插件的工作原理讲起探讨几种主流的技术实现路径然后通过一个具体的、可操作的示例项目手把手教你构建一个简易的“Excel 数据源”插件。即使你不是专业的 C 开发者也能通过本文理解其核心机制并利用脚本或现有工具实现类似功能。文章最后会详细分析常见问题、排查路径并给出生产环境下的最佳实践建议。1. 理解 OBS 插件与数据源的工作机制在动手之前必须先理解 OBS 是如何工作的以及插件在其中扮演的角色。OBS 的核心是一个基于场景Scene和源Source的合成引擎。一个“源”可以是图像、文本、浏览器窗口、游戏捕获、媒体文件等。插件的主要作用就是为 OBS 创建新的“源”类型或提供新的“滤镜”Filter功能。1.1 OBS 插件的基本结构一个标准的 OBS 插件通常是一个动态链接库DLL 文件在 Windows 上它需要导出 OBS 定义好的一系列接口函数。最关键的两个接口是obs_module_load: 插件被加载时调用用于注册插件提供的所有“源”、“滤镜”、“输出”、“编码器”等。obs_source_info结构体: 定义一个“源”的全部行为包括其名称、创建函数、销毁函数、渲染函数、更新函数、获取属性函数等。当你在 OBS 的“来源”面板点击“添加”列表中出现的每一项如“图像”、“文本”、“浏览器”等都对应一个已注册的obs_source_info结构体。我们想要创建的“Excel 数据源”本质上就是定义一个新的obs_source_info并在其渲染函数中将读取到的 Excel 数据绘制成纹理Texture最终显示在 OBS 的画面上。1.2 数据动态更新的挑战OBS 的主循环会不断刷新画面。对于静态源如图片渲染一次即可。但对于需要动态更新的源如我们的 Excel 数据源必须在每次渲染前检查数据是否有变化。这通常通过以下两种机制实现定时器Timer: 在插件内部启动一个线程或利用 OBS 的obs_source_tick回调定期例如每秒读取 Excel 文件检查内容是否改变并标记源为“需要更新”。文件监控File Watcher: 使用操作系统 API 监控 Excel 文件的修改事件当文件被保存时立即触发数据读取和更新。考虑到 Excel 文件可能被其他程序如 Microsoft Excel、WPS频繁保存文件监控是更实时、更高效的方案。1.3 Excel 文件解析方案选型OBS 插件通常使用 C/C 编写以获得最佳性能。在 C 中读取 Excel 文件有多种选择各有优劣方案库/工具优点缺点适用场景直接解析libxlsxwriter (仅写), libxl (商业)无需依赖 Office部署简单。功能可能受限复杂格式支持差。商业库需授权。读取结构简单的.xlsx文件数据。通过 COM 调用Windows COM API功能最全能利用本地安装的 Excel 所有能力。严重依赖本地 Excel 安装跨平台差稳定性受 Excel 进程影响。Windows 环境且需要读取复杂公式、图表等。中间格式转换将 Excel 另存为 CSV/JSON实现极其简单通用性强。需要手动或借助脚本转换非实时。对实时性要求不高或数据由其他系统生成。脚本桥接使用 Lua/Python 脚本插件开发快速可利用丰富的脚本库如 pandas。性能开销大依赖脚本解释器环境。原型验证、快速实现或 OBS 已安装对应脚本插件。对于大多数直播数据展示场景数据格式相对简单我们追求稳定和轻量。因此将 Excel 文件实时转换为 CSV再由插件读取 CSV是一个平衡了实现难度、性能和稳定性的务实方案。下文将主要围绕此方案展开。2. 环境准备与项目结构规划在开始编码前需要搭建一个完整的 OBS 插件开发环境。这不仅仅是安装一个 IDE还需要配置 OBS 的 SDK 和构建系统。2.1 开发环境配置操作系统: Windows 10/11 (本文以 Windows 为例Linux/macOS 原理类似但构建细节不同)核心工具:OBS Studio: 安装最新稳定版。我们将以其为运行时和参考。OBS 源码与 Libobs: 从 GitHub (obsproject/obs-studio) 克隆源码。我们主要需要其头文件include目录和库文件build或lib目录。CMake: 用于生成跨平台的构建文件如 Visual Studio 的.sln。C 编译器: Visual Studio 2019/2022 的 MSVC 编译器。Git: 用于版本管理和获取源码。具体步骤:安装 Visual Studio确保勾选“使用 C 的桌面开发”工作负载。安装 CMake 并确保其已加入系统 PATH。从 GitHub 克隆 OBS Studio 源码git clone --recursive https://github.com/obsproject/obs-studio.git按照 OBS 官方 Wiki 的指引使用 CMake 配置并生成 Libobs 的库文件。通常在源码的build目录下执行cmake ..并指定生成器为 Visual Studio然后打开生成的.sln文件编译libobs项目。2.2 插件项目结构一个典型的 OBS 插件项目目录结构如下obs-excel-source-plugin/ ├── CMakeLists.txt # 项目构建主文件 ├── data/ │ ├── locale/ │ │ └── en-US.ini # 插件界面语言文件 │ └── obs-excel-source.png # 插件图标 ├── src/ │ ├── excel-source.cpp # 插件核心实现 │ ├── excel-source.h │ ├── plugin-main.cpp # 插件入口obs_module_load等 │ └── excel-file-watcher.cpp # 文件监控实现 └── cmake/ └── Modules/ └── FindLibobs.cmake # 查找Libobs的CMake模块关键文件说明:CMakeLists.txt: 定义了如何查找 OBS 库、编译源文件、生成 DLL 以及将插件安装到 OBS 的插件目录。src/plugin-main.cpp: 必须包含obs_module_load和obs_module_unload函数在这里注册我们的“Excel 数据源”。src/excel-source.cpp/h: 实现obs_source_info结构体定义的所有回调函数是插件的核心逻辑所在。excel-file-watcher.cpp: 封装系统相关的文件监控逻辑用于监听 Excel 文件变化。2.3 依赖库引入我们的方案需要读取 CSV。C 标准库没有直接的 CSV 解析器我们可以使用一个轻量级、头文件-only 的库如csv2或fast-cpp-csv-parser。这里以fast-cpp-csv-parser为例从其 GitHub 仓库下载csv.h头文件。将其放入项目的include/目录或src/目录。在CMakeLists.txt中通过include_directories将其路径加入头文件搜索路径。对于文件监控Windows 上可以使用ReadDirectoryChangesWAPI或者使用跨平台库如efsw。为了简化我们将在核心实现部分展示一个简化的轮询Polling方案生产环境建议使用更高效的通知机制。3. 核心实现构建一个简易 Excel (CSV) 数据源现在我们开始实现插件的核心逻辑。我们将创建一个名为“Excel Table”的源它读取一个 CSV 文件由 Excel 另存而来并将其内容以文本表格的形式渲染到 OBS 中。3.1 定义源信息结构首先在excel-source.h中定义我们的源类和数据结构。// excel-source.h #pragma once #include obs-module.h #include string #include vector #include chrono #include mutex class ExcelSource { public: ExcelSource(obs_source_t *source, obs_data_t *settings); ~ExcelSource(); // OBS 标准回调函数 void update(obs_data_t *settings); void tick(float seconds); void render(gs_effect_t *effect); obs_properties_t *getProperties(); void save(obs_data_t *settings); // 自定义函数 bool loadCSVData(); private: obs_source_t *source; // 关联的 OBS 源对象 std::string filePath; // CSV 文件路径 long long lastModTime; // 文件最后修改时间 std::vectorstd::vectorstd::string tableData; // 存储解析后的表格数据 std::mutex dataMutex; // 保护 tableData 的互斥锁 // 渲染相关 uint32_t width 0; uint32_t height 0; gs_vertbuffer_t *vertexBuffer nullptr; void updateRenderData(); };3.2 实现源的生命周期回调在excel-source.cpp中实现构造函数、析构函数和 OBS 要求的几个关键回调。// excel-source.cpp #include excel-source.h #include csv.h // 引入 CSV 解析库 #include fstream #include sys/stat.h // 获取文件修改时间Windows 示例 long long getFileModTime(const std::string path) { struct _stat64 fileStat; if (_stat64(path.c_str(), fileStat) 0) { return fileStat.st_mtime; } return 0; } ExcelSource::ExcelSource(obs_source_t *source, obs_data_t *settings) : source(source) { // 从设置中初始化文件路径 update(settings); } ExcelSource::~ExcelSource() { if (vertexBuffer) { gs_vertexbuffer_destroy(vertexBuffer); } } void ExcelSource::update(obs_data_t *settings) { // 从 OBS 属性对话框获取用户设置 const char *newPath obs_data_get_string(settings, file_path); bool pathChanged (filePath ! newPath); filePath newPath ? newPath : ; // 如果文件路径改变强制重新加载数据 if (pathChanged !filePath.empty()) { loadCSVData(); } } void ExcelSource::tick(float seconds) { // 每秒检查一次文件是否被修改简化方案生产环境应用文件通知 static float counter 0.0f; counter seconds; if (counter 1.0f) { // 约每秒一次 counter 0.0f; if (!filePath.empty()) { long long currentModTime getFileModTime(filePath); if (currentModTime ! lastModTime) { lastModTime currentModTime; loadCSVData(); // 文件已修改重新加载 } } } } bool ExcelSource::loadCSVData() { if (filePath.empty()) return false; std::ifstream file(filePath); if (!file.is_open()) { blog(LOG_WARNING, [Excel Source] Failed to open file: %s, filePath.c_str()); return false; } io::CSVReader0 csvReader(filePath); // 0 表示自动检测列数 // 假设我们不知道列数逐行读取 std::vectorstd::vectorstd::string newData; std::string line; while (std::getline(file, line)) { std::vectorstd::string row; // 这里需要实现一个简单的 CSV 行解析考虑引号和逗号。 // 为简化示例我们使用 stringstream 按逗号分割不处理带引号的逗号。 std::stringstream ss(line); std::string cell; while (std::getline(ss, cell, ,)) { row.push_back(cell); } if (!row.empty()) { newData.push_back(std::move(row)); } } // 加锁更新数据 { std::lock_guardstd::mutex lock(dataMutex); tableData std::move(newData); } blog(LOG_INFO, [Excel Source] Loaded %llu rows from %s, (unsigned long long)tableData.size(), filePath.c_str()); // 标记源需要重新渲染 obs_source_update(source, nullptr); return true; }3.3 实现渲染逻辑渲染部分负责将tableData中的文本绘制到屏幕上。OBS 使用 Graphics API 进行渲染。为了简化我们使用 OBS 提供的gs_draw_sprite和文本渲染 API 的辅助功能。更复杂的表格渲染可能需要自己计算顶点和纹理坐标。void ExcelSource::render(gs_effect_t *effect) { if (tableData.empty()) { // 可以渲染一个“无数据”提示 return; } // 获取当前源的宽度和高度由用户在 OBS 中拖动设置 width obs_source_get_width(source); height obs_source_get_height(source); if (width 0 || height 0) return; // 计算字体大小和单元格尺寸 float cellHeight height / (float)tableData.size(); float fontSize cellHeight * 0.8f; // 字体大小为单元格高度的80% // 开始渲染 gs_matrix_push(); gs_matrix_identity(); // 简单遍历并绘制每一行每一列的文本 // 注意这是一个非常简化的示例实际需要处理列宽、对齐、换行等。 for (size_t row 0; row tableData.size(); row) { float yPos row * cellHeight; for (size_t col 0; col tableData[row].size(); col) { float xPos col * 100.0f; // 固定列宽实际应根据内容计算 // 使用 OBS 的文本渲染功能需创建并管理 obs_source_t 作为子源此处省略 // 更简单的方案使用 gs_texture 和 sprite 绘制但文本需先转为纹理。 // 此处仅示意流程。 // drawText(tableData[row][col], xPos, yPos, fontSize); } } gs_matrix_pop(); }注意上述render函数中的文本绘制是示意性的。OBS 没有直接绘制文本的简单 API。通常的做法是创建一个“文本源”obs_source_t作为子源或者使用 FreeType 等库自己渲染文本到纹理再绘制纹理。这是实现中最复杂的部分之一需要大量的图形编程工作。许多现有插件选择嵌入一个浏览器源来显示 HTML 表格以规避复杂的文本渲染。3.4 定义插件属性用户界面getProperties函数定义了用户在 OBS 源属性对话框中看到的控件。obs_properties_t *ExcelSource::getProperties() { obs_properties_t *props obs_properties_create(); // 添加一个文件路径选择器 obs_properties_add_path(props, file_path, CSV File Path, OBS_PATH_FILE, CSV files (*.csv), nullptr); // 可以添加其他设置如字体、颜色、刷新间隔等 // obs_properties_add_font(props, font, Font); // obs_properties_add_color(props, text_color, Text Color); // obs_properties_add_int(props, update_interval, Update Interval (ms), 100, 10000, 100); return props; } void ExcelSource::save(obs_data_t *settings) { // 保存当前设置OBS 会自动处理基本类型复杂类型需要手动序列化 obs_data_set_string(settings, file_path, filePath.c_str()); }3.5 注册源到 OBS最后在plugin-main.cpp中我们需要定义obs_source_info并注册它。// plugin-main.cpp #include obs-module.h #include excel-source.h OBS_DECLARE_MODULE() OBS_MODULE_USE_DEFAULT_LOCALE(obs-excel-source, en-US) // 声明源的信息结构 extern struct obs_source_info excel_source_info; bool obs_module_load(void) { obs_register_source(excel_source_info); blog(LOG_INFO, [Excel Source] Plugin loaded successfully.); return true; } void obs_module_unload(void) { blog(LOG_INFO, [Excel Source] Plugin unloaded.); } // 实现 obs_source_info struct obs_source_info excel_source_info { .id excel_table_source, // 源的唯一标识符 .type OBS_SOURCE_TYPE_INPUT, // 输入源 .output_flags OBS_SOURCE_VIDEO, // 输出视频 .get_name [](void*) { return Excel Table; }, // 显示名称 .create [](obs_data_t *settings, obs_source_t *source) { return (void*)new ExcelSource(source, settings); }, .destroy [](void *data) { delete static_castExcelSource*(data); }, .get_width [](void *data) { auto s static_castExcelSource*(data); return s-width; // 需要根据实际渲染内容计算 }, .get_height [](void *data) { auto s static_castExcelSource*(data); return s-height; }, .get_defaults [](obs_data_t *settings) { // 设置默认值 obs_data_set_default_string(settings, file_path, ); }, .get_properties [](void *data) { auto s static_castExcelSource*(data); return s-getProperties(); }, .update [](void *data, obs_data_t *settings) { auto s static_castExcelSource*(data); s-update(settings); }, .video_tick [](void *data, float seconds) { auto s static_castExcelSource*(data); s-tick(seconds); }, .video_render [](void *data, gs_effect_t *effect) { auto s static_castExcelSource*(data); s-render(effect); }, .save [](void *data, obs_data_t *settings) { auto s static_castExcelSource*(data); s-save(settings); }, };4. 构建、部署与验证流程完成代码编写后下一步是将其编译成 OBS 可加载的插件。4.1 配置 CMake 构建CMakeLists.txt是关键它需要正确找到 OBS 的开发库。cmake_minimum_required(VERSION 3.15) project(obs-excel-source) set(CMAKE_CXX_STANDARD 17) # 查找 OBS 库 find_package(Libobs REQUIRED) find_package(obs-frontend-api REQUIRED) # 如果需要前端API include_directories(${LIBOBS_INCLUDE_DIR}) include_directories(${CMAKE_CURRENT_SOURCE_DIR}/src) # 添加插件目标 add_library(obs-excel-source MODULE src/plugin-main.cpp src/excel-source.cpp src/excel-file-watcher.cpp ) target_link_libraries(obs-excel-source ${LIBOBS_LIBRARIES} # 其他库如 csv 解析库如果是头文件库则不需要链接 ) # 设置输出目录为 OBS 插件目录开发时方便调试 set_target_properties(obs-excel-source PROPERTIES PREFIX SUFFIX .dll # Windows LIBRARY_OUTPUT_DIRECTORY ${CMAKE_SOURCE_DIR}/output ) # 安装规则make install 或 Visual Studio 的 INSTALL 项目 install(TARGETS obs-excel-source DESTINATION ${OBS_PLUGIN_DESTINATION}/obs-plugins/64bit ) install(DIRECTORY data/ DESTINATION ${OBS_DATA_DESTINATION}/obs-plugins/obs-excel-source )4.2 编译与安装在项目根目录创建build文件夹。打开命令行进入build目录。执行 CMake 配置命令指定 OBS 库的路径cmake .. -DLIBOBS_INCLUDE_DIRD:/path/to/obs-studio/libobs -DLIBOBS_LIBD:/path/to/obs-studio/build/libobs/Release/obs.lib -DOBS_PLUGIN_DESTINATIOND:/path/to/obs-studio/build/rundir/Release/obs-plugins请将路径替换为你实际的 OBS 源码和构建目录使用 CMake 生成的解决方案文件如obs-excel-source.sln在 Visual Studio 中打开选择Release配置进行编译。编译成功后生成的obs-excel-source.dll文件会自动或手动被复制到 OBS 的插件目录例如%ProgramFiles%\obs-studio\obs-plugins\64bit\。4.3 在 OBS 中验证启动 OBS Studio。在“来源”面板点击“”你应该能在列表中找到“Excel Table”我们定义的名称。添加该源会弹出属性对话框。点击“浏览”选择一个 CSV 文件你可以先在 Excel 中创建一个简单的表格然后另存为 CSV。点击“确定”。此时OBS 画布上应该会出现一个源。由于我们的渲染函数是简化的可能看不到内容或者只看到一个空的矩形。打开 OBS 的“视图”菜单下的“日志”窗口查看是否有我们插件打印的日志信息如“Loaded X rows from...”。这是验证插件是否被加载和数据是否被读取的关键。5. 常见问题排查与解决方案开发和使用此类插件时会遇到各种问题。下面是一个从现象到原因的排查指南。问题现象可能原因检查方式解决方案插件在来源列表中不显示1. DLL 未放入正确的插件目录。2. DLL 依赖项缺失如 VC 运行时。3. 插件编译架构32/64位与 OBS 不匹配。4.obs_module_load函数未正确导出或返回 false。1. 检查obs-plugins/64bit/目录。2. 使用 Dependency Walker 或dumpbin /dependents检查 DLL。3. 确认 OBS 和插件都是 64 位。4. 查看 OBS 日志文件%APPDATA%\obs-studio\logs。1. 将 DLL 复制到正确位置。2. 安装对应版本的 Visual C Redistributable。3. 重新编译为正确架构。4. 检查插件代码确保OBS_DECLARE_MODULE宏使用正确。添加源后 OBS 崩溃1. 插件代码存在内存访问越界、空指针解引用。2. 在错误的线程调用了 OBS API如渲染函数中创建源。3. 全局或静态变量初始化顺序问题。1. 使用调试器如 Visual Studio附加到 OBS 进程进行调试。2. 检查所有 OBS API 调用是否在主线程视频渲染线程。3. 简化代码排查初始化逻辑。1. 使用try-catch捕获 C 异常但无法捕获所有崩溃。2. 确保只在create,destroy,update,get_properties,video_tick,video_render回调中操作数据。3. 将复杂初始化移到create函数中。文件修改后数据不更新1. 文件监控未生效轮询间隔太长或逻辑错误。2. 文件路径被占用或权限不足。3. CSV 解析失败但未记录错误日志。1. 在tick或文件监控回调中打印调试日志。2. 检查文件是否被 Excel 以独占模式打开。3. 在loadCSVData函数中添加更详细的错误日志。1. 缩短轮询间隔或改用ReadDirectoryChangesW。2. 提示用户关闭在 Excel 中打开的文件或使用文件副本。3. 实现更健壮的 CSV 解析处理引号、换行符等特殊情况。能看到源但内容为空白或错乱1. 渲染逻辑错误未正确计算顶点或纹理坐标。2. 文本渲染失败字体未加载或纹理创建失败。3. 数据加载成功但未传递给渲染函数。1. 在render函数中绘制一个纯色背景确认渲染通道正常。2. 检查文本渲染相关的 OBS API 返回值或日志。3. 在render函数中打印tableData的大小和内容到日志。1. 先从绘制简单几何体如矩形开始逐步增加文本渲染复杂度。2. 考虑使用浏览器源作为替代方案显示 HTML 表格将插件核心功能简化为“数据提供者”。3. 确保dataMutex锁的使用正确防止渲染时数据被修改。性能低下OBS 卡顿1. 每帧都重新解析整个 CSV 文件。2. 每帧都重新创建纹理或顶点缓冲区。3. 文件监控或轮询开销过大。1. 使用性能分析工具如 VS Profiler定位热点函数。2. 检查video_tick和video_render中的耗时操作。1. 仅在文件变化时解析 CSV。2. 缓存渲染资源纹理、顶点缓冲区仅在数据变化时更新。3. 将文件监控放在独立线程并通过线程安全队列通知主线程。6. 生产环境最佳实践与扩展方向一个用于实际直播的插件除了核心功能还需要考虑稳定性、易用性和可维护性。6.1 稳定性保障异常处理: 在所有文件 I/O、内存分配、外部 API 调用处添加健壮的异常处理。OBS 插件崩溃可能导致整个 OBS 退出。资源管理: 遵循 RAII 原则确保纹理、缓冲区等图形资源在析构函数中被正确释放。线程安全: OBS 的回调函数可能在不同线程被调用。确保对共享数据如tableData的访问都受到锁mutex的保护。配置验证: 对用户输入的路径、刷新间隔等参数进行有效性检查并提供清晰的错误提示。6.2 功能扩展建议支持真正的 Excel 文件: 集成如libxlsxwriter或通过 COM仅 Windows直接读取.xlsx文件避免手动转换为 CSV。数据过滤与格式化: 允许用户选择读取特定的工作表Sheet、单元格范围如A1:D10并支持数字格式、日期格式等。丰富的渲染样式: 实现真正的表格渲染支持边框、背景色、字体样式、对齐方式、单元格合并等。模板化输出: 允许用户定义 HTML/CSS 模板插件将数据填充到模板后通过内置的浏览器源渲染这样可以实现极其复杂的视觉效果而无需自己处理图形渲染。网络数据源: 不仅从本地文件读取还可以从 HTTP API 接口获取 JSON/CSV 数据实现真正的动态数据看板。条件化显示: 根据单元格的数值高亮显示特定行或列例如数值低于阈值时显示为红色。6.3 替代方案使用现有工具组合如果你觉得开发原生 C 插件过于复杂可以考虑以下“曲线救国”的方案这些方案同样能实现 OBS 读取 Excel 并动态展示的目标“浏览器源” 本地 Web 服务器: 这是最强大和灵活的方案。使用 Python 的Flask或FastAPI搭建一个简单的 Web 服务器。编写一个后台线程定时读取 Excel 文件使用pandas库并将其转换为 JSON。创建一个 HTML 页面使用 JavaScript如fetch定期从本地 Web 服务器获取 JSON 数据并利用DOM操作或Vue/React动态更新表格。在 OBS 中添加一个“浏览器源”URL 指向http://localhost:5000/your-page。优点开发速度快前端样式完全可控功能强大。缺点需要运行一个额外的 Python 进程。“文本源” 外部脚本: 适用于显示单行或少量数据。编写一个脚本Python、PowerShell 等定时读取 Excel 的某个单元格并将其输出到一个文本文件。在 OBS 中添加一个“文本源”选择“从文件读取文本”指向上述脚本输出的文本文件。优点极其简单无需开发插件。缺点只能显示纯文本格式单一刷新有延迟。利用“窗口捕获”或“显示器捕获”: 直接捕获 Excel 软件窗口或包含 Excel 的浏览器页面。优点零开发所见即所得。缺点性能开销大布局不灵活容易暴露无关的 UI 元素。对于大多数希望深度集成、追求性能和稳定性的场景开发原生插件仍然是终极解决方案。通过本文的梳理你应该已经对 OBS 插件开发的全链路有了清晰的认识可以从一个简单的 CSV 读取器开始逐步迭代最终打造出一个功能完备的 Excel 数据驱动直播工具。
返回列表