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', ]