playback-tutorial-3/basic-tutorial-8: Fix buffer duration calculations
[platform/upstream/gstreamer.git] / markdown / tutorials / basic / short-cutting-the-pipeline.md
1 #  Basic tutorial 8: Short-cutting the pipeline
2
3 ## Goal
4
5 Pipelines constructed with GStreamer do not need to be completely
6 closed. Data can be injected into the pipeline and extracted from it at
7 any time, in a variety of ways. This tutorial shows:
8
9   - How to inject external data into a general GStreamer pipeline.
10
11   - How to extract data from a general GStreamer pipeline.
12
13   - How to access and manipulate this data.
14
15 [](tutorials/playback/short-cutting-the-pipeline.md) explains
16 how to achieve the same goals in a playbin-based pipeline.
17
18 ## Introduction
19
20 Applications can interact with the data flowing through a GStreamer
21 pipeline in several ways. This tutorial describes the easiest one, since
22 it uses elements that have been created for this sole purpose.
23
24 The element used to inject application data into a GStreamer pipeline is
25 `appsrc`, and its counterpart, used to extract GStreamer data back to
26 the application is `appsink`. To avoid confusing the names, think of it
27 from GStreamer's point of view: `appsrc` is just a regular source, that
28 provides data magically fallen from the sky (provided by the
29 application, actually). `appsink` is a regular sink, where the data
30 flowing through a GStreamer pipeline goes to die (it is recovered by the
31 application, actually).
32
33 `appsrc` and `appsink` are so versatile that they offer their own API
34 (see their documentation), which can be accessed by linking against the
35 `gstreamer-app` library. In this tutorial, however, we will use a
36 simpler approach and control them through signals.
37
38 `appsrc` can work in a variety of modes: in **pull** mode, it requests
39 data from the application every time it needs it. In **push** mode, the
40 application pushes data at its own pace. Furthermore, in push mode, the
41 application can choose to be blocked in the push function when enough
42 data has already been provided, or it can listen to the
43 `enough-data` and `need-data` signals to control flow. This example
44 implements the latter approach. Information regarding the other methods
45 can be found in the `appsrc` documentation.
46
47 ### Buffers
48
49 Data travels through a GStreamer pipeline in chunks called **buffers**.
50 Since this example produces and consumes data, we need to know about
51 `GstBuffer`s.
52
53 Source Pads produce buffers, that are consumed by Sink Pads; GStreamer
54 takes these buffers and passes them from element to element.
55
56 A buffer simply represents a unit of data, do not assume that all
57 buffers will have the same size, or represent the same amount of time.
58 Neither should you assume that if a single buffer enters an element, a
59 single buffer will come out. Elements are free to do with the received
60 buffers as they please. `GstBuffer`s may also contain more than one
61 actual memory buffer. Actual memory buffers are abstracted away using
62 `GstMemory` objects, and a `GstBuffer` can contain multiple `GstMemory` objects.
63
64 Every buffer has attached time-stamps and duration, that describe in
65 which moment the content of the buffer should be decoded, rendered or
66 displayed. Time stamping is a very complex and delicate subject, but
67 this simplified vision should suffice for now.
68
69 As an example, a `filesrc` (a GStreamer element that reads files)
70 produces buffers with the “ANY” caps and no time-stamping information.
71 After demuxing (see [](tutorials/basic/dynamic-pipelines.md))
72 buffers can have some specific caps, for example “video/x-h264”. After
73 decoding, each buffer will contain a single video frame with raw caps
74 (for example, “video/x-raw-yuv”) and very precise time stamps indicating
75 when should that frame be displayed.
76
77 ### This tutorial
78
79 This tutorial expands [](tutorials/basic/multithreading-and-pad-availability.md) in
80 two ways: firstly, the `audiotestsrc` is replaced by an `appsrc` that
81 will generate the audio data. Secondly, a new branch is added to the
82 `tee` so data going into the audio sink and the wave display is also
83 replicated into an `appsink`. The `appsink` uploads the information back
84 into the application, which then just notifies the user that data has
85 been received, but it could obviously perform more complex tasks.
86
87 ![](images/tutorials/basic-tutorial-8.png)
88
89 ## A crude waveform generator
90
91 Copy this code into a text file named `basic-tutorial-8.c` (or find it
92 in your GStreamer installation).
93
94 ``` c
95 #include <gst/gst.h>
96 #include <gst/audio/audio.h>
97 #include <string.h>
98
99 #define CHUNK_SIZE 1024   /* Amount of bytes we are sending in each buffer */
100 #define SAMPLE_RATE 44100 /* Samples per second we are sending */
101
102 /* Structure to contain all our information, so we can pass it to callbacks */
103 typedef struct _CustomData {
104   GstElement *pipeline, *app_source, *tee, *audio_queue, *audio_convert1, *audio_resample, *audio_sink;
105   GstElement *video_queue, *audio_convert2, *visual, *video_convert, *video_sink;
106   GstElement *app_queue, *app_sink;
107
108   guint64 num_samples;   /* Number of samples generated so far (for timestamp generation) */
109   gfloat a, b, c, d;     /* For waveform generation */
110
111   guint sourceid;        /* To control the GSource */
112
113   GMainLoop *main_loop;  /* GLib's Main Loop */
114 } CustomData;
115
116 /* This method is called by the idle GSource in the mainloop, to feed CHUNK_SIZE bytes into appsrc.
117  * The ide handler is added to the mainloop when appsrc requests us to start sending data (need-data signal)
118  * and is removed when appsrc has enough data (enough-data signal).
119  */
120 static gboolean push_data (CustomData *data) {
121   GstBuffer *buffer;
122   GstFlowReturn ret;
123   int i;
124   GstMapInfo map;
125   gint16 *raw;
126   gint num_samples = CHUNK_SIZE / 2; /* Because each sample is 16 bits */
127   gfloat freq;
128
129   /* Create a new empty buffer */
130   buffer = gst_buffer_new_and_alloc (CHUNK_SIZE);
131
132   /* Set its timestamp and duration */
133   GST_BUFFER_TIMESTAMP (buffer) = gst_util_uint64_scale (data->num_samples, GST_SECOND, SAMPLE_RATE);
134   GST_BUFFER_DURATION (buffer) = gst_util_uint64_scale (num_samples, GST_SECOND, SAMPLE_RATE);
135
136   /* Generate some psychodelic waveforms */
137   gst_buffer_map (buffer, &map, GST_MAP_WRITE);
138   raw = (gint16 *)map.data;
139   data->c += data->d;
140   data->d -= data->c / 1000;
141   freq = 1100 + 1000 * data->d;
142   for (i = 0; i < num_samples; i++) {
143     data->a += data->b;
144     data->b -= data->a / freq;
145     raw[i] = (gint16)(500 * data->a);
146   }
147   gst_buffer_unmap (buffer, &map);
148   data->num_samples += num_samples;
149
150   /* Push the buffer into the appsrc */
151   g_signal_emit_by_name (data->app_source, "push-buffer", buffer, &ret);
152
153   /* Free the buffer now that we are done with it */
154   gst_buffer_unref (buffer);
155
156   if (ret != GST_FLOW_OK) {
157     /* We got some error, stop sending data */
158     return FALSE;
159   }
160
161   return TRUE;
162 }
163
164 /* This signal callback triggers when appsrc needs data. Here, we add an idle handler
165  * to the mainloop to start pushing data into the appsrc */
166 static void start_feed (GstElement *source, guint size, CustomData *data) {
167   if (data->sourceid == 0) {
168     g_print ("Start feeding\n");
169     data->sourceid = g_idle_add ((GSourceFunc) push_data, data);
170   }
171 }
172
173 /* This callback triggers when appsrc has enough data and we can stop sending.
174  * We remove the idle handler from the mainloop */
175 static void stop_feed (GstElement *source, CustomData *data) {
176   if (data->sourceid != 0) {
177     g_print ("Stop feeding\n");
178     g_source_remove (data->sourceid);
179     data->sourceid = 0;
180   }
181 }
182
183 /* The appsink has received a buffer */
184 static void new_sample (GstElement *sink, CustomData *data) {
185   GstSample *sample;
186
187   /* Retrieve the buffer */
188   g_signal_emit_by_name (sink, "pull-sample", &sample);
189   if (sample) {
190     /* The only thing we do in this example is print a * to indicate a received buffer */
191     g_print ("*");
192     gst_sample_unref (sample);
193   }
194 }
195
196 /* This function is called when an error message is posted on the bus */
197 static void error_cb (GstBus *bus, GstMessage *msg, CustomData *data) {
198   GError *err;
199   gchar *debug_info;
200
201   /* Print error details on the screen */
202   gst_message_parse_error (msg, &err, &debug_info);
203   g_printerr ("Error received from element %s: %s\n", GST_OBJECT_NAME (msg->src), err->message);
204   g_printerr ("Debugging information: %s\n", debug_info ? debug_info : "none");
205   g_clear_error (&err);
206   g_free (debug_info);
207
208   g_main_loop_quit (data->main_loop);
209 }
210
211 int main(int argc, char *argv[]) {
212   CustomData data;
213   GstPad *tee_audio_pad, *tee_video_pad, *tee_app_pad;
214   GstPad *queue_audio_pad, *queue_video_pad, *queue_app_pad;
215   GstAudioInfo info;
216   GstCaps *audio_caps;
217   GstBus *bus;
218
219   /* Initialize cumstom data structure */
220   memset (&data, 0, sizeof (data));
221   data.b = 1; /* For waveform generation */
222   data.d = 1;
223
224   /* Initialize GStreamer */
225   gst_init (&argc, &argv);
226
227   /* Create the elements */
228   data.app_source = gst_element_factory_make ("appsrc", "audio_source");
229   data.tee = gst_element_factory_make ("tee", "tee");
230   data.audio_queue = gst_element_factory_make ("queue", "audio_queue");
231   data.audio_convert1 = gst_element_factory_make ("audioconvert", "audio_convert1");
232   data.audio_resample = gst_element_factory_make ("audioresample", "audio_resample");
233   data.audio_sink = gst_element_factory_make ("autoaudiosink", "audio_sink");
234   data.video_queue = gst_element_factory_make ("queue", "video_queue");
235   data.audio_convert2 = gst_element_factory_make ("audioconvert", "audio_convert2");
236   data.visual = gst_element_factory_make ("wavescope", "visual");
237   data.video_convert = gst_element_factory_make ("videoconvert", "csp");
238   data.video_sink = gst_element_factory_make ("autovideosink", "video_sink");
239   data.app_queue = gst_element_factory_make ("queue", "app_queue");
240   data.app_sink = gst_element_factory_make ("appsink", "app_sink");
241
242   /* Create the empty pipeline */
243   data.pipeline = gst_pipeline_new ("test-pipeline");
244
245   if (!data.pipeline || !data.app_source || !data.tee || !data.audio_queue || !data.audio_convert1 ||
246       !data.audio_resample || !data.audio_sink || !data.video_queue || !data.audio_convert2 || !data.visual ||
247       !data.video_convert || !data.video_sink || !data.app_queue || !data.app_sink) {
248     g_printerr ("Not all elements could be created.\n");
249     return -1;
250   }
251
252   /* Configure wavescope */
253   g_object_set (data.visual, "shader", 0, "style", 0, NULL);
254
255   /* Configure appsrc */
256   gst_audio_info_set_format (&info, GST_AUDIO_FORMAT_S16, SAMPLE_RATE, 1, NULL);
257   audio_caps = gst_audio_info_to_caps (&info);
258   g_object_set (data.app_source, "caps", audio_caps, "format", GST_FORMAT_TIME, NULL);
259   g_signal_connect (data.app_source, "need-data", G_CALLBACK (start_feed), &data);
260   g_signal_connect (data.app_source, "enough-data", G_CALLBACK (stop_feed), &data);
261
262   /* Configure appsink */
263   g_object_set (data.app_sink, "emit-signals", TRUE, "caps", audio_caps, NULL);
264   g_signal_connect (data.app_sink, "new-sample", G_CALLBACK (new_sample), &data);
265   gst_caps_unref (audio_caps);
266
267   /* Link all elements that can be automatically linked because they have "Always" pads */
268   gst_bin_add_many (GST_BIN (data.pipeline), data.app_source, data.tee, data.audio_queue, data.audio_convert1, data.audio_resample,
269       data.audio_sink, data.video_queue, data.audio_convert2, data.visual, data.video_convert, data.video_sink, data.app_queue,
270       data.app_sink, NULL);
271   if (gst_element_link_many (data.app_source, data.tee, NULL) != TRUE ||
272       gst_element_link_many (data.audio_queue, data.audio_convert1, data.audio_resample, data.audio_sink, NULL) != TRUE ||
273       gst_element_link_many (data.video_queue, data.audio_convert2, data.visual, data.video_convert, data.video_sink, NULL) != TRUE ||
274       gst_element_link_many (data.app_queue, data.app_sink, NULL) != TRUE) {
275     g_printerr ("Elements could not be linked.\n");
276     gst_object_unref (data.pipeline);
277     return -1;
278   }
279
280   /* Manually link the Tee, which has "Request" pads */
281   tee_audio_pad = gst_element_get_request_pad (data.tee, "src_%u");
282   g_print ("Obtained request pad %s for audio branch.\n", gst_pad_get_name (tee_audio_pad));
283   queue_audio_pad = gst_element_get_static_pad (data.audio_queue, "sink");
284   tee_video_pad = gst_element_get_request_pad (data.tee, "src_%u");
285   g_print ("Obtained request pad %s for video branch.\n", gst_pad_get_name (tee_video_pad));
286   queue_video_pad = gst_element_get_static_pad (data.video_queue, "sink");
287   tee_app_pad = gst_element_get_request_pad (data.tee, "src_%u");
288   g_print ("Obtained request pad %s for app branch.\n", gst_pad_get_name (tee_app_pad));
289   queue_app_pad = gst_element_get_static_pad (data.app_queue, "sink");
290   if (gst_pad_link (tee_audio_pad, queue_audio_pad) != GST_PAD_LINK_OK ||
291       gst_pad_link (tee_video_pad, queue_video_pad) != GST_PAD_LINK_OK ||
292       gst_pad_link (tee_app_pad, queue_app_pad) != GST_PAD_LINK_OK) {
293     g_printerr ("Tee could not be linked\n");
294     gst_object_unref (data.pipeline);
295     return -1;
296   }
297   gst_object_unref (queue_audio_pad);
298   gst_object_unref (queue_video_pad);
299   gst_object_unref (queue_app_pad);
300
301   /* Instruct the bus to emit signals for each received message, and connect to the interesting signals */
302   bus = gst_element_get_bus (data.pipeline);
303   gst_bus_add_signal_watch (bus);
304   g_signal_connect (G_OBJECT (bus), "message::error", (GCallback)error_cb, &data);
305   gst_object_unref (bus);
306
307   /* Start playing the pipeline */
308   gst_element_set_state (data.pipeline, GST_STATE_PLAYING);
309
310   /* Create a GLib Main Loop and set it to run */
311   data.main_loop = g_main_loop_new (NULL, FALSE);
312   g_main_loop_run (data.main_loop);
313
314   /* Release the request pads from the Tee, and unref them */
315   gst_element_release_request_pad (data.tee, tee_audio_pad);
316   gst_element_release_request_pad (data.tee, tee_video_pad);
317   gst_element_release_request_pad (data.tee, tee_app_pad);
318   gst_object_unref (tee_audio_pad);
319   gst_object_unref (tee_video_pad);
320   gst_object_unref (tee_app_pad);
321
322   /* Free resources */
323   gst_element_set_state (data.pipeline, GST_STATE_NULL);
324   gst_object_unref (data.pipeline);
325   return 0;
326 }
327 ```
328
329 > ![Information](images/icons/emoticons/information.png)
330 > Need help?
331 >
332 > If you need help to compile this code, refer to the **Building the tutorials**  section for your platform: [Linux](installing/on-linux.md#InstallingonLinux-Build), [Mac OS X](installing/on-mac-osx.md#InstallingonMacOSX-Build) or [Windows](installing/on-windows.md#InstallingonWindows-Build), or use this specific command on Linux:
333 >
334 > `` gcc basic-tutorial-8.c -o basic-tutorial-8 `pkg-config --cflags --libs gstreamer-1.0 gstreamer-audio-1.0` ``
335 >
336 >If you need help to run this code, refer to the **Running the tutorials** section for your platform: [Linux](installing/on-linux.md#InstallingonLinux-Run), [Mac OS X](installing/on-mac-osx.md#InstallingonMacOSX-Run) or [Windows](installing/on-windows.md#InstallingonWindows-Run).
337 >
338 > This tutorial plays an audible tone for varying frequency through the audio card and opens a window with a waveform representation of the tone. The waveform should be a sinusoid, but due to the refreshing of the window might not appear so.
339 >
340 > Required libraries: `gstreamer-1.0`
341
342 ## Walkthrough
343
344 The code to create the pipeline (Lines 131 to 205) is an enlarged
345 version of [Basic tutorial 7: Multithreading and Pad
346 Availability](tutorials/basic/multithreading-and-pad-availability.md).
347 It involves instantiating all the elements, link the elements with
348 Always Pads, and manually link the Request Pads of the `tee` element.
349
350 Regarding the configuration of the `appsrc` and `appsink` elements:
351
352 ``` c
353 /* Configure appsrc */
354 gst_audio_info_set_format (&info, GST_AUDIO_FORMAT_S16, SAMPLE_RATE, 1, NULL);
355 audio_caps = gst_audio_info_to_caps (&info);
356 g_object_set (data.app_source, "caps", audio_caps, NULL);
357 g_signal_connect (data.app_source, "need-data", G_CALLBACK (start_feed), &data);
358 g_signal_connect (data.app_source, "enough-data", G_CALLBACK (stop_feed), &data);
359 ```
360
361 The first property that needs to be set on the `appsrc` is `caps`. It
362 specifies the kind of data that the element is going to produce, so
363 GStreamer can check if linking with downstream elements is possible
364 (this is, if the downstream elements will understand this kind of data).
365 This property must be a `GstCaps` object, which is easily built from a
366 string with `gst_caps_from_string()`.
367
368 We then connect to the `need-data` and `enough-data` signals. These are
369 fired by `appsrc` when its internal queue of data is running low or
370 almost full, respectively. We will use these signals to start and stop
371 (respectively) our signal generation process.
372
373 ``` c
374 /* Configure appsink */
375 g_object_set (data.app_sink, "emit-signals", TRUE, "caps", audio_caps, NULL);
376 g_signal_connect (data.app_sink, "new-sample", G_CALLBACK (new_sample), &data);
377 gst_caps_unref (audio_caps);
378 ```
379
380 Regarding the `appsink` configuration, we connect to the
381 `new-sample` signal, which is emitted every time the sink receives a
382 buffer. Also, the signal emission needs to be enabled through the
383 `emit-signals` property, because, by default, it is disabled.
384
385 Starting the pipeline, waiting for messages and final cleanup is done as
386 usual. Let's review the callbacks we have just
387 registered:
388
389 ``` c
390 /* This signal callback triggers when appsrc needs data. Here, we add an idle handler
391  * to the mainloop to start pushing data into the appsrc */
392 static void start_feed (GstElement *source, guint size, CustomData *data) {
393   if (data->sourceid == 0) {
394     g_print ("Start feeding\n");
395     data->sourceid = g_idle_add ((GSourceFunc) push_data, data);
396   }
397 }
398 ```
399
400 This function is called when the internal queue of `appsrc` is about to
401 starve (run out of data). The only thing we do here is register a GLib
402 idle function with `g_idle_add()` that feeds data to `appsrc` until it
403 is full again. A GLib idle function is a method that GLib will call from
404 its main loop whenever it is “idle”, this is, when it has no
405 higher-priority tasks to perform. It requires a GLib `GMainLoop` to be
406 instantiated and running, obviously.
407
408 This is only one of the multiple approaches that `appsrc` allows. In
409 particular, buffers do not need to be fed into `appsrc` from the main
410 thread using GLib, and you do not need to use the `need-data` and
411 `enough-data` signals to synchronize with `appsrc` (although this is
412 allegedly the most convenient).
413
414 We take note of the sourceid that `g_idle_add()` returns, so we can
415 disable it
416 later.
417
418 ``` c
419 /* This callback triggers when appsrc has enough data and we can stop sending.
420  * We remove the idle handler from the mainloop */
421 static void stop_feed (GstElement *source, CustomData *data) {
422   if (data->sourceid != 0) {
423     g_print ("Stop feeding\n");
424     g_source_remove (data->sourceid);
425     data->sourceid = 0;
426   }
427 }
428 ```
429
430 This function is called when the internal queue of `appsrc` is full
431 enough so we stop pushing data. Here we simply remove the idle function
432 by using `g_source_remove()` (The idle function is implemented as a
433 `GSource`).
434
435 ``` c
436 /* This method is called by the idle GSource in the mainloop, to feed CHUNK_SIZE bytes into appsrc.
437  * The ide handler is added to the mainloop when appsrc requests us to start sending data (need-data signal)
438  * and is removed when appsrc has enough data (enough-data signal).
439  */
440 static gboolean push_data (CustomData *data) {
441   GstBuffer *buffer;
442   GstFlowReturn ret;
443   int i;
444   gint16 *raw;
445   gint num_samples = CHUNK_SIZE / 2; /* Because each sample is 16 bits */
446   gfloat freq;
447
448   /* Create a new empty buffer */
449   buffer = gst_buffer_new_and_alloc (CHUNK_SIZE);
450
451   /* Set its timestamp and duration */
452   GST_BUFFER_TIMESTAMP (buffer) = gst_util_uint64_scale (data->num_samples, GST_SECOND, SAMPLE_RATE);
453   GST_BUFFER_DURATION (buffer) = gst_util_uint64_scale (num_samples, GST_SECOND, SAMPLE_RATE);
454
455   /* Generate some psychodelic waveforms */
456   raw = (gint16 *)GST_BUFFER_DATA (buffer);
457 ```
458
459 This is the function that feeds `appsrc`. It will be called by GLib at
460 times and rates which are out of our control, but we know that we will
461 disable it when its job is done (when the queue in `appsrc` is full).
462
463 Its first task is to create a new buffer with a given size (in this
464 example, it is arbitrarily set to 1024 bytes) with
465 `gst_buffer_new_and_alloc()`.
466
467 We count the number of samples that we have generated so far with the
468 `CustomData.num_samples` variable, so we can time-stamp this buffer
469 using the `GST_BUFFER_TIMESTAMP` macro in `GstBuffer`.
470
471 Since we are producing buffers of the same size, their duration is the
472 same and is set using the `GST_BUFFER_DURATION` in `GstBuffer`.
473
474 `gst_util_uint64_scale()` is a utility function that scales (multiply
475 and divide) numbers which can be large, without fear of overflows.
476
477 The bytes that for the buffer can be accessed with GST\_BUFFER\_DATA in
478 `GstBuffer` (Be careful not to write past the end of the buffer: you
479 allocated it, so you know its size).
480
481 We will skip over the waveform generation, since it is outside the scope
482 of this tutorial (it is simply a funny way of generating a pretty
483 psychedelic wave).
484
485 ``` c
486 /* Push the buffer into the appsrc */
487 g_signal_emit_by_name (data->app_source, "push-buffer", buffer, &ret);
488
489 /* Free the buffer now that we are done with it */
490 gst_buffer_unref (buffer);
491 ```
492
493 Once we have the buffer ready, we pass it to `appsrc` with the
494 `push-buffer` action signal (see information box at the end of [](tutorials/playback/playbin-usage.md)), and then
495 `gst_buffer_unref()` it since we no longer need it.
496
497 ``` c
498 /* The appsink has received a buffer */
499 static void new_sample (GstElement *sink, CustomData *data) {
500   GstSample *sample;
501   /* Retrieve the buffer */
502   g_signal_emit_by_name (sink, "pull-sample", &sample);
503   if (sample) {
504     /* The only thing we do in this example is print a * to indicate a received buffer */
505     g_print ("*");
506     gst_sample_unref (sample);
507   }
508 }
509 ```
510
511 Finally, this is the function that gets called when the
512 `appsink` receives a buffer. We use the `pull-sample` action signal to
513 retrieve the buffer and then just print some indicator on the screen. We
514 can retrieve the data pointer using the `GST_BUFFER_DATA` macro and the
515 data size using the `GST_BUFFER_SIZE` macro in `GstBuffer`. Remember
516 that this buffer does not have to match the buffer that we produced in
517 the `push_data` function, any element in the path could have altered the
518 buffers in any way (Not in this example: there is only a `tee` in the
519 path between `appsrc` and `appsink`, and it does not change the content
520 of the buffers).
521
522 We then `gst_buffer_unref()` the buffer, and this tutorial is done.
523
524 ## Conclusion
525
526 This tutorial has shown how applications can:
527
528   - Inject data into a pipeline using the `appsrc`element.
529   - Retrieve data from a pipeline using the `appsink` element.
530   - Manipulate this data by accessing the `GstBuffer`.
531
532 In a playbin-based pipeline, the same goals are achieved in a slightly
533 different way. [](tutorials/playback/short-cutting-the-pipeline.md) shows
534 how to do it.
535
536 It has been a pleasure having you here, and see you soon\!