From 9eda151348f60d131a46f585e1af8f833a6863b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Dr=C3=B6ge?= Date: Wed, 19 Apr 2017 13:47:57 +0100 Subject: [PATCH] hlssink2: New HLS sink element based on splitmuxsink This embeds the muxer inside the sink and accepts elementary streams while the old HLS sink required the muxer outside. Apart from that the interface is the same as before. Currently only mpegtsmux is supported, but support for other muxers is just a matter of adding a property. The advantage of the new sink is that it reduces complexity a lot and properly handles pre-encoded streams with appropriately spaced keyframes. https://bugzilla.gnome.org/show_bug.cgi?id=781496 --- ext/hls/Makefile.am | 2 + ext/hls/gsthlsplugin.c | 4 + ext/hls/gsthlssink2.c | 479 +++++++++++++++++++++++++++++++++++++++++ ext/hls/gsthlssink2.h | 69 ++++++ ext/hls/meson.build | 1 + 5 files changed, 555 insertions(+) create mode 100644 ext/hls/gsthlssink2.c create mode 100644 ext/hls/gsthlssink2.h diff --git a/ext/hls/Makefile.am b/ext/hls/Makefile.am index 8314ff03fc..111e9d47ac 100644 --- a/ext/hls/Makefile.am +++ b/ext/hls/Makefile.am @@ -7,6 +7,7 @@ libgsthls_la_SOURCES = \ gsthlsdemux-util.c \ gsthlsplugin.c \ gsthlssink.c \ + gsthlssink2.c \ gstm3u8playlist.c libgsthls_la_CFLAGS = $(GST_PLUGINS_BAD_CFLAGS) $(GST_PLUGINS_BASE_CFLAGS) $(GST_BASE_CFLAGS) $(GST_CFLAGS) $(LIBGCRYPT_CFLAGS) $(NETTLE_CFLAGS) $(OPENSSL_CFLAGS) @@ -23,5 +24,6 @@ noinst_HEADERS = \ gsthls.h \ gsthlsdemux.h \ gsthlssink.h \ + gsthlssink2.h \ gstm3u8playlist.h \ m3u8.h diff --git a/ext/hls/gsthlsplugin.c b/ext/hls/gsthlsplugin.c index 552f514deb..fc26588474 100644 --- a/ext/hls/gsthlsplugin.c +++ b/ext/hls/gsthlsplugin.c @@ -7,6 +7,7 @@ #include "gsthls.h" #include "gsthlsdemux.h" #include "gsthlssink.h" +#include "gsthlssink2.h" GST_DEBUG_CATEGORY (hls_debug); @@ -22,6 +23,9 @@ hls_init (GstPlugin * plugin) if (!gst_hls_sink_plugin_init (plugin)) return FALSE; + if (!gst_hls_sink2_plugin_init (plugin)) + return FALSE; + return TRUE; } diff --git a/ext/hls/gsthlssink2.c b/ext/hls/gsthlssink2.c new file mode 100644 index 0000000000..7a47a2f024 --- /dev/null +++ b/ext/hls/gsthlssink2.c @@ -0,0 +1,479 @@ +/* GStreamer + * Copyright (C) 2011 Alessandro Decina + * Copyright (C) 2017 Sebastian Dröge + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ + +/** + * SECTION:element-hlssink + * @title: hlssink + * + * HTTP Live Streaming sink/server + * + * ## Example launch line + * |[ + * gst-launch-1.0 videotestsrc is-live=true ! x264enc ! hlssink max-files=5 + * ]| + * + */ +#ifdef HAVE_CONFIG_H +#include "config.h" +#endif + +#include "gsthlssink2.h" +#include +#include +#include +#include + + +GST_DEBUG_CATEGORY_STATIC (gst_hls_sink2_debug); +#define GST_CAT_DEFAULT gst_hls_sink2_debug + +#define DEFAULT_LOCATION "segment%05d.ts" +#define DEFAULT_PLAYLIST_LOCATION "playlist.m3u8" +#define DEFAULT_PLAYLIST_ROOT NULL +#define DEFAULT_MAX_FILES 10 +#define DEFAULT_TARGET_DURATION 15 +#define DEFAULT_PLAYLIST_LENGTH 5 + +#define GST_M3U8_PLAYLIST_VERSION 3 + +enum +{ + PROP_0, + PROP_LOCATION, + PROP_PLAYLIST_LOCATION, + PROP_PLAYLIST_ROOT, + PROP_MAX_FILES, + PROP_TARGET_DURATION, + PROP_PLAYLIST_LENGTH +}; + +static GstStaticPadTemplate video_template = GST_STATIC_PAD_TEMPLATE ("video", + GST_PAD_SINK, + GST_PAD_REQUEST, + GST_STATIC_CAPS_ANY); +static GstStaticPadTemplate audio_template = GST_STATIC_PAD_TEMPLATE ("audio", + GST_PAD_SINK, + GST_PAD_REQUEST, + GST_STATIC_CAPS_ANY); + +#define gst_hls_sink2_parent_class parent_class +G_DEFINE_TYPE (GstHlsSink2, gst_hls_sink2, GST_TYPE_BIN); + +static void gst_hls_sink2_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * spec); +static void gst_hls_sink2_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * spec); +static void gst_hls_sink2_handle_message (GstBin * bin, GstMessage * message); +static void gst_hls_sink2_reset (GstHlsSink2 * sink); +static GstStateChangeReturn +gst_hls_sink2_change_state (GstElement * element, GstStateChange trans); +static GstPad *gst_hls_sink2_request_new_pad (GstElement * element, + GstPadTemplate * templ, const gchar * name, const GstCaps * caps); +static void gst_hls_sink2_release_pad (GstElement * element, GstPad * pad); + +static void +gst_hls_sink2_dispose (GObject * object) +{ + GstHlsSink2 *sink = GST_HLS_SINK2_CAST (object); + + G_OBJECT_CLASS (parent_class)->dispose ((GObject *) sink); +} + +static void +gst_hls_sink2_finalize (GObject * object) +{ + GstHlsSink2 *sink = GST_HLS_SINK2_CAST (object); + + g_free (sink->location); + g_free (sink->playlist_location); + g_free (sink->playlist_root); + if (sink->playlist) + gst_m3u8_playlist_free (sink->playlist); + + g_queue_foreach (&sink->old_locations, (GFunc) g_free, NULL); + g_queue_clear (&sink->old_locations); + + G_OBJECT_CLASS (parent_class)->finalize ((GObject *) sink); +} + +static void +gst_hls_sink2_class_init (GstHlsSink2Class * klass) +{ + GObjectClass *gobject_class; + GstElementClass *element_class; + GstBinClass *bin_class; + + gobject_class = (GObjectClass *) klass; + element_class = GST_ELEMENT_CLASS (klass); + bin_class = GST_BIN_CLASS (klass); + + gst_element_class_add_static_pad_template (element_class, &video_template); + gst_element_class_add_static_pad_template (element_class, &audio_template); + + gst_element_class_set_static_metadata (element_class, + "HTTP Live Streaming sink", "Sink", "HTTP Live Streaming sink", + "Alessandro Decina , " + "Sebastian Dröge "); + + element_class->change_state = GST_DEBUG_FUNCPTR (gst_hls_sink2_change_state); + element_class->request_new_pad = + GST_DEBUG_FUNCPTR (gst_hls_sink2_request_new_pad); + element_class->release_pad = GST_DEBUG_FUNCPTR (gst_hls_sink2_release_pad); + + bin_class->handle_message = gst_hls_sink2_handle_message; + + gobject_class->dispose = gst_hls_sink2_dispose; + gobject_class->finalize = gst_hls_sink2_finalize; + gobject_class->set_property = gst_hls_sink2_set_property; + gobject_class->get_property = gst_hls_sink2_get_property; + + g_object_class_install_property (gobject_class, PROP_LOCATION, + g_param_spec_string ("location", "File Location", + "Location of the file to write", DEFAULT_LOCATION, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (gobject_class, PROP_PLAYLIST_LOCATION, + g_param_spec_string ("playlist-location", "Playlist Location", + "Location of the playlist to write", DEFAULT_PLAYLIST_LOCATION, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (gobject_class, PROP_PLAYLIST_ROOT, + g_param_spec_string ("playlist-root", "Playlist Root", + "Location of the playlist to write", DEFAULT_PLAYLIST_ROOT, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (gobject_class, PROP_MAX_FILES, + g_param_spec_uint ("max-files", "Max files", + "Maximum number of files to keep on disk. Once the maximum is reached," + "old files start to be deleted to make room for new ones.", + 0, G_MAXUINT, DEFAULT_MAX_FILES, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (gobject_class, PROP_TARGET_DURATION, + g_param_spec_uint ("target-duration", "Target duration", + "The target duration in seconds of a segment/file. " + "(0 - disabled, useful for management of segment duration by the " + "streaming server)", + 0, G_MAXUINT, DEFAULT_TARGET_DURATION, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); + g_object_class_install_property (gobject_class, PROP_PLAYLIST_LENGTH, + g_param_spec_uint ("playlist-length", "Playlist length", + "Length of HLS playlist. To allow players to conform to section 6.3.3 " + "of the HLS specification, this should be at least 3. If set to 0, " + "the playlist will be infinite.", + 0, G_MAXUINT, DEFAULT_PLAYLIST_LENGTH, + G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); +} + +static void +gst_hls_sink2_init (GstHlsSink2 * sink) +{ + GstElement *mux; + + sink->location = g_strdup (DEFAULT_LOCATION); + sink->playlist_location = g_strdup (DEFAULT_PLAYLIST_LOCATION); + sink->playlist_root = g_strdup (DEFAULT_PLAYLIST_ROOT); + sink->playlist_length = DEFAULT_PLAYLIST_LENGTH; + sink->max_files = DEFAULT_MAX_FILES; + sink->target_duration = DEFAULT_TARGET_DURATION; + g_queue_init (&sink->old_locations); + + sink->splitmuxsink = gst_element_factory_make ("splitmuxsink", NULL); + gst_bin_add (GST_BIN (sink), sink->splitmuxsink); + + mux = gst_element_factory_make ("mpegtsmux", NULL); + g_object_set (sink->splitmuxsink, "location", sink->location, "max-size-time", + ((GstClockTime) sink->target_duration * GST_SECOND), + "send-keyframe-requests", TRUE, "muxer", mux, NULL); + + GST_OBJECT_FLAG_SET (sink, GST_ELEMENT_FLAG_SINK); + + gst_hls_sink2_reset (sink); +} + +static void +gst_hls_sink2_reset (GstHlsSink2 * sink) +{ + sink->index = 0; + + if (sink->playlist) + gst_m3u8_playlist_free (sink->playlist); + sink->playlist = + gst_m3u8_playlist_new (GST_M3U8_PLAYLIST_VERSION, sink->playlist_length, + FALSE); + + g_queue_foreach (&sink->old_locations, (GFunc) g_free, NULL); + g_queue_clear (&sink->old_locations); +} + +static void +gst_hls_sink2_write_playlist (GstHlsSink2 * sink) +{ + char *playlist_content; + GError *error = NULL; + + playlist_content = gst_m3u8_playlist_render (sink->playlist); + if (!g_file_set_contents (sink->playlist_location, + playlist_content, -1, &error)) { + GST_ERROR ("Failed to write playlist: %s", error->message); + GST_ELEMENT_ERROR (sink, RESOURCE, OPEN_WRITE, + (("Failed to write playlist '%s'."), error->message), (NULL)); + g_error_free (error); + error = NULL; + } + g_free (playlist_content); + +} + +static void +gst_hls_sink2_handle_message (GstBin * bin, GstMessage * message) +{ + GstHlsSink2 *sink = GST_HLS_SINK2_CAST (bin); + + switch (message->type) { + case GST_MESSAGE_ELEMENT: + { + const GstStructure *s = gst_message_get_structure (message); + if (message->src == GST_OBJECT_CAST (sink->splitmuxsink)) { + if (gst_structure_has_name (s, "splitmuxsink-fragment-opened")) { + g_free (sink->current_location); + sink->current_location = + g_strdup (gst_structure_get_string (s, "location")); + gst_structure_get_clock_time (s, "running-time", + &sink->current_running_time_start); + } else if (gst_structure_has_name (s, "splitmuxsink-fragment-closed")) { + GstClockTime running_time; + gchar *entry_location; + + g_assert (strcmp (sink->current_location, gst_structure_get_string (s, + "location")) == 0); + + gst_structure_get_clock_time (s, "running-time", &running_time); + + GST_INFO_OBJECT (sink, "COUNT %d", sink->index); + if (sink->playlist_root == NULL) { + entry_location = g_path_get_basename (sink->current_location); + } else { + gchar *name = g_path_get_basename (sink->current_location); + entry_location = g_build_filename (sink->playlist_root, name, NULL); + g_free (name); + } + + gst_m3u8_playlist_add_entry (sink->playlist, entry_location, + NULL, running_time - sink->current_running_time_start, + sink->index++, FALSE); + g_free (entry_location); + + gst_hls_sink2_write_playlist (sink); + + g_queue_push_tail (&sink->old_locations, + g_strdup (sink->current_location)); + + while (g_queue_get_length (&sink->old_locations) > + g_queue_get_length (sink->playlist->entries)) { + gchar *old_location = g_queue_pop_head (&sink->old_locations); + g_remove (old_location); + g_free (old_location); + } + } + } + break; + } + case GST_MESSAGE_EOS:{ + sink->playlist->end_list = TRUE; + gst_hls_sink2_write_playlist (sink); + break; + } + default: + break; + } + + if (message) + GST_BIN_CLASS (parent_class)->handle_message (bin, message); +} + +static GstPad * +gst_hls_sink2_request_new_pad (GstElement * element, GstPadTemplate * templ, + const gchar * name, const GstCaps * caps) +{ + GstHlsSink2 *sink = GST_HLS_SINK2_CAST (element); + GstPad *pad, *peer; + gboolean is_audio; + + g_return_val_if_fail (strcmp (templ->name_template, "audio") == 0 + || strcmp (templ->name_template, "video") == 0, NULL); + g_return_val_if_fail (strcmp (templ->name_template, "audio") != 0 + || !sink->audio_sink, NULL); + g_return_val_if_fail (strcmp (templ->name_template, "video") != 0 + || !sink->video_sink, NULL); + + is_audio = strcmp (templ->name_template, "audio") == 0; + + peer = + gst_element_get_request_pad (sink->splitmuxsink, + is_audio ? "audio_0" : "video"); + if (!peer) + return NULL; + + pad = gst_ghost_pad_new_from_template (templ->name_template, peer, templ); + gst_pad_set_active (pad, TRUE); + gst_element_add_pad (element, pad); + gst_object_unref (peer); + + if (is_audio) + sink->audio_sink = pad; + else + sink->video_sink = pad; + + return pad; +} + +static void +gst_hls_sink2_release_pad (GstElement * element, GstPad * pad) +{ + GstHlsSink2 *sink = GST_HLS_SINK2_CAST (element); + GstPad *peer; + + g_return_if_fail (pad == sink->audio_sink || pad == sink->video_sink); + + peer = gst_pad_get_peer (pad); + if (peer) { + gst_element_release_request_pad (sink->splitmuxsink, pad); + gst_object_unref (peer); + } + + gst_object_ref (pad); + gst_element_remove_pad (element, pad); + gst_pad_set_active (pad, FALSE); + if (pad == sink->audio_sink) + sink->audio_sink = NULL; + else + sink->video_sink = NULL; + + gst_object_unref (pad); +} + +static GstStateChangeReturn +gst_hls_sink2_change_state (GstElement * element, GstStateChange trans) +{ + GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS; + GstHlsSink2 *sink = GST_HLS_SINK2_CAST (element); + + switch (trans) { + case GST_STATE_CHANGE_NULL_TO_READY: + if (!sink->splitmuxsink) { + return GST_STATE_CHANGE_FAILURE; + } + break; + default: + break; + } + + ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, trans); + + switch (trans) { + case GST_STATE_CHANGE_PLAYING_TO_PAUSED: + break; + case GST_STATE_CHANGE_PAUSED_TO_READY: + case GST_STATE_CHANGE_READY_TO_NULL: + gst_hls_sink2_reset (sink); + break; + default: + break; + } + + return ret; +} + +static void +gst_hls_sink2_set_property (GObject * object, guint prop_id, + const GValue * value, GParamSpec * pspec) +{ + GstHlsSink2 *sink = GST_HLS_SINK2_CAST (object); + + switch (prop_id) { + case PROP_LOCATION: + g_free (sink->location); + sink->location = g_value_dup_string (value); + if (sink->splitmuxsink) + g_object_set (sink->splitmuxsink, "location", sink->location, NULL); + break; + case PROP_PLAYLIST_LOCATION: + g_free (sink->playlist_location); + sink->playlist_location = g_value_dup_string (value); + break; + case PROP_PLAYLIST_ROOT: + g_free (sink->playlist_root); + sink->playlist_root = g_value_dup_string (value); + break; + case PROP_MAX_FILES: + sink->max_files = g_value_get_uint (value); + break; + case PROP_TARGET_DURATION: + sink->target_duration = g_value_get_uint (value); + if (sink->splitmuxsink) { + g_object_set (sink->splitmuxsink, "max-size-time", + ((GstClockTime) sink->target_duration * GST_SECOND), NULL); + } + break; + case PROP_PLAYLIST_LENGTH: + sink->playlist_length = g_value_get_uint (value); + sink->playlist->window_size = sink->playlist_length; + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +gst_hls_sink2_get_property (GObject * object, guint prop_id, + GValue * value, GParamSpec * pspec) +{ + GstHlsSink2 *sink = GST_HLS_SINK2_CAST (object); + + switch (prop_id) { + case PROP_LOCATION: + g_value_set_string (value, sink->location); + break; + case PROP_PLAYLIST_LOCATION: + g_value_set_string (value, sink->playlist_location); + break; + case PROP_PLAYLIST_ROOT: + g_value_set_string (value, sink->playlist_root); + break; + case PROP_MAX_FILES: + g_value_set_uint (value, sink->max_files); + break; + case PROP_TARGET_DURATION: + g_value_set_uint (value, sink->target_duration); + break; + case PROP_PLAYLIST_LENGTH: + g_value_set_uint (value, sink->playlist_length); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +gboolean +gst_hls_sink2_plugin_init (GstPlugin * plugin) +{ + GST_DEBUG_CATEGORY_INIT (gst_hls_sink2_debug, "hlssink2", 0, "HlsSink2"); + return gst_element_register (plugin, "hlssink2", GST_RANK_NONE, + gst_hls_sink2_get_type ()); +} diff --git a/ext/hls/gsthlssink2.h b/ext/hls/gsthlssink2.h new file mode 100644 index 0000000000..effefd6823 --- /dev/null +++ b/ext/hls/gsthlssink2.h @@ -0,0 +1,69 @@ +/* GStreamer + * Copyright (C) 2011 Alessandro Decina + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Library General Public + * License as published by the Free Software Foundation; either + * version 2 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Library General Public License for more details. + * + * You should have received a copy of the GNU Library General Public + * License along with this library; if not, write to the + * Free Software Foundation, Inc., 51 Franklin St, Fifth Floor, + * Boston, MA 02110-1301, USA. + */ +#ifndef _GST_HLS_SINK2_H_ +#define _GST_HLS_SINK2_H_ + +#include "gstm3u8playlist.h" +#include + +G_BEGIN_DECLS + +#define GST_TYPE_HLS_SINK2 (gst_hls_sink2_get_type()) +#define GST_HLS_SINK2(obj) (G_TYPE_CHECK_INSTANCE_CAST((obj),GST_TYPE_HLS_SINK2,GstHlsSink2)) +#define GST_HLS_SINK2_CAST(obj) ((GstHlsSink2 *) obj) +#define GST_HLS_SINK2_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST((klass),GST_TYPE_HLS_SINK2,GstHlsSink2Class)) +#define GST_IS_HLS_SINK2(obj) (G_TYPE_CHECK_INSTANCE_TYPE((obj),GST_TYPE_HLS_SINK2)) +#define GST_IS_HLS_SINK2_CLASS(obj) (G_TYPE_CHECK_CLASS_TYPE((klass),GST_TYPE_HLS_SINK2)) + +typedef struct _GstHlsSink2 GstHlsSink2; +typedef struct _GstHlsSink2Class GstHlsSink2Class; + +struct _GstHlsSink2 +{ + GstBin bin; + + GstElement *splitmuxsink; + GstPad *audio_sink, *video_sink; + + gchar *location; + gchar *playlist_location; + gchar *playlist_root; + guint playlist_length; + gint max_files; + gint target_duration; + + GstM3U8Playlist *playlist; + guint index; + + gchar *current_location; + GstClockTime current_running_time_start; + GQueue old_locations; +}; + +struct _GstHlsSink2Class +{ + GstBinClass bin_class; +}; + +GType gst_hls_sink2_get_type (void); +gboolean gst_hls_sink2_plugin_init (GstPlugin * plugin); + +G_END_DECLS + +#endif diff --git a/ext/hls/meson.build b/ext/hls/meson.build index 64a5520e27..1a0157e14a 100644 --- a/ext/hls/meson.build +++ b/ext/hls/meson.build @@ -3,6 +3,7 @@ hls_sources = [ 'gsthlsdemux-util.c', 'gsthlsplugin.c', 'gsthlssink.c', + 'gsthlssink2.c', 'gstm3u8playlist.c', 'm3u8.c', ]