1 /* GStreamer
2 * Copyright (C) 2011 Alessandro Decina <alessandro.d@gmail.com>
3 * Copyright (C) 2017 Sebastian Dröge <sebastian@centricular.com>
4 *
5 * This library is free software; you can redistribute it and/or
6 * modify it under the terms of the GNU Library General Public
7 * License as published by the Free Software Foundation; either
8 * version 2 of the License, or (at your option) any later version.
9 *
10 * This library is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
13 * Library General Public License for more details.
14 *
15 * You should have received a copy of the GNU Library General Public
16 * License along with this library; if not, write to the
17 * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor,
18 * Boston, MA 02110-1301, USA.
19 */
20
21 /**
22 * SECTION:element-hlssink
23 * @title: hlssink
24 *
25 * HTTP Live Streaming sink/server
26 *
27 * ## Example launch line
28 * |[
29 * gst-launch-1.0 videotestsrc is-live=true ! x264enc ! hlssink max-files=5
30 * ]|
31 *
32 */
33 #ifdef HAVE_CONFIG_H
34 #include "config.h"
35 #endif
36
37 #include "gsthlssink2.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_sink2_debug);
45 #define GST_CAT_DEFAULT gst_hls_sink2_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 #define DEFAULT_SEND_KEYFRAME_REQUESTS TRUE
54
55 #define GST_M3U8_PLAYLIST_VERSION 3
56
57 enum
58 {
59 PROP_0,
60 PROP_LOCATION,
61 PROP_PLAYLIST_LOCATION,
62 PROP_PLAYLIST_ROOT,
63 PROP_MAX_FILES,
64 PROP_TARGET_DURATION,
65 PROP_PLAYLIST_LENGTH,
66 PROP_SEND_KEYFRAME_REQUESTS,
67 };
68
69 static GstStaticPadTemplate video_template = GST_STATIC_PAD_TEMPLATE ("video",
70 GST_PAD_SINK,
71 GST_PAD_REQUEST,
72 GST_STATIC_CAPS_ANY);
73 static GstStaticPadTemplate audio_template = GST_STATIC_PAD_TEMPLATE ("audio",
74 GST_PAD_SINK,
75 GST_PAD_REQUEST,
76 GST_STATIC_CAPS_ANY);
77
78 #define gst_hls_sink2_parent_class parent_class
79 G_DEFINE_TYPE (GstHlsSink2, gst_hls_sink2, GST_TYPE_BIN);
80
81 static void gst_hls_sink2_set_property (GObject * object, guint prop_id,
82 const GValue * value, GParamSpec * spec);
83 static void gst_hls_sink2_get_property (GObject * object, guint prop_id,
84 GValue * value, GParamSpec * spec);
85 static void gst_hls_sink2_handle_message (GstBin * bin, GstMessage * message);
86 static void gst_hls_sink2_reset (GstHlsSink2 * sink);
87 static GstStateChangeReturn
88 gst_hls_sink2_change_state (GstElement * element, GstStateChange trans);
89 static GstPad *gst_hls_sink2_request_new_pad (GstElement * element,
90 GstPadTemplate * templ, const gchar * name, const GstCaps * caps);
91 static void gst_hls_sink2_release_pad (GstElement * element, GstPad * pad);
92
93 static void
gst_hls_sink2_dispose(GObject * object)94 gst_hls_sink2_dispose (GObject * object)
95 {
96 GstHlsSink2 *sink = GST_HLS_SINK2_CAST (object);
97
98 G_OBJECT_CLASS (parent_class)->dispose ((GObject *) sink);
99 }
100
101 static void
gst_hls_sink2_finalize(GObject * object)102 gst_hls_sink2_finalize (GObject * object)
103 {
104 GstHlsSink2 *sink = GST_HLS_SINK2_CAST (object);
105
106 g_free (sink->location);
107 g_free (sink->playlist_location);
108 g_free (sink->playlist_root);
109 g_free (sink->current_location);
110 if (sink->playlist)
111 gst_m3u8_playlist_free (sink->playlist);
112
113 g_queue_foreach (&sink->old_locations, (GFunc) g_free, NULL);
114 g_queue_clear (&sink->old_locations);
115
116 G_OBJECT_CLASS (parent_class)->finalize ((GObject *) sink);
117 }
118
119 static void
gst_hls_sink2_class_init(GstHlsSink2Class * klass)120 gst_hls_sink2_class_init (GstHlsSink2Class * klass)
121 {
122 GObjectClass *gobject_class;
123 GstElementClass *element_class;
124 GstBinClass *bin_class;
125
126 gobject_class = (GObjectClass *) klass;
127 element_class = GST_ELEMENT_CLASS (klass);
128 bin_class = GST_BIN_CLASS (klass);
129
130 gst_element_class_add_static_pad_template (element_class, &video_template);
131 gst_element_class_add_static_pad_template (element_class, &audio_template);
132
133 gst_element_class_set_static_metadata (element_class,
134 "HTTP Live Streaming sink", "Sink", "HTTP Live Streaming sink",
135 "Alessandro Decina <alessandro.d@gmail.com>, "
136 "Sebastian Dröge <sebastian@centricular.com>");
137
138 element_class->change_state = GST_DEBUG_FUNCPTR (gst_hls_sink2_change_state);
139 element_class->request_new_pad =
140 GST_DEBUG_FUNCPTR (gst_hls_sink2_request_new_pad);
141 element_class->release_pad = GST_DEBUG_FUNCPTR (gst_hls_sink2_release_pad);
142
143 bin_class->handle_message = gst_hls_sink2_handle_message;
144
145 gobject_class->dispose = gst_hls_sink2_dispose;
146 gobject_class->finalize = gst_hls_sink2_finalize;
147 gobject_class->set_property = gst_hls_sink2_set_property;
148 gobject_class->get_property = gst_hls_sink2_get_property;
149
150 g_object_class_install_property (gobject_class, PROP_LOCATION,
151 g_param_spec_string ("location", "File Location",
152 "Location of the file to write", DEFAULT_LOCATION,
153 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
154 g_object_class_install_property (gobject_class, PROP_PLAYLIST_LOCATION,
155 g_param_spec_string ("playlist-location", "Playlist Location",
156 "Location of the playlist to write", DEFAULT_PLAYLIST_LOCATION,
157 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
158 g_object_class_install_property (gobject_class, PROP_PLAYLIST_ROOT,
159 g_param_spec_string ("playlist-root", "Playlist Root",
160 "Location of the playlist to write", DEFAULT_PLAYLIST_ROOT,
161 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
162 g_object_class_install_property (gobject_class, PROP_MAX_FILES,
163 g_param_spec_uint ("max-files", "Max files",
164 "Maximum number of files to keep on disk. Once the maximum is reached,"
165 "old files start to be deleted to make room for new ones.",
166 0, G_MAXUINT, DEFAULT_MAX_FILES,
167 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
168 g_object_class_install_property (gobject_class, PROP_TARGET_DURATION,
169 g_param_spec_uint ("target-duration", "Target duration",
170 "The target duration in seconds of a segment/file. "
171 "(0 - disabled, useful for management of segment duration by the "
172 "streaming server)",
173 0, G_MAXUINT, DEFAULT_TARGET_DURATION,
174 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
175 g_object_class_install_property (gobject_class, PROP_PLAYLIST_LENGTH,
176 g_param_spec_uint ("playlist-length", "Playlist length",
177 "Length of HLS playlist. To allow players to conform to section 6.3.3 "
178 "of the HLS specification, this should be at least 3. If set to 0, "
179 "the playlist will be infinite.",
180 0, G_MAXUINT, DEFAULT_PLAYLIST_LENGTH,
181 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
182 g_object_class_install_property (gobject_class, PROP_SEND_KEYFRAME_REQUESTS,
183 g_param_spec_boolean ("send-keyframe-requests", "Send Keyframe Requests",
184 "Send keyframe requests to ensure correct fragmentation. If this is disabled "
185 "then the input must have keyframes in regular intervals",
186 DEFAULT_SEND_KEYFRAME_REQUESTS,
187 G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
188 }
189
190 static void
gst_hls_sink2_init(GstHlsSink2 * sink)191 gst_hls_sink2_init (GstHlsSink2 * sink)
192 {
193 GstElement *mux;
194
195 sink->location = g_strdup (DEFAULT_LOCATION);
196 sink->playlist_location = g_strdup (DEFAULT_PLAYLIST_LOCATION);
197 sink->playlist_root = g_strdup (DEFAULT_PLAYLIST_ROOT);
198 sink->playlist_length = DEFAULT_PLAYLIST_LENGTH;
199 sink->max_files = DEFAULT_MAX_FILES;
200 sink->target_duration = DEFAULT_TARGET_DURATION;
201 sink->send_keyframe_requests = DEFAULT_SEND_KEYFRAME_REQUESTS;
202 g_queue_init (&sink->old_locations);
203
204 sink->splitmuxsink = gst_element_factory_make ("splitmuxsink", NULL);
205 gst_bin_add (GST_BIN (sink), sink->splitmuxsink);
206
207 mux = gst_element_factory_make ("mpegtsmux", NULL);
208 g_object_set (sink->splitmuxsink, "location", sink->location, "max-size-time",
209 ((GstClockTime) sink->target_duration * GST_SECOND),
210 "send-keyframe-requests", TRUE, "muxer", mux, "reset-muxer", FALSE, NULL);
211
212 GST_OBJECT_FLAG_SET (sink, GST_ELEMENT_FLAG_SINK);
213
214 gst_hls_sink2_reset (sink);
215 }
216
217 static void
gst_hls_sink2_reset(GstHlsSink2 * sink)218 gst_hls_sink2_reset (GstHlsSink2 * sink)
219 {
220 sink->index = 0;
221
222 if (sink->playlist)
223 gst_m3u8_playlist_free (sink->playlist);
224 sink->playlist =
225 gst_m3u8_playlist_new (GST_M3U8_PLAYLIST_VERSION, sink->playlist_length,
226 FALSE);
227
228 g_queue_foreach (&sink->old_locations, (GFunc) g_free, NULL);
229 g_queue_clear (&sink->old_locations);
230 }
231
232 static void
gst_hls_sink2_write_playlist(GstHlsSink2 * sink)233 gst_hls_sink2_write_playlist (GstHlsSink2 * sink)
234 {
235 char *playlist_content;
236 GError *error = NULL;
237
238 playlist_content = gst_m3u8_playlist_render (sink->playlist);
239 if (!g_file_set_contents (sink->playlist_location,
240 playlist_content, -1, &error)) {
241 GST_ERROR ("Failed to write playlist: %s", error->message);
242 GST_ELEMENT_ERROR (sink, RESOURCE, OPEN_WRITE,
243 (("Failed to write playlist '%s'."), error->message), (NULL));
244 g_error_free (error);
245 error = NULL;
246 }
247 g_free (playlist_content);
248
249 }
250
251 static void
gst_hls_sink2_handle_message(GstBin * bin,GstMessage * message)252 gst_hls_sink2_handle_message (GstBin * bin, GstMessage * message)
253 {
254 GstHlsSink2 *sink = GST_HLS_SINK2_CAST (bin);
255
256 switch (message->type) {
257 case GST_MESSAGE_ELEMENT:
258 {
259 const GstStructure *s = gst_message_get_structure (message);
260 if (message->src == GST_OBJECT_CAST (sink->splitmuxsink)) {
261 if (gst_structure_has_name (s, "splitmuxsink-fragment-opened")) {
262 g_free (sink->current_location);
263 sink->current_location =
264 g_strdup (gst_structure_get_string (s, "location"));
265 gst_structure_get_clock_time (s, "running-time",
266 &sink->current_running_time_start);
267 } else if (gst_structure_has_name (s, "splitmuxsink-fragment-closed")) {
268 GstClockTime running_time;
269 gchar *entry_location;
270
271 g_assert (strcmp (sink->current_location, gst_structure_get_string (s,
272 "location")) == 0);
273
274 gst_structure_get_clock_time (s, "running-time", &running_time);
275
276 GST_INFO_OBJECT (sink, "COUNT %d", sink->index);
277 if (sink->playlist_root == NULL) {
278 entry_location = g_path_get_basename (sink->current_location);
279 } else {
280 gchar *name = g_path_get_basename (sink->current_location);
281 entry_location = g_build_filename (sink->playlist_root, name, NULL);
282 g_free (name);
283 }
284
285 gst_m3u8_playlist_add_entry (sink->playlist, entry_location,
286 NULL, running_time - sink->current_running_time_start,
287 sink->index++, FALSE);
288 g_free (entry_location);
289
290 gst_hls_sink2_write_playlist (sink);
291
292 g_queue_push_tail (&sink->old_locations,
293 g_strdup (sink->current_location));
294
295 while (g_queue_get_length (&sink->old_locations) >
296 g_queue_get_length (sink->playlist->entries)) {
297 gchar *old_location = g_queue_pop_head (&sink->old_locations);
298 g_remove (old_location);
299 g_free (old_location);
300 }
301 }
302 }
303 break;
304 }
305 case GST_MESSAGE_EOS:{
306 sink->playlist->end_list = TRUE;
307 gst_hls_sink2_write_playlist (sink);
308 break;
309 }
310 default:
311 break;
312 }
313
314 GST_BIN_CLASS (parent_class)->handle_message (bin, message);
315 }
316
317 static GstPad *
gst_hls_sink2_request_new_pad(GstElement * element,GstPadTemplate * templ,const gchar * name,const GstCaps * caps)318 gst_hls_sink2_request_new_pad (GstElement * element, GstPadTemplate * templ,
319 const gchar * name, const GstCaps * caps)
320 {
321 GstHlsSink2 *sink = GST_HLS_SINK2_CAST (element);
322 GstPad *pad, *peer;
323 gboolean is_audio;
324
325 g_return_val_if_fail (strcmp (templ->name_template, "audio") == 0
326 || strcmp (templ->name_template, "video") == 0, NULL);
327 g_return_val_if_fail (strcmp (templ->name_template, "audio") != 0
328 || !sink->audio_sink, NULL);
329 g_return_val_if_fail (strcmp (templ->name_template, "video") != 0
330 || !sink->video_sink, NULL);
331
332 is_audio = strcmp (templ->name_template, "audio") == 0;
333
334 peer =
335 gst_element_get_request_pad (sink->splitmuxsink,
336 is_audio ? "audio_0" : "video");
337 if (!peer)
338 return NULL;
339
340 pad = gst_ghost_pad_new_from_template (templ->name_template, peer, templ);
341 gst_pad_set_active (pad, TRUE);
342 gst_element_add_pad (element, pad);
343 gst_object_unref (peer);
344
345 if (is_audio)
346 sink->audio_sink = pad;
347 else
348 sink->video_sink = pad;
349
350 return pad;
351 }
352
353 static void
gst_hls_sink2_release_pad(GstElement * element,GstPad * pad)354 gst_hls_sink2_release_pad (GstElement * element, GstPad * pad)
355 {
356 GstHlsSink2 *sink = GST_HLS_SINK2_CAST (element);
357 GstPad *peer;
358
359 g_return_if_fail (pad == sink->audio_sink || pad == sink->video_sink);
360
361 peer = gst_pad_get_peer (pad);
362 if (peer) {
363 gst_element_release_request_pad (sink->splitmuxsink, pad);
364 gst_object_unref (peer);
365 }
366
367 gst_object_ref (pad);
368 gst_element_remove_pad (element, pad);
369 gst_pad_set_active (pad, FALSE);
370 if (pad == sink->audio_sink)
371 sink->audio_sink = NULL;
372 else
373 sink->video_sink = NULL;
374
375 gst_object_unref (pad);
376 }
377
378 static GstStateChangeReturn
gst_hls_sink2_change_state(GstElement * element,GstStateChange trans)379 gst_hls_sink2_change_state (GstElement * element, GstStateChange trans)
380 {
381 GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS;
382 GstHlsSink2 *sink = GST_HLS_SINK2_CAST (element);
383
384 switch (trans) {
385 case GST_STATE_CHANGE_NULL_TO_READY:
386 if (!sink->splitmuxsink) {
387 return GST_STATE_CHANGE_FAILURE;
388 }
389 break;
390 default:
391 break;
392 }
393
394 ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, trans);
395
396 switch (trans) {
397 case GST_STATE_CHANGE_PLAYING_TO_PAUSED:
398 break;
399 case GST_STATE_CHANGE_PAUSED_TO_READY:
400 case GST_STATE_CHANGE_READY_TO_NULL:
401 gst_hls_sink2_reset (sink);
402 break;
403 default:
404 break;
405 }
406
407 return ret;
408 }
409
410 static void
gst_hls_sink2_set_property(GObject * object,guint prop_id,const GValue * value,GParamSpec * pspec)411 gst_hls_sink2_set_property (GObject * object, guint prop_id,
412 const GValue * value, GParamSpec * pspec)
413 {
414 GstHlsSink2 *sink = GST_HLS_SINK2_CAST (object);
415
416 switch (prop_id) {
417 case PROP_LOCATION:
418 g_free (sink->location);
419 sink->location = g_value_dup_string (value);
420 if (sink->splitmuxsink)
421 g_object_set (sink->splitmuxsink, "location", sink->location, NULL);
422 break;
423 case PROP_PLAYLIST_LOCATION:
424 g_free (sink->playlist_location);
425 sink->playlist_location = g_value_dup_string (value);
426 break;
427 case PROP_PLAYLIST_ROOT:
428 g_free (sink->playlist_root);
429 sink->playlist_root = g_value_dup_string (value);
430 break;
431 case PROP_MAX_FILES:
432 sink->max_files = g_value_get_uint (value);
433 break;
434 case PROP_TARGET_DURATION:
435 sink->target_duration = g_value_get_uint (value);
436 if (sink->splitmuxsink) {
437 g_object_set (sink->splitmuxsink, "max-size-time",
438 ((GstClockTime) sink->target_duration * GST_SECOND), NULL);
439 }
440 break;
441 case PROP_PLAYLIST_LENGTH:
442 sink->playlist_length = g_value_get_uint (value);
443 sink->playlist->window_size = sink->playlist_length;
444 break;
445 case PROP_SEND_KEYFRAME_REQUESTS:
446 sink->send_keyframe_requests = g_value_get_boolean (value);
447 if (sink->splitmuxsink) {
448 g_object_set (sink->splitmuxsink, "send-keyframe-requests",
449 sink->send_keyframe_requests, NULL);
450 }
451 break;
452 default:
453 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
454 break;
455 }
456 }
457
458 static void
gst_hls_sink2_get_property(GObject * object,guint prop_id,GValue * value,GParamSpec * pspec)459 gst_hls_sink2_get_property (GObject * object, guint prop_id,
460 GValue * value, GParamSpec * pspec)
461 {
462 GstHlsSink2 *sink = GST_HLS_SINK2_CAST (object);
463
464 switch (prop_id) {
465 case PROP_LOCATION:
466 g_value_set_string (value, sink->location);
467 break;
468 case PROP_PLAYLIST_LOCATION:
469 g_value_set_string (value, sink->playlist_location);
470 break;
471 case PROP_PLAYLIST_ROOT:
472 g_value_set_string (value, sink->playlist_root);
473 break;
474 case PROP_MAX_FILES:
475 g_value_set_uint (value, sink->max_files);
476 break;
477 case PROP_TARGET_DURATION:
478 g_value_set_uint (value, sink->target_duration);
479 break;
480 case PROP_PLAYLIST_LENGTH:
481 g_value_set_uint (value, sink->playlist_length);
482 break;
483 case PROP_SEND_KEYFRAME_REQUESTS:
484 g_value_set_boolean (value, sink->send_keyframe_requests);
485 break;
486 default:
487 G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
488 break;
489 }
490 }
491
492 gboolean
gst_hls_sink2_plugin_init(GstPlugin * plugin)493 gst_hls_sink2_plugin_init (GstPlugin * plugin)
494 {
495 GST_DEBUG_CATEGORY_INIT (gst_hls_sink2_debug, "hlssink2", 0, "HlsSink2");
496 return gst_element_register (plugin, "hlssink2", GST_RANK_NONE,
497 gst_hls_sink2_get_type ());
498 }
499