/* GStreamer * Copyright (C) 2019 Stéphane Cerveau * * 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-dashsink * @title: dashsink * * Dynamic Adaptive Streaming over HTTP sink/server * * ## Example launch line * |[ * gst-launch-1.0 dashsink name=dashsink max-files=5 audiotestsrc is-live=true ! avenc_aac ! dashsink.audio_0 videotestsrc is-live=true ! x264enc ! dashsink.video_0 * ]| * */ /* Implementation notes: * * The following section describes how dashsink works internally. * * Introduction: * * This element aims to generate the Media Pressentation Description XML based file * used as DASH content in addition to the necessary media frgaments. * Based on splitmuxsink branches to generate the media fragments, * the element will generate a new adaptation set for each media type (video/audio/test) * and a new representation for each additional stream for a media type. * ,----------------dashsink------------------, * ; ,----------splitmuxsink--------------, ; * ,-videotestsrc-, ,-x264enc-, ; ; ,-Queue-, ,-tsdemux-, ,-filesink-, ; ; * ; o--o o---o--o ; o-o o-o , ; ; * '--------------' '---------' ; ; '-------' '---------' '----------' ; ; * ; '------------------------------------' ; * ; ; * ; ,----------splitmuxsink--------------, ; * ,-audiotestsrc-, ,-avenc_aac-, ; ; ,-Queue-, ,-tsdemux-, ,-filesink-, ; ; * ; o--o o-o--o o-o o-o ; ; ; * '--------------' '-----------' ; ; '-------' '---------' '----------' ; ; * ; '------------------------------------' ; * ' -----------------------------------------' * * "DASH Sink" * |_ Period 1 * | |_ Video Adaptation Set * | | |_ Representation 1 - Container/Codec - bitrate X * | |_ Representation 2 - Container/Codec - bitrate Y * | |_ Audio Adaptation Set * | |_ Representation 1 - Container/Codec - bitrate X * | |_ Representation 2 - Container/Codec - bitrate Y * * This element is able to generate static or dynamic MPD with multiple adaptation sets, * multiple representations and multiple periods for three kind of . * * It supports any kind of stream input codec * which can be encapsulated in Transport Stream or ISO media format. * The current implementation is generating compliant MPDs for both static and dynamic * prfiles with https://conformance.dashif.org/ * * Limitations: * * The fragments during the DASH generation does not look reliable enough to be used as * a production solution. Some additional or fine tuning work needs to be performed to address * these issues, especially for MP4 fragments. * */ #ifdef HAVE_CONFIG_H #include "config.h" #endif #include "gstdashsink.h" #include "gstmpdparser.h" #include #include #include #include #include GST_DEBUG_CATEGORY_STATIC (gst_dash_sink_debug); #define GST_CAT_DEFAULT gst_dash_sink_debug /** * GstDashSinkMuxerType: * @GST_DASH_SINK_MUXER_TS: Use mpegtsmux * @GST_DASH_SINK_MUXER_MP4: Use mp4mux * * Muxer type */ typedef enum { GST_DASH_SINK_MUXER_TS = 0, GST_DASH_SINK_MUXER_MP4 = 1, } GstDashSinkMuxerType; typedef struct _DashSinkMuxer { GstDashSinkMuxerType type; const gchar *element_name; const gchar *mimetype; const gchar *file_ext; } DashSinkMuxer; #define GST_TYPE_DASH_SINK_MUXER (gst_dash_sink_muxer_get_type()) static GType gst_dash_sink_muxer_get_type (void) { static GType dash_sink_muxer_type = 0; static const GEnumValue muxer_type[] = { {GST_DASH_SINK_MUXER_TS, "Use mpegtsmux", "ts"}, {GST_DASH_SINK_MUXER_MP4, "Use mp4mux", "mp4"}, {0, NULL, NULL}, }; if (!dash_sink_muxer_type) { dash_sink_muxer_type = g_enum_register_static ("GstDashSinkMuxerType", muxer_type); } return dash_sink_muxer_type; } static const DashSinkMuxer dash_muxer_list[] = { { GST_DASH_SINK_MUXER_TS, "mpegtsmux", "video/mp2t", "ts"}, { GST_DASH_SINK_MUXER_MP4, "mp4mux", "video/mp4", "mp4"}, }; #define DEFAULT_SEGMENT_LIST_TPL "_%05d" #define DEFAULT_SEGMENT_TEMPLATE_TPL "_%d" #define DEFAULT_MPD_FILENAME "dash.mpd" #define DEFAULT_MPD_ROOT_PATH NULL #define DEFAULT_TARGET_DURATION 15 #define DEFAULT_SEND_KEYFRAME_REQUESTS TRUE #define DEFAULT_MPD_NAMESPACE "urn:mpeg:dash:schema:mpd:2011" #define DEFAULT_MPD_PROFILES "urn:mpeg:dash:profile:isoff-main:2011" #define DEFAULT_MPD_SCHEMA_LOCATION "urn:mpeg:DASH:schema:MPD:2011 DASH-MPD.xsd" #define DEFAULT_MPD_USE_SEGMENT_LIST FALSE #define DEFAULT_MPD_MIN_BUFFER_TIME 2000 #define DEFAULT_MPD_PERIOD_DURATION GST_CLOCK_TIME_NONE #define DEFAULT_DASH_SINK_MUXER GST_DASH_SINK_MUXER_TS enum { ADAPTATION_SET_ID_VIDEO = 1, ADAPTATION_SET_ID_AUDIO, ADAPTATION_SET_ID_SUBTITLE, }; enum { PROP_0, PROP_MPD_FILENAME, PROP_MPD_ROOT_PATH, PROP_TARGET_DURATION, PROP_SEND_KEYFRAME_REQUESTS, PROP_USE_SEGMENT_LIST, PROP_MPD_DYNAMIC, PROP_MUXER, PROP_MPD_MINIMUM_UPDATE_PERIOD, PROP_MPD_MIN_BUFFER_TIME, PROP_MPD_BASEURL, PROP_MPD_PERIOD_DURATION, }; enum { SIGNAL_GET_PLAYLIST_STREAM, SIGNAL_GET_FRAGMENT_STREAM, SIGNAL_LAST }; static guint signals[SIGNAL_LAST]; typedef enum { DASH_SINK_STREAM_TYPE_VIDEO = 0, DASH_SINK_STREAM_TYPE_AUDIO, DASH_SINK_STREAM_TYPE_SUBTITLE, DASH_SINK_STREAM_TYPE_UNKNOWN, } GstDashSinkStreamType; typedef struct _GstDashSinkStreamVideoInfo { gint width; gint height; } GstDashSinkStreamVideoInfo; typedef struct _GstDashSinkStreamAudioInfo { gint channels; gint rate; } GstDashSinkStreamAudioInfo; typedef struct GstDashSinkStreamSubtitleInfo { gchar *codec; } GstDashSinkStreamSubtitleInfo; typedef union _GstDashSinkStreamInfo { GstDashSinkStreamVideoInfo video; GstDashSinkStreamAudioInfo audio; GstDashSinkStreamSubtitleInfo subtitle; } GstDashSinkStreamInfo; struct _GstDashSink { GstBin bin; GMutex mpd_lock; gchar *location; gchar *mpd_filename; gchar *mpd_root_path; gchar *mpd_profiles; gchar *mpd_baseurl; GstDashSinkMuxerType muxer; GstMPDClient *mpd_client; gchar *current_period_id; gint target_duration; GstClockTime running_time; gboolean send_keyframe_requests; gboolean use_segment_list; gboolean is_dynamic; gchar *segment_file_tpl; guint index; GList *streams; guint64 minimum_update_period; guint64 min_buffer_time; gint64 period_duration; }; typedef struct _GstDashSinkStream { GstDashSink *sink; GstDashSinkStreamType type; GstPad *pad; gint buffer_probe; GstElement *splitmuxsink; gint adaptation_set_id; gchar *representation_id; gchar *current_segment_location; gint current_segment_id; gint next_segment_id; gchar *mimetype; gint bitrate; gchar *codec; GstClockTime current_running_time_start; GstDashSinkStreamInfo info; GstElement *giostreamsink; } GstDashSinkStream; static GstStaticPadTemplate video_sink_template = GST_STATIC_PAD_TEMPLATE ("video_%u", GST_PAD_SINK, GST_PAD_REQUEST, GST_STATIC_CAPS_ANY); static GstStaticPadTemplate audio_sink_template = GST_STATIC_PAD_TEMPLATE ("audio_%u", GST_PAD_SINK, GST_PAD_REQUEST, GST_STATIC_CAPS_ANY); static GstStaticPadTemplate subtitle_sink_template = GST_STATIC_PAD_TEMPLATE ("subtitle_%u", GST_PAD_SINK, GST_PAD_REQUEST, GST_STATIC_CAPS_ANY); #define gst_dash_sink_parent_class parent_class G_DEFINE_TYPE_WITH_CODE (GstDashSink, gst_dash_sink, GST_TYPE_BIN, GST_DEBUG_CATEGORY_INIT (gst_dash_sink_debug, "dashsink", 0, "DashSink")); GST_ELEMENT_REGISTER_DEFINE (dashsink, "dashsink", GST_RANK_NONE, gst_dash_sink_get_type ()); static void gst_dash_sink_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * spec); static void gst_dash_sink_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * spec); static void gst_dash_sink_handle_message (GstBin * bin, GstMessage * message); static void gst_dash_sink_reset (GstDashSink * sink); static GstStateChangeReturn gst_dash_sink_change_state (GstElement * element, GstStateChange trans); static GstPad *gst_dash_sink_request_new_pad (GstElement * element, GstPadTemplate * templ, const gchar * name, const GstCaps * caps); static void gst_dash_sink_release_pad (GstElement * element, GstPad * pad); static GstDashSinkStream * gst_dash_sink_stream_from_pad (GList * streams, GstPad * pad) { GList *l; GstDashSinkStream *stream = NULL; for (l = streams; l != NULL; l = l->next) { stream = l->data; if (stream->pad == pad) return stream; } return NULL; } static GstDashSinkStream * gst_dash_sink_stream_from_splitmuxsink (GList * streams, GstElement * element) { GList *l; GstDashSinkStream *stream = NULL; for (l = streams; l != NULL; l = l->next) { stream = l->data; if (stream->splitmuxsink == element) return stream; } return NULL; } static gchar * gst_dash_sink_stream_get_next_name (GList * streams, GstDashSinkStreamType type) { GList *l; guint count = 0; GstDashSinkStream *stream = NULL; gchar *name = NULL; for (l = streams; l != NULL; l = l->next) { stream = l->data; if (stream->type == type) count++; } switch (type) { case DASH_SINK_STREAM_TYPE_VIDEO: name = g_strdup_printf ("video_%d", count); break; case DASH_SINK_STREAM_TYPE_AUDIO: name = g_strdup_printf ("audio_%d", count); break; case DASH_SINK_STREAM_TYPE_SUBTITLE: name = g_strdup_printf ("sub_%d", count); break; default: name = g_strdup_printf ("unknown_%d", count); } return name; } static void gst_dash_sink_stream_free (gpointer s) { GstDashSinkStream *stream = (GstDashSinkStream *) s; g_object_unref (stream->sink); g_free (stream->current_segment_location); g_free (stream->representation_id); g_free (stream->mimetype); g_free (stream->codec); g_free (stream); } static void gst_dash_sink_dispose (GObject * object) { GstDashSink *sink = GST_DASH_SINK (object); G_OBJECT_CLASS (parent_class)->dispose ((GObject *) sink); } static void gst_dash_sink_finalize (GObject * object) { GstDashSink *sink = GST_DASH_SINK (object); g_free (sink->mpd_filename); g_free (sink->mpd_root_path); g_free (sink->mpd_profiles); if (sink->mpd_client) gst_mpd_client_free (sink->mpd_client); g_mutex_clear (&sink->mpd_lock); g_list_free_full (sink->streams, gst_dash_sink_stream_free); G_OBJECT_CLASS (parent_class)->finalize ((GObject *) sink); } /* Default implementations for the signal handlers */ static GOutputStream * gst_dash_sink_get_playlist_stream (GstDashSink * 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_dash_sink_get_fragment_stream (GstDashSink * 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_dash_sink_class_init (GstDashSinkClass * 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_sink_template); gst_element_class_add_static_pad_template (element_class, &audio_sink_template); gst_element_class_add_static_pad_template (element_class, &subtitle_sink_template); gst_element_class_set_static_metadata (element_class, "DASH Sink", "Sink", "Dynamic Adaptive Streaming over HTTP sink", "Stéphane Cerveau "); element_class->change_state = GST_DEBUG_FUNCPTR (gst_dash_sink_change_state); element_class->request_new_pad = GST_DEBUG_FUNCPTR (gst_dash_sink_request_new_pad); element_class->release_pad = GST_DEBUG_FUNCPTR (gst_dash_sink_release_pad); bin_class->handle_message = gst_dash_sink_handle_message; gobject_class->dispose = gst_dash_sink_dispose; gobject_class->finalize = gst_dash_sink_finalize; gobject_class->set_property = gst_dash_sink_set_property; gobject_class->get_property = gst_dash_sink_get_property; g_object_class_install_property (gobject_class, PROP_MPD_FILENAME, g_param_spec_string ("mpd-filename", "MPD filename", "filename of the mpd to write", DEFAULT_MPD_FILENAME, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_MPD_ROOT_PATH, g_param_spec_string ("mpd-root-path", "MPD Root Path", "Path where the MPD and its fragents will be written", DEFAULT_MPD_ROOT_PATH, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_MPD_BASEURL, g_param_spec_string ("mpd-baseurl", "MPD BaseURL", "BaseURL to set in the MPD", DEFAULT_MPD_ROOT_PATH, 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_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)); g_object_class_install_property (gobject_class, PROP_USE_SEGMENT_LIST, g_param_spec_boolean ("use-segment-list", "Use segment list", "Use segment list instead of segment template to create the segments", DEFAULT_MPD_USE_SEGMENT_LIST, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_MPD_DYNAMIC, g_param_spec_boolean ("dynamic", "dynamic", "Provides a dynamic mpd", FALSE, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_MUXER, g_param_spec_enum ("muxer", "Muxer", "Muxer type to be used by dashsink to generate the fragment", GST_TYPE_DASH_SINK_MUXER, DEFAULT_DASH_SINK_MUXER, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_MPD_MINIMUM_UPDATE_PERIOD, g_param_spec_uint64 ("minimum-update-period", "Minimum update period", "Provides to the manifest a minimum update period in milliseconds", 0, G_MAXUINT64, 0, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_MPD_MIN_BUFFER_TIME, g_param_spec_uint64 ("min-buffer-time", "Mininim buffer time", "Provides to the manifest a minimum buffer time in milliseconds", 0, G_MAXUINT64, DEFAULT_MPD_MIN_BUFFER_TIME, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); g_object_class_install_property (gobject_class, PROP_MPD_PERIOD_DURATION, g_param_spec_uint64 ("period-duration", "period duration", "Provides the explicit duration of a period in milliseconds", 0, G_MAXUINT64, DEFAULT_MPD_PERIOD_DURATION, G_PARAM_READWRITE | G_PARAM_STATIC_STRINGS)); /** * GstDashSink::get-playlist-stream: * @sink: the #GstDashSink * @location: Location for the playlist file * * Returns: #GOutputStream for writing the playlist file. * * Since: 1.20 */ signals[SIGNAL_GET_PLAYLIST_STREAM] = g_signal_new_class_handler ("get-playlist-stream", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_CALLBACK (gst_dash_sink_get_playlist_stream), NULL, NULL, NULL, G_TYPE_OUTPUT_STREAM, 1, G_TYPE_STRING); /** * GstDashSink::get-fragment-stream: * @sink: the #GstDashSink * @location: Location for the fragment file * * Returns: #GOutputStream for writing the fragment file. * * Since: 1.20 */ signals[SIGNAL_GET_FRAGMENT_STREAM] = g_signal_new_class_handler ("get-fragment-stream", G_TYPE_FROM_CLASS (klass), G_SIGNAL_RUN_LAST, G_CALLBACK (gst_dash_sink_get_fragment_stream), NULL, NULL, NULL, G_TYPE_OUTPUT_STREAM, 1, G_TYPE_STRING); gst_type_mark_as_plugin_api (GST_TYPE_DASH_SINK_MUXER, 0); } static gchar * on_format_location (GstElement * splitmuxsink, guint fragment_id, GstDashSinkStream * dash_stream) { GOutputStream *stream = NULL; GstDashSink *sink = dash_stream->sink; gchar *segment_tpl_path; dash_stream->current_segment_id = dash_stream->next_segment_id; g_free (dash_stream->current_segment_location); if (sink->use_segment_list) dash_stream->current_segment_location = g_strdup_printf ("%s" DEFAULT_SEGMENT_LIST_TPL ".%s", dash_stream->representation_id, dash_stream->current_segment_id, dash_muxer_list[sink->muxer].file_ext); else { dash_stream->current_segment_location = g_strdup_printf ("%s" DEFAULT_SEGMENT_TEMPLATE_TPL ".%s", dash_stream->representation_id, dash_stream->current_segment_id, dash_muxer_list[sink->muxer].file_ext); } dash_stream->next_segment_id++; if (sink->mpd_root_path) segment_tpl_path = g_build_path (G_DIR_SEPARATOR_S, sink->mpd_root_path, dash_stream->current_segment_location, NULL); else segment_tpl_path = g_strdup (dash_stream->current_segment_location); g_signal_emit (sink, signals[SIGNAL_GET_FRAGMENT_STREAM], 0, segment_tpl_path, &stream); if (!stream) GST_ELEMENT_ERROR (sink, RESOURCE, OPEN_WRITE, (("Got no output stream for fragment '%s'."), segment_tpl_path), (NULL)); else g_object_set (dash_stream->giostreamsink, "stream", stream, NULL); if (stream) g_object_unref (stream); g_free (segment_tpl_path); return NULL; } static gboolean gst_dash_sink_add_splitmuxsink (GstDashSink * sink, GstDashSinkStream * stream) { GstElement *mux = gst_element_factory_make (dash_muxer_list[sink->muxer].element_name, NULL); if (sink->muxer == GST_DASH_SINK_MUXER_MP4) g_object_set (mux, "fragment-duration", sink->target_duration * GST_MSECOND, NULL); g_return_val_if_fail (mux != NULL, FALSE); stream->splitmuxsink = gst_element_factory_make ("splitmuxsink", NULL); if (!stream->splitmuxsink) { gst_object_unref (mux); return FALSE; } stream->giostreamsink = gst_element_factory_make ("giostreamsink", NULL); if (!stream->giostreamsink) { gst_object_unref (stream->splitmuxsink); gst_object_unref (mux); return FALSE; } gst_bin_add (GST_BIN (sink), stream->splitmuxsink); if (!sink->use_segment_list) stream->current_segment_id = 1; else stream->current_segment_id = 0; stream->next_segment_id = stream->current_segment_id; g_object_set (stream->splitmuxsink, "location", NULL, "max-size-time", ((GstClockTime) sink->target_duration * GST_SECOND), "send-keyframe-requests", TRUE, "muxer", mux, "sink", stream->giostreamsink, "reset-muxer", FALSE, "send-keyframe-requests", sink->send_keyframe_requests, NULL); g_signal_connect (stream->splitmuxsink, "format-location", G_CALLBACK (on_format_location), stream); return TRUE; } static void gst_dash_sink_init (GstDashSink * sink) { sink->mpd_filename = g_strdup (DEFAULT_MPD_FILENAME); sink->mpd_root_path = g_strdup (DEFAULT_MPD_ROOT_PATH); sink->mpd_client = NULL; sink->target_duration = DEFAULT_TARGET_DURATION; sink->send_keyframe_requests = DEFAULT_SEND_KEYFRAME_REQUESTS; sink->mpd_profiles = g_strdup (DEFAULT_MPD_PROFILES); sink->use_segment_list = DEFAULT_MPD_USE_SEGMENT_LIST; sink->min_buffer_time = DEFAULT_MPD_MIN_BUFFER_TIME; sink->period_duration = DEFAULT_MPD_PERIOD_DURATION; g_mutex_init (&sink->mpd_lock); GST_OBJECT_FLAG_SET (sink, GST_ELEMENT_FLAG_SINK); gst_dash_sink_reset (sink); } static void gst_dash_sink_reset (GstDashSink * sink) { sink->index = 0; } static void gst_dash_sink_get_stream_metadata (GstDashSink * sink, GstDashSinkStream * stream) { GstStructure *s; GstCaps *caps = gst_pad_get_current_caps (stream->pad); GST_DEBUG_OBJECT (sink, "stream caps %s", gst_caps_to_string (caps)); s = gst_caps_get_structure (caps, 0); switch (stream->type) { case DASH_SINK_STREAM_TYPE_VIDEO: { gst_structure_get_int (s, "width", &stream->info.video.width); gst_structure_get_int (s, "height", &stream->info.video.height); g_free (stream->codec); stream->codec = g_strdup (gst_mpd_helper_get_video_codec_from_mime (caps)); break; } case DASH_SINK_STREAM_TYPE_AUDIO: { gst_structure_get_int (s, "channels", &stream->info.audio.channels); gst_structure_get_int (s, "rate", &stream->info.audio.rate); g_free (stream->codec); stream->codec = g_strdup (gst_mpd_helper_get_audio_codec_from_mime (caps)); break; } case DASH_SINK_STREAM_TYPE_SUBTITLE: { break; } default: break; } gst_caps_unref (caps); } static void gst_dash_sink_generate_mpd_content (GstDashSink * sink, GstDashSinkStream * stream) { if (!sink->mpd_client) { GList *l; sink->mpd_client = gst_mpd_client_new (); /* Add or set root node with stream ids */ gst_mpd_client_set_root_node (sink->mpd_client, "profiles", sink->mpd_profiles, "default-namespace", DEFAULT_MPD_NAMESPACE, "min-buffer-time", sink->min_buffer_time, NULL); if (sink->is_dynamic) { GstDateTime *now = gst_date_time_new_now_utc (); gst_mpd_client_set_root_node (sink->mpd_client, "type", GST_MPD_FILE_TYPE_DYNAMIC, "availability-start-time", now, "publish-time", now, NULL); gst_date_time_unref (now); } if (sink->minimum_update_period) gst_mpd_client_set_root_node (sink->mpd_client, "minimum-update-period", sink->minimum_update_period, NULL); if (sink->mpd_baseurl) gst_mpd_client_add_baseurl_node (sink->mpd_client, "url", sink->mpd_baseurl, NULL); /* Add or set period node with stream ids * TODO support multiple period * */ sink->current_period_id = gst_mpd_client_set_period_node (sink->mpd_client, sink->current_period_id, NULL); for (l = sink->streams; l != NULL; l = l->next) { GstDashSinkStream *stream = (GstDashSinkStream *) l->data; /* Add or set adaptation_set node with stream ids * AdaptationSet per stream type * */ gst_mpd_client_set_adaptation_set_node (sink->mpd_client, sink->current_period_id, stream->adaptation_set_id, NULL); /* Add or set representation node with stream ids */ gst_mpd_client_set_representation_node (sink->mpd_client, sink->current_period_id, stream->adaptation_set_id, stream->representation_id, "bandwidth", stream->bitrate, "mime-type", stream->mimetype, "codecs", stream->codec, NULL); /* Set specific to stream type */ if (stream->type == DASH_SINK_STREAM_TYPE_VIDEO) { gst_mpd_client_set_adaptation_set_node (sink->mpd_client, sink->current_period_id, stream->adaptation_set_id, "content-type", "video", NULL); gst_mpd_client_set_representation_node (sink->mpd_client, sink->current_period_id, stream->adaptation_set_id, stream->representation_id, "width", stream->info.video.width, "height", stream->info.video.height, NULL); } else if (stream->type == DASH_SINK_STREAM_TYPE_AUDIO) { gst_mpd_client_set_adaptation_set_node (sink->mpd_client, sink->current_period_id, stream->adaptation_set_id, "content-type", "audio", NULL); gst_mpd_client_set_representation_node (sink->mpd_client, sink->current_period_id, stream->adaptation_set_id, stream->representation_id, "audio-sampling-rate", stream->info.audio.rate, NULL); } if (sink->use_segment_list) { /* Add a default segment list */ gst_mpd_client_set_segment_list (sink->mpd_client, sink->current_period_id, stream->adaptation_set_id, stream->representation_id, "duration", sink->target_duration, NULL); } else { gchar *media_segment_template = g_strconcat (stream->representation_id, "_$Number$", ".", dash_muxer_list[sink->muxer].file_ext, NULL); gst_mpd_client_set_segment_template (sink->mpd_client, sink->current_period_id, stream->adaptation_set_id, stream->representation_id, "media", media_segment_template, "duration", sink->target_duration, NULL); g_free (media_segment_template); } } } /* MPD updates */ if (sink->use_segment_list) { GST_INFO_OBJECT (sink, "Add segment URL: %s", stream->current_segment_location); gst_mpd_client_add_segment_url (sink->mpd_client, sink->current_period_id, stream->adaptation_set_id, stream->representation_id, "media", stream->current_segment_location, NULL); } else { if (!sink->is_dynamic) { if (sink->period_duration != DEFAULT_MPD_PERIOD_DURATION) gst_mpd_client_set_period_node (sink->mpd_client, sink->current_period_id, "duration", sink->period_duration, NULL); else gst_mpd_client_set_period_node (sink->mpd_client, sink->current_period_id, "duration", gst_util_uint64_scale (sink->running_time, 1, GST_MSECOND), NULL); } if (!sink->minimum_update_period) { if (sink->period_duration != DEFAULT_MPD_PERIOD_DURATION) gst_mpd_client_set_root_node (sink->mpd_client, "media-presentation-duration", sink->period_duration, NULL); else gst_mpd_client_set_root_node (sink->mpd_client, "media-presentation-duration", gst_util_uint64_scale (sink->running_time, 1, GST_MSECOND), NULL); } } } static void gst_dash_sink_write_mpd_file (GstDashSink * sink, GstDashSinkStream * current_stream) { char *mpd_content = NULL; gint size; GError *error = NULL; gchar *mpd_filepath = NULL; GOutputStream *file_stream = NULL; gsize bytes_to_write; g_mutex_lock (&sink->mpd_lock); gst_dash_sink_generate_mpd_content (sink, current_stream); if (!gst_mpd_client_get_xml_content (sink->mpd_client, &mpd_content, &size)) return; g_mutex_unlock (&sink->mpd_lock); if (sink->mpd_root_path) mpd_filepath = g_build_path (G_DIR_SEPARATOR_S, sink->mpd_root_path, sink->mpd_filename, NULL); else mpd_filepath = g_strdup (sink->mpd_filename); GST_DEBUG_OBJECT (sink, "a new mpd content is available: %s", mpd_content); GST_DEBUG_OBJECT (sink, "write mpd to %s", mpd_filepath); g_signal_emit (sink, signals[SIGNAL_GET_PLAYLIST_STREAM], 0, mpd_filepath, &file_stream); if (!file_stream) { GST_ELEMENT_ERROR (sink, RESOURCE, OPEN_WRITE, (("Got no output stream for fragment '%s'."), mpd_filepath), (NULL)); } bytes_to_write = strlen (mpd_content); if (!g_output_stream_write_all (file_stream, mpd_content, bytes_to_write, NULL, NULL, &error)) { GST_ERROR ("Failed to write mpd content: %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 (mpd_content); g_free (mpd_filepath); g_object_unref (file_stream); } static void gst_dash_sink_handle_message (GstBin * bin, GstMessage * message) { GstDashSink *sink = GST_DASH_SINK (bin); GstDashSinkStream *stream = NULL; switch (message->type) { case GST_MESSAGE_ELEMENT: { const GstStructure *s = gst_message_get_structure (message); GST_DEBUG_OBJECT (sink, "Received message with name %s", gst_structure_get_name (s)); stream = gst_dash_sink_stream_from_splitmuxsink (sink->streams, GST_ELEMENT (message->src)); if (stream) { if (gst_structure_has_name (s, "splitmuxsink-fragment-opened")) { gst_dash_sink_get_stream_metadata (sink, stream); gst_structure_get_clock_time (s, "running-time", &stream->current_running_time_start); } else if (gst_structure_has_name (s, "splitmuxsink-fragment-closed")) { GstClockTime running_time; gst_structure_get_clock_time (s, "running-time", &running_time); if (sink->running_time < running_time) sink->running_time = running_time; gst_dash_sink_write_mpd_file (sink, stream); } } break; } case GST_MESSAGE_EOS:{ gst_dash_sink_write_mpd_file (sink, NULL); break; } default: break; } GST_BIN_CLASS (parent_class)->handle_message (bin, message); } static GstPadProbeReturn _dash_sink_buffers_probe (GstPad * pad, GstPadProbeInfo * probe_info, gpointer user_data) { GstBuffer *buffer = GST_PAD_PROBE_INFO_BUFFER (probe_info); GstDashSinkStream *stream = (GstDashSinkStream *) user_data; if (GST_BUFFER_DURATION (buffer)) stream->bitrate = gst_buffer_get_size (buffer) * GST_SECOND / GST_BUFFER_DURATION (buffer); return GST_PAD_PROBE_OK; } static GstPad * gst_dash_sink_request_new_pad (GstElement * element, GstPadTemplate * templ, const gchar * pad_name, const GstCaps * caps) { GstDashSink *sink = GST_DASH_SINK (element); GstDashSinkStream *stream = NULL; GstPad *pad = NULL; GstPad *peer = NULL; const gchar *split_pad_name = pad_name; stream = g_new0 (GstDashSinkStream, 1); stream->sink = g_object_ref (sink); if (g_str_has_prefix (templ->name_template, "video")) { stream->type = DASH_SINK_STREAM_TYPE_VIDEO; stream->adaptation_set_id = ADAPTATION_SET_ID_VIDEO; split_pad_name = "video"; } else if (g_str_has_prefix (templ->name_template, "audio")) { stream->type = DASH_SINK_STREAM_TYPE_AUDIO; stream->adaptation_set_id = ADAPTATION_SET_ID_AUDIO; } else if (g_str_has_prefix (templ->name_template, "subtitle")) { stream->type = DASH_SINK_STREAM_TYPE_SUBTITLE; stream->adaptation_set_id = ADAPTATION_SET_ID_SUBTITLE; } if (pad_name) stream->representation_id = g_strdup (pad_name); else stream->representation_id = gst_dash_sink_stream_get_next_name (sink->streams, stream->type); stream->mimetype = g_strdup (dash_muxer_list[sink->muxer].mimetype); if (!gst_dash_sink_add_splitmuxsink (sink, stream)) { GST_ERROR_OBJECT (sink, "Unable to create splitmuxsink element for pad template name %s", templ->name_template); gst_dash_sink_stream_free (stream); goto done; } peer = gst_element_request_pad_simple (stream->splitmuxsink, split_pad_name); if (!peer) { GST_ERROR_OBJECT (sink, "Unable to request pad name %s", split_pad_name); return NULL; } pad = gst_ghost_pad_new_from_template (pad_name, peer, templ); gst_pad_set_active (pad, TRUE); gst_element_add_pad (element, pad); gst_object_unref (peer); stream->pad = pad; stream->buffer_probe = gst_pad_add_probe (stream->pad, GST_PAD_PROBE_TYPE_BUFFER, _dash_sink_buffers_probe, stream, NULL); sink->streams = g_list_append (sink->streams, stream); GST_DEBUG_OBJECT (sink, "Adding a new stream with id %s", stream->representation_id); done: return pad; } static void gst_dash_sink_release_pad (GstElement * element, GstPad * pad) { GstDashSink *sink = GST_DASH_SINK (element); GstPad *peer; GstDashSinkStream *stream = gst_dash_sink_stream_from_pad (sink->streams, pad); g_return_if_fail (stream != NULL); peer = gst_pad_get_peer (pad); if (peer) { gst_element_release_request_pad (stream->splitmuxsink, pad); gst_object_unref (peer); } if (stream->buffer_probe > 0) { gst_pad_remove_probe (pad, stream->buffer_probe); stream->buffer_probe = 0; } gst_object_ref (pad); gst_element_remove_pad (element, pad); gst_pad_set_active (pad, FALSE); stream->pad = NULL; gst_object_unref (pad); } static GstStateChangeReturn gst_dash_sink_change_state (GstElement * element, GstStateChange trans) { GstStateChangeReturn ret = GST_STATE_CHANGE_SUCCESS; GstDashSink *sink = GST_DASH_SINK (element); switch (trans) { case GST_STATE_CHANGE_NULL_TO_READY: if (!g_list_length (sink->streams)) { 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_dash_sink_reset (sink); break; default: break; } return ret; } static void gst_dash_sink_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec) { GstDashSink *sink = GST_DASH_SINK (object); switch (prop_id) { case PROP_MPD_FILENAME: g_free (sink->mpd_filename); sink->mpd_filename = g_value_dup_string (value); break; case PROP_MPD_ROOT_PATH: g_free (sink->mpd_root_path); sink->mpd_root_path = g_value_dup_string (value); break; case PROP_MPD_BASEURL: g_free (sink->mpd_baseurl); sink->mpd_baseurl = g_value_dup_string (value); break; case PROP_TARGET_DURATION: sink->target_duration = g_value_get_uint (value); break; case PROP_SEND_KEYFRAME_REQUESTS: sink->send_keyframe_requests = g_value_get_boolean (value); break; case PROP_USE_SEGMENT_LIST: sink->use_segment_list = g_value_get_boolean (value); break; case PROP_MPD_DYNAMIC: sink->is_dynamic = g_value_get_boolean (value); break; case PROP_MUXER: sink->muxer = g_value_get_enum (value); break; case PROP_MPD_MINIMUM_UPDATE_PERIOD: sink->minimum_update_period = g_value_get_uint64 (value); break; case PROP_MPD_MIN_BUFFER_TIME: sink->min_buffer_time = g_value_get_uint64 (value); break; case PROP_MPD_PERIOD_DURATION: sink->period_duration = g_value_get_uint64 (value); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } } static void gst_dash_sink_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * pspec) { GstDashSink *sink = GST_DASH_SINK (object); switch (prop_id) { case PROP_MPD_FILENAME: g_value_set_string (value, sink->mpd_filename); break; case PROP_MPD_ROOT_PATH: g_value_set_string (value, sink->mpd_root_path); break; case PROP_MPD_BASEURL: g_value_set_string (value, sink->mpd_baseurl); break; case PROP_TARGET_DURATION: g_value_set_uint (value, sink->target_duration); break; case PROP_SEND_KEYFRAME_REQUESTS: g_value_set_boolean (value, sink->send_keyframe_requests); break; case PROP_USE_SEGMENT_LIST: g_value_set_boolean (value, sink->use_segment_list); break; case PROP_MPD_DYNAMIC: g_value_set_boolean (value, sink->is_dynamic); break; case PROP_MUXER: g_value_set_enum (value, sink->muxer); break; case PROP_MPD_MINIMUM_UPDATE_PERIOD: g_value_set_uint64 (value, sink->minimum_update_period); break; case PROP_MPD_MIN_BUFFER_TIME: g_value_set_uint64 (value, sink->min_buffer_time); break; case PROP_MPD_PERIOD_DURATION: g_value_set_uint64 (value, sink->period_duration); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } }