gstreamer/ext/hls/gsthlssink2.c
Sebastian Dröge 6e412d42c7 hlssink2: Don't assert if we don't have a current location when receiving the fragment-closed message
This can happen if the application did not provide an output stream for
the fragment and didn't handle the error message before splitmuxsink
decided to consider the fragment closed.

Part-of: <https://gitlab.freedesktop.org/gstreamer/gst-plugins-bad/-/merge_requests/1469>
2020-08-03 11:23:36 +00:00

683 lines
22 KiB
C

/* GStreamer
* Copyright (C) 2011 Alessandro Decina <alessandro.d@gmail.com>
* Copyright (C) 2017 Sebastian Dröge <sebastian@centricular.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., 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 <gst/pbutils/pbutils.h>
#include <gst/video/video.h>
#include <glib/gstdio.h>
#include <memory.h>
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 DEFAULT_SEND_KEYFRAME_REQUESTS TRUE
#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,
PROP_SEND_KEYFRAME_REQUESTS,
};
enum
{
SIGNAL_GET_PLAYLIST_STREAM,
SIGNAL_GET_FRAGMENT_STREAM,
SIGNAL_DELETE_FRAGMENT,
SIGNAL_LAST
};
static guint signals[SIGNAL_LAST];
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);
g_free (sink->current_location);
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);
}
/* Default implementations for the signal handlers */
static GOutputStream *
gst_hls_sink2_get_playlist_stream (GstHlsSink2 * sink, const gchar * location)
{
GFile *file = g_file_new_for_path (location);
GOutputStream *ostream;
GError *err = NULL;
ostream =
G_OUTPUT_STREAM (g_file_replace (file, NULL, FALSE,
G_FILE_CREATE_REPLACE_DESTINATION, NULL, &err));
if (!ostream) {
GST_ELEMENT_ERROR (sink, RESOURCE, OPEN_WRITE,
(("Got no output stream for playlist '%s': %s."), location,
err->message), (NULL));
g_clear_error (&err);
}
g_object_unref (file);
return ostream;
}
static GOutputStream *
gst_hls_sink2_get_fragment_stream (GstHlsSink2 * sink, const gchar * location)
{
GFile *file = g_file_new_for_path (location);
GOutputStream *ostream;
GError *err = NULL;
ostream =
G_OUTPUT_STREAM (g_file_replace (file, NULL, FALSE,
G_FILE_CREATE_REPLACE_DESTINATION, NULL, &err));
if (!ostream) {
GST_ELEMENT_ERROR (sink, RESOURCE, OPEN_WRITE,
(("Got no output stream for fragment '%s': %s."), location,
err->message), (NULL));
g_clear_error (&err);
}
g_object_unref (file);
return ostream;
}
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 <alessandro.d@gmail.com>, "
"Sebastian Dröge <sebastian@centricular.com>");
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));
g_object_class_install_property (gobject_class, PROP_SEND_KEYFRAME_REQUESTS,
g_param_spec_boolean ("send-keyframe-requests", "Send Keyframe Requests",
"Send keyframe requests to ensure correct fragmentation. If this is disabled "
"then the input must have keyframes in regular intervals",
DEFAULT_SEND_KEYFRAME_REQUESTS,
G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS));
/**
* GstHlsSink2::get-playlist-stream:
* @sink: the #GstHlsSink2
* @location: Location for the playlist file
*
* Returns: #GOutputStream for writing the playlist file.
*
* Since: 1.18
*/
signals[SIGNAL_GET_PLAYLIST_STREAM] =
g_signal_new ("get-playlist-stream", G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GstHlsSink2Class, get_playlist_stream),
g_signal_accumulator_first_wins, NULL, NULL, G_TYPE_OUTPUT_STREAM, 1,
G_TYPE_STRING);
/**
* GstHlsSink2::get-fragment-stream:
* @sink: the #GstHlsSink2
* @location: Location for the fragment file
*
* Returns: #GOutputStream for writing the fragment file.
*
* Since: 1.18
*/
signals[SIGNAL_GET_FRAGMENT_STREAM] =
g_signal_new ("get-fragment-stream", G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST,
G_STRUCT_OFFSET (GstHlsSink2Class, get_fragment_stream),
g_signal_accumulator_first_wins, NULL, NULL, G_TYPE_OUTPUT_STREAM, 1,
G_TYPE_STRING);
/**
* GstHlsSink2::delete-fragment:
* @sink: the #GstHlsSink2
* @location: Location for the fragment file to delete
*
* Requests deletion of an old fragment file that is not needed anymore.
*
* Since: 1.18
*/
signals[SIGNAL_DELETE_FRAGMENT] =
g_signal_new ("delete-fragment", G_TYPE_FROM_CLASS (klass),
G_SIGNAL_RUN_LAST, 0, NULL, NULL, NULL, G_TYPE_NONE, 1, G_TYPE_STRING);
klass->get_playlist_stream = gst_hls_sink2_get_playlist_stream;
klass->get_fragment_stream = gst_hls_sink2_get_fragment_stream;
}
static gchar *
on_format_location (GstElement * splitmuxsink, guint fragment_id,
GstHlsSink2 * sink)
{
GOutputStream *stream = NULL;
gchar *location;
location = g_strdup_printf (sink->location, fragment_id);
g_signal_emit (sink, signals[SIGNAL_GET_FRAGMENT_STREAM], 0, location,
&stream);
if (!stream) {
GST_ELEMENT_ERROR (sink, RESOURCE, OPEN_WRITE,
(("Got no output stream for fragment '%s'."), location), (NULL));
g_free (sink->current_location);
sink->current_location = NULL;
} else {
g_free (sink->current_location);
sink->current_location = g_steal_pointer (&location);
}
g_object_set (sink->giostreamsink, "stream", stream, NULL);
if (stream)
g_object_unref (stream);
g_free (location);
return NULL;
}
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;
sink->send_keyframe_requests = DEFAULT_SEND_KEYFRAME_REQUESTS;
g_queue_init (&sink->old_locations);
sink->splitmuxsink = gst_element_factory_make ("splitmuxsink", NULL);
gst_bin_add (GST_BIN (sink), sink->splitmuxsink);
sink->giostreamsink = gst_element_factory_make ("giostreamsink", NULL);
mux = gst_element_factory_make ("mpegtsmux", NULL);
g_object_set (sink->splitmuxsink, "location", NULL, "max-size-time",
((GstClockTime) sink->target_duration * GST_SECOND),
"send-keyframe-requests", TRUE, "muxer", mux, "sink", sink->giostreamsink,
"reset-muxer", FALSE, NULL);
g_signal_connect (sink->splitmuxsink, "format-location",
G_CALLBACK (on_format_location), sink);
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);
sink->state = GST_M3U8_PLAYLIST_RENDER_INIT;
}
static void
gst_hls_sink2_write_playlist (GstHlsSink2 * sink)
{
char *playlist_content;
GError *error = NULL;
GOutputStream *stream = NULL;
gsize bytes_to_write;
g_signal_emit (sink, signals[SIGNAL_GET_PLAYLIST_STREAM], 0,
sink->playlist_location, &stream);
if (!stream) {
GST_ELEMENT_ERROR (sink, RESOURCE, OPEN_WRITE,
(("Got no output stream for playlist '%s'."), sink->playlist_location),
(NULL));
return;
}
playlist_content = gst_m3u8_playlist_render (sink->playlist);
bytes_to_write = strlen (playlist_content);
if (!g_output_stream_write_all (stream, playlist_content, bytes_to_write,
NULL, NULL, &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);
g_object_unref (stream);
}
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")) {
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;
if (!sink->current_location) {
GST_ELEMENT_ERROR (sink, RESOURCE, OPEN_WRITE, ((NULL)),
("Fragment closed without knowing its location"));
break;
}
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);
sink->state |= GST_M3U8_PLAYLIST_RENDER_STARTED;
g_queue_push_tail (&sink->old_locations,
g_strdup (sink->current_location));
if (sink->max_files > 0) {
while (g_queue_get_length (&sink->old_locations) > sink->max_files) {
gchar *old_location = g_queue_pop_head (&sink->old_locations);
if (g_signal_has_handler_pending (sink,
signals[SIGNAL_DELETE_FRAGMENT], 0, FALSE)) {
g_signal_emit (sink, signals[SIGNAL_DELETE_FRAGMENT], 0,
old_location);
} else {
GFile *file = g_file_new_for_path (old_location);
GError *err = NULL;
if (!g_file_delete (file, NULL, &err)) {
GST_ELEMENT_ERROR (sink, RESOURCE, OPEN_WRITE,
(("Failed to delete fragment file '%s': %s."),
old_location, err->message), (NULL));
g_clear_error (&err);
}
g_object_unref (file);
}
g_free (old_location);
}
}
g_free (sink->current_location);
sink->current_location = NULL;
}
}
break;
}
case GST_MESSAGE_EOS:{
sink->playlist->end_list = TRUE;
gst_hls_sink2_write_playlist (sink);
sink->state |= GST_M3U8_PLAYLIST_RENDER_ENDED;
break;
}
default:
break;
}
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:
/* drain playlist with #EXT-X-ENDLIST */
if (sink->playlist && (sink->state & GST_M3U8_PLAYLIST_RENDER_STARTED) &&
!(sink->state & GST_M3U8_PLAYLIST_RENDER_ENDED)) {
sink->playlist->end_list = TRUE;
gst_hls_sink2_write_playlist (sink);
}
/* fall-through */
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;
case PROP_SEND_KEYFRAME_REQUESTS:
sink->send_keyframe_requests = g_value_get_boolean (value);
if (sink->splitmuxsink) {
g_object_set (sink->splitmuxsink, "send-keyframe-requests",
sink->send_keyframe_requests, NULL);
}
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;
case PROP_SEND_KEYFRAME_REQUESTS:
g_value_set_boolean (value, sink->send_keyframe_requests);
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 ());
}