| /* | |
| * Copyright (C) 2013 Sebastian Dröge <slomo@circular-chaos.org> | |
| * | |
| * 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. | |
| */ | |
| #ifdef HAVE_CONFIG_H | |
| #include "config.h" | |
| #endif | |
| #include <string.h> | |
| #include <gst/gst.h> | |
| #include <gio/gio.h> | |
| typedef struct | |
| { | |
| gchar *name; | |
| GSocketConnection *connection; | |
| GSocket *socket; | |
| GInputStream *istream; | |
| GOutputStream *ostream; | |
| GSource *isource, *tosource; | |
| GByteArray *current_message; | |
| gchar *http_version; | |
| gboolean waiting_200_ok; | |
| } Client; | |
| static const char *known_mimetypes[] = { | |
| "video/webm", | |
| "multipart/x-mixed-replace", | |
| NULL | |
| }; | |
| static GMainLoop *loop = NULL; | |
| G_LOCK_DEFINE_STATIC (clients); | |
| static GList *clients = NULL; | |
| static GstElement *pipeline = NULL; | |
| static GstElement *multisocketsink = NULL; | |
| static gboolean started = FALSE; | |
| static gchar *content_type; | |
| G_LOCK_DEFINE_STATIC (caps); | |
| static gboolean caps_resolved = FALSE; | |
| static void | |
| remove_client (Client * client) | |
| { | |
| gst_print ("Removing connection %s\n", client->name); | |
| G_LOCK (clients); | |
| clients = g_list_remove (clients, client); | |
| G_UNLOCK (clients); | |
| g_free (client->name); | |
| g_free (client->http_version); | |
| if (client->isource) { | |
| g_source_destroy (client->isource); | |
| g_source_unref (client->isource); | |
| } | |
| if (client->tosource) { | |
| g_source_destroy (client->tosource); | |
| g_source_unref (client->tosource); | |
| } | |
| g_object_unref (client->connection); | |
| g_byte_array_unref (client->current_message); | |
| g_free (client); | |
| } | |
| static void | |
| write_bytes (Client * client, const gchar * data, guint len) | |
| { | |
| gssize w; | |
| GError *err = NULL; | |
| /* TODO: We assume this never blocks */ | |
| do { | |
| w = g_output_stream_write (client->ostream, data, len, NULL, &err); | |
| if (w > 0) { | |
| len -= w; | |
| data += w; | |
| } | |
| } while (w > 0 && len > 0); | |
| if (w <= 0) { | |
| if (err) { | |
| gst_print ("Write error %s\n", err->message); | |
| g_clear_error (&err); | |
| } | |
| remove_client (client); | |
| } | |
| } | |
| static void | |
| send_response_200_ok (Client * client) | |
| { | |
| gchar *response; | |
| response = g_strdup_printf ("%s 200 OK\r\n%s\r\n", client->http_version, | |
| content_type); | |
| write_bytes (client, response, strlen (response)); | |
| g_free (response); | |
| } | |
| static void | |
| send_response_404_not_found (Client * client) | |
| { | |
| gchar *response; | |
| response = g_strdup_printf ("%s 404 Not Found\r\n\r\n", client->http_version); | |
| write_bytes (client, response, strlen (response)); | |
| g_free (response); | |
| } | |
| static void | |
| client_message (Client * client, const gchar * data, guint len) | |
| { | |
| gboolean http_head_request = FALSE; | |
| gboolean http_get_request = FALSE; | |
| gchar **lines = g_strsplit_set (data, "\r\n", -1); | |
| if (g_str_has_prefix (lines[0], "HEAD")) | |
| http_head_request = TRUE; | |
| else if (g_str_has_prefix (lines[0], "GET")) | |
| http_get_request = TRUE; | |
| if (http_head_request || http_get_request) { | |
| gchar **parts = g_strsplit (lines[0], " ", -1); | |
| gboolean ok = FALSE; | |
| g_free (client->http_version); | |
| if (parts[1] && parts[2] && *parts[2] != '\0') | |
| client->http_version = g_strdup (parts[2]); | |
| else | |
| client->http_version = g_strdup ("HTTP/1.0"); | |
| if (parts[1] && strcmp (parts[1], "/") == 0) { | |
| G_LOCK (caps); | |
| if (caps_resolved) | |
| send_response_200_ok (client); | |
| else | |
| client->waiting_200_ok = TRUE; | |
| G_UNLOCK (caps); | |
| ok = TRUE; | |
| } else { | |
| send_response_404_not_found (client); | |
| } | |
| g_strfreev (parts); | |
| if (ok) { | |
| if (http_get_request) { | |
| /* Start streaming to client socket */ | |
| g_source_destroy (client->isource); | |
| g_source_unref (client->isource); | |
| client->isource = NULL; | |
| g_source_destroy (client->tosource); | |
| g_source_unref (client->tosource); | |
| client->tosource = NULL; | |
| gst_print ("Starting to stream to %s\n", client->name); | |
| g_signal_emit_by_name (multisocketsink, "add", client->socket); | |
| } | |
| if (!started) { | |
| gst_print ("Starting pipeline\n"); | |
| if (gst_element_set_state (pipeline, | |
| GST_STATE_PLAYING) == GST_STATE_CHANGE_FAILURE) { | |
| gst_print ("Failed to start pipeline\n"); | |
| g_main_loop_quit (loop); | |
| } | |
| started = TRUE; | |
| } | |
| } | |
| } else { | |
| gchar **parts = g_strsplit (lines[0], " ", -1); | |
| gchar *response; | |
| const gchar *http_version; | |
| if (parts[1] && parts[2] && *parts[2] != '\0') | |
| http_version = parts[2]; | |
| else | |
| http_version = "HTTP/1.0"; | |
| response = g_strdup_printf ("%s 400 Bad Request\r\n\r\n", http_version); | |
| write_bytes (client, response, strlen (response)); | |
| g_free (response); | |
| g_strfreev (parts); | |
| remove_client (client); | |
| } | |
| g_strfreev (lines); | |
| } | |
| static gboolean | |
| on_timeout (Client * client) | |
| { | |
| gst_print ("Timeout\n"); | |
| remove_client (client); | |
| return FALSE; | |
| } | |
| static gboolean | |
| on_read_bytes (GPollableInputStream * stream, Client * client) | |
| { | |
| gssize r; | |
| gchar data[4096]; | |
| GError *err = NULL; | |
| do { | |
| r = g_pollable_input_stream_read_nonblocking (G_POLLABLE_INPUT_STREAM | |
| (client->istream), data, sizeof (data), NULL, &err); | |
| if (r > 0) | |
| g_byte_array_append (client->current_message, (guint8 *) data, r); | |
| } while (r > 0); | |
| if (r == 0) { | |
| remove_client (client); | |
| return FALSE; | |
| } else if (g_error_matches (err, G_IO_ERROR, G_IO_ERROR_WOULD_BLOCK)) { | |
| guint8 *tmp = client->current_message->data; | |
| guint tmp_len = client->current_message->len; | |
| g_clear_error (&err); | |
| while (tmp_len > 3) { | |
| if (tmp[0] == 0x0d && tmp[1] == 0x0a && tmp[2] == 0x0d && tmp[3] == 0x0a) { | |
| guint len; | |
| g_byte_array_append (client->current_message, (const guint8 *) "\0", 1); | |
| len = tmp - client->current_message->data + 5; | |
| client_message (client, (gchar *) client->current_message->data, len); | |
| g_byte_array_remove_range (client->current_message, 0, len); | |
| tmp = client->current_message->data; | |
| tmp_len = client->current_message->len; | |
| } else { | |
| tmp++; | |
| tmp_len--; | |
| } | |
| } | |
| if (client->current_message->len >= 1024 * 1024) { | |
| gst_print ("No complete request after 1MB of data\n"); | |
| remove_client (client); | |
| return FALSE; | |
| } | |
| return TRUE; | |
| } else { | |
| gst_print ("Read error %s\n", err->message); | |
| g_clear_error (&err); | |
| remove_client (client); | |
| return FALSE; | |
| } | |
| return FALSE; | |
| } | |
| static gboolean | |
| on_new_connection (GSocketService * service, GSocketConnection * connection, | |
| GObject * source_object, gpointer user_data) | |
| { | |
| Client *client = g_new0 (Client, 1); | |
| GSocketAddress *addr; | |
| GInetAddress *iaddr; | |
| gchar *ip; | |
| guint16 port; | |
| addr = g_socket_connection_get_remote_address (connection, NULL); | |
| iaddr = g_inet_socket_address_get_address (G_INET_SOCKET_ADDRESS (addr)); | |
| port = g_inet_socket_address_get_port (G_INET_SOCKET_ADDRESS (addr)); | |
| ip = g_inet_address_to_string (iaddr); | |
| client->name = g_strdup_printf ("%s:%u", ip, port); | |
| g_free (ip); | |
| g_object_unref (addr); | |
| gst_print ("New connection %s\n", client->name); | |
| client->waiting_200_ok = FALSE; | |
| client->http_version = g_strdup (""); | |
| client->connection = g_object_ref (connection); | |
| client->socket = g_socket_connection_get_socket (connection); | |
| client->istream = | |
| g_io_stream_get_input_stream (G_IO_STREAM (client->connection)); | |
| client->ostream = | |
| g_io_stream_get_output_stream (G_IO_STREAM (client->connection)); | |
| client->current_message = g_byte_array_sized_new (1024); | |
| client->tosource = g_timeout_source_new_seconds (5); | |
| g_source_set_callback (client->tosource, (GSourceFunc) on_timeout, client, | |
| NULL); | |
| g_source_attach (client->tosource, NULL); | |
| client->isource = | |
| g_pollable_input_stream_create_source (G_POLLABLE_INPUT_STREAM | |
| (client->istream), NULL); | |
| g_source_set_callback (client->isource, (GSourceFunc) on_read_bytes, client, | |
| NULL); | |
| g_source_attach (client->isource, NULL); | |
| G_LOCK (clients); | |
| clients = g_list_prepend (clients, client); | |
| G_UNLOCK (clients); | |
| return TRUE; | |
| } | |
| static gboolean | |
| on_message (GstBus * bus, GstMessage * message, gpointer user_data) | |
| { | |
| switch (GST_MESSAGE_TYPE (message)) { | |
| case GST_MESSAGE_ERROR:{ | |
| gchar *debug; | |
| GError *err; | |
| gst_message_parse_error (message, &err, &debug); | |
| gst_print ("Error %s\n", err->message); | |
| g_error_free (err); | |
| g_free (debug); | |
| g_main_loop_quit (loop); | |
| break; | |
| } | |
| case GST_MESSAGE_WARNING:{ | |
| gchar *debug; | |
| GError *err; | |
| gst_message_parse_warning (message, &err, &debug); | |
| gst_print ("Warning %s\n", err->message); | |
| g_error_free (err); | |
| g_free (debug); | |
| break; | |
| } | |
| case GST_MESSAGE_EOS:{ | |
| gst_print ("EOS\n"); | |
| g_main_loop_quit (loop); | |
| } | |
| default: | |
| break; | |
| } | |
| return TRUE; | |
| } | |
| static void | |
| on_client_socket_removed (GstElement * element, GSocket * socket, | |
| gpointer user_data) | |
| { | |
| GList *l; | |
| Client *client = NULL; | |
| G_LOCK (clients); | |
| for (l = clients; l; l = l->next) { | |
| Client *tmp = l->data; | |
| if (socket == tmp->socket) { | |
| client = tmp; | |
| break; | |
| } | |
| } | |
| G_UNLOCK (clients); | |
| if (client) | |
| remove_client (client); | |
| } | |
| static void | |
| on_stream_caps_changed (GObject * obj, GParamSpec * pspec, gpointer user_data) | |
| { | |
| GstPad *src_pad; | |
| GstCaps *src_caps; | |
| GstStructure *gstrc; | |
| GList *l; | |
| src_pad = (GstPad *) obj; | |
| src_caps = gst_pad_get_current_caps (src_pad); | |
| gstrc = gst_caps_get_structure (src_caps, 0); | |
| /* | |
| * Include a Content-type header in the case we know the mime | |
| * type is OK in HTTP. Required for MJPEG streams. | |
| */ | |
| int i = 0; | |
| const gchar *mimetype = gst_structure_get_name (gstrc); | |
| while (known_mimetypes[i] != NULL) { | |
| if (strcmp (mimetype, known_mimetypes[i]) == 0) { | |
| if (content_type) | |
| g_free (content_type); | |
| /* Handle the (maybe not so) especial case of multipart to add boundary */ | |
| if (strcmp (mimetype, "multipart/x-mixed-replace") == 0 && | |
| gst_structure_has_field_typed (gstrc, "boundary", G_TYPE_STRING)) { | |
| const gchar *boundary = gst_structure_get_string (gstrc, "boundary"); | |
| content_type = g_strdup_printf ("Content-Type: " | |
| "multipart/x-mixed-replace;boundary=--%s\r\n", boundary); | |
| } else { | |
| content_type = g_strdup_printf ("Content-Type: %s\r\n", mimetype); | |
| } | |
| gst_print ("%s", content_type); | |
| break; | |
| } | |
| i++; | |
| } | |
| gst_caps_unref (src_caps); | |
| /* Send 200 OK to those clients waiting for it */ | |
| G_LOCK (caps); | |
| caps_resolved = TRUE; | |
| G_LOCK (clients); | |
| for (l = clients; l; l = l->next) { | |
| Client *cl = l->data; | |
| if (cl->waiting_200_ok) { | |
| send_response_200_ok (cl); | |
| cl->waiting_200_ok = FALSE; | |
| break; | |
| } | |
| } | |
| G_UNLOCK (clients); | |
| G_UNLOCK (caps); | |
| } | |
| int | |
| main (gint argc, gchar ** argv) | |
| { | |
| GSocketService *service; | |
| GstElement *bin, *stream; | |
| GstPad *srcpad, *ghostpad, *sinkpad; | |
| GError *err = NULL; | |
| GstBus *bus; | |
| gst_init (&argc, &argv); | |
| if (argc < 4) { | |
| gst_print ("usage: %s PORT <launch line>\n" | |
| "example: %s 8080 ( videotestsrc ! theoraenc ! oggmux name=stream )\n", | |
| argv[0], argv[0]); | |
| return -1; | |
| } | |
| const gchar *port_str = argv[1]; | |
| const int port = (int) g_ascii_strtoll (port_str, NULL, 10); | |
| bin = gst_parse_launchv ((const gchar **) argv + 2, &err); | |
| if (!bin) { | |
| gst_print ("invalid pipeline: %s\n", err->message); | |
| g_clear_error (&err); | |
| return -2; | |
| } | |
| stream = gst_bin_get_by_name (GST_BIN (bin), "stream"); | |
| if (!stream) { | |
| gst_print ("no element with name \"stream\" found\n"); | |
| gst_object_unref (bin); | |
| return -3; | |
| } | |
| srcpad = gst_element_get_static_pad (stream, "src"); | |
| if (!srcpad) { | |
| gst_print ("no \"src\" pad in element \"stream\" found\n"); | |
| gst_object_unref (stream); | |
| gst_object_unref (bin); | |
| return -4; | |
| } | |
| content_type = g_strdup (""); | |
| g_signal_connect (srcpad, "notify::caps", | |
| G_CALLBACK (on_stream_caps_changed), NULL); | |
| ghostpad = gst_ghost_pad_new ("src", srcpad); | |
| gst_element_add_pad (GST_ELEMENT (bin), ghostpad); | |
| gst_object_unref (srcpad); | |
| pipeline = gst_pipeline_new (NULL); | |
| multisocketsink = gst_element_factory_make ("multisocketsink", NULL); | |
| g_object_set (multisocketsink, | |
| "unit-format", GST_FORMAT_TIME, | |
| "units-max", (gint64) 7 * GST_SECOND, | |
| "units-soft-max", (gint64) 3 * GST_SECOND, | |
| "recover-policy", 3 /* keyframe */ , | |
| "timeout", (guint64) 10 * GST_SECOND, | |
| "sync-method", 1 /* next-keyframe */ , | |
| NULL); | |
| gst_bin_add_many (GST_BIN (pipeline), bin, multisocketsink, NULL); | |
| sinkpad = gst_element_get_static_pad (multisocketsink, "sink"); | |
| gst_pad_link (ghostpad, sinkpad); | |
| gst_object_unref (sinkpad); | |
| bus = gst_element_get_bus (pipeline); | |
| gst_bus_add_signal_watch (bus); | |
| g_signal_connect (bus, "message", G_CALLBACK (on_message), NULL); | |
| gst_object_unref (bus); | |
| g_signal_connect (multisocketsink, "client-socket-removed", | |
| G_CALLBACK (on_client_socket_removed), NULL); | |
| loop = g_main_loop_new (NULL, FALSE); | |
| if (gst_element_set_state (pipeline, | |
| GST_STATE_READY) == GST_STATE_CHANGE_FAILURE) { | |
| gst_object_unref (pipeline); | |
| g_main_loop_unref (loop); | |
| gst_print ("Failed to set pipeline to ready\n"); | |
| return -5; | |
| } | |
| service = g_socket_service_new (); | |
| g_socket_listener_add_inet_port (G_SOCKET_LISTENER (service), port, NULL, | |
| NULL); | |
| g_signal_connect (service, "incoming", G_CALLBACK (on_new_connection), NULL); | |
| g_socket_service_start (service); | |
| gst_print ("Listening on http://127.0.0.1:%d/\n", port); | |
| g_main_loop_run (loop); | |
| g_socket_service_stop (service); | |
| g_object_unref (service); | |
| gst_element_set_state (pipeline, GST_STATE_NULL); | |
| gst_object_unref (pipeline); | |
| g_main_loop_unref (loop); | |
| return 0; | |
| } |