开源直播工具OBS研究
项目简介
OBS - Free and open source software for live streaming and screen recording(OBS是一款开源的用于录屏直播的工具软件)。
旧版的OBS只能支持Windows,目前已经停止开发。作者为了支持Windows/Mac/Linux重写了整个软件,项目地址为obs-studio in Github。
新版的OBS的目标有以下几点:
- Make it multiplatform.(跨平台支持)
- Separate the application from the core, allowing custom application of the core if desired, and easier extending of the user interface.(模块化、易扩展)
- Simplify complex systems to not only make it easier to use, but easier to maintain.(简化系统,使其易用易维护)
- Make a better/cleaner code base, use better coding standards, use standard libraries where possible (not just STL and C standard library, but also things like ffmpeg as well), and improve maintainability of the project as a whole.(尽量利用其他开源软件成果)
- Implement a new API-independent shader/effect system allowing better and easier shaders usage and customization without having to duplicate shader code.(实现独立于API的shader/effect系统)
- Better device support. (更好的支持有录屏需求的设备)
OBS项目的语言分布:
- C: 57.6%
- C++: 36.3%
- Objective-C/Objective-C++: 4%
- others: 3%
OBS代码主要包含这些部分:
- libobs: 核心代码,定义项目框架以及核心API,主要用C语言编写。
- UI: 界面代码,采用C++的QT框架,开发出适用三大平台的界面。
- plugins: 插件代码,可独立编译成dll(windows平台)或so(*nix平台),包含Source(录屏输入源)、Output、Service(各种流播服务)等全部被定义为插件。
- libobs-d3d11: 基于D3D的图形子系统,主要用在Windows系统。
- libobs-opengl: 基于opengl的图形子系统,主要用在*uix系统。
OBS软件功能概述
OBS项目工程中以场景组的方式呈现给用户,可以自由设置场景、输入源、效果处理,配置直播服务。
OBS项目工程结构
OBS项目中一个工程结构如下
一个场景组包含多个场景,OBS直播的时候是把整个场景流播给用户,那为什么需要多个场景?因为播主在直播时有快速切换场景的需要,所以播主需要在直播前编辑好多个场景(比如纯游戏场景;游戏+头像;解说;休息场景等),然后直播的过程中可以根据不同的需要快速切换。
OBS场景的转场
OBS中的转场,是场景切换时的动画效果,目前支持 Fade和Switch等多种效果。
OBS输入源的种类
一个场景可以包含多个输入源,一个直播工具可以支持的输入源种类反应了其强大性。OBS支持 输入源种类如下
OBS输入源的效果设置
针对每个输入源可以增加各种滤镜效果,以下列出我觉得最实用的几种:
- 音频效果:
- Video Delay: 设置延迟时间,用来处理音视频不同步的的场景。
- Noise Suppression: 噪音抑制
- Gain: 音频增益
- Noise Gate: 噪声门,把小噪音去掉
- 视频效果:
- Crop: 就是最实用的Crop,不过OBS里不能用鼠标拖拽来控制Crop区域,略显不便
- Chroma Key: 如果有绿幕背景,可以用来去背景,在摄像头的输入源中最常用。
- Image Mask: 打水印
- Scroll: 滚动效果,在一些浏览器的输入源上最实用。
OBS工作室模式
左边是预览界面,可以进行编辑。右边是正在直播的界面。中间是把预览界面切到直播界面的各种转场效果。
一般比较专业的直播都是使用这个模式,可以在预览界面编辑好画面之后再推送到直播画面。
OBS插件系统
OBS项目中把除了核心框架以及渲染系统之外的 部件全部抽象成了Module,一个或多个Module最后封装到插件中(以dll或so的形式),只要把插件放入特定的目录即可被主程序使用。
Mac版OBS的插件目录在/Applications/OBS.app/Contents/Resources/obs-plugins,其中
- mac-avcapture.so 对应Mac的视频捕获设备
- mac-capture.so 对应屏幕捕获 和 窗口捕获
- mac-syphon.so 对应注入捕获游戏画面
OBS插件定义
一个典型的OBS插件代码包含三个部分:
- 插件定义 -> plugin-main.c
- 编译打包 -> CMakeList.txt
- 内部实现代码 -> XX.c/YY.c …
下面以mac-capture.so插件为例来看看它的插件定义代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <obs-module.h>OBS_DECLARE_MODULE() OBS_MODULE_USE_DEFAULT_LOCALE("mac-capture", "en-US") //多语言支持extern struct obs_source_info coreaudio_input_capture_info; //输入源1 extern struct obs_source_info coreaudio_output_capture_info;//输入源2 extern struct obs_source_info display_capture_info;//输入源3 extern struct obs_source_info window_capture_info;//输入源4bool obs_module_load(void) //注册支持的输入源 {obs_register_source(&coreaudio_input_capture_info);obs_register_source(&coreaudio_output_capture_info);obs_register_source(&display_capture_info);obs_register_source(&window_capture_info);return true; } |
可以看出这个插件定义了四个输入源,这里你可能有个疑问,为什么不是一个插件 对应 一个输入源,因为功能相近的输入源集成到一个插件里可以减少冗余代码。
所以OBS中插件可以定义为 包含 一个 或 多个 输入(或 输出/编码/服务)模块的动态库代码。
任意一个开源库比如FFmpeg,经过OBS统一的接口定义封装即可编译成OBS的一个插件为OBS系统所用。
OBS插件加载流程
插件系统大体都有一个类似的套路,OBS的也不例外。简单来说就是定义插件存放在特定目录,在程序启动时,动态加载所有的插件(存储为对象或一系列函数指针),存储在字典 或者 链表这样的数据结构里。
下面来详细分析一下OBS中插件加载流程:
然后以mac-capture.so中的display_capture_info,来看看它的结构定义,可以看出它主要定义了id、type、name 以及一些接口API。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
struct obs_source_info display_capture_info = {.id = "display_capture",.type = OBS_SOURCE_TYPE_INPUT,.get_name = display_capture_getname,.create = display_capture_create,.destroy = display_capture_destroy,.output_flags = OBS_SOURCE_VIDEO | OBS_SOURCE_CUSTOM_DRAW |OBS_SOURCE_DO_NOT_DUPLICATE,.video_tick = display_capture_video_tick,.video_render = display_capture_video_render,.get_width = display_capture_getwidth,.get_height = display_capture_getheight,.get_defaults = display_capture_defaults,.get_properties = display_capture_properties,.update = display_capture_update, }; |
其中需要注意一下get_properties这个接口,这个接口是干啥用的?顾名思义是获取模块的属性数据,按我的理解 UI层可以利用这个属性数据来构建这个模块对应的界面,并设置这个模块的属性参数。
OBS视频处理流程
视频渲染输出流程
OBS视频渲染和输出是系统的核心流程,我们以Mac桌面录制输入,以及ffmpeg输出为例来分析一下整个流程(多路输入 和 多路输出道理也是类似的), 图中为了简单起见忽略了输出编码流程仅包含非编码流程。
可以看出OBS创建了两个线程,一个用于显示渲染,另一个用于编码输出。
渲染部分最终调用的是 输入模块里的渲染代码,而编码输出部分最终也是调用 输出模块的代码。
另外在渲染线程中 也负责把图形系统的数据 拷贝到 输出数据的缓存中,以便于输出线程进行处理。
视频输出数据结构分析
OBS的核心数据结构定义在libobs/obs-internal.h中 主体结构为obs_core如下图所示(仅保留的主要的数据结构)
右下方的video(结构为video_output)用在输出模块的raw_video接口进行处理,把 输出数据中的cache转成实际的输出,以下是video_output详细数据结构:
OBS音频处理流程
OBS音频处理是在一个线程中完成了先渲染后输出的过程。而视频处理则是 分别开了渲染线程 和 输出线程。
具体流程如下, 在输出函数中在判断是否需要编码,再调用对应的非编码流程 或 编码流程:
OBS图形系统架构
OBS的图形系统主要负责 场景的渲染、场景的切换、以及各种输入源的音视频效果的处理,属于OBS的核心之一。
通过使用软件以及视频渲染流程的分析 得到OBS图形系统的大体的逻辑关系。
针对图形系统主要分析以下三个问题:
- 多滤镜叠加的渲染处理。
- 滤镜和转场效果的实现与集成。
- 图形API的封装。
单个场景的渲染流程
场景(Scene)也被封装成输入源(Source)的一种,所以UI层只要把当前的场景取出来,调用它的obs_source_video_render即可。
在场景内部会渲染其包含的renderitem(也就是实际的输入源),比如前一个图所展示的游戏录制、桌面录制输入源等。
多个滤镜叠加的输入源渲染流程
这部分分析了很长时间一直没看懂,主要有两个原因:
- 之前不熟悉OpenGl的渲染流程,所以搞不懂滤镜的渲染流程。
- 这部分的逻辑比较绕,没分析出多个滤镜是怎么叠加渲染的。
前段时间花了点时间好好学习了一下OpenGl(仅仅学习和音视频处理相关的章节),写了一些demo。
现在再来分析这部分相对轻松一些,简述一下:当渲染带滤镜的输入源时,会先渲染它最后一个滤镜,然后在这个滤镜的渲染代码又会调用渲染前一个滤镜,最后调用第一个滤镜的渲染代码。
在第一个滤镜的渲染代码里 直接渲染 调用输入源的渲染流程,然后生成texture。
每个滤镜都在前一个滤镜渲染生成的texture的基础渲染生成新的texture。
流程图如下:
滤镜和转场效果的实现与集成
OBS项目中,滤镜和转场效果都被抽象成插件。
以Mac版OBS为例:
- 所有的滤镜都在obs-filters.so这个插件里;
- 所有的转场效果都在obs-transitions.so里;
滤镜和转场其实分析起来是类似的,所以后续的暂时以滤镜为例来加以说明。
首先如果滤镜个数太多,拆分到两个插件里是没问题的。不过OBS项目中全部集中在一个插件里。
每个滤镜其实都被定义成了输入模块,以obs_source_info定义暴露API,以crop_filter为例见如下定义,唯一和普通输入模块不同的是类型定义(.type)。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
struct obs_source_info crop_filter = {.id = "crop_filter",.type = OBS_SOURCE_TYPE_FILTER,.output_flags = OBS_SOURCE_VIDEO,.get_name = crop_filter_get_name,.create = crop_filter_create,.destroy = crop_filter_destroy,.update = crop_filter_update,.get_properties = crop_filter_properties,.get_defaults = crop_filter_defaults,.video_tick = crop_filter_tick,.video_render = crop_filter_render,.get_width = crop_filter_width,.get_height = crop_filter_height }; |
细心观察每个滤镜的代码组成发现都是一个套路,主要由两部分组成:
- XXX.c (C代码,用于暴露API以及例行处理)
- XXX.effect (自定义文件,实际效果处理逻辑)
关于这个effect留到后续讲解。
看到这里得出一个结论,OBS项目要增加新的滤镜效果只要编写对应的XXX.c 和 XXX.effect,放到对应的插件以输入模块的API暴露出来,并注册就可以了。
图形API的封装处理
目前OBS系统的图形API包括OpenGl以及d3d11:
- 在图形库加载层利用了Multi-Language GL/GLES/EGL/GLX/WGL Loader-Generator对不同平台加载图形库代码进行了封装。
- 在API调用层面也进行了抽象统一,具体可以查看 libobs/graphics/graphics-imports.c的定义。
- 自定义了效果描述文件 XXX.effect,这样就不用针对OpenGl和d3d11写两遍Shader。
我们以chroma-key-filter为例分析一下它的创建流程:
- 其中AddNewFilter是在界面中触发的添加效果的功能。
- ep_parse把xxx.effect配置文件解析成对应的配置结构。
- ep_compile把对应的配置结构解析 效果数据结构。
effect文件的作用可以参考程序中的注释
1 2 3 4 5 6 7 8 9 10 |
/** Effects introduce a means of bundling together shader text into one* file with shared functions and parameters. This is done because often* shaders must be duplicated when you need to alter minor aspects of the code* that cannot be done via constants. Effects allow developers to easily* switch shaders and set constants that can be used between shaders.** Effects are built via the effect parser, and shaders are automatically* generated for each technique's pass.*/ |
effect文件包括这几个部分:
pass对应 vertex_shader 和 pixel_shader
technique 对应一个具体效果的渲染设置,包含多个pass
effect文件包含多个technique渲染设置、可以共享文件中的参数和函数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
SolidVertInOut VSSolid(SolidVertInOut vert_in) {SolidVertInOut vert_out;vert_out.pos = mul(float4(vert_in.pos.xyz, 1.0), ViewProj);return vert_out; }float4 PSSolid(SolidVertInOut vert_in) : TARGET {return color; }technique Solid {pass{vertex_shader = VSSolid(vert_in);pixel_shader = PSSolid(vert_in);} } |
effect文件支持基本的C语法,支持宏定义和include包含其他文件,由libobs/util中的cf-lexer.c和cf-parser.c提供解析支持。
支持OBS插件
OBS的插件是在OBS项目定义的比较宽泛,插件的范畴包括 整个录屏、处理、推流 中的各个功能模块.
如果我们的软件中可以直接支持OBS插件,就可以节省大量的开发、测试的时间。但由于我们的程序框架和OBS的完全不同,要如何支持OBS项目的插件呢?
想了两种方法,并尝试分析一下优缺点。
支持OBS的方法分析
二进制级别的支持
顾名思义,就是把OBS的插件直接放到我们程序的相应目录就可以用。这种方式下维护、更新、新增 OBS插件 代价是最小的。也是我心中理想的支持方式。
但是以这种方式支持的遇到较大困难。先看OBS项目中的代码的各个模块:
- libobs: 核心代码,定义项目框架以及核心API,主要用C语言编写。
- UI: 界面代码,采用C++的QT框架,开发出适用三大平台的界面。
- plugins: 插件代码,可独立编译成dll(windows平台)或so(*nix平台),包含Source(录屏输入源)、Output、Service(各种流播服务)等全部被定义为插件。
- libobs-d3d11: 基于D3D的图形子系统,主要用在Windows系统。
- libobs-opengl: 基于opengl的图形子系统,主要用在*uix系统。
我们想要支持plugins中的插件,
但是plugins中的插件要依赖libobs,
而libobs又要依赖libobs-opengl 和 QT界面库。
也就是除非 我们的项目支持基于OBS项目改写,否则这种支持方式的不太现实。
代码级别的支持
这是退而求其次的方式,简单的说就是把OBS的插件代码扣出来,确保其不依赖于libobs,然后集成我们的项目中。这种方式每支持一个插件都存在集成的工作量,也可能会引入Bug,不过不失为一个较为可行的方案。
OBS项目编译
尝试了在Mac平台上编译OBS项目,还比较顺利。具体可以参考install help
有个小问题,在cmake后报错提示无法找到QT5的cmake模块。需要给cmake指定一下QT5的安装目录,以我的安装目录为例,命令如下:
1 |
cmake .. -DCMAKE_PREFIX_PATH=/usr/local/Cellar/qt5/5.3.1/ |
cmake命令我是不熟悉的,不过看了这篇文章也基本懂了。
OBS项目最后的编译结果如下:
主要有三个目录:
- bin: 主程序
- data: 国际化资源 以及 视频effect效果资源
- obs-plugin: 插件编译结果
使用otool -L分析中其中主要动态库和插件(仅以mac-capture为例)的依赖关系如下:
前面在代码层面分析直接二进制支持OBS插件感觉很困难。但这里基于编译后的动态库依赖关系分析好像又有一定可行性。我们把mac-capture.so、libobs.0.dylib、ffmpeg独立出来,去掉OBS主程序和QT等库,自己写代码来调用libobs.0.dylib提供的功能,以此直接支持OBS的插件。
后续进行完相关的实验,看看到底是否可行,再来补充。
模块列表
OBS项目中类型为OBS_SOURCE_TYPE_INPUT是我们可以考虑优先支持的模块。以下是功能说明。
插件 | 子模块 | 功能描述 |
---|---|---|
mac-capture | coreaudio_input_capture | 音频输入获取 |
coreaudio_output_capture | ||
display_capture | 桌面获取 | |
window_capture | 窗体获取 | |
mac-avcapture | av_capture | 摄像头获取 |
mac-syphon | syphon | 程序注入获取界面 |
obs-ffmpeg | ffmpeg_source | ffmpeg输入源 |
obs-browser | browser_source | 浏览器输入 |
text-freetype2 | freetype2_source | text输入 |
decklink | decklink-input | |
image-source | image_source | 图片输入 |
slideshow | ||
vlc-video | vlc_source | vlc输入 |
mac-syphon插件分析
mac-syphon是OBS项目中用于获取游戏画面(仅用于mac平台)的插件,十分重要。下面来分析一下它的实现原理。
首先mac-syphon是OBS的输入源插件,所以遵循OBS的插件API设定,具体可以查看OBS插件分析章节的介绍。
mac-syphon内部其实组合了多个开源项目功能来完成:获取游戏画面,并展示到OBS界面上的功能。
我们以OBS获取MineCraft这个游戏的画面为例,来看一张总的实现原理图:
简单解释一下这个过程:
注入游戏进程的方法这里用的是Scripting Additions的方式,这是macOS独有的技术,windows上肯定要用其他方式,到时候再单独研究。除了注入方式的区别,其他流程Win和Mac平台应该类似的。
另外由于注入的函数中替换的是OpenGL的渲染API,所以这个插件支持的游戏必然是使用OpenGL渲染的。假如某个程序或游戏不使用OpenGL则无法注入。
ScriptingAdditions
ScriptingAdditions就是macOS中Applescript中一个技术,不太好解释,反正它的作用就是帮助注入到游戏进程里,直接上两个文档:
- Scripting Additions for Mac OS X
- Open Scripting Architecture
mach_override
这里其实利用了两个项目jrswizzle和mach_override,功能就是把游戏进程中的flushBuffer替换为自己写的flushBufferSyphon,把orig_CGLFlushDrawable替换为CGLFlushDrawableOverride,从而实现把自己写的功能注入到游戏的渲染API中。
Syphon
Syphon项目是一套传输图形画面的Client/Server框架。
项目还提供了Client/Server Demo可以很方便的测试画面传输的功能。
Syphon Inject
SyphonInject项目组合Syphon的功能以及注入游戏的功能,提供了一个Demo。
下图中我用Syphon Inject注入到Dota2游戏,然后把界面传送给Client Demo。
SyphonInject编译注意事项: SyphonInject项目早期利用mach_inject项目进行注入,后期已经修改为ScriptingAdditions方式注入。最新的代码没有依赖mach_inject,所以可以把mach_inject项目依赖去掉再编译。
最后OBS项目其实上述项目组合,用插件的API包装成了mac-Syphon插件。
mac-capture插件分析
mac-capture插件是OBS项目中对应mac平台的 屏幕界面获取、窗口界面获取、输入音频获取、输出音频获取 四大模块的具体实现。
obs模块的具体结构和API不再列出,详细情况可查看之前的文档,这里着重分析模块内部的功能实现。
mac-display模块
主要利用Quartz Display Services获取界面图像数据转换成texuture提供给主程序。
其中调用CGDisplayStreamCreateWithDispatchQueue接口获取的显示界面图像数据,数据结构是IOSurfaceRef。具体用法可以查看上述Quartz Display Services链接文档。
主要的实现流程如下图:
mac-window-capture模块
获取其他程序窗体界面的模块,也是利用了Quartz Display Services。
使用CGWindowListCreateImage接口,通过windowID把对应程序的界面以 CGImage 的形式返回,直接放到输出的Cache里。
这里有个疑问,模块没有实现.video_render这个渲染API。可能和模块的output_flags设置的是异步有关。
OBS_SOURCE_ASYNC_VIDEO异步的渲染流程可能未放到模块内部实现。
mac-audio模块
音频模块包括两个,音频输入模块 和 音频输出模块。
音频输入设备比如 iMac上自带的外置麦克风;音频输出模块 比如 插入的耳机等。
由于mac平台限制,无法直接录制 音频输出设备的声音。比如我要录制浏览器上youtube视频的声音,在不借助第三方程序的情况下是做不到。使用OBS也做不到。
不过利用第三方开源程序soundflower可以解决这个问题。在安装soundflower之后,OBS可以设置 音频输出捕获模块,设备选择soundflower。同时系统声音输出设备 也选择为soundflower。
这样系统的任何程序发出的声音就会先经过soundflower,然后被OBS捕获。
mac-audio中的音频输入模块 和 输出模块 实现流程基本相同,如下所示:
基本原理是 在初始化模块的时候 使用系统接口AudioObjectAddPropertyListener注册音频的回调函数。然后在回调函数中完成音频渲染以及输出音频数据缓存。
原文网址:http://fancywt.cn/2017/10/28/obs/?hmsr=toutiao.io&utm_medium=toutiao.io&utm_source=toutiao.io
开源直播工具OBS研究相关推荐
- obs多推流地址_最热门直播工具OBS的下载和设置教程,值得一看
随着直播行业的再度火热,各个直播平台也推出了自己的直播工具,但各自都有一些不足需要改善.今天我来教大家来下载和设置主播们最常用的直播工具OBS,以开启自己的直播路程. 大多数人都在用的OBS直播工具 ...
- obs 推流编码在哪设置_直播软件OBS推流的设置方法
如今是一个直播盛行的时代,在我们身边有着形形色色的直播内容,以及五花八门的直播看点,还有千奇百怪故事内容,更有危险性极高的探险直播,总之直播已经无所不在,它也是现在生活中不可缺少的一部分,看电脑时要看 ...
- 类似obs直播工具有哪些_如何使用OBS直播职业玩家之类的游戏
类似obs直播工具有哪些 现在,OBS Studio (以前称为Open Broadcaster Software)的版本为19.0.3,可用于Mac和Windows的安装程序下载 ,以及适用于各种L ...
- 开源/免费界面自动化测试工具对比研究
摘要: 随着我行自动化测试实施范围的不断扩大,参与界面自动化测试的应用系统越来越多.我行的应用系统现阶段多采用商用工具QTP(UFT)作为执行工具来进行界面自动化测试,采购的QTP license是有 ...
- [论文分享] 开源 C/C++静态软件缺陷检测工具实证研究
前言 之前读了一遍这篇论文 开源 C/C++静态软件缺陷检测工具实证研究[1] [软件学报 2022] 属于静态软件分析与漏洞挖掘工具及技术的综述性文章, 今天总结一下 (其他的水话就不多说了, 就像 ...
- linux可用直播软件,免费直播软件OBS Studio下载 支持Windows/Mac和Linux
OBS Studio是一款视频直播录制软件,为用户提供了视频.文本.图像等的捕获录制功能.当然你也可以用来录像,OBS Studio是免费的,并且自带中文语言.大多数的直播网站都有主播用这个软件进行直 ...
- 8 亿邮件地址泄露,源于邮件验证服务;腾讯推出微信公众号直播工具
(给技术最前线加星标,每天看技术热点) 转自:开源中国.solidot.cnBeta.腾讯科技.快科技等 [技术资讯] 0.8 亿邮件地址遭泄露,源于邮件验证服务 近日,Security Discov ...
- vs2015编译基于obs-studio的阿里巴巴直播工具tblive
vs2015编译基于obs-studio的阿里巴巴直播工具tblive 开发环境 vs2015+QT5.9.7 因为tblive使用的是vs2013,故需要用到v120平台工具集,如果没有安装可通过控 ...
- 源自Google、Facebook、Netflix和Cisco的10款开源安全工具很值得回味
源自Google.Facebook.Netflix和Cisco的10款开源安全工具很值得回味 选择自由的开源安全软件是一个重要话题,随着新品牌的出现,在选择上也出现很多不确定性.但这没有影响开源 ...
最新文章
- c面试题总结(含答案)
- 计算机操作系统专题一:多道环境下进程同步与互斥制约关系的学习
- SQL SERVER的锁机制(四)——概述(各种事务隔离级别发生的影响)
- css3制作炫酷导航栏效果 转
- linux通过时间查询日志,linux按时间查询日志
- MOT with a hierachical single branch network
- linux登录pg数据库密码,PostgreSQL:修改数据库用户的密码
- 《锁王创造营》 第一关:初出茅庐
- distinct 多列详解
- 西门子plc 用c语言编程,西门子PLC常用指令举例
- EXIF App for Mac(EXIF查看器)
- 【GPT-3】第1章 大型语言模型时代
- unable to find account data for the submitted AuthenticationToken
- 年产2000t搅拌型发酵酸奶车间工艺设计
- Oracle 递归查询详解
- 互联网名称与数字地址分配机构ICANN简介
- html静态页面作业——酷酷动物主题响应式网页(5页) 大学生动物主题网页作品 动物网页设计作业模板 学生网页制作源代码下载
- 直线裁剪算法-Cohen-Sutherland算法
- 硬盘数据如何恢复?电脑硬盘资料恢复,方法就是这么简单!
- 20110830 寝室物语
热门文章
- 心理咨询领域首个开放的QA语料库
- 简单php登陆注册模块
- [DB]ORACLE监控锁表(锁表与解表)
- 专访MongoDB官方大中华区首席架构师唐建法:MongoDB将会领导数据处理新趋势 —— 一个操作型的数据平台...
- 项目总结(0730)
- 请问大神 APACK景观分析软件怎么用啊 第一次用不会用
- 【golang roadmap注册邀请码】
- 办公小技巧:word打印怎么做?
- 智能车基础四轮组”氢气球“----(1)软件基础框架
- 拥抱人工智能 | 智安协携手华为共同举办第五届智慧城市建设高峰论坛圆满成功!