1 /* GStreamer
2 * Copyright (C) 2011 Alessandro Decina <alessandro.d@gmail.com>
3 *
4 * This library is free software; you can redistribute it and/or
5 * modify it under the terms of the GNU Library General Public
6 * License as published by the Free Software Foundation; either
7 * version 2 of the License, or (at your option) any later version.
8 *
9 * This library is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
12 * Library General Public License for more details.
13 *
14 * You should have received a copy of the GNU Library General Public
15 * License along with this library; if not, write to the
16 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
17 * Boston, MA 02110-1301, USA.
18 */
19
20 /**
21 * SECTION:element-hlssink
22 * @title: hlssink
23 *
24 * HTTP Live Streaming sink/server
25 *
26 * ## Example launch line
27 * |[
28 * gst-launch-1.0 videotestsrc is-live=true ! x264enc ! mpegtsmux ! hlssink max-files=5
29 * ]|
30 *
31 */
32 #ifdef HAVE_CONFIG_H
33 #include "config.h"
34 #endif
35
36 #include "gsthlselements.h"
37 #include "gsthlssink.h"
38 #include <gst/pbutils/pbutils.h>
39 #include <gst/video/video.h>
40 #include <glib/gstdio.h>
41 #include <memory.h>
42
43
44 GST_DEBUG_CATEGORY_STATIC (gst_hls_sink_debug);
45 #define GST_CAT_DEFAULT gst_hls_sink_debug
46
47 #define DEFAULT_LOCATION "segment%05d.ts"
48 #define DEFAULT_PLAYLIST_LOCATION "playlist.m3u8"
49 #define DEFAULT_PLAYLIST_ROOT NULL
50 #define DEFAULT_MAX_FILES 10
51 #define DEFAULT_TARGET_DURATION 15
52 #define DEFAULT_PLAYLIST_LENGTH 5
53
54 #define GST_M3U8_PLAYLIST_VERSION 3
55
56 enum
57 {
58 PROP_0,
59 PROP_LOCATION,
60 PROP_PLAYLIST_LOCATION,
61 PROP_PLAYLIST_ROOT,
62 PROP_MAX_FILES,
63 PROP_TARGET_DURATION,
64 PROP_PLAYLIST_LENGTH
65 };
66
67 static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE ("sink",
68 GST_PAD_SINK,
69 GST_PAD_ALWAYS,
70 GST_STATIC_CAPS_ANY);
71
72 #define gst_hls_sink_parent_class parent_class
73 G_DEFINE_TYPE (GstHlsSink, gst_hls_sink, GST_TYPE_BIN);
74 #define _do_init \
75 hls_element_init (plugin); \
76 GST_DEBUG_CATEGORY_INIT (gst_hls_sink_debug, "hlssink", 0, "HlsSink");
77 GST_ELEMENT_REGISTER_DEFINE_WITH_CODE (hlssink, "hlssink", GST_RANK_NONE,
78 GST_TYPE_HLS_SINK, _do_init);
79
80 static void gst_hls_sink_set_property (GObject * object, guint prop_id,
81 const GValue * value, GParamSpec * spec);
82 static void gst_hls_sink_get_property (GObject * object, guint prop_id,
83 GValue * value, GParamSpec * spec);
84 static void gst_hls_sink_handle_message (GstBin * bin, GstMessage * message);
85 static GstPadProbeReturn gst_hls_sink_ghost_event_probe (GstPad * pad,
86 GstPadProbeInfo * info, gpointer data);
87 static GstPadProbeReturn gst_hls_sink_ghost_buffer_probe (GstPad * pad,
88 GstPadProbeInfo * info, gpointer data);
89 static void gst_hls_sink_reset (GstHlsSink * sink);
90 static GstStateChangeReturn
91 gst_hls_sink_change_state (GstElement * element, GstStateChange trans);
92 static gboolean schedule_next_key_unit (GstHlsSink * sink);
93 static GstFlowReturn gst_hls_sink_chain_list (GstPad * pad, GstObject * parent,
94 GstBufferList * list);
95
96 static void
gst_hls_sink_dispose(GObject * object)97 gst_hls_sink_dispose (GObject * object)
98 {
99 GstHlsSink *sink = GST_HLS_SINK_CAST (object);
100
101 G_OBJECT_CLASS (parent_class)->dispose ((GObject *) sink);
102 }
103
104 static void
gst_hls_sink_finalize(GObject * object)105 gst_hls_sink_finalize (GObject * object)
106 {
107 GstHlsSink *sink = GST_HLS_SINK_CAST (object);
108
109 g_free (sink->location);
110 g_free (sink->playlist_location);
111 g_free (sink->playlist_root);
112 if (sink->playlist)
113 gst_m3u8_playlist_free (sink->playlist);
114
115 G_OBJECT_CLASS (parent_class)->finalize ((GObject *) sink);
116 }
117
118 static void
gst_hls_sink_class_init(GstHlsSinkClass * klass)119 gst_hls_sink_class_init (GstHlsSinkClass * klass)
120 {
121 GObjectClass *gobject_class;
122 GstElementClass *element_class;
123 GstBinClass *bin_class;
124
125 gobject_class = (GObjectClass *) klass;
126 element_class = GST_ELEMENT_CLASS (klass);
127 bin_class = GST_BIN_CLASS (klass);
128
129 gst_element_class_add_static_pad_template (element_class, &sink_template);
130
131 gst_element_class_set_static_metadata (element_class,
132 "HTTP Live Streaming sink", "Sink", "HTTP Live Streaming sink",
133 "Alessandro Decina <alessandro.d@gmail.com>");
134
135 element_class->change_state = GST_DEBUG_FUNCPTR (gst_hls_sink_change_state);
136
137 bin_class->handle_message = gst_hls_sink_handle_message;
138
139 gobject_class->dispose = gst_hls_sink_dispose;
140 gobject_class->finalize = gst_hls_sink_finalize;
141 gobject_class->set_property = gst_hls_sink_set_property;
142 gobject_class->get_property = gst_hls_sink_get_property;
143
144 g_object_class_install_property (gobject_class, PROP_LOCATION,
145 g_param_spec_string ("location", "File Location",
146 "Location of the file to write", DEFAULT_LOCATION,
147 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
148 g_object_class_install_property (gobject_class, PROP_PLAYLIST_LOCATION,
149 g_param_spec_string ("playlist-location", "Playlist Location",
150 "Location of the playlist to write", DEFAULT_PLAYLIST_LOCATION,
151 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
152 g_object_class_install_property (gobject_class, PROP_PLAYLIST_ROOT,
153 g_param_spec_string ("playlist-root", "Playlist Root",
154 "Location of the playlist to write", DEFAULT_PLAYLIST_ROOT,
155 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
156 g_object_class_install_property (gobject_class, PROP_MAX_FILES,
157 g_param_spec_uint ("max-files", "Max files",
158 "Maximum number of files to keep on disk. Once the maximum is reached,"
159 "old files start to be deleted to make room for new ones.",
160 0, G_MAXUINT, DEFAULT_MAX_FILES,
161 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
162 g_object_class_install_property (gobject_class, PROP_TARGET_DURATION,
163 g_param_spec_uint ("target-duration", "Target duration",
164 "The target duration in seconds of a segment/file. "
165 "(0 - disabled, useful for management of segment duration by the "
166 "streaming server)",
167 0, G_MAXUINT, DEFAULT_TARGET_DURATION,
168 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
169 g_object_class_install_property (gobject_class, PROP_PLAYLIST_LENGTH,
170 g_param_spec_uint ("playlist-length", "Playlist length",
171 "Length of HLS playlist. To allow players to conform to section 6.3.3 "
172 "of the HLS specification, this should be at least 3. If set to 0, "
173 "the playlist will be infinite.",
174 0, G_MAXUINT, DEFAULT_PLAYLIST_LENGTH,
175 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
176 }
177
178 static void
gst_hls_sink_init(GstHlsSink * sink)179 gst_hls_sink_init (GstHlsSink * sink)
180 {
181 GstPadTemplate *templ = gst_static_pad_template_get (&sink_template);
182 sink->ghostpad = gst_ghost_pad_new_no_target_from_template ("sink", templ);
183 gst_object_unref (templ);
184 gst_element_add_pad (GST_ELEMENT_CAST (sink), sink->ghostpad);
185 gst_pad_add_probe (sink->ghostpad, GST_PAD_PROBE_TYPE_EVENT_DOWNSTREAM,
186 gst_hls_sink_ghost_event_probe, sink, NULL);
187 gst_pad_add_probe (sink->ghostpad, GST_PAD_PROBE_TYPE_BUFFER,
188 gst_hls_sink_ghost_buffer_probe, sink, NULL);
189 gst_pad_set_chain_list_function (sink->ghostpad, gst_hls_sink_chain_list);
190
191 sink->location = g_strdup (DEFAULT_LOCATION);
192 sink->playlist_location = g_strdup (DEFAULT_PLAYLIST_LOCATION);
193 sink->playlist_root = g_strdup (DEFAULT_PLAYLIST_ROOT);
194 sink->playlist_length = DEFAULT_PLAYLIST_LENGTH;
195 sink->max_files = DEFAULT_MAX_FILES;
196 sink->target_duration = DEFAULT_TARGET_DURATION;
197
198 /* haven't added a sink yet, make it is detected as a sink meanwhile */
199 GST_OBJECT_FLAG_SET (sink, GST_ELEMENT_FLAG_SINK);
200
201 gst_hls_sink_reset (sink);
202 }
203
204 static void
gst_hls_sink_reset(GstHlsSink * sink)205 gst_hls_sink_reset (GstHlsSink * sink)
206 {
207 sink->index = 0;
208 sink->last_running_time = 0;
209 sink->waiting_fku = FALSE;
210 gst_event_replace (&sink->force_key_unit_event, NULL);
211 gst_segment_init (&sink->segment, GST_FORMAT_UNDEFINED);
212
213 if (sink->playlist)
214 gst_m3u8_playlist_free (sink->playlist);
215 sink->playlist =
216 gst_m3u8_playlist_new (GST_M3U8_PLAYLIST_VERSION, sink->playlist_length);
217
218 sink->state = GST_M3U8_PLAYLIST_RENDER_INIT;
219 }
220
221 static gboolean
gst_hls_sink_create_elements(GstHlsSink * sink)222 gst_hls_sink_create_elements (GstHlsSink * sink)
223 {
224 GstPad *pad = NULL;
225
226 GST_DEBUG_OBJECT (sink, "Creating internal elements");
227
228 if (sink->elements_created)
229 return TRUE;
230
231 sink->multifilesink = gst_element_factory_make ("multifilesink", NULL);
232 if (sink->multifilesink == NULL)
233 goto missing_element;
234
235 g_object_set (sink->multifilesink, "location", sink->location,
236 "next-file", 3, "post-messages", TRUE, "max-files", sink->max_files,
237 NULL);
238
239 gst_bin_add (GST_BIN_CAST (sink), sink->multifilesink);
240
241 pad = gst_element_get_static_pad (sink->multifilesink, "sink");
242 gst_ghost_pad_set_target (GST_GHOST_PAD (sink->ghostpad), pad);
243 gst_object_unref (pad);
244
245 sink->elements_created = TRUE;
246 return TRUE;
247
248 missing_element:
249 gst_element_post_message (GST_ELEMENT_CAST (sink),
250 gst_missing_element_message_new (GST_ELEMENT_CAST (sink),
251 "multifilesink"));
252 GST_ELEMENT_ERROR (sink, CORE, MISSING_PLUGIN,
253 (("Missing element '%s' - check your GStreamer installation."),
254 "multifilesink"), (NULL));
255 return FALSE;
256 }
257
258 static void
gst_hls_sink_write_playlist(GstHlsSink * sink)259 gst_hls_sink_write_playlist (GstHlsSink * sink)
260 {
261 char *playlist_content;
262 GError *error = NULL;
263
264 playlist_content = gst_m3u8_playlist_render (sink->playlist);
265 if (!g_file_set_contents (sink->playlist_location,
266 playlist_content, -1, &error)) {
267 GST_ERROR ("Failed to write playlist: %s", error->message);
268 GST_ELEMENT_ERROR (sink, RESOURCE, OPEN_WRITE,
269 (("Failed to write playlist '%s'."), error->message), (NULL));
270 g_error_free (error);
271 error = NULL;
272 }
273 g_free (playlist_content);
274
275 }
276
277 static void
gst_hls_sink_handle_message(GstBin * bin,GstMessage * message)278 gst_hls_sink_handle_message (GstBin * bin, GstMessage * message)
279 {
280 GstHlsSink *sink = GST_HLS_SINK_CAST (bin);
281
282 switch (message->type) {
283 case GST_MESSAGE_ELEMENT:
284 {
285 const char *filename;
286 GstClockTime running_time, duration;
287 gboolean discont = FALSE;
288 gchar *entry_location;
289 const GstStructure *structure;
290
291 structure = gst_message_get_structure (message);
292 if (strcmp (gst_structure_get_name (structure), "GstMultiFileSink"))
293 break;
294
295 filename = gst_structure_get_string (structure, "filename");
296 gst_structure_get_clock_time (structure, "running-time", &running_time);
297 duration = running_time - sink->last_running_time;
298 sink->last_running_time = running_time;
299
300 GST_INFO_OBJECT (sink, "COUNT %d", sink->index);
301 if (sink->playlist_root == NULL)
302 entry_location = g_path_get_basename (filename);
303 else {
304 gchar *name = g_path_get_basename (filename);
305 entry_location = g_build_filename (sink->playlist_root, name, NULL);
306 g_free (name);
307 }
308
309 gst_m3u8_playlist_add_entry (sink->playlist, entry_location,
310 NULL, duration, sink->index, discont);
311 g_free (entry_location);
312
313 gst_hls_sink_write_playlist (sink);
314 sink->state |= GST_M3U8_PLAYLIST_RENDER_STARTED;
315
316 /* multifilesink is starting a new file. It means that upstream sent a key
317 * unit and we can schedule the next key unit now.
318 */
319 sink->waiting_fku = FALSE;
320 schedule_next_key_unit (sink);
321
322 /* multifilesink is an internal implementation detail. If applications
323 * need a notification, we should probably do our own message */
324 GST_DEBUG_OBJECT (bin, "dropping message %" GST_PTR_FORMAT, message);
325 gst_message_unref (message);
326 message = NULL;
327 break;
328 }
329 case GST_MESSAGE_EOS:{
330 sink->playlist->end_list = TRUE;
331 gst_hls_sink_write_playlist (sink);
332 sink->state |= GST_M3U8_PLAYLIST_RENDER_ENDED;
333 break;
334 }
335 default:
336 break;
337 }
338
339 if (message)
340 GST_BIN_CLASS (parent_class)->handle_message (bin, message);
341 }
342
343 static GstStateChangeReturn
gst_hls_sink_change_state(GstElement * element,GstStateChange trans)344 gst_hls_sink_change_state (GstElement * element, GstStateChange trans)
345 {
346 GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS;
347 GstHlsSink *sink = GST_HLS_SINK_CAST (element);
348
349 switch (trans) {
350 case GST_STATE_CHANGE_NULL_TO_READY:
351 if (!gst_hls_sink_create_elements (sink)) {
352 return GST_STATE_CHANGE_FAILURE;
353 }
354 break;
355 case GST_STATE_CHANGE_PAUSED_TO_PLAYING:
356 break;
357 default:
358 break;
359 }
360
361 ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, trans);
362
363 switch (trans) {
364 case GST_STATE_CHANGE_PLAYING_TO_PAUSED:
365 break;
366 case GST_STATE_CHANGE_PAUSED_TO_READY:
367 /* drain playlist with #EXT-X-ENDLIST */
368 if (sink->playlist && (sink->state & GST_M3U8_PLAYLIST_RENDER_STARTED) &&
369 !(sink->state & GST_M3U8_PLAYLIST_RENDER_ENDED)) {
370 sink->playlist->end_list = TRUE;
371 gst_hls_sink_write_playlist (sink);
372 }
373 gst_hls_sink_reset (sink);
374 break;
375 case GST_STATE_CHANGE_READY_TO_NULL:
376 gst_hls_sink_reset (sink);
377 break;
378 default:
379 break;
380 }
381
382 return ret;
383 }
384
385 static void
gst_hls_sink_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)386 gst_hls_sink_set_property (GObject * object, guint prop_id,
387 const GValue * value, GParamSpec * pspec)
388 {
389 GstHlsSink *sink = GST_HLS_SINK_CAST (object);
390
391 switch (prop_id) {
392 case PROP_LOCATION:
393 g_free (sink->location);
394 sink->location = g_value_dup_string (value);
395 if (sink->multifilesink)
396 g_object_set (sink->multifilesink, "location", sink->location, NULL);
397 break;
398 case PROP_PLAYLIST_LOCATION:
399 g_free (sink->playlist_location);
400 sink->playlist_location = g_value_dup_string (value);
401 break;
402 case PROP_PLAYLIST_ROOT:
403 g_free (sink->playlist_root);
404 sink->playlist_root = g_value_dup_string (value);
405 break;
406 case PROP_MAX_FILES:
407 sink->max_files = g_value_get_uint (value);
408 if (sink->multifilesink) {
409 g_object_set (sink->multifilesink, "location", sink->location,
410 "next-file", 3, "post-messages", TRUE, "max-files", sink->max_files,
411 NULL);
412 }
413 break;
414 case PROP_TARGET_DURATION:
415 sink->target_duration = g_value_get_uint (value);
416 break;
417 case PROP_PLAYLIST_LENGTH:
418 sink->playlist_length = g_value_get_uint (value);
419 sink->playlist->window_size = sink->playlist_length;
420 break;
421 default:
422 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
423 break;
424 }
425 }
426
427 static void
gst_hls_sink_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)428 gst_hls_sink_get_property (GObject * object, guint prop_id,
429 GValue * value, GParamSpec * pspec)
430 {
431 GstHlsSink *sink = GST_HLS_SINK_CAST (object);
432
433 switch (prop_id) {
434 case PROP_LOCATION:
435 g_value_set_string (value, sink->location);
436 break;
437 case PROP_PLAYLIST_LOCATION:
438 g_value_set_string (value, sink->playlist_location);
439 break;
440 case PROP_PLAYLIST_ROOT:
441 g_value_set_string (value, sink->playlist_root);
442 break;
443 case PROP_MAX_FILES:
444 g_value_set_uint (value, sink->max_files);
445 break;
446 case PROP_TARGET_DURATION:
447 g_value_set_uint (value, sink->target_duration);
448 break;
449 case PROP_PLAYLIST_LENGTH:
450 g_value_set_uint (value, sink->playlist_length);
451 break;
452 default:
453 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
454 break;
455 }
456 }
457
458 static GstPadProbeReturn
gst_hls_sink_ghost_event_probe(GstPad * pad,GstPadProbeInfo * info,gpointer data)459 gst_hls_sink_ghost_event_probe (GstPad * pad, GstPadProbeInfo * info,
460 gpointer data)
461 {
462 GstHlsSink *sink = GST_HLS_SINK_CAST (data);
463 GstEvent *event = gst_pad_probe_info_get_event (info);
464
465 switch (GST_EVENT_TYPE (event)) {
466 case GST_EVENT_SEGMENT:
467 {
468 gst_event_copy_segment (event, &sink->segment);
469 break;
470 }
471 case GST_EVENT_FLUSH_STOP:
472 gst_segment_init (&sink->segment, GST_FORMAT_UNDEFINED);
473 break;
474 case GST_EVENT_CUSTOM_DOWNSTREAM:
475 {
476 GstClockTime timestamp;
477 GstClockTime running_time, stream_time;
478 gboolean all_headers;
479 guint count;
480
481 if (!gst_video_event_is_force_key_unit (event))
482 break;
483
484 gst_event_replace (&sink->force_key_unit_event, event);
485 gst_video_event_parse_downstream_force_key_unit (event,
486 ×tamp, &stream_time, &running_time, &all_headers, &count);
487 GST_INFO_OBJECT (sink, "setting index %d", count);
488 sink->index = count;
489 break;
490 }
491 default:
492 break;
493 }
494
495 return GST_PAD_PROBE_OK;
496 }
497
498 static gboolean
schedule_next_key_unit(GstHlsSink * sink)499 schedule_next_key_unit (GstHlsSink * sink)
500 {
501 gboolean res = TRUE;
502 GstClockTime running_time;
503 GstPad *sinkpad = gst_element_get_static_pad (GST_ELEMENT (sink), "sink");
504
505 if (sink->target_duration == 0)
506 /* target-duration == 0 means that the app schedules key units itself */
507 goto out;
508
509 running_time = sink->last_running_time + sink->target_duration * GST_SECOND;
510 GST_INFO_OBJECT (sink, "sending upstream force-key-unit, index %d "
511 "now %" GST_TIME_FORMAT " target %" GST_TIME_FORMAT,
512 sink->index + 1, GST_TIME_ARGS (sink->last_running_time),
513 GST_TIME_ARGS (running_time));
514
515 if (!(res = gst_pad_push_event (sinkpad,
516 gst_video_event_new_upstream_force_key_unit (running_time,
517 TRUE, sink->index + 1)))) {
518 GST_ERROR_OBJECT (sink, "Failed to push upstream force key unit event");
519 }
520
521 out:
522 /* mark as waiting for a fku event if the app schedules them or if we just
523 * successfully scheduled one
524 */
525 sink->waiting_fku = res;
526 gst_object_unref (sinkpad);
527 return res;
528 }
529
530 static void
gst_hls_sink_check_schedule_next_key_unit(GstHlsSink * sink,GstBuffer * buf)531 gst_hls_sink_check_schedule_next_key_unit (GstHlsSink * sink, GstBuffer * buf)
532 {
533 GstClockTime timestamp;
534
535 timestamp = GST_BUFFER_TIMESTAMP (buf);
536 if (!GST_CLOCK_TIME_IS_VALID (timestamp))
537 return;
538
539 sink->last_running_time = gst_segment_to_running_time (&sink->segment,
540 GST_FORMAT_TIME, timestamp);
541 schedule_next_key_unit (sink);
542 }
543
544 static GstPadProbeReturn
gst_hls_sink_ghost_buffer_probe(GstPad * pad,GstPadProbeInfo * info,gpointer data)545 gst_hls_sink_ghost_buffer_probe (GstPad * pad, GstPadProbeInfo * info,
546 gpointer data)
547 {
548 GstHlsSink *sink = GST_HLS_SINK_CAST (data);
549 GstBuffer *buffer = gst_pad_probe_info_get_buffer (info);
550
551 if (sink->target_duration == 0 || sink->waiting_fku)
552 return GST_PAD_PROBE_OK;
553
554 gst_hls_sink_check_schedule_next_key_unit (sink, buffer);
555 return GST_PAD_PROBE_OK;
556 }
557
558 static GstFlowReturn
gst_hls_sink_chain_list(GstPad * pad,GstObject * parent,GstBufferList * list)559 gst_hls_sink_chain_list (GstPad * pad, GstObject * parent, GstBufferList * list)
560 {
561 guint i, len;
562 GstBuffer *buffer;
563 GstFlowReturn ret;
564 GstHlsSink *sink = GST_HLS_SINK_CAST (parent);
565
566 if (sink->target_duration == 0 || sink->waiting_fku)
567 return gst_proxy_pad_chain_list_default (pad, parent, list);
568
569 GST_DEBUG_OBJECT (pad, "chaining each group in list as a merged buffer");
570
571 len = gst_buffer_list_length (list);
572
573 ret = GST_FLOW_OK;
574 for (i = 0; i < len; i++) {
575 buffer = gst_buffer_list_get (list, i);
576
577 if (!sink->waiting_fku)
578 gst_hls_sink_check_schedule_next_key_unit (sink, buffer);
579
580 ret = gst_pad_chain (pad, gst_buffer_ref (buffer));
581 if (ret != GST_FLOW_OK)
582 break;
583 }
584 gst_buffer_list_unref (list);
585
586 return ret;
587 }
588