--- /dev/null
+/* GStreamer
+ * Copyright (C) 2011 Alessandro Decina <alessandro.d@gmail.com>
+ *
+ * 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., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#ifdef HAVE_CONFIG_H
+#include "config.h"
+#endif
+
+#include "gsthlssink.h"
+#include <gst/pbutils/pbutils.h>
+#include <gst/video/video.h>
+#include <gio/gio.h>
+#include <glib/gstdio.h>
+#include <memory.h>
+
+
+GST_DEBUG_CATEGORY_STATIC (gst_hls_sink_debug);
+#define GST_CAT_DEFAULT gst_hls_sink_debug
+
+#define DEFAULT_LOCATION "segment%05d.ts"
+#define DEFAULT_PLAYLIST_LOCATION "playlist.m3u8"
+#define DEFAULT_PLAYLIST_ROOT NULL
+#define DEFAULT_MAX_FILES 10
+
+enum
+{
+ PROP_0,
+ PROP_LOCATION,
+ PROP_PLAYLIST_LOCATION,
+ PROP_PLAYLIST_ROOT,
+ PROP_MAX_FILES
+};
+
+static GstStaticPadTemplate sink_template = GST_STATIC_PAD_TEMPLATE ("sink",
+ GST_PAD_SINK,
+ GST_PAD_ALWAYS,
+ GST_STATIC_CAPS_ANY);
+
+GST_BOILERPLATE (GstHlsSink, gst_hls_sink, GstBin, GST_TYPE_BIN);
+
+static void gst_hls_sink_set_property (GObject * object, guint prop_id,
+ const GValue * value, GParamSpec * spec);
+static void gst_hls_sink_get_property (GObject * object, guint prop_id,
+ GValue * value, GParamSpec * spec);
+static void gst_hls_sink_handle_message (GstBin * bin, GstMessage * message);
+static gboolean gst_hls_sink_ghost_event_probe (GstPad * pad,
+ GstEvent * event, gpointer data);
+
+static GstStateChangeReturn
+gst_hls_sink_change_state (GstElement * element, GstStateChange trans);
+
+static void
+gst_hls_sink_dispose (GObject * object)
+{
+ GstHlsSink *sink = GST_HLS_SINK_CAST (object);
+
+ if (sink->multifilesink)
+ g_object_unref (sink->multifilesink);
+
+ G_OBJECT_CLASS (parent_class)->dispose ((GObject *) sink);
+}
+
+static void
+gst_hls_sink_finalize (GObject * object)
+{
+ GstHlsSink *sink = GST_HLS_SINK_CAST (object);
+
+ gst_event_replace (&sink->force_key_unit_event, NULL);
+ g_free (sink->location);
+ g_free (sink->playlist_location);
+ g_free (sink->playlist_root);
+ gst_m3u8_playlist_free (sink->playlist);
+
+ G_OBJECT_CLASS (parent_class)->finalize ((GObject *) sink);
+}
+
+static void
+gst_hls_sink_base_init (gpointer g_class)
+{
+ GstElementClass *element_class = GST_ELEMENT_CLASS (g_class);
+
+ gst_element_class_add_pad_template (element_class,
+ gst_static_pad_template_get (&sink_template));
+
+ gst_element_class_set_details_simple (element_class,
+ "HTTP Live Streaming sink", "Sink", "HTTP Live Streaming sink",
+ "Alessandro Decina <alessandro.decina@gmail.com>");
+}
+
+static void
+gst_hls_sink_class_init (GstHlsSinkClass * 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);
+
+ bin_class->handle_message = gst_hls_sink_handle_message;
+
+ element_class->change_state = GST_DEBUG_FUNCPTR (gst_hls_sink_change_state);
+
+ gobject_class->dispose = gst_hls_sink_dispose;
+ gobject_class->finalize = gst_hls_sink_finalize;
+ gobject_class->set_property = gst_hls_sink_set_property;
+ gobject_class->get_property = gst_hls_sink_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));
+}
+
+static void
+gst_hls_sink_init (GstHlsSink * sink, GstHlsSinkClass * sink_class)
+{
+ GstPadTemplate *templ = gst_static_pad_template_get (&sink_template);
+ sink->ghostpad = gst_ghost_pad_new_no_target_from_template ("sink", templ);
+ gst_object_unref (templ);
+ gst_element_add_pad (GST_ELEMENT_CAST (sink), sink->ghostpad);
+ gst_pad_add_event_probe (sink->ghostpad,
+ G_CALLBACK (gst_hls_sink_ghost_event_probe), sink);
+
+ sink->index = 0;
+ sink->multifilesink = NULL;
+ sink->last_stream_time = 0;
+ sink->location = g_strdup (DEFAULT_LOCATION);
+ sink->playlist_location = g_strdup (DEFAULT_PLAYLIST_LOCATION);
+ sink->playlist_root = g_strdup (DEFAULT_PLAYLIST_ROOT);
+ sink->playlist = gst_m3u8_playlist_new (6, 5, FALSE);
+ sink->max_files = DEFAULT_MAX_FILES;
+}
+
+static gboolean
+gst_hls_sink_create_elements (GstHlsSink * sink)
+{
+ GstPad *pad = NULL;
+
+ GST_DEBUG_OBJECT (sink, "Creating internal elements");
+
+ if (sink->elements_created)
+ return TRUE;
+
+ sink->multifilesink = gst_element_factory_make ("multifilesink", NULL);
+ if (sink->multifilesink == NULL)
+ goto missing_element;
+
+ g_object_set (sink->multifilesink, "location", sink->location,
+ "next-file", 3, "post-messages", TRUE, "max-files", sink->max_files,
+ NULL);
+
+ gst_bin_add (GST_BIN_CAST (sink), sink->multifilesink);
+
+ pad = gst_element_get_static_pad (sink->multifilesink, "sink");
+ gst_ghost_pad_set_target (GST_GHOST_PAD (sink->ghostpad), pad);
+ gst_object_unref (pad);
+
+ sink->elements_created = TRUE;
+ return TRUE;
+
+missing_element:
+ gst_element_post_message (GST_ELEMENT_CAST (sink),
+ gst_missing_element_message_new (GST_ELEMENT_CAST (sink),
+ "multifilesink"));
+ GST_ELEMENT_ERROR (sink, CORE, MISSING_PLUGIN,
+ (("Missing element '%s' - check your GStreamer installation."),
+ "multifilesink"), (NULL));
+ return FALSE;
+}
+
+static void
+gst_hls_sink_handle_message (GstBin * bin, GstMessage * message)
+{
+ GstHlsSink *sink = GST_HLS_SINK_CAST (bin);
+
+ switch (message->type) {
+ case GST_MESSAGE_ELEMENT:
+ {
+ GFile *file;
+ const char *filename, *title;
+ char *playlist_content;
+ GstClockTime stream_time, duration;
+ gboolean discont = FALSE;
+ GError *error = NULL;
+ gchar *entry_location;
+
+ if (strcmp (gst_structure_get_name (message->structure),
+ "GstMultiFileSink"))
+ break;
+
+ filename = gst_structure_get_string (message->structure, "filename");
+ gst_structure_get_clock_time (message->structure, "stream-time",
+ &stream_time);
+ duration = stream_time - sink->last_stream_time;
+ sink->last_stream_time = stream_time;
+ file = g_file_new_for_path (filename);
+ title = "ciao";
+ GST_INFO_OBJECT (sink, "COUNT %d", sink->index);
+ if (sink->playlist_root == NULL)
+ entry_location = g_strdup (filename);
+ else {
+ gchar *name = g_path_get_basename (filename);
+ entry_location = g_build_filename (sink->playlist_root, name, NULL);
+ g_free (name);
+ }
+ gst_m3u8_playlist_add_entry (sink->playlist, entry_location, file,
+ title, duration, sink->index, discont);
+ g_free (entry_location);
+ playlist_content = gst_m3u8_playlist_render (sink->playlist);
+ g_file_set_contents (sink->playlist_location,
+ playlist_content, -1, &error);
+ g_free (playlist_content);
+ break;
+ }
+ default:
+ break;
+ }
+
+ GST_BIN_CLASS (parent_class)->handle_message (bin, message);
+}
+
+static GstStateChangeReturn
+gst_hls_sink_change_state (GstElement * element, GstStateChange trans)
+{
+ GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS;
+ GstHlsSink *sink = GST_HLS_SINK_CAST (element);
+
+ switch (trans) {
+ case GST_STATE_CHANGE_NULL_TO_READY:
+ if (!gst_hls_sink_create_elements (sink)) {
+ return GST_STATE_CHANGE_FAILURE;
+ }
+ break;
+ default:
+ break;
+ }
+
+ ret = GST_ELEMENT_CLASS (parent_class)->change_state (element, trans);
+
+ switch (trans) {
+ case GST_STATE_CHANGE_READY_TO_NULL:
+ break;
+ default:
+ break;
+ }
+
+ return ret;
+}
+
+static void
+gst_hls_sink_set_property (GObject * object, guint prop_id,
+ const GValue * value, GParamSpec * pspec)
+{
+ GstHlsSink *sink = GST_HLS_SINK_CAST (object);
+
+ switch (prop_id) {
+ case PROP_LOCATION:
+ g_free (sink->location);
+ sink->location = g_value_dup_string (value);
+ if (sink->multifilesink)
+ g_object_set (sink->multifilesink, "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);
+ if (sink->multifilesink) {
+ g_object_set (sink->multifilesink, "location", sink->location,
+ "next-file", 3, "post-messages", TRUE, "max-files", sink->max_files,
+ NULL);
+ }
+ break;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static void
+gst_hls_sink_get_property (GObject * object, guint prop_id,
+ GValue * value, GParamSpec * pspec)
+{
+ GstHlsSink *sink = GST_HLS_SINK_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;
+ default:
+ G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec);
+ break;
+ }
+}
+
+static gboolean
+gst_hls_sink_ghost_event_probe (GstPad * pad, GstEvent * event, gpointer data)
+{
+ GstHlsSink *sink = GST_HLS_SINK_CAST (data);
+
+ switch (GST_EVENT_TYPE (event)) {
+ case GST_EVENT_CUSTOM_DOWNSTREAM:
+ {
+ GstClockTime timestamp;
+ GstClockTime running_time, stream_time;
+ gboolean all_headers;
+ guint count;
+
+ if (!gst_video_event_is_force_key_unit (event))
+ break;
+
+ gst_event_replace (&sink->force_key_unit_event, event);
+ gst_video_event_parse_downstream_force_key_unit (event,
+ ×tamp, &stream_time, &running_time, &all_headers, &count);
+ GST_INFO_OBJECT (sink, "setting index %d", count);
+ sink->index = count;
+ break;
+ }
+ default:
+ break;
+ }
+
+ return TRUE;
+}
+
+
+gboolean
+gst_hls_sink_plugin_init (GstPlugin * plugin)
+{
+ GST_DEBUG_CATEGORY_INIT (gst_hls_sink_debug, "hlssink", 0, "HlsSink");
+ return gst_element_register (plugin, "hlssink", GST_RANK_NONE,
+ gst_hls_sink_get_type ());
+}
--- /dev/null
+/* GStreamer
+ * Copyright (C) 2011 Andoni Morales Alastruey <ylatuya@gmail.com>
+ *
+ * gstm3u8playlist.c:
+ *
+ * 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., 59 Temple Place - Suite 330,
+ * Boston, MA 02111-1307, USA.
+ */
+
+#include <glib.h>
+
+#include "gstfragmented.h"
+#include "gstm3u8playlist.h"
+
+#define GST_CAT_DEFAULT fragmented_debug
+
+#define M3U8_HEADER_TAG "#EXTM3U\n"
+#define M3U8_VERSION_TAG "#EXT-X-VERSION:%d\n"
+#define M3U8_TARGETDURATION_TAG "#EXT-X-TARGETDURATION:%d\n"
+#define M3U8_MEDIA_SEQUENCE_TAG "#EXT-X-MEDIA-SEQUENCE:%d\n"
+#define M3U8_DISCONTINUITY_TAG "#EXT-X-DISCONTINUITY\n"
+#define M3U8_INT_INF_TAG "#EXTINF:%d,%s\n%s\n"
+#define M3U8_FLOAT_INF_TAG "#EXTINF:%.2f,%s\n%s\n"
+#define M3U8_ENDLIST_TAG "#EXT-X-ENDLIST"
+
+enum
+{
+ GST_M3U8_PLAYLIST_TYPE_EVENT,
+ GST_M3U8_PLAYLIST_TYPE_VOD,
+};
+
+static GstM3U8Entry *
+gst_m3u8_entry_new (const gchar * url, GFile * file, const gchar * title,
+ gfloat duration, gboolean discontinuous)
+{
+ GstM3U8Entry *entry;
+
+ g_return_val_if_fail (url != NULL, NULL);
+ g_return_val_if_fail (title != NULL, NULL);
+
+ entry = g_new0 (GstM3U8Entry, 1);
+ entry->url = g_strdup (url);
+ entry->title = g_strdup (title);
+ entry->duration = duration;
+ entry->file = file;
+ entry->discontinuous = discontinuous;
+ return entry;
+}
+
+static void
+gst_m3u8_entry_free (GstM3U8Entry * entry)
+{
+ g_return_if_fail (entry != NULL);
+
+ g_free (entry->url);
+ g_free (entry->title);
+ if (entry->file != NULL)
+ g_object_unref (entry->file);
+ g_free (entry);
+}
+
+static gchar *
+gst_m3u8_entry_render (GstM3U8Entry * entry, guint version)
+{
+ g_return_val_if_fail (entry != NULL, NULL);
+
+ /* FIXME: Ensure the radix is always a '.' and not a ',' when printing
+ * floating point number, but for now only use integers*/
+ /* if (version < 3) */
+ if (TRUE)
+ return g_strdup_printf ("%s" M3U8_INT_INF_TAG,
+ entry->discontinuous ? M3U8_DISCONTINUITY_TAG : "",
+ (gint) (entry->duration / GST_SECOND), entry->title, entry->url);
+
+ return g_strdup_printf ("%s" M3U8_FLOAT_INF_TAG,
+ entry->discontinuous ? M3U8_DISCONTINUITY_TAG : "",
+ (entry->duration / GST_SECOND), entry->title, entry->url);
+}
+
+GstM3U8Playlist *
+gst_m3u8_playlist_new (guint version, guint window_size, gboolean allow_cache)
+{
+ GstM3U8Playlist *playlist;
+
+ playlist = g_new0 (GstM3U8Playlist, 1);
+ playlist->version = version;
+ playlist->window_size = window_size;
+ playlist->allow_cache = allow_cache;
+ playlist->type = GST_M3U8_PLAYLIST_TYPE_EVENT;
+ playlist->end_list = FALSE;
+ playlist->entries = g_queue_new ();
+
+ return playlist;
+}
+
+void
+gst_m3u8_playlist_free (GstM3U8Playlist * playlist)
+{
+ g_return_if_fail (playlist != NULL);
+
+ g_queue_foreach (playlist->entries, (GFunc) gst_m3u8_entry_free, NULL);
+ g_queue_free (playlist->entries);
+ g_free (playlist);
+}
+
+
+GList *
+gst_m3u8_playlist_add_entry (GstM3U8Playlist * playlist,
+ const gchar * url, GFile * file, const gchar * title,
+ gfloat duration, guint index, gboolean discontinuous)
+{
+ GstM3U8Entry *entry;
+ GList *old_files = NULL;
+
+ g_return_val_if_fail (playlist != NULL, FALSE);
+ g_return_val_if_fail (url != NULL, FALSE);
+ g_return_val_if_fail (title != NULL, FALSE);
+
+ if (playlist->type == GST_M3U8_PLAYLIST_TYPE_VOD)
+ return FALSE;
+
+ entry = gst_m3u8_entry_new (url, file, title, duration, discontinuous);
+
+ if (playlist->window_size != -1) {
+ /* Delete old entries from the playlist */
+ while (playlist->entries->length >= playlist->window_size) {
+ GstM3U8Entry *old_entry;
+
+ old_entry = g_queue_pop_head (playlist->entries);
+ g_object_ref (old_entry->file);
+ old_files = g_list_prepend (old_files, old_entry->file);
+ gst_m3u8_entry_free (old_entry);
+ }
+ }
+
+ playlist->sequence_number = index + 1;
+ g_queue_push_tail (playlist->entries, entry);
+
+ return old_files;
+}
+
+static guint
+gst_m3u8_playlist_target_duration (GstM3U8Playlist * playlist)
+{
+ gint i;
+ GstM3U8Entry *entry;
+ guint64 target_duration = 0;
+
+ for (i = 0; i < playlist->entries->length; i++) {
+ entry = (GstM3U8Entry *) g_queue_peek_nth (playlist->entries, i);
+ if (entry->duration > target_duration)
+ target_duration = entry->duration;
+ }
+
+ return (guint) (target_duration / GST_SECOND);
+}
+
+static void
+render_entry (GstM3U8Entry * entry, GstM3U8Playlist * playlist)
+{
+ gchar *entry_str;
+
+ entry_str = gst_m3u8_entry_render (entry, playlist->version);
+ g_string_append_printf (playlist->playlist_str, "%s", entry_str);
+ g_free (entry_str);
+}
+
+gchar *
+gst_m3u8_playlist_render (GstM3U8Playlist * playlist)
+{
+ gchar *pl;
+
+ g_return_val_if_fail (playlist != NULL, NULL);
+
+ playlist->playlist_str = g_string_new ("");
+
+ /* #EXTM3U */
+ g_string_append_printf (playlist->playlist_str, M3U8_HEADER_TAG);
+ /* #EXT-X-VERSION */
+// g_string_append_printf (playlist->playlist_str, M3U8_VERSION_TAG,
+// playlist->version);
+ /* #EXT-X-MEDIA-SEQUENCE */
+ g_string_append_printf (playlist->playlist_str, M3U8_MEDIA_SEQUENCE_TAG,
+ playlist->sequence_number - playlist->entries->length);
+ /* #EXT-X-TARGETDURATION */
+ g_string_append_printf (playlist->playlist_str, M3U8_TARGETDURATION_TAG,
+ gst_m3u8_playlist_target_duration (playlist));
+ g_string_append_printf (playlist->playlist_str, "\n");
+
+ /* Entries */
+ g_queue_foreach (playlist->entries, (GFunc) render_entry, playlist);
+
+ if (playlist->end_list)
+ g_string_append_printf (playlist->playlist_str, M3U8_ENDLIST_TAG);
+
+ pl = playlist->playlist_str->str;
+ g_string_free (playlist->playlist_str, FALSE);
+ return pl;
+}
+
+void
+gst_m3u8_playlist_clear (GstM3U8Playlist * playlist)
+{
+ g_return_if_fail (playlist != NULL);
+
+ g_queue_foreach (playlist->entries, (GFunc) gst_m3u8_entry_free, NULL);
+ g_queue_clear (playlist->entries);
+}
+
+guint
+gst_m3u8_playlist_n_entries (GstM3U8Playlist * playlist)
+{
+ g_return_val_if_fail (playlist != NULL, 0);
+
+ return playlist->entries->length;
+}