• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 #include <locale.h>
2 #include <glib.h>
3 #include <glib-unix.h>
4 #include <gst/gst.h>
5 #include <gst/sdp/sdp.h>
6 
7 #define GST_USE_UNSTABLE_API
8 #include <gst/webrtc/webrtc.h>
9 
10 #include <libsoup/soup.h>
11 #include <json-glib/json-glib.h>
12 #include <string.h>
13 
14 
15 
16 #define RTP_PAYLOAD_TYPE "96"
17 #define SOUP_HTTP_PORT 57778
18 #define STUN_SERVER "stun.l.google.com:19302"
19 
20 
21 
22 typedef struct _ReceiverEntry ReceiverEntry;
23 
24 ReceiverEntry *create_receiver_entry (SoupWebsocketConnection * connection);
25 void destroy_receiver_entry (gpointer receiver_entry_ptr);
26 
27 GstPadProbeReturn payloader_caps_event_probe_cb (GstPad * pad,
28     GstPadProbeInfo * info, gpointer user_data);
29 
30 void on_offer_created_cb (GstPromise * promise, gpointer user_data);
31 void on_negotiation_needed_cb (GstElement * webrtcbin, gpointer user_data);
32 void on_ice_candidate_cb (GstElement * webrtcbin, guint mline_index,
33     gchar * candidate, gpointer user_data);
34 
35 void soup_websocket_message_cb (SoupWebsocketConnection * connection,
36     SoupWebsocketDataType data_type, GBytes * message, gpointer user_data);
37 void soup_websocket_closed_cb (SoupWebsocketConnection * connection,
38     gpointer user_data);
39 
40 void soup_http_handler (SoupServer * soup_server, SoupMessage * message,
41     const char *path, GHashTable * query, SoupClientContext * client_context,
42     gpointer user_data);
43 void soup_websocket_handler (G_GNUC_UNUSED SoupServer * server,
44     SoupWebsocketConnection * connection, const char *path,
45     SoupClientContext * client_context, gpointer user_data);
46 
47 static gchar *get_string_from_json_object (JsonObject * object);
48 
49 gboolean exit_sighandler (gpointer user_data);
50 
51 
52 
53 
54 struct _ReceiverEntry
55 {
56   SoupWebsocketConnection *connection;
57 
58   GstElement *pipeline;
59   GstElement *webrtcbin;
60 };
61 
62 
63 
64 const gchar *html_source = " \n \
65 <html> \n \
66   <head> \n \
67     <script type=\"text/javascript\" src=\"https://webrtc.github.io/adapter/adapter-latest.js\"></script> \n \
68     <script type=\"text/javascript\"> \n \
69       var html5VideoElement; \n \
70       var websocketConnection; \n \
71       var webrtcPeerConnection; \n \
72       var webrtcConfiguration; \n \
73       var reportError; \n \
74  \n \
75  \n \
76       function onLocalDescription(desc) { \n \
77         console.log(\"Local description: \" + JSON.stringify(desc)); \n \
78         webrtcPeerConnection.setLocalDescription(desc).then(function() { \n \
79           websocketConnection.send(JSON.stringify({ type: \"sdp\", \"data\": webrtcPeerConnection.localDescription })); \n \
80         }).catch(reportError); \n \
81       } \n \
82  \n \
83  \n \
84       function onIncomingSDP(sdp) { \n \
85         console.log(\"Incoming SDP: \" + JSON.stringify(sdp)); \n \
86         webrtcPeerConnection.setRemoteDescription(sdp).catch(reportError); \n \
87         webrtcPeerConnection.createAnswer().then(onLocalDescription).catch(reportError); \n \
88       } \n \
89  \n \
90  \n \
91       function onIncomingICE(ice) { \n \
92         var candidate = new RTCIceCandidate(ice); \n \
93         console.log(\"Incoming ICE: \" + JSON.stringify(ice)); \n \
94         webrtcPeerConnection.addIceCandidate(candidate).catch(reportError); \n \
95       } \n \
96  \n \
97  \n \
98       function onAddRemoteStream(event) { \n \
99         html5VideoElement.srcObject = event.streams[0]; \n \
100       } \n \
101  \n \
102  \n \
103       function onIceCandidate(event) { \n \
104         if (event.candidate == null) \n \
105           return; \n \
106  \n \
107         console.log(\"Sending ICE candidate out: \" + JSON.stringify(event.candidate)); \n \
108         websocketConnection.send(JSON.stringify({ \"type\": \"ice\", \"data\": event.candidate })); \n \
109       } \n \
110  \n \
111  \n \
112       function onServerMessage(event) { \n \
113         var msg; \n \
114  \n \
115         try { \n \
116           msg = JSON.parse(event.data); \n \
117         } catch (e) { \n \
118           return; \n \
119         } \n \
120  \n \
121         if (!webrtcPeerConnection) { \n \
122           webrtcPeerConnection = new RTCPeerConnection(webrtcConfiguration); \n \
123           webrtcPeerConnection.ontrack = onAddRemoteStream; \n \
124           webrtcPeerConnection.onicecandidate = onIceCandidate; \n \
125         } \n \
126  \n \
127         switch (msg.type) { \n \
128           case \"sdp\": onIncomingSDP(msg.data); break; \n \
129           case \"ice\": onIncomingICE(msg.data); break; \n \
130           default: break; \n \
131         } \n \
132       } \n \
133  \n \
134  \n \
135       function playStream(videoElement, hostname, port, path, configuration, reportErrorCB) { \n \
136         var l = window.location;\n \
137         var wsHost = (hostname != undefined) ? hostname : l.hostname; \n \
138         var wsPort = (port != undefined) ? port : l.port; \n \
139         var wsPath = (path != undefined) ? path : \"ws\"; \n \
140         if (wsPort) \n\
141           wsPort = \":\" + wsPort; \n\
142         var wsUrl = \"ws://\" + wsHost + wsPort + \"/\" + wsPath; \n \
143  \n \
144         html5VideoElement = videoElement; \n \
145         webrtcConfiguration = configuration; \n \
146         reportError = (reportErrorCB != undefined) ? reportErrorCB : function(text) {}; \n \
147  \n \
148         websocketConnection = new WebSocket(wsUrl); \n \
149         websocketConnection.addEventListener(\"message\", onServerMessage); \n \
150       } \n \
151  \n \
152       window.onload = function() { \n \
153         var vidstream = document.getElementById(\"stream\"); \n \
154         var config = { 'iceServers': [{ 'urls': 'stun:" STUN_SERVER "' }] }; \n\
155         playStream(vidstream, null, null, null, config, function (errmsg) { console.error(errmsg); }); \n \
156       }; \n \
157  \n \
158     </script> \n \
159   </head> \n \
160  \n \
161   <body> \n \
162     <div> \n \
163       <video id=\"stream\" autoplay>Your browser does not support video</video> \n \
164     </div> \n \
165   </body> \n \
166 </html> \n \
167 ";
168 
169 
170 
171 
172 ReceiverEntry *
create_receiver_entry(SoupWebsocketConnection * connection)173 create_receiver_entry (SoupWebsocketConnection * connection)
174 {
175   GError *error;
176   ReceiverEntry *receiver_entry;
177 
178   receiver_entry = g_slice_alloc0 (sizeof (ReceiverEntry));
179   receiver_entry->connection = connection;
180 
181   g_object_ref (G_OBJECT (connection));
182 
183   g_signal_connect (G_OBJECT (connection), "message",
184       G_CALLBACK (soup_websocket_message_cb), (gpointer) receiver_entry);
185 
186   error = NULL;
187   receiver_entry->pipeline =
188       gst_parse_launch ("webrtcbin name=webrtcbin stun-server=stun://"
189       STUN_SERVER " "
190       "rpicamsrc bitrate=600000 annotation-mode=12 preview=false ! video/x-h264,profile=constrained-baseline,width=640,height=360,level=3.0 ! queue max-size-time=100000000 ! h264parse ! "
191       "rtph264pay config-interval=-1 name=payloader ! "
192       "application/x-rtp,media=video,encoding-name=H264,payload="
193       RTP_PAYLOAD_TYPE " ! webrtcbin. ", &error);
194   if (error != NULL) {
195     g_error ("Could not create WebRTC pipeline: %s\n", error->message);
196     g_error_free (error);
197     goto cleanup;
198   }
199 
200   receiver_entry->webrtcbin =
201       gst_bin_get_by_name (GST_BIN (receiver_entry->pipeline), "webrtcbin");
202   g_assert (receiver_entry->webrtcbin != NULL);
203 
204   g_signal_connect (receiver_entry->webrtcbin, "on-negotiation-needed",
205       G_CALLBACK (on_negotiation_needed_cb), (gpointer) receiver_entry);
206 
207   g_signal_connect (receiver_entry->webrtcbin, "on-ice-candidate",
208       G_CALLBACK (on_ice_candidate_cb), (gpointer) receiver_entry);
209 
210   gst_element_set_state (receiver_entry->pipeline, GST_STATE_PLAYING);
211 
212   return receiver_entry;
213 
214 cleanup:
215   destroy_receiver_entry ((gpointer) receiver_entry);
216   return NULL;
217 }
218 
219 void
destroy_receiver_entry(gpointer receiver_entry_ptr)220 destroy_receiver_entry (gpointer receiver_entry_ptr)
221 {
222   ReceiverEntry *receiver_entry = (ReceiverEntry *) receiver_entry_ptr;
223 
224   g_assert (receiver_entry != NULL);
225 
226   if (receiver_entry->pipeline != NULL) {
227     gst_element_set_state (GST_ELEMENT (receiver_entry->pipeline),
228         GST_STATE_NULL);
229 
230     gst_object_unref (GST_OBJECT (receiver_entry->webrtcbin));
231     gst_object_unref (GST_OBJECT (receiver_entry->pipeline));
232   }
233 
234   if (receiver_entry->connection != NULL)
235     g_object_unref (G_OBJECT (receiver_entry->connection));
236 
237   g_slice_free1 (sizeof (ReceiverEntry), receiver_entry);
238 }
239 
240 
241 void
on_offer_created_cb(GstPromise * promise,gpointer user_data)242 on_offer_created_cb (GstPromise * promise, gpointer user_data)
243 {
244   gchar *sdp_string;
245   gchar *json_string;
246   JsonObject *sdp_json;
247   JsonObject *sdp_data_json;
248   GstStructure const *reply;
249   GstPromise *local_desc_promise;
250   GstWebRTCSessionDescription *offer = NULL;
251   ReceiverEntry *receiver_entry = (ReceiverEntry *) user_data;
252 
253   reply = gst_promise_get_reply (promise);
254   gst_structure_get (reply, "offer", GST_TYPE_WEBRTC_SESSION_DESCRIPTION,
255       &offer, NULL);
256   gst_promise_unref (promise);
257 
258   local_desc_promise = gst_promise_new ();
259   g_signal_emit_by_name (receiver_entry->webrtcbin, "set-local-description",
260       offer, local_desc_promise);
261   gst_promise_interrupt (local_desc_promise);
262   gst_promise_unref (local_desc_promise);
263 
264   sdp_string = gst_sdp_message_as_text (offer->sdp);
265   g_print ("Negotiation offer created:\n%s\n", sdp_string);
266 
267   sdp_json = json_object_new ();
268   json_object_set_string_member (sdp_json, "type", "sdp");
269 
270   sdp_data_json = json_object_new ();
271   json_object_set_string_member (sdp_data_json, "type", "offer");
272   json_object_set_string_member (sdp_data_json, "sdp", sdp_string);
273   json_object_set_object_member (sdp_json, "data", sdp_data_json);
274 
275   json_string = get_string_from_json_object (sdp_json);
276   json_object_unref (sdp_json);
277 
278   soup_websocket_connection_send_text (receiver_entry->connection, json_string);
279   g_free (json_string);
280 
281   gst_webrtc_session_description_free (offer);
282 }
283 
284 
285 void
on_negotiation_needed_cb(GstElement * webrtcbin,gpointer user_data)286 on_negotiation_needed_cb (GstElement * webrtcbin, gpointer user_data)
287 {
288   GstPromise *promise;
289   ReceiverEntry *receiver_entry = (ReceiverEntry *) user_data;
290 
291   g_print ("Creating negotiation offer\n");
292 
293   promise = gst_promise_new_with_change_func (on_offer_created_cb,
294       (gpointer) receiver_entry, NULL);
295   g_signal_emit_by_name (G_OBJECT (webrtcbin), "create-offer", NULL, promise);
296 }
297 
298 
299 void
on_ice_candidate_cb(G_GNUC_UNUSED GstElement * webrtcbin,guint mline_index,gchar * candidate,gpointer user_data)300 on_ice_candidate_cb (G_GNUC_UNUSED GstElement * webrtcbin, guint mline_index,
301     gchar * candidate, gpointer user_data)
302 {
303   JsonObject *ice_json;
304   JsonObject *ice_data_json;
305   gchar *json_string;
306   ReceiverEntry *receiver_entry = (ReceiverEntry *) user_data;
307 
308   ice_json = json_object_new ();
309   json_object_set_string_member (ice_json, "type", "ice");
310 
311   ice_data_json = json_object_new ();
312   json_object_set_int_member (ice_data_json, "sdpMLineIndex", mline_index);
313   json_object_set_string_member (ice_data_json, "candidate", candidate);
314   json_object_set_object_member (ice_json, "data", ice_data_json);
315 
316   json_string = get_string_from_json_object (ice_json);
317   json_object_unref (ice_json);
318 
319   soup_websocket_connection_send_text (receiver_entry->connection, json_string);
320   g_free (json_string);
321 }
322 
323 
324 void
soup_websocket_message_cb(G_GNUC_UNUSED SoupWebsocketConnection * connection,SoupWebsocketDataType data_type,GBytes * message,gpointer user_data)325 soup_websocket_message_cb (G_GNUC_UNUSED SoupWebsocketConnection * connection,
326     SoupWebsocketDataType data_type, GBytes * message, gpointer user_data)
327 {
328   gsize size;
329   gchar *data;
330   gchar *data_string;
331   const gchar *type_string;
332   JsonNode *root_json;
333   JsonObject *root_json_object;
334   JsonObject *data_json_object;
335   JsonParser *json_parser = NULL;
336   ReceiverEntry *receiver_entry = (ReceiverEntry *) user_data;
337 
338   switch (data_type) {
339     case SOUP_WEBSOCKET_DATA_BINARY:
340       g_error ("Received unknown binary message, ignoring\n");
341       g_bytes_unref (message);
342       return;
343 
344     case SOUP_WEBSOCKET_DATA_TEXT:
345       data = g_bytes_unref_to_data (message, &size);
346       /* Convert to NULL-terminated string */
347       data_string = g_strndup (data, size);
348       g_free (data);
349       break;
350 
351     default:
352       g_assert_not_reached ();
353   }
354 
355   json_parser = json_parser_new ();
356   if (!json_parser_load_from_data (json_parser, data_string, -1, NULL))
357     goto unknown_message;
358 
359   root_json = json_parser_get_root (json_parser);
360   if (!JSON_NODE_HOLDS_OBJECT (root_json))
361     goto unknown_message;
362 
363   root_json_object = json_node_get_object (root_json);
364 
365   if (!json_object_has_member (root_json_object, "type")) {
366     g_error ("Received message without type field\n");
367     goto cleanup;
368   }
369   type_string = json_object_get_string_member (root_json_object, "type");
370 
371   if (!json_object_has_member (root_json_object, "data")) {
372     g_error ("Received message without data field\n");
373     goto cleanup;
374   }
375   data_json_object = json_object_get_object_member (root_json_object, "data");
376 
377   if (g_strcmp0 (type_string, "sdp") == 0) {
378     const gchar *sdp_type_string;
379     const gchar *sdp_string;
380     GstPromise *promise;
381     GstSDPMessage *sdp;
382     GstWebRTCSessionDescription *answer;
383     int ret;
384 
385     if (!json_object_has_member (data_json_object, "type")) {
386       g_error ("Received SDP message without type field\n");
387       goto cleanup;
388     }
389     sdp_type_string = json_object_get_string_member (data_json_object, "type");
390 
391     if (g_strcmp0 (sdp_type_string, "answer") != 0) {
392       g_error ("Expected SDP message type \"answer\", got \"%s\"\n",
393           sdp_type_string);
394       goto cleanup;
395     }
396 
397     if (!json_object_has_member (data_json_object, "sdp")) {
398       g_error ("Received SDP message without SDP string\n");
399       goto cleanup;
400     }
401     sdp_string = json_object_get_string_member (data_json_object, "sdp");
402 
403     g_print ("Received SDP:\n%s\n", sdp_string);
404 
405     ret = gst_sdp_message_new (&sdp);
406     g_assert_cmphex (ret, ==, GST_SDP_OK);
407 
408     ret =
409         gst_sdp_message_parse_buffer ((guint8 *) sdp_string,
410         strlen (sdp_string), sdp);
411     if (ret != GST_SDP_OK) {
412       g_error ("Could not parse SDP string\n");
413       goto cleanup;
414     }
415 
416     answer = gst_webrtc_session_description_new (GST_WEBRTC_SDP_TYPE_ANSWER,
417         sdp);
418     g_assert_nonnull (answer);
419 
420     promise = gst_promise_new ();
421     g_signal_emit_by_name (receiver_entry->webrtcbin, "set-remote-description",
422         answer, promise);
423     gst_promise_interrupt (promise);
424     gst_promise_unref (promise);
425   } else if (g_strcmp0 (type_string, "ice") == 0) {
426     guint mline_index;
427     const gchar *candidate_string;
428 
429     if (!json_object_has_member (data_json_object, "sdpMLineIndex")) {
430       g_error ("Received ICE message without mline index\n");
431       goto cleanup;
432     }
433     mline_index =
434         json_object_get_int_member (data_json_object, "sdpMLineIndex");
435 
436     if (!json_object_has_member (data_json_object, "candidate")) {
437       g_error ("Received ICE message without ICE candidate string\n");
438       goto cleanup;
439     }
440     candidate_string = json_object_get_string_member (data_json_object,
441         "candidate");
442 
443     g_print ("Received ICE candidate with mline index %u; candidate: %s\n",
444         mline_index, candidate_string);
445 
446     g_signal_emit_by_name (receiver_entry->webrtcbin, "add-ice-candidate",
447         mline_index, candidate_string);
448   } else
449     goto unknown_message;
450 
451 cleanup:
452   if (json_parser != NULL)
453     g_object_unref (G_OBJECT (json_parser));
454   g_free (data_string);
455   return;
456 
457 unknown_message:
458   g_error ("Unknown message \"%s\", ignoring", data_string);
459   goto cleanup;
460 }
461 
462 
463 void
soup_websocket_closed_cb(SoupWebsocketConnection * connection,gpointer user_data)464 soup_websocket_closed_cb (SoupWebsocketConnection * connection,
465     gpointer user_data)
466 {
467   GHashTable *receiver_entry_table = (GHashTable *) user_data;
468   g_hash_table_remove (receiver_entry_table, connection);
469   g_print ("Closed websocket connection %p\n", (gpointer) connection);
470 }
471 
472 
473 void
soup_http_handler(G_GNUC_UNUSED SoupServer * soup_server,SoupMessage * message,const char * path,G_GNUC_UNUSED GHashTable * query,G_GNUC_UNUSED SoupClientContext * client_context,G_GNUC_UNUSED gpointer user_data)474 soup_http_handler (G_GNUC_UNUSED SoupServer * soup_server,
475     SoupMessage * message, const char *path, G_GNUC_UNUSED GHashTable * query,
476     G_GNUC_UNUSED SoupClientContext * client_context,
477     G_GNUC_UNUSED gpointer user_data)
478 {
479   SoupBuffer *soup_buffer;
480 
481   if ((g_strcmp0 (path, "/") != 0) && (g_strcmp0 (path, "/index.html") != 0)) {
482     soup_message_set_status (message, SOUP_STATUS_NOT_FOUND);
483     return;
484   }
485 
486   soup_buffer =
487       soup_buffer_new (SOUP_MEMORY_STATIC, html_source, strlen (html_source));
488 
489   soup_message_headers_set_content_type (message->response_headers, "text/html",
490       NULL);
491   soup_message_body_append_buffer (message->response_body, soup_buffer);
492   soup_buffer_free (soup_buffer);
493 
494   soup_message_set_status (message, SOUP_STATUS_OK);
495 }
496 
497 
498 void
soup_websocket_handler(G_GNUC_UNUSED SoupServer * server,SoupWebsocketConnection * connection,G_GNUC_UNUSED const char * path,G_GNUC_UNUSED SoupClientContext * client_context,gpointer user_data)499 soup_websocket_handler (G_GNUC_UNUSED SoupServer * server,
500     SoupWebsocketConnection * connection, G_GNUC_UNUSED const char *path,
501     G_GNUC_UNUSED SoupClientContext * client_context, gpointer user_data)
502 {
503   ReceiverEntry *receiver_entry;
504   GHashTable *receiver_entry_table = (GHashTable *) user_data;
505 
506   g_print ("Processing new websocket connection %p", (gpointer) connection);
507 
508   g_signal_connect (G_OBJECT (connection), "closed",
509       G_CALLBACK (soup_websocket_closed_cb), (gpointer) receiver_entry_table);
510 
511   receiver_entry = create_receiver_entry (connection);
512   g_hash_table_replace (receiver_entry_table, connection, receiver_entry);
513 }
514 
515 
516 static gchar *
get_string_from_json_object(JsonObject * object)517 get_string_from_json_object (JsonObject * object)
518 {
519   JsonNode *root;
520   JsonGenerator *generator;
521   gchar *text;
522 
523   /* Make it the root node */
524   root = json_node_init_object (json_node_alloc (), object);
525   generator = json_generator_new ();
526   json_generator_set_root (generator, root);
527   text = json_generator_to_data (generator, NULL);
528 
529   /* Release everything */
530   g_object_unref (generator);
531   json_node_free (root);
532   return text;
533 }
534 
535 
536 gboolean
exit_sighandler(gpointer user_data)537 exit_sighandler (gpointer user_data)
538 {
539   g_print ("Caught signal, stopping mainloop\n");
540   GMainLoop *mainloop = (GMainLoop *) user_data;
541   g_main_loop_quit (mainloop);
542   return TRUE;
543 }
544 
545 
546 int
main(int argc,char * argv[])547 main (int argc, char *argv[])
548 {
549   GMainLoop *mainloop;
550   SoupServer *soup_server;
551   GHashTable *receiver_entry_table;
552 
553   setlocale (LC_ALL, "");
554   gst_init (&argc, &argv);
555 
556   receiver_entry_table =
557       g_hash_table_new_full (g_direct_hash, g_direct_equal, NULL,
558       destroy_receiver_entry);
559 
560   mainloop = g_main_loop_new (NULL, FALSE);
561   g_assert (mainloop != NULL);
562 
563   g_unix_signal_add (SIGINT, exit_sighandler, mainloop);
564   g_unix_signal_add (SIGTERM, exit_sighandler, mainloop);
565 
566   soup_server =
567       soup_server_new (SOUP_SERVER_SERVER_HEADER, "webrtc-soup-server", NULL);
568   soup_server_add_handler (soup_server, "/", soup_http_handler, NULL, NULL);
569   soup_server_add_websocket_handler (soup_server, "/ws", NULL, NULL,
570       soup_websocket_handler, (gpointer) receiver_entry_table, NULL);
571   soup_server_listen_all (soup_server, SOUP_HTTP_PORT,
572       (SoupServerListenOptions) 0, NULL);
573 
574   g_print ("WebRTC page link: http://127.0.0.1:%d/\n", (gint) SOUP_HTTP_PORT);
575 
576   g_main_loop_run (mainloop);
577 
578   g_object_unref (G_OBJECT (soup_server));
579   g_hash_table_destroy (receiver_entry_table);
580   g_main_loop_unref (mainloop);
581 
582   gst_deinit ();
583 
584   return 0;
585 }
586