gstreamer教程(二)-初始化_组件(Element)_箱柜(Bins)的使用
Gstreamer基础知识:
1、gstreamer基本概念
2、gstreamer初始化、组件、箱柜
3、gstreamer总线、衬垫、缓冲区、事件
1、初始化GStreamer
当你准备写一个 GStreamer 应用程序时,你仅需要通过包含头档 gst/gst.h 来访问库函数。除此之外,不要忘记初始化 GStreamer 库。
1.1、简易初始化
在 GStreamer 库被使用前,主应用程序中应该先调用函数 gst_init,这个函数将会对 GStreamer 库做一些必要的初始化工作,同时也能够对 GStreamer 的命令行参数进行解析。一个典型的初始化GStreamer库的代码如下所示:
例初始化 GStreamer,通过gst_version和宏获取GStreamer版本信息:
qt工程.pro配置(其它示例一样):
QT -= guiCONFIG += c++11 console
CONFIG -= app_bundleDEFINES += QT_DEPRECATED_WARNINGSCONFIG += link_pkgconfig
PKGCONFIG += gstreamer-1.0SOURCES += main.cpp
main函数源文件代码:
#include <gst/gst.h>int main (int argc, char *argv[])
{const gchar *nano_str;guint major, minor, micro, nano;gst_init (&argc, &argv);gst_version (&major, &minor, µ, &nano);if (nano == 1)nano_str = "(CVS)";else if (nano == 2)nano_str = "(Prerelease)";elsenano_str = "";printf ("This program is linked against GStreamer %d.%d.%d %s\n",major, minor, micro, nano_str);printf("macro vision is: %d.%d.%d\n", GST_VERSION_MAJOR, GST_VERSION_MINOR, GST_VERSION_MICRO);return 0;
}
运行结果:
可以使用 GST_VERSION_MAJOR, GST_VERSION_MINOR 以及GST_VERSION_MICRO 三个宏得到你的 GStreamer 版本信息,或者使用函数gst_version 得到当前你所调用的链接库的版本信息。目前 GStreamer 使用了一种保证主要版本和次要版本中 API-/以及 ABI 兼容的策略。
当命令行参数不需要被 GStreamer 解析的时候,你可以在调用函数 gst_init 时使用2 个 NULL 参数。
1.2、使用 GOption 接口来初始化
同样可以使用 GOption 表来初始化你的参数。例子如下:
#include <gst/gst.h>int main (int argc, char *argv[])
{gboolean silent = FALSE;gchar *savefile = NULL;GOptionContext *ctx;GError *err = NULL;GOptionEntry entries[] = {{ "silent", 's', 0, G_OPTION_ARG_NONE, &silent,"do not output status information", NULL },{ "output", 'o', 0, G_OPTION_ARG_STRING, &savefile,"save xml representation of pipeline to FILE and exit", "FILE" },{ NULL }};ctx = g_option_context_new ("- Your application");g_option_context_add_main_entries (ctx, entries, NULL);g_option_context_add_group (ctx, gst_init_get_option_group ());if (!g_option_context_parse (ctx, &argc, &argv, &err)) {g_print ("Failed to initialize: %s\n", err->message);g_error_free (err);return 1;}printf ("Run me with --help to see the Application options appended.\n");return 0;
}
运行结果:
如例子中的代码所示,你可以通过 GOption 表来定义你的命令行选项。将表与由gst_init_get_option_group函数返回的选项组一同传给GLib初始化函数。通过使用GOption表来初始化GSreamer,你的程序还可以解析除标准GStreamer选项以外的命令行选项。
2、组件(Element)
对程序员来说, GStreamer 中最重要的一个概念就是 GstElement 对象。组件是构建一个媒体管道的基本块。所有上层(high-level)部件都源自GstElement物件。任何一个译码器编码器、分离器、视频/音频输出部件实际上都是一个 GstElement对象。
2.1、什么是组件?
对程序员来说,组件就像一个黑盒子。你从组件的一端输入数据,组件对数据进行一些处理,然后数据从组件的另一段输出。拿一个译码组件来说,你输入一些有特定编码的数据,组件会输出相应的译码数据。在下一章 (Pads and capabilities),你将学习到更多关于对组件进行数据输入输出的知识,以及如何在你的程序中实现数据的输入输出。
2.1.1、源组件
源组件(Source elements)为管道产生数据,比如从磁盘或者声卡读取数据。 图2-1形象化的源组件。我们总是将源衬垫(source pad)画在组件的右端。
图 2-1、形象化的源组件
源组件不接收数据,仅产生数据。你可从上图中明白这一点,因为上图仅有一个源衬垫(右端),同样的, 源衬垫也仅产生数据(对外部而言)。
2.1.2、过滤器(filters)、转换器(convertors)、分流器(demuxers)、整流器(muxers)以及编译码器(codecs)
过滤器(Filters)以及类过滤组件(Filter-like elements)都同时拥有输入和输出衬垫。他们对从输入衬垫得到的数据进行操作,然后将数据提供给输出衬垫。音量组件(filter)、视频转换器 (convertor)、 Ogg 分流器或者 Vorbis 译码器都是这种类型的组件。
类过滤组件可以拥有任意个的源衬垫或者接收衬垫。像一个视频分流器可能有一个接收衬垫以及多个(1-N)源衬垫,每个接收衬垫对应一种元数据流 (elementarystream)。相反地,译码器只有一个源衬垫及一个接收衬垫。
图2-2、形象化的过滤组件
图 2-2 形象化了类过滤组件。这个特殊的组件同时拥有源端和接收端。接收输入数据的接收衬垫在组件的左端,源衬垫在右端。
图2-3、形象化的拥有多个输出的过滤组件
图2-3显示了另一种了类过滤组件。它有多个输出衬垫(source pad)。 Ogg分流器是个很好的实例。因为Ogg流包含了视频和音频。一个源衬垫可能包含视频元数据流,另一个则包含音频元数据流。当一个新的衬垫被创建时,分流器通常会产生一个信号。程序员可以在信号处理事件中处理新的元数据流。
2.1.3、接收组件
接收组件是媒体管道的末端,它接收数据但不产生任何数据。写磁盘、利用声卡播放声音以及视频输出等都是由接收组件实现的。 图2-4显示了接收组件。
图2-4、形象化的接收组件
2.2、创建一个GstElement组件
创建一个组件的最简单的方法是通过函数gst_element_factory_make ()。这个函数使用一个已存在的工厂对象名和一个新的组件名来创建组件。创建完之后,你可以用新的组件名在箱柜( bin)中查询得到这个组件。这个名字同样可以用来调试程序的输出。你可以通过传递 NULL 来得到一个默认的具有唯一性的名字。
当你不再需要一个组件时,你需要使用 gst_object_unref ()来对它进行解引用。 这会将一个组件的引用数减少 1。任何一个组件在创建时,其引用记数为 1。当其引用记数为 0 时,该组件会被销毁。
下面的例子,显示了如果通过一个fakesrc工厂对象来创建一个名叫source的组件。程序会检查组件是否创建成功。检查完毕后,程序会销毁组件。
#include <gst/gst.h>int main (int argc, char *argv[])
{GstElement *element;/* init GStreamer */gst_init (&argc, &argv);/* create element */element = gst_element_factory_make ("fakesrc", "source");if (!element){g_print ("Failed to create element of type 'fakesrc'\n");return -1;}gst_object_unref (GST_OBJECT (element)); // 解引用return 0;
}
gst_element_factory_make 是 2 个函数的速记。一个GstElement 对象由工厂对象创建而来。为了创建一个组件,你需要使用一个唯一的工厂对象名字来访问一个 GstElementFactory 对象。 gst_element_factory_find ()就是做了这样的事。
即:gst_element_factory_make等价于gst_element_factory_find+gst_element_factory_create。
下面的代码段,创建了一个工厂对象,这个工厂对象被用来创建一个fakesrc组件—— 伪装的数据源。函数gst_element_factory_create()将会使用组件工厂并根据给定的名字来创建一个组件。
#include <gst/gst.h>int main (int argc, char *argv[])
{GstElementFactory *factory;GstElement * element;/* init GStreamer */gst_init (&argc, &argv);/* create element, method #2 */factory = gst_element_factory_find ("fakesrc");if (!factory){g_print ("Failed to find factory of type 'fakesrc'\n");return -1;}element = gst_element_factory_create (factory, "source");if (!element){g_print ("Failed to create element, even though its factory exists!\n");return -1;}gst_object_unref (GST_OBJECT (element));return 0;
}
2.3、使用组件作为GObject 对象
GstElement的属性大多通过标准的 GObject 对象实现的。使用 GObject 的方法可以对GstElement实行查询、设置、获取属性的值,同样 GParamSpecs 也被支持。
每个 GstElement 都从其基类 GstObject 继承了至少一个“名字" 属性。这个名字属性将在函数gst_element_factory_make ()或者函数 gst_element_factory_create ()中使用到。你可通过函数 gst_object_set_name 设置该属性,通过gst_object_get_name 得到一个对象的名字属性。你也可以通过下面的方法来得到一个对象的名字属性。
#include <gst/gst.h>int main (int argc, char *argv[])
{GstElement *element;gchar *name;/* init GStreamer */gst_init (&argc, &argv);/* create element */element = gst_element_factory_make ("fakesrc", "source");/* get name */g_object_get (G_OBJECT (element), "name", &name, NULL);g_print ("The name of the element is '%s'.\n", name);g_free (name);gst_object_unref (GST_OBJECT (element));return 0;
}
运行结果:
大多数的插件(plugins)都提供了一些额外的方法,这些方法给程序员提供了更多的关于该组件的注册信息或配置信息。 gst-inspect 是一个用来查询特定组件特性( properties)的实用工具。它也提供了诸如函数简短介绍,参数的类型及其支的范围等信息。关于 gst-inspect 更详细的信息请参考附录。
关于GObject特性更详细的信息,我们推荐你去阅读 GObject手册以及 Glib 对象系统介绍。(这两个链接已失效)
GstElement对象同样提供了许多的 GObject 信号方法来实现一个灵活的回调机制。你同样可以使用 gst-inspect来检查一个特定组件所支持的信号。总之,信号和特性是组件与应用程序交互的最基本的方式。
2.4、深入了解组件工厂
在前面的部分,我们简要介绍过 GstElementFactory 可以用来创建一个组件的实例,但是工厂组件不仅仅只能做这件事,工厂组件作为在 GStreamer 注册系统中的一个基本类型,它可以描述所有的插件(plugins)以及由GStreamer创建的组件。这意味着工厂组件可以应用于一些自动组件实例,像自动插件(autopluggers); 或者创建一个可用组件列表,像管道对应用程序的类似操作(像GStreamer Editor) 。
2.4.1、通过组件工厂得到组件的信息
像 gst-inspect 这样的工具可以给出一个组件的概要: 插件(plugin)的作者、描述性的组件名称(或者简称)、组件的等级( rank)以及组件的类别(category)。类别可以用来得到一个组件的类型,这个类型是在使用工厂组件创建该组件时做创建的。例如类别可以是 Codec/Decoder/Video(视频译码器)、 Source/Video(视频发生器)、 Sink/Video(视频输出器)。音频也有类似的类别。同样还存在 Codec/Demuxer和 Codec/Muxer,甚至更多的类别。 Gst-inspect 将会列出当前所有的工厂对象,gst-inspect <factory-name> 将会列出特定工厂对象的所有概要信息。
#include <gst/gst.h>int main (int argc, char *argv[])
{GstElementFactory *factory;/* init GStreamer */gst_init (&argc, &argv);/* get factory */factory = gst_element_factory_find ("audiotestsrc");if (!factory){g_print ("You don't have the 'audiotestsrc' element installed!\n");return -1;}/* display information */g_print ("The '%s' element is a member of the category %s.\n""Description: %s\n",gst_plugin_feature_get_name (GST_PLUGIN_FEATURE (factory)),gst_element_factory_get_klass (factory),gst_element_factory_get_description (factory));return 0;
}
运行结果:
你可以通过 gst_registry_pool_feature_list (GST_TYPE_ELEMENT_FACTORY)得到所有在 GStreamer 中注册过的工厂组件。
2.4.2、找出组件所包含的衬垫
工厂组件最有用处的功能可能是它包含了对组件所能产生的衬垫的一个详细描述,以及这些衬垫的功能(以行外话讲: 就是指这些衬垫所支持的媒体类型),而得到这些信息是不需要将所有的插件(plugins)都装载到内存中。这可用来给一个编码器提供一个编码列表,或在多媒体播放器自动加载插件时发挥作用。目前所有基于 GStreamer 的多媒体播放器以及自动加载器(autoplugger)都是以上述方式工作。当我们在下一章:衬垫与功能( Pads and capabilities)中学习到 GstPad 与GstCaps 时,会对上面的特性有个更清晰的了解。
2.5、链接组件
通过将一个源组件,零个或多个类过滤组件,和一个接收组件链接在一起,你可以建立起一条媒体管道。数据将在这些组件间流过。这是 GStreamer 中处理媒体的基本概念。图2-5 用 3 个链接的组件形象化了媒体管道。
图2-5、形象化 3 个链接的组件
通过链接这三个组件,我们创建了一条简单的组件链。组件链中源组件("element1")的输出将会是类过滤组件 ("element2")的输入。类过滤组件将会对数据进行某些操作,然后将数据输出给最终的接收组件("element3")。
把上述过程想象成一个简单的 Ogg/Vorbis 音频译码器。 源组件从磁盘读取文件。第二个组件就是 Ogg/Vorbis 音频译码器。最终的接收组件是你的声卡,它用来播放经过译码的音频数据。我们将在该手册的后部分用一个简单的图来构建这个Ogg/Vorbis 播放器。上述的过程用代码表示为:
#include <gst/gst.h>int main (int argc, char *argv[])
{GstElement *pipeline;GstElement *source, *filter, *sink;/* init */gst_init (&argc, &argv);/* create pipeline */pipeline = gst_pipeline_new ("my-pipeline");/* create elements */source = gst_element_factory_make ("fakesrc", "source");filter = gst_element_factory_make ("identity", "filter");sink = gst_element_factory_make ("fakesink", "sink");/* must add elements to pipeline before linking them */gst_bin_add_many (GST_BIN (pipeline), source, filter, sink, NULL);/* link */if (!gst_element_link_many (source, filter, sink, NULL)) {g_warning ("Failed to link elements!");}// [..]return 0;
}
对于一些特定的链接行为,可以通过函数 gst_element_link () 以及gst_element_link_pads()来实现。你可以使用不同的 gst_pad_link_* ()函数来得到单个衬垫的引用并将它们链接起来。更详细的信息请参考 API 手册。
注意:在链接不同的组件之前,你需要确保这些组件都被加在同一个箱柜中,因为将一个组件加载到一个箱柜中会破坏该组件已存在的一些链接关系。同时,你不能直接链接不在同一箱柜或管道中的组件。如果你想要连接处于不同层次中的组件或衬垫,你将使用到精灵衬垫(关于精灵衬垫更多的信息将在后续章节中讲到) 。
2.6、组件状态
一个组件在被创建后,它不会执行任何操作。所以你需要改变组件的状态,使得它能够做某些事情。 Gstreamer 中,组件有四种状态,每种状态都有其特定的意义。这四种状态为:
• GST_STATE_NULL: 默认状态。该状态将会回收所有被该组件占用的资源。
• GST_STATE_READY: 准备状态。组件会得到所有所需的全局资源,这些全局资源将被通过该组件的数据流所使用。例如打开设备、分配缓存等。但在这种状态下,数据流仍未开始被处理,所以数据流的位置信息应该自动置 0。如果数据流先前被打开过,它应该被关闭,并且其位置信息、特性信息应该被重新置为初始状态。
• GST_STATE_PAUSED: 在这种状态下,组件已经对流开始了处理,但此刻暂停了处理。因此该状态下组件可以修改流的位置信息,读取或者处理流数据,以及一旦状态变为 PLAYING,流可以重放数据流。这种情况下,时钟是禁止运行的。总之, PAUSED 状态除了不能运行时钟外,其它与PLAYING 状态一模一样。
处于 PAUSED 状态的组件会很快变换到 PLAYING 状态。举例来说,视频或音频输出组件会等待数据的到来,并将它们压入队列。一旦状态改变,组件就会处理接收到的数据。同样,视频接收组件能够播放数据的第一帧。(因为这并不会影响时钟)。自动加载器( Autopluggers)可以对已经加载进管道的插件进行这种状态转换。其它更多的像 codecs 或者 filters 这种组件不需要在这个状态上做任何事情。
• GST_STATE_PLAYING: PLAYING 状态除了当前运行时钟外,其它与PAUSED 状态一模一样。
你可以通过函数 gst_element_set_state()来改变一个组件的状态。你如果显式地改变一个组件的状态, GStreamer 可能会使它在内部经过一些中间状态。例如你将一个组件从 NULL 状态设置为 PLAYING 状态, GStreamer 在其内部会使得组件经历过 READY 以及 PAUSED 状态。
当处于GST_STATE_PLAYING 状态,管道会自动处理数据。它们不需要任何形式的迭代。 GStreamer 会开启一个新的线程来处理数据。 GStreamer 同样可以使用 GstBus在管道线程和应用程序现成间交互信息。
3、箱柜( Bins)
箱柜是一种容器组件。你可以往箱柜中添加组件。由于箱柜本身也是一种组件,所以你可以像普通组件一样操作箱柜。因此,先前关于组件(Elements) 那章的内容同样可以应用于箱柜。
3.1、什么是箱柜
箱柜允许你将一组有链接的组件组合成一个大的逻辑组件。你不再需要对单个组件进行操作,而仅仅操作箱柜。当你在构建一个复杂的管道时,你会发现箱柜的巨大优势,因为它允许你将复杂的管道分解成一些小块。
箱柜同样可以对包含在其中的组件进行管理。它会计算数据怎样流入箱柜,并对流入的数据流制定一个最佳的计划( generate an optimal plan)。计划制定( Plangeneration)是GStreamer中最复杂的步骤之一。
图3-1、形象化的箱柜
GStreamer 程序员经常会用到的一个特殊的箱柜:
• 管道:是一种允许对所包含的组件进行安排( scheduling)的普通容器。顶层( toplevel)箱柜必须为一个管道。因此每个 GStreamer 应用程序都至少需要一个管道。当应用程序启动后,管道会自动运行在后台线程中。
3.2、创建箱柜
你可以通过使用创建其它组件的方法来创建一个箱柜,如使用组件工厂等。当然也有一些更便利的函数来创建箱柜— (gst_bin_new()和 gst_pipeline_new ())。你可以使用 gst_bin_add()往箱柜中增加组件,使用 gst_bin_remove()移除箱柜中的组件。当你往箱柜中增加一个组件后,箱柜会对该组件产生一个所属关系;当你销毁一个箱柜后,箱柜中的组件同样被销毁 (dereferenced);当你将一个组件从箱柜移除后,该组件会被自动销毁(dereferenced)。
#include <gst/gst.h>int main (int argc, char *argv[])
{GstElement *bin, *pipeline, *source, *sink;/* init */gst_init (&argc, &argv);/* create */pipeline = gst_pipeline_new ("my_pipeline");bin = gst_pipeline_new ("my_bin");source = gst_element_factory_make ("fakesrc", "source");sink = gst_element_factory_make ("fakesink", "sink");/* set up pipeline */gst_bin_add_many (GST_BIN (bin), source, sink, NULL);gst_bin_add (GST_BIN (pipeline), bin);gst_element_link (source, sink);// [..]return 0;
}
有多种方法来查询一个箱柜中的组件。你可以通过函数gst_bin_get_list()得到一个箱柜中所有组件的一个列表。详细信息请参考API手册 GstBin 部分。https://gstreamer.freedesktop.org/documentation/gstreamer/gstbin.html
3.3、自定义箱柜
程序员可以自定义能执行特定任务的箱柜。例如,你可以参照下面的代码写一个Ogg/Vorbis 译码器。
#include <gst/gst.h>int main (int argc, char *argv[])
{GstElement *player;/* init */gst_init (&argc, &argv);/* create player */player = gst_element_factory_make ("oggvorbisplayer", "player");/* set the source audio file */g_object_set (player, "location", "helloworld.ogg", NULL);/* start playback */gst_element_set_state (GST_ELEMENT (player), GST_STATE_PLAYING);// [..]return 0;
}
自定义的箱柜可以同插件或XML解释器一起被创建。你可从 Plugin Writers Guide得到更多关于创建自定义箱柜的信息。https://gstreamer.freedesktop.org/documentation/plugin-development/?gi-language=c
传送门:deepstream系列文章分类目录整理
gstreamer教程(二)-初始化_组件(Element)_箱柜(Bins)的使用相关推荐
- gstreamer教程(一)-基本概念
Gstreamer基础知识: 1.gstreamer基本概念 2.gstreamer初始化.组件.箱柜 3.gstreamer总线.衬垫.缓冲区.事件 为什么需要看gstreamer教程:deepst ...
- gstreamer教程及在DM3730上的应用
感谢原文作者:goalie高义http://blog.csdn.net/goalietech/article/details/24887955 1 Gstreamer基本概念 GStreamer 是一 ...
- Java 接受reactjs数据_[Java教程]react.js 父子组件数据绑定实时通讯
[Java教程]react.js 父子组件数据绑定实时通讯 0 2017-09-23 17:00:14 import React,{Component} from 'react'import Reac ...
- ps文字换行_零基础一周内熟悉使用PS基础工具【Photoshop教程二】
零基础一周内熟悉使用PS基础工具[Photoshop教程一]这篇的后台数据显示有很多知友都有收藏了.由此可见现在的视频教程,网络上太多太多但,但好多知识都太"碎片化"今天学习这个技 ...
- 强大的DataGrid组件[4]_实现CURD[上]——Silverlight学习笔记[12]
在本教程中,主要为大家讲述如何使用DataGrid来对后台数据库进行CURD操作.由于CURD操作是与数据库交互中最为常用的,因此掌握其使用方法就显得尤为必要.本教程将使用Linq to SQL Cl ...
- 【源码+教程】Java课设项目_12款最热最新Java游戏项目_Java游戏开发_Java小游戏_飞翔的小鸟_王者荣耀_超级玛丽_推箱子_黄金矿工_贪吃蛇
马上就要期末了,同学们课设做的如何了呢?本篇为大家带来了12款热门Java小游戏项目的源码和教程,助力大家顺利迎接暑假![源码+教程]Java课设项目_12款最热最新Java游戏项目_Java游戏开发 ...
- C# 传递数组参数_一维数组_二维数组
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.T ...
- 微信小程序系统教程Java版[3/3阶段]_微信小程序电商系统-翟东平-专题视频课程...
微信小程序系统教程Java版[3/3阶段]_微信小程序电商系统-2445人已学习 课程介绍 微信小程序系统教程[初级阶段],微信小程序0基础学起,讲解微信小程序开发的基础知识. 微信小 ...
- 视频教程-微信小程序系统教程Java版[3/3阶段]_微信小程序电商系统-微信开发
微信小程序系统教程Java版[3/3阶段]_微信小程序电商系统 微信企业号星级会员.10多年软件从业经历,国家级软件项目负责人,主要从事软件研发.软件企业员工技能培训.已经取得计算机技术与软件资格考试 ...
最新文章
- sql server分布式事务解决方案[新事务不能登记到指定的事务处理器中错误]
- wxWidgets:wxPixelData< Image, PixelFormat >类模板用法
- 找不到托盘菜单配置文件_Windows 10最新更新导致用户文件丢失和加载错误配置文件...
- Linux内核 获取本机mac,Linux获取本机MAC地址
- 如何解决华为手机“杀后台”严重的情况呢?
- 知识点 - DataList中CheckBox实现单选
- 运行MonkeyRunner时使用Genymotion模拟器
- python中turtle画圆填充颜色_Python之turtle绘图
- 管理感悟:如何处理不干活的主管
- C#使用TCP/UDP协议通信并用Wireshark抓包分析数据
- java jsp乱码怎么解决_Java/JSP中文乱码问题解决心得
- 网络流24题23. 火星探险问题
- Axure RP8 进度条
- 京东微信、手机QQ引领社交化购物趋势
- Dfine2 for mac(图片降噪工具)
- 西电计科数据库系统期末复习笔记
- 马克思逝世140周年纪念|朋友一生一起走!马克思与恩格斯之间的感情有多深?...
- 关于相对论的一个猜想——二维空间理论
- 2021-04-29 微信登录简易版
- UniAPP HBuilderX 运行到各个小程序开发工具
热门文章
- Spring Security 4 整合Hibernate 实现持久化登录验证(带源码)
- python爬取小说项目概述_Python实现的爬取小说爬虫功能示例
- 灵遁者:时空一体化和能量守恒,决定了时间存在起点的观点值得怀疑
- vue下载pdf、word、excel、png图片
- 《2017云计算评测报告》:带你了解 AWS、阿里云、腾讯云等八家云计算服务提供商的综合用户体验情况...
- War3快捷键大全—ORC
- linux路径跟踪命令,linux的tracert命令的详细解释
- JavaScript高级程序设计---第五章 基本引用类型
- 维度诅咒和降维(curse of dimension and dimension rduction)
- 为svn服务增加自助修改密码功能