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