目标

GStreamer建立的pipeline不需要完全关闭。有多种方法可以让数据在任何时候送到pipeline中或者从pipeline中取出。本教程会展示:

如何把外部数据送到pipeline中

如何把数据从pipeline中取出

如何操作这些数据

介绍

有几种方法可以让应用通过pipeline和数据流交互。本教程讲述了最简单的一种,因为使用了专门为这个而创建的element。

专门让应用可以往pipeline里面传入数据的element时appsrc,而appsink就正好相反,让应用可以从pipeline中获得数据。为了避免混淆,我们可以这么来理解,appsrc是一个普通的source element,不过它的数据都是来自外太空,而appsink是一个普通的sink
element,数据从这里出去的就消失不见了。

appsrc和appsink用得非常多,所以他们都自己提供API,你只要连接了gstreamer-app库,那么就可以访问到。在本教程里,我们会使用一种简单地方法通过信号来实现。

appsrc可以有不同的工作模式:在pull模式,在需要时向应用请求数据;在push模式,应用根据自己的节奏把数据推送过来。而且,在push模式,如果已经有了足够的数据,应用可以在push时被阻塞,或者可以经由enough-data和need-data信号来控制。本教程中得例子就采用了这种信号控制的方式,其他没有提及的方法可以在appsrc的文档中查阅。

Buffers

通过pipeline传递的大块数据被称为buffers。因为本例子会制造数据同时也消耗数据,所以我们需要了解GstBuffer。

Source Pads负责制造buffer,这些buffer被sink pad消耗掉。GStreamer在一个个element之间传递这些buffer。

一个buffer只能简单地描述一小片数据,不要认为我们所有的buffer都是一样大小的。而且,buffer有一个时间戳和有效期,这个就描述了什么时候buffer里的数据需要渲染出来。时间戳是个非常复杂和精深的话题,但目前这个简单地解释也足够了。

作为一个例子,一个filesrc会提供“ANY”属性的buffers并且没有时间戳信息。在demux(《GStreamer基础教程03——动态pipeline》)之后,buffers会有一些特定的cap了,比如"video/x-h264",在解码后,每一个buffer都会包含一帧有原始caps的视频帧(比如:video/x-raw-yuv),并且有非常明确地时间戳用来指示这一帧在什么时候显示。

教程

本教程是上一篇教程(《GStreamer基础教程07——多线程和Pad的有效性》)在两个方面的扩展:第一是用appsrc来取代audiotestsrc来生成音频数据;第二是在tee里新加了一个分支,这样流入audio
sink和波形显示的数据同样复制了一份传给appsink。这个appsink就把信息回传给应用,应用就可以通知用户收到了数据或者做其他更复杂的工作。

一个粗糙的波形发生器

[objc] view
plain
 copy

  1. #include <gst/gst.h>
  2. #include <string.h>
  3. #define CHUNK_SIZE 1024   /* Amount of bytes we are sending in each buffer */
  4. #define SAMPLE_RATE 44100 /* Samples per second we are sending */
  5. #define AUDIO_CAPS "audio/x-raw-int,channels=1,rate=%d,signed=(boolean)true,width=16,depth=16,endianness=BYTE_ORDER"
  6. /* Structure to contain all our information, so we can pass it to callbacks */
  7. typedef struct _CustomData {
  8. , *audio_resample, *audio_sink;
  9. , *visual, *video_convert, *video_sink;
  10. GstElement *app_queue, *app_sink;
  11. 4 num_samples;   /* Number of samples generated so far (for timestamp generation) */
  12. gfloat a, b, c, d;     /* For waveform generation */
  13. guint sourceid;        /* To control the GSource */
  14. GMainLoop *main_loop;  /* GLib's Main Loop */
  15. } CustomData;
  16. /* This method is called by the idle GSource in the mainloop, to feed CHUNK_SIZE bytes into appsrc.
  17. * The idle handler is added to the mainloop when appsrc requests us to start sending data (need-data signal)
  18. * and is removed when appsrc has enough data (enough-data signal).
  19. */
  20. static gboolean push_data (CustomData *data) {
  21. GstBuffer *buffer;
  22. GstFlowReturn ret;
  23. int i;
  24. gint16 *raw;
  25. ; /* Because each sample is 16 bits */
  26. gfloat freq;
  27. /* Create a new empty buffer */
  28. buffer = gst_buffer_new_and_alloc (CHUNK_SIZE);
  29. /* Set its timestamp and duration */
  30. 4_scale (data->num_samples, GST_SECOND, SAMPLE_RATE);
  31. 4_scale (CHUNK_SIZE, GST_SECOND, SAMPLE_RATE);
  32. /* Generate some psychodelic waveforms */
  33. raw = (gint16 *)GST_BUFFER_DATA (buffer);
  34. data->c += data->d;
  35. 000;
  36. 100 + 11000 * data->d;
  37. ; i < num_samples; i++) {
  38. data->a += data->b;
  39. data->b -= data->a / freq;
  40. 6)(5500 * data->a);
  41. }
  42. data->num_samples += num_samples;
  43. /* Push the buffer into the appsrc */
  44. g_signal_emit_by_name (data->app_source, "push-buffer", buffer, &ret);
  45. /* Free the buffer now that we are done with it */
  46. gst_buffer_unref (buffer);
  47. if (ret != GST_FLOW_OK) {
  48. /* We got some error, stop sending data */
  49. return FALSE;
  50. }
  51. return TRUE;
  52. }
  53. /* This signal callback triggers when appsrc needs data. Here, we add an idle handler
  54. * to the mainloop to start pushing data into the appsrc */
  55. static void start_feed (GstElement *source, guint size, CustomData *data) {
  56. ) {
  57. g_print ("Start feeding\n");
  58. data->sourceid = g_idle_add ((GSourceFunc) push_data, data);
  59. }
  60. }
  61. /* This callback triggers when appsrc has enough data and we can stop sending.
  62. * We remove the idle handler from the mainloop */
  63. static void stop_feed (GstElement *source, CustomData *data) {
  64. ) {
  65. g_print ("Stop feeding\n");
  66. g_source_remove (data->sourceid);
  67. ;
  68. }
  69. }
  70. /* The appsink has received a buffer */
  71. static void new_buffer (GstElement *sink, CustomData *data) {
  72. GstBuffer *buffer;
  73. /* Retrieve the buffer */
  74. g_signal_emit_by_name (sink, "pull-buffer", &buffer);
  75. if (buffer) {
  76. /* The only thing we do in this example is print a * to indicate a received buffer */
  77. g_print ("*");
  78. gst_buffer_unref (buffer);
  79. }
  80. }
  81. /* This function is called when an error message is posted on the bus */
  82. static void error_cb (GstBus *bus, GstMessage *msg, CustomData *data) {
  83. GError *err;
  84. gchar *debug_info;
  85. /* Print error details on the screen */
  86. gst_message_parse_error (msg, &err, &debug_info);
  87. g_printerr ("Error received from element %s: %s\n", GST_OBJECT_NAME (msg->src), err->message);
  88. g_printerr ("Debugging information: %s\n", debug_info ? debug_info : "none");
  89. g_clear_error (&err);
  90. g_free (debug_info);
  91. g_main_loop_quit (data->main_loop);
  92. }
  93. int main(int argc, charchar *argv[]) {
  94. CustomData data;
  95. GstPadTemplate *tee_src_pad_template;
  96. GstPad *tee_audio_pad, *tee_video_pad, *tee_app_pad;
  97. GstPad *queue_audio_pad, *queue_video_pad, *queue_app_pad;
  98. gchar *audio_caps_text;
  99. GstCaps *audio_caps;
  100. GstBus *bus;
  101. /* Initialize cumstom data structure */
  102. , sizeof (data));
  103. ; /* For waveform generation */
  104. ;
  105. /* Initialize GStreamer */
  106. gst_init (&argc, &argv);
  107. /* Create the elements */
  108. data.app_source = gst_element_factory_make ("appsrc", "audio_source");
  109. data.tee = gst_element_factory_make ("tee", "tee");
  110. data.audio_queue = gst_element_factory_make ("queue", "audio_queue");
  111. data.audio_convert1 = gst_element_factory_make ("audioconvert", "audio_convert1");
  112. data.audio_resample = gst_element_factory_make ("audioresample", "audio_resample");
  113. data.audio_sink = gst_element_factory_make ("autoaudiosink", "audio_sink");
  114. data.video_queue = gst_element_factory_make ("queue", "video_queue");
  115. data.audio_convert2 = gst_element_factory_make ("audioconvert", "audio_convert2");
  116. data.visual = gst_element_factory_make ("wavescope", "visual");
  117. data.video_convert = gst_element_factory_make ("ffmpegcolorspace", "csp");
  118. data.video_sink = gst_element_factory_make ("autovideosink", "video_sink");
  119. data.app_queue = gst_element_factory_make ("queue", "app_queue");
  120. data.app_sink = gst_element_factory_make ("appsink", "app_sink");
  121. /* Create the empty pipeline */
  122. data.pipeline = gst_pipeline_new ("test-pipeline");
  123. if (!data.pipeline || !data.app_source || !data.tee || !data.audio_queue || !data.audio_convert1 ||
  124. !data.audio_resample || !data.audio_sink || !data.video_queue || !data.audio_convert2 || !data.visual ||
  125. !data.video_convert || !data.video_sink || !data.app_queue || !data.app_sink) {
  126. g_printerr ("Not all elements could be created.\n");
  127. ;
  128. }
  129. /* Configure wavescope */
  130. , "style", 0, NULL);
  131. /* Configure appsrc */
  132. audio_caps_text = g_strdup_printf (AUDIO_CAPS, SAMPLE_RATE);
  133. audio_caps = gst_caps_from_string (audio_caps_text);
  134. g_object_set (data.app_source, "caps", audio_caps, NULL);
  135. g_signal_connect (data.app_source, "need-data", G_CALLBACK (start_feed), &data);
  136. g_signal_connect (data.app_source, "enough-data", G_CALLBACK (stop_feed), &data);
  137. /* Configure appsink */
  138. g_object_set (data.app_sink, "emit-signals", TRUE, "caps", audio_caps, NULL);
  139. g_signal_connect (data.app_sink, "new-buffer", G_CALLBACK (new_buffer), &data);
  140. gst_caps_unref (audio_caps);
  141. g_free (audio_caps_text);
  142. /* Link all elements that can be automatically linked because they have "Always" pads */
  143. gst_bin_add_many (GST_BIN (data.pipeline), data.app_source, data.tee, data.audio_queue, data.audio_convert1, data.audio_resample,
  144. data.audio_sink, data.video_queue, data.audio_convert2, data.visual, data.video_convert, data.video_sink, data.app_queue,
  145. data.app_sink, NULL);
  146. if (gst_element_link_many (data.app_source, data.tee, NULL) != TRUE ||
  147. gst_element_link_many (data.audio_queue, data.audio_convert1, data.audio_resample, data.audio_sink, NULL) != TRUE ||
  148. gst_element_link_many (data.video_queue, data.audio_convert2, data.visual, data.video_convert, data.video_sink, NULL) != TRUE ||
  149. gst_element_link_many (data.app_queue, data.app_sink, NULL) != TRUE) {
  150. g_printerr ("Elements could not be linked.\n");
  151. gst_object_unref (data.pipeline);
  152. ;
  153. }
  154. /* Manually link the Tee, which has "Request" pads */
  155. tee_src_pad_template = gst_element_class_get_pad_template (GST_ELEMENT_GET_CLASS (data.tee), "src%d");
  156. tee_audio_pad = gst_element_request_pad (data.tee, tee_src_pad_template, NULL, NULL);
  157. g_print ("Obtained request pad %s for audio branch.\n", gst_pad_get_name (tee_audio_pad));
  158. queue_audio_pad = gst_element_get_static_pad (data.audio_queue, "sink");
  159. tee_video_pad = gst_element_request_pad (data.tee, tee_src_pad_template, NULL, NULL);
  160. g_print ("Obtained request pad %s for video branch.\n", gst_pad_get_name (tee_video_pad));
  161. queue_video_pad = gst_element_get_static_pad (data.video_queue, "sink");
  162. tee_app_pad = gst_element_request_pad (data.tee, tee_src_pad_template, NULL, NULL);
  163. g_print ("Obtained request pad %s for app branch.\n", gst_pad_get_name (tee_app_pad));
  164. queue_app_pad = gst_element_get_static_pad (data.app_queue, "sink");
  165. if (gst_pad_link (tee_audio_pad, queue_audio_pad) != GST_PAD_LINK_OK ||
  166. gst_pad_link (tee_video_pad, queue_video_pad) != GST_PAD_LINK_OK ||
  167. gst_pad_link (tee_app_pad, queue_app_pad) != GST_PAD_LINK_OK) {
  168. g_printerr ("Tee could not be linked\n");
  169. gst_object_unref (data.pipeline);
  170. ;
  171. }
  172. gst_object_unref (queue_audio_pad);
  173. gst_object_unref (queue_video_pad);
  174. gst_object_unref (queue_app_pad);
  175. /* Instruct the bus to emit signals for each received message, and connect to the interesting signals */
  176. bus = gst_element_get_bus (data.pipeline);
  177. gst_bus_add_signal_watch (bus);
  178. g_signal_connect (G_OBJECT (bus), "message::error", (GCallback)error_cb, &data);
  179. gst_object_unref (bus);
  180. /* Start playing the pipeline */
  181. gst_element_set_state (data.pipeline, GST_STATE_PLAYING);
  182. /* Create a GLib Main Loop and set it to run */
  183. data.main_loop = g_main_loop_new (NULL, FALSE);
  184. g_main_loop_run (data.main_loop);
  185. /* Release the request pads from the Tee, and unref them */
  186. gst_element_release_request_pad (data.tee, tee_audio_pad);
  187. gst_element_release_request_pad (data.tee, tee_video_pad);
  188. gst_element_release_request_pad (data.tee, tee_app_pad);
  189. gst_object_unref (tee_audio_pad);
  190. gst_object_unref (tee_video_pad);
  191. gst_object_unref (tee_app_pad);
  192. /* Free resources */
  193. gst_element_set_state (data.pipeline, GST_STATE_NULL);
  194. gst_object_unref (data.pipeline);
  195. ;
  196. }

工作流程

创建pipeline段的代码就是上一篇的教程中得例子的扩大版。包括初始或所有的element,连接有Always Pad的element然后手动连接tee element的Request Pad。

下面我们关注一下appsrc和appsink这两个element的配置:

[objc] view
plain
 copy

  1. /* Configure appsrc */
  2. audio_caps_text = g_strdup_printf (AUDIO_CAPS, SAMPLE_RATE);
  3. audio_caps = gst_caps_from_string (audio_caps_text);
  4. g_object_set (data.app_source, "caps", audio_caps, NULL);
  5. g_signal_connect (data.app_source, "need-data", G_CALLBACK (start_feed), &data);
  6. g_signal_connect (data.app_source, "enough-data", G_CALLBACK (stop_feed), &data);

appsrc里面第一个需要关注的属性是caps。它说明了element准备生成的数据的类型,这样GStreamer就可以检查下游的element看看是否支持。这个属性必须是一个GstCaps对象,这个对象可以很方便的由gst_caps_from_string()来生成。

然后我们把need-data和enough-data信号和回调连接起来,这样在appsrc内部的队列里面数据不足或将要满地时候会发送信号,我们就用这些信号来启动/停止我们的信号发生过程。

[objc] view
plain
 copy

  1. /* Configure appsink */
  2. g_object_set (data.app_sink, "emit-signals", TRUE, "caps", audio_caps, NULL);
  3. g_signal_connect (data.app_sink, "new-buffer", G_CALLBACK (new_buffer), &data);
  4. gst_caps_unref (audio_caps);
  5. g_free (audio_caps_text);

关于appsink的配置,我们连接了new-buffer的信号,这个信号在每次收到buffer的时候发出。当然,这个信号的发出需要emit-signals这个信号属性被开启(默认是关闭的)。

启动pipeline,等到消息和最后的清理资源都和以前的没什么区别。让我们关注我们刚刚注册的回调吧。

[objc] view
plain
 copy

  1. /* This signal callback triggers when appsrc needs data. Here, we add an idle handler
  2. * to the mainloop to start pushing data into the appsrc */
  3. static void start_feed (GstElement *source, guint size, CustomData *data) {
  4. ) {
  5. g_print ("Start feeding\n");
  6. data->sourceid = g_idle_add ((GSourceFunc) push_data, data);
  7. }
  8. }

这个函数在appsrc内部队列将要空的时候调用,在这里我们做的事情仅仅是用g_idle_add()方法注册一个GLib的idle函数,这个函数会给appsrc输入数据知道内部队列满为止。一个GLib的idle函数是一个GLib在主循环在“idle”时会调用的方法,也就是说,当时没有更高优先级的任务运行。

这只是appsrc多种发出数据方法中的一个。特别需要指出的是,buffer不是必须要在主线程中用GLib方法来传递给appsrc的,你也不是一定要用need-data和enough-data信号来同步appsrc的(据说这样最方便)。

我们记录下g_idle_add()的返回的sourceid,这样后面可以关掉它。

[objc] view
plain
 copy

  1. /* This callback triggers when appsrc has enough data and we can stop sending.
  2. * We remove the idle handler from the mainloop */
  3. static void stop_feed (GstElement *source, CustomData *data) {
  4. ) {
  5. g_print ("Stop feeding\n");
  6. g_source_remove (data->sourceid);
  7. ;
  8. }
  9. }

这个函数当appsrc内部的队列满的时候调用,所以我们需要停止发送数据。这里我们简单地用g_source_remove()来把idle函数移走。

[objc] view
plain
 copy

  1. /* This method is called by the idle GSource in the mainloop, to feed CHUNK_SIZE bytes into appsrc.
  2. * The idle handler is added to the mainloop when appsrc requests us to start sending data (need-data signal)
  3. * and is removed when appsrc has enough data (enough-data signal).
  4. */
  5. static gboolean push_data (CustomData *data) {
  6. GstBuffer *buffer;
  7. GstFlowReturn ret;
  8. int i;
  9. gint16 *raw;
  10. ; /* Because each sample is 16 bits */
  11. gfloat freq;
  12. /* Create a new empty buffer */
  13. buffer = gst_buffer_new_and_alloc (CHUNK_SIZE);
  14. /* Set its timestamp and duration */
  15. 4_scale (data->num_samples, GST_SECOND, SAMPLE_RATE);
  16. 4_scale (CHUNK_SIZE, GST_SECOND, SAMPLE_RATE);
  17. /* Generate some psychodelic waveforms */
  18. raw = (gint16 *)GST_BUFFER_DATA (buffer);

这个函数给appsrc发送数据。它被GLib调用的次数和频率我们不加以控制,但我们会在它任务完成时关闭它(appsrc内部队列满)。

这里第一步是用gst_buffer_new_and_alloc()方法和给定的大小创建一个新buffer(例子中是1024字节)。

我们计算我们生成的采样数据的数据量,把数据存在CustomData.num_samples里面,这样我们可以用GstBuffer提供的GST_BUFFER_TIMESTAMP宏来生成buffer的时间戳。

gst_util_uint64_scale是一个工具函数,用来缩放数据,确保不会溢出。

这些给buffer的数据可以用GstBuffer提供的GST_BUFFER_DATA宏来访问。

我们会跳过波形的生成部分,因为这不是本教程要讲述的内容。

[objc] view
plain
 copy

  1. /* Push the buffer into the appsrc */
  2. g_signal_emit_by_name (data->app_source, "push-buffer", buffer, &ret);
  3. /* Free the buffer now that we are done with it */
  4. gst_buffer_unref (buffer);

一旦我们的buffer已经准备好,我们把带着这个buffer的push-buffer信号传给appsrc,然后就调用gst_buffer_unref()方法,因为我们不会再用到它了。

[objc] view
plain
 copy

  1. /* The appsink has received a buffer */
  2. static void new_buffer (GstElement *sink, CustomData *data) {
  3. GstBuffer *buffer;
  4. /* Retrieve the buffer */
  5. g_signal_emit_by_name (sink, "pull-buffer", &buffer);
  6. if (buffer) {
  7. /* The only thing we do in this example is print a * to indicate a received buffer */
  8. g_print ("*");
  9. gst_buffer_unref (buffer);
  10. }
  11. }

最后,这个函数在appsink收到buffer时被调用。我们使用了pull-buffer的信号来重新获得buffer,因为是例子,所以仅仅在屏幕上打印一些内容。我们可以用GstBuffer的GST_BUFFER_DATA宏来获得数据指针和用GST_BUFFER_SIZE宏来获得数据大小。请记住,这里的buffer不是一定要和我们在push_data函数里面创建的buffer完全一致的,在传输路径上得任何一个element都可能对buffer进行一些改变。(这个例子中仅仅是在appsrc和appsink中间通过一个tee
element,所以buffer没有变化)。

请不要忘记调用gst_buffer_unref()来释放buffer,就讲这么多吧。

【GStreamer开发】GStreamer基础教程08——pipeline的快捷访问的更多相关文章

  1. 【GStreamer开发】GStreamer播放教程03——pipeline的快捷访问

    目的 <GStreamer08--pipeline的快捷访问>展示了一个应用如何用appsrc和appsink这两个特殊的element在pipeline中手动输入/提取数据.playbi ...

  2. Android程序开发0基础教程(一)

    程序猿学英语就上视觉英语网 Android程序开发0基础教程(一)   平台简单介绍   令人激动的Google手机操作系统平台-Android在2007年11月13日正式公布了,这是一个开放源码的操 ...

  3. GStreamer基础教程08 - 多线程

    摘要 GStreamer框架会自动处理多线程的逻辑,但在某些情况下,我们仍然需要根据实际的情况自己将部分Pipeline在单独的线程中执行,本文将介绍如何处理这种情况. GStreamer多线程 GS ...

  4. iOS开发零基础教程之生成git所需的SSH keys

    在我们github看到了一个不错的第三方库时,可能我们想把他git clone到本地,我们需要复制他的SSH URL,如下图: 复制完地址之后,我们需要打开终端,然后输入命令: git clone + ...

  5. GStreamer基础教程09 - Appsrc及Appsink

    摘要 在我们前面的文章中,我们的Pipline都是使用GStreamer自带的插件去产生/消费数据.在实际的情况中,我们的数据源可能没有相应的gstreamer插件,但我们又需要将数据发送到GStre ...

  6. Chrome扩展开发基础教程(附HelloWorld)

    1 概述 Chrome扩展开发的基础教程,代码基于原生JS+H5,教程内容基于谷歌扩展开发官方文档. 2 环境 Chrome 88.0.4324.96 Chromium 87.0.4280.141 B ...

  7. 【GStreamer开发】GStreamer基础教程03——动态pipeline

    本教程介绍pipeline的一种新的创建方式--在运行中创建,而不是在运行前一次性的创建结束. 介绍 在这篇教程里的pipeline并非在运行前就全部创建结束的.放松一下,这样做没有任何问题.如果我们 ...

  8. 【GStreamer开发】GStreamer基础教程14——常用的element

    目标 本教程给出了一系列开发中常用的element.它们包括大杂烩般的eleemnt(比如playbin2)以及一些调试时很有用的element. 简单来说,下面用gst-launch这个工具给出一个 ...

  9. 【GStreamer开发】GStreamer基础教程13——播放速度

    目标 快进,倒放和慢放是trick模式的共同技巧,它们有一个共同点就是它们都修改了播放的速度.本教程会展示如何来获得这些效果和如何进行逐帧的跳跃.主要内容是: 如何来变换播放的速度,变快或者变慢,前进 ...

随机推荐

  1. Linux下搭建iSCSI共享存储的方法 TGT 方式 Debian9.5系统下

    iSCSI(internet SCSI)技术由IBM公司研究开发,是一个供硬件设备使用的.可以在IP协议的上层运行的SCSI指令集,这种指令集合可以实现在IP网络上运行SCSI协议,使其能够在诸如高速 ...

  2. codecs 1264 芳香数

    1264 芳香数 题目描述 Description This question involves calculating the value of aromatic numbers which are ...

  3. 洛谷 P1006 传纸条 题解

    P1006 传纸条 题目描述 小渊和小轩是好朋友也是同班同学,他们在一起总有谈不完的话题.一次素质拓展活动中,班上同学安排做成一个m行n列的矩阵,而小渊和小轩被安排在矩阵对角线的两端,因此,他们就无法 ...

  4. 洛谷P1288取数游戏2

    题目 博弈论. 考虑先手和后手的关系.然后可以通过统计数值不是0的数的个数来得出答案. \(Code\) #include <bits/stdc++.h> using namespace ...

  5. 数据结构实验之查找六:顺序查找(SDUT 3378)

    (不知道为啥开个数组就 TLE .QAQ) #include <stdio.h> #include <stdlib.h> #include <string.h> / ...

  6. android studio 使用第三方模拟器连接方法

    安装完模拟器后,要使用adb命令Android studio才能识别出来: 打开cmd,输入:adb connect 127.0.0.1:26944.如下: 海马玩模拟器的端口号是26944. 逍遥安 ...

  7. Chrome远程调试之WebSocket

    var ws = new WebSocket('ws://localhost:9222/devtools/page/3c5c05fa-80b7-4cfe-8d1d-ebe79a7a5730');ws. ...

  8. Python自动化测试常用库

    基本库: sys 程序和Python解析器的交互 os 启动新进程:操作文件和目录 re 正则表达式,字符串匹配 string 基本字符串操作 inspect 提供自省和反射功能 importlib ...

  9. 10个超漂亮的CSS 3D特效

    10个超漂亮的CSS 3D特效 一.总结 一句话总结: 后面有空得好好练一练,也可以作为录课素材 二.10个超漂亮的CSS 3D特效 转自或参考:10个超漂亮的CSS 3D特效https://blog ...

  10. UE4虚幻引擎独立游戏制作教程 UE4编程教学 虚幻引擎4

    非常好的一套UE4入门教学课程,语言诙谐幽默,并且是中文语音中文语音中文语音 赠送[精通Unreal引擎技术——关卡设计艺术]PDF版 目录 FLV格式,大小5G,中文语音 扫码时备注或说明中留下邮箱 ...