/* * * GStreamer * Copyright (C) 1999-2001 Erik Walthinsen * * 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 #endif #include #include #include "gstmms.h" #define DEFAULT_CONNECTION_SPEED 0 enum { ARG_0, ARG_LOCATION, ARG_CONNECTION_SPEED }; GST_DEBUG_CATEGORY_STATIC (mmssrc_debug); #define GST_CAT_DEFAULT mmssrc_debug static const GstElementDetails plugin_details = GST_ELEMENT_DETAILS ("MMS streaming source", "Source/Network", "Receive data streamed via MSFT Multi Media Server protocol", "Maciej Katafiasz "); static GstStaticPadTemplate src_factory = GST_STATIC_PAD_TEMPLATE ("src", GST_PAD_SRC, GST_PAD_ALWAYS, GST_STATIC_CAPS ("video/x-ms-asf") ); static void gst_mms_finalize (GObject * gobject); static void gst_mms_uri_handler_init (gpointer g_iface, gpointer iface_data); static void gst_mms_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec); static void gst_mms_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * pspec); static gboolean gst_mms_query (GstBaseSrc * src, GstQuery * query); static gboolean gst_mms_start (GstBaseSrc * bsrc); static gboolean gst_mms_stop (GstBaseSrc * bsrc); static gboolean gst_mms_is_seekable (GstBaseSrc * src); static gboolean gst_mms_get_size (GstBaseSrc * src, guint64 * size); static gboolean gst_mms_prepare_seek_segment (GstBaseSrc * src, GstEvent * event, GstSegment * segment); static gboolean gst_mms_do_seek (GstBaseSrc * src, GstSegment * segment); static GstFlowReturn gst_mms_create (GstPushSrc * psrc, GstBuffer ** buf); static void gst_mms_urihandler_init (GType mms_type) { static const GInterfaceInfo urihandler_info = { gst_mms_uri_handler_init, NULL, NULL }; g_type_add_interface_static (mms_type, GST_TYPE_URI_HANDLER, &urihandler_info); } GST_BOILERPLATE_FULL (GstMMS, gst_mms, GstPushSrc, GST_TYPE_PUSH_SRC, gst_mms_urihandler_init); static void gst_mms_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 (&src_factory)); gst_element_class_set_details (element_class, &plugin_details); GST_DEBUG_CATEGORY_INIT (mmssrc_debug, "mmssrc", 0, "MMS Source Element"); } /* initialize the plugin's class */ static void gst_mms_class_init (GstMMSClass * klass) { GObjectClass *gobject_class; GstBaseSrcClass *gstbasesrc_class; GstPushSrcClass *gstpushsrc_class; gobject_class = (GObjectClass *) klass; gstbasesrc_class = (GstBaseSrcClass *) klass; gstpushsrc_class = (GstPushSrcClass *) klass; gobject_class->set_property = gst_mms_set_property; gobject_class->get_property = gst_mms_get_property; gobject_class->finalize = gst_mms_finalize; g_object_class_install_property (gobject_class, ARG_LOCATION, g_param_spec_string ("location", "location", "Host URL to connect to. Accepted are mms://, mmsu://, mmst:// URL types", NULL, G_PARAM_READWRITE)); g_object_class_install_property (gobject_class, ARG_CONNECTION_SPEED, g_param_spec_uint ("connection-speed", "Connection Speed", "Network connection speed in kbps (0 = unknown)", 0, G_MAXINT / 1000, DEFAULT_CONNECTION_SPEED, G_PARAM_READWRITE)); /* Note: connection-speed is intentionaly limited to G_MAXINT as libmms use int for it */ gstbasesrc_class->start = GST_DEBUG_FUNCPTR (gst_mms_start); gstbasesrc_class->stop = GST_DEBUG_FUNCPTR (gst_mms_stop); gstpushsrc_class->create = GST_DEBUG_FUNCPTR (gst_mms_create); gstbasesrc_class->is_seekable = GST_DEBUG_FUNCPTR (gst_mms_is_seekable); gstbasesrc_class->get_size = GST_DEBUG_FUNCPTR (gst_mms_get_size); gstbasesrc_class->prepare_seek_segment = GST_DEBUG_FUNCPTR (gst_mms_prepare_seek_segment); gstbasesrc_class->do_seek = GST_DEBUG_FUNCPTR (gst_mms_do_seek); gstbasesrc_class->query = GST_DEBUG_FUNCPTR (gst_mms_query); } /* initialize the new element * instantiate pads and add them to element * set functions * initialize structure */ static void gst_mms_init (GstMMS * mmssrc, GstMMSClass * g_class) { mmssrc->uri_name = NULL; mmssrc->current_connection_uri_name = NULL; mmssrc->connection = NULL; mmssrc->connection_speed = DEFAULT_CONNECTION_SPEED; } static void gst_mms_finalize (GObject * gobject) { GstMMS *mmssrc = GST_MMS (gobject); /* We may still have a connection open, as we preserve unused / pristine open connections in stop to reuse them in start. */ if (mmssrc->connection) { mmsx_close (mmssrc->connection); mmssrc->connection = NULL; } if (mmssrc->current_connection_uri_name) { g_free (mmssrc->current_connection_uri_name); mmssrc->current_connection_uri_name = NULL; } if (mmssrc->uri_name) { g_free (mmssrc->uri_name); mmssrc->uri_name = NULL; } if (G_OBJECT_CLASS (parent_class)->finalize) G_OBJECT_CLASS (parent_class)->finalize (gobject); } /* FIXME operating in TIME rather than BYTES could remove this altogether * and be more convenient elsewhere */ static gboolean gst_mms_query (GstBaseSrc * src, GstQuery * query) { GstMMS *mmssrc = GST_MMS (src); gboolean res = TRUE; GstFormat format; gint64 value; switch (GST_QUERY_TYPE (query)) { case GST_QUERY_POSITION: gst_query_parse_position (query, &format, &value); if (format != GST_FORMAT_BYTES) { res = FALSE; break; } value = (gint64) mmsx_get_current_pos (mmssrc->connection); gst_query_set_position (query, format, value); break; case GST_QUERY_DURATION: if (!mmsx_get_seekable (mmssrc->connection)) { res = FALSE; break; } gst_query_parse_duration (query, &format, &value); switch (format) { case GST_FORMAT_BYTES: value = (gint64) mmsx_get_length (mmssrc->connection); gst_query_set_duration (query, format, value); break; case GST_FORMAT_TIME: value = mmsx_get_time_length (mmssrc->connection) * GST_SECOND; gst_query_set_duration (query, format, value); break; default: res = FALSE; } break; default: /* chain to parent */ res = GST_BASE_SRC_CLASS (parent_class)->query (GST_BASE_SRC (src), query); break; } return res; } static gboolean gst_mms_prepare_seek_segment (GstBaseSrc * src, GstEvent * event, GstSegment * segment) { GstSeekType cur_type, stop_type; gint64 cur, stop; GstSeekFlags flags; GstFormat seek_format; gdouble rate; gst_event_parse_seek (event, &rate, &seek_format, &flags, &cur_type, &cur, &stop_type, &stop); if (seek_format != GST_FORMAT_BYTES && seek_format != GST_FORMAT_TIME) { GST_LOG_OBJECT (src, "Only byte or time seeking is supported"); return FALSE; } if (stop_type != GST_SEEK_TYPE_NONE) { GST_LOG_OBJECT (src, "Stop seeking not supported"); return FALSE; } if (cur_type != GST_SEEK_TYPE_NONE && cur_type != GST_SEEK_TYPE_SET) { GST_LOG_OBJECT (src, "Only absolute seeking is supported"); return FALSE; } /* We would like to convert from GST_FORMAT_TIME to GST_FORMAT_BYTES here when needed, but we cannot as to do that we need to actually do the seek, so we handle this in do_seek instead. */ /* FIXME implement relative seeking, we could do any needed relevant seeking calculations here (in seek_format metrics), before the re-init of the segment. */ gst_segment_init (segment, seek_format); gst_segment_set_seek (segment, rate, seek_format, flags, cur_type, cur, stop_type, stop, NULL); return TRUE; } static gboolean gst_mms_do_seek (GstBaseSrc * src, GstSegment * segment) { mms_off_t start; GstMMS *mmssrc = GST_MMS (src); if (segment->format == GST_FORMAT_TIME) { if (!mmsx_time_seek (NULL, mmssrc->connection, (double) segment->start / GST_SECOND)) { GST_LOG_OBJECT (mmssrc, "mmsx_time_seek() failed"); return FALSE; } start = mmsx_get_current_pos (mmssrc->connection); GST_LOG_OBJECT (mmssrc, "sought to %f sec, offset after seek: %lld\n", (double) segment->start / GST_SECOND, start); } else if (segment->format == GST_FORMAT_BYTES) { start = mmsx_seek (NULL, mmssrc->connection, segment->start, SEEK_SET); /* mmsx_seek will close and reopen the connection when seeking with the mmsh protocol, if the reopening fails this is indicated with -1 */ if (start == -1) { GST_DEBUG_OBJECT (mmssrc, "connection broken during seek"); return FALSE; } GST_DEBUG_OBJECT (mmssrc, "sought to: %llu bytes, result: %lld", segment->start, start); } else { GST_DEBUG_OBJECT (mmssrc, "unsupported seek segment format: %d", (int) segment->format); return FALSE; } gst_segment_init (segment, GST_FORMAT_BYTES); gst_segment_set_seek (segment, segment->rate, GST_FORMAT_BYTES, segment->flags, GST_SEEK_TYPE_SET, start, GST_SEEK_TYPE_NONE, segment->stop, NULL); return TRUE; } /* get function * this function generates new data when needed */ static GstFlowReturn gst_mms_create (GstPushSrc * psrc, GstBuffer ** buf) { GstMMS *mmssrc; guint8 *data; guint blocksize; gint result; mms_off_t offset; *buf = NULL; mmssrc = GST_MMS (psrc); offset = mmsx_get_current_pos (mmssrc->connection); /* Check if a seek perhaps has wrecked our connection */ if (offset == -1) { GST_DEBUG_OBJECT (mmssrc, "connection broken (probably an error during mmsx_seek_time during a convert query) returning FLOW_ERROR"); return GST_FLOW_ERROR; } /* Choose blocksize best for optimum performance */ if (offset == 0) blocksize = mmsx_get_asf_header_len (mmssrc->connection); else blocksize = mmsx_get_asf_packet_len (mmssrc->connection); *buf = gst_buffer_new_and_alloc (blocksize); data = GST_BUFFER_DATA (*buf); GST_BUFFER_SIZE (*buf) = 0; GST_LOG_OBJECT (mmssrc, "reading %d bytes", blocksize); result = mmsx_read (NULL, mmssrc->connection, (char *) data, blocksize); /* EOS? */ if (result == 0) goto eos; GST_BUFFER_OFFSET (*buf) = offset; GST_BUFFER_SIZE (*buf) = result; GST_LOG_OBJECT (mmssrc, "Returning buffer with offset %" G_GINT64_FORMAT " and size %u", GST_BUFFER_OFFSET (*buf), GST_BUFFER_SIZE (*buf)); gst_buffer_set_caps (*buf, GST_PAD_CAPS (GST_BASE_SRC_PAD (mmssrc))); return GST_FLOW_OK; eos: { GST_DEBUG_OBJECT (mmssrc, "EOS"); gst_buffer_unref (*buf); *buf = NULL; return GST_FLOW_UNEXPECTED; } } static gboolean gst_mms_is_seekable (GstBaseSrc * src) { GstMMS *mmssrc = GST_MMS (src); return mmsx_get_seekable (mmssrc->connection); } static gboolean gst_mms_get_size (GstBaseSrc * src, guint64 * size) { GstMMS *mmssrc = GST_MMS (src); /* non seekable usually means live streams, and get_length() returns, erm, interesting values for live streams */ if (!mmsx_get_seekable (mmssrc->connection)) return FALSE; *size = mmsx_get_length (mmssrc->connection); return TRUE; } static gboolean gst_mms_start (GstBaseSrc * bsrc) { GstMMS *mms; guint bandwidth_avail; mms = GST_MMS (bsrc); if (!mms->uri_name || *mms->uri_name == '\0') goto no_uri; if (mms->connection_speed) bandwidth_avail = mms->connection_speed; else bandwidth_avail = G_MAXINT; /* If we already have a connection, and the uri isn't changed, reuse it, as connecting is expensive. */ if (mms->connection) { if (!strcmp (mms->uri_name, mms->current_connection_uri_name)) { GST_DEBUG_OBJECT (mms, "Reusing existing connection for %s", mms->uri_name); return TRUE; } else { mmsx_close (mms->connection); g_free (mms->current_connection_uri_name); mms->current_connection_uri_name = NULL; } } /* FIXME: pass some sane arguments here */ GST_DEBUG_OBJECT (mms, "Trying mms_connect (%s) with bandwidth constraint of %d bps", mms->uri_name, bandwidth_avail); mms->connection = mmsx_connect (NULL, NULL, mms->uri_name, bandwidth_avail); if (mms->connection) { /* Save the uri name so that it can be checked for connection reusing, see above. */ mms->current_connection_uri_name = g_strdup (mms->uri_name); GST_DEBUG_OBJECT (mms, "Connect successful"); return TRUE; } else { gchar *url, *location; GST_ERROR_OBJECT (mms, "Could not connect to this stream, redirecting to rtsp"); location = gst_uri_get_location (mms->uri_name); url = g_strdup_printf ("rtsp://%s", location); g_free (location); gst_element_post_message (GST_ELEMENT_CAST (mms), gst_message_new_element (GST_OBJECT_CAST (mms), gst_structure_new ("redirect", "new-location", G_TYPE_STRING, url, NULL))); return FALSE; } no_uri: { GST_ELEMENT_ERROR (mms, RESOURCE, OPEN_READ, ("No URI to open specified"), (NULL)); return FALSE; } } static gboolean gst_mms_stop (GstBaseSrc * bsrc) { GstMMS *mms; mms = GST_MMS (bsrc); if (mms->connection != NULL) { /* Check if the connection is still pristine, that is if no more then just the mmslib cached asf header has been read. If it is still pristine preserve it as we often are re-started with the same URL and connecting is expensive */ if (mmsx_get_current_pos (mms->connection) > mmsx_get_asf_header_len (mms->connection)) { mmsx_close (mms->connection); mms->connection = NULL; g_free (mms->current_connection_uri_name); mms->current_connection_uri_name = NULL; } } return TRUE; } static void gst_mms_set_property (GObject * object, guint prop_id, const GValue * value, GParamSpec * pspec) { GstMMS *mmssrc; mmssrc = GST_MMS (object); GST_OBJECT_LOCK (mmssrc); switch (prop_id) { case ARG_LOCATION: if (mmssrc->uri_name) { g_free (mmssrc->uri_name); mmssrc->uri_name = NULL; } mmssrc->uri_name = g_value_dup_string (value); break; case ARG_CONNECTION_SPEED: mmssrc->connection_speed = g_value_get_uint (value) * 1000; break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } GST_OBJECT_UNLOCK (mmssrc); } static void gst_mms_get_property (GObject * object, guint prop_id, GValue * value, GParamSpec * pspec) { GstMMS *mmssrc; mmssrc = GST_MMS (object); GST_OBJECT_LOCK (mmssrc); switch (prop_id) { case ARG_LOCATION: if (mmssrc->uri_name) g_value_set_string (value, mmssrc->uri_name); break; case ARG_CONNECTION_SPEED: g_value_set_uint (value, mmssrc->connection_speed / 1000); break; default: G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); break; } GST_OBJECT_UNLOCK (mmssrc); } /* entry point to initialize the plug-in * initialize the plug-in itself * register the element factories and pad templates * register the features */ static gboolean plugin_init (GstPlugin * plugin) { return gst_element_register (plugin, "mmssrc", GST_RANK_NONE, GST_TYPE_MMS); } static GstURIType gst_mms_uri_get_type (void) { return GST_URI_SRC; } static gchar ** gst_mms_uri_get_protocols (void) { static gchar *protocols[] = { "mms", "mmsh", "mmst", "mmsu", NULL }; return protocols; } static const gchar * gst_mms_uri_get_uri (GstURIHandler * handler) { GstMMS *src = GST_MMS (handler); return src->uri_name; } static gboolean gst_mms_uri_set_uri (GstURIHandler * handler, const gchar * uri) { gchar *protocol; GstMMS *src = GST_MMS (handler); protocol = gst_uri_get_protocol (uri); if ((strcmp (protocol, "mms") != 0) && (strcmp (protocol, "mmsh") != 0)) { g_free (protocol); return FALSE; } g_free (protocol); g_object_set (src, "location", uri, NULL); return TRUE; } static void gst_mms_uri_handler_init (gpointer g_iface, gpointer iface_data) { GstURIHandlerInterface *iface = (GstURIHandlerInterface *) g_iface; iface->get_type = gst_mms_uri_get_type; iface->get_protocols = gst_mms_uri_get_protocols; iface->get_uri = gst_mms_uri_get_uri; iface->set_uri = gst_mms_uri_set_uri; } /* this is the structure that gst-register looks for * so keep the name plugin_desc, or you cannot get your plug-in registered */ GST_PLUGIN_DEFINE (GST_VERSION_MAJOR, GST_VERSION_MINOR, "mms", "Microsoft Multi Media Server streaming protocol support", plugin_init, VERSION, GST_LICENSE, GST_PACKAGE_NAME, GST_PACKAGE_ORIGIN)