-
Notifications
You must be signed in to change notification settings - Fork 2.5k
/
janus_audiobridge.c
3888 lines (3794 loc) · 159 KB
/
janus_audiobridge.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*! \file janus_audiobridge.c
* \author Lorenzo Miniero <[email protected]>
* \copyright GNU General Public License v3
* \brief Janus AudioBridge plugin
* \details This is a plugin implementing an audio conference bridge for
* Janus, specifically mixing Opus streams. This means that it replies
* by providing in the SDP only support for Opus, and disabling video.
* Opus encoding and decoding is implemented using libopus (http://opus.codec.org).
* The plugin provides an API to allow peers to join and leave conference
* rooms. Peers can then mute/unmute themselves by sending specific messages
* to the plugin: any way a peer mutes/unmutes, an event is triggered
* to the other participants, so that it can be rendered in the UI
* accordingly.
*
* Rooms to make available are listed in the plugin configuration file.
* A pre-filled configuration file is provided in \c conf/janus.plugin.audiobridge.cfg
* and includes a demo room for testing.
*
* To add more rooms or modify the existing one, you can use the following
* syntax:
*
* \verbatim
[<unique room ID>]
description = This is my awesome room
is_private = yes|no (private rooms don't appear when you do a 'list' request)
secret = <optional password needed for manipulating (e.g. destroying) the room>
pin = <optional password needed for joining the room>
sampling_rate = <sampling rate> (e.g., 16000 for wideband mixing)
audiolevel_ext = yes|no (whether the ssrc-audio-level RTP extension must be
negotiated/used or not for new joins, default=yes)
record = true|false (whether this room should be recorded, default=false)
record_file = /path/to/recording.wav (where to save the recording)
\endverbatim
*
* \section bridgeapi Audio Bridge API
*
* The Audio Bridge API supports several requests, some of which are
* synchronous and some asynchronous. There are some situations, though,
* (invalid JSON, invalid request) which will always result in a
* synchronous error response even for asynchronous requests.
*
* \c create , \c destroy , \c exists, \c allowed, \c kick, \c list,
* \c listparticipants and \c resetdecoder are synchronous requests,
* which means you'll get a response directly within the context of the
* transaction. \c create allows you to create a new audio conference bridge
* dynamically, as an alternative to using the configuration file;
* \c destroy removes an audio conference bridge and destroys it, kicking
* all the users out as part of the process; \c exists allows you to
* check whether a specific audio conference exists; \c allowed allows
* you to edit who's allowed to join a room via ad-hoc tokens; \c list
* lists all the available rooms, while \c listparticipants lists all
* the participants of a specific room and their details; finally,
* \c resetdecoder marks the Opus decoder for the participant as invalid,
* and forces it to be recreated (which might be needed if the audio
* for generated by the participant becomes garbled).
*
* The \c join , \c configure , \c changeroom and \c leave requests
* instead are all asynchronous, which means you'll get a notification
* about their success or failure in an event. \c join allows you to
* join a specific audio conference bridge; \c configure can be used
* to modify some of the participation settings (e.g., mute/unmute);
* \c changeroom can be used to leave the current room and move to a
* different one without having to tear down the PeerConnection and
* recreate it again (useful for sidebars and "waiting rooms"); finally,
* \c leave allows you to leave an audio conference bridge for good.
*
* The AudioBridge plugin also allows you to forward the mix to an
* external listener, e.g., a gstreamer/ffmpeg pipeline waiting to
* process the mixer audio stream. You can add new RTP forwarders with
* the \c rtp_forward request; a \c stop_rtp_forward request removes an
* existing RTP forwarder; \c listforwarders lists all the current RTP
* forwarders on a specific AudioBridge room instance.
*
* \c create can be used to create a new audio room, and has to be
* formatted as follows:
*
\verbatim
{
"request" : "create",
"room" : <unique numeric ID, optional, chosen by plugin if missing>,
"permanent" : <true|false, whether the room should be saved in the config file, default false>,
"description" : "<pretty name of the room, optional>",
"secret" : "<password required to edit/destroy the room, optional>",
"pin" : "<password required to join the room, optional>",
"is_private" : <true|false, whether the room should appear in a list request>,
"allowed" : [ array of string tokens users can use to join this room, optional],
"sampling" : <sampling rate of the room, optional, 16000 by default>,
"audiolevel_ext" : <true|false, whether the ssrc-audio-level RTP extension must be negotiated for new joins, default true>,
"audiolevel_event" : yes|no (whether to emit event to other users or not),
"audio_active_packets" : 100 (number of packets with audio level, default=100, 2 seconds),
"audio_level_average" : 25 (average value of audio level, 127=muted, 0='too loud', default=25),
"record" : <true|false, whether to record the room or not, default false>,
"record_file" : "</path/to/the/recording.wav, optional>",
}
\endverbatim
*
* A successful creation procedure will result in a \c created response:
*
\verbatim
{
"audiobridge" : "created",
"room" : <unique numeric ID>,
"permanent" : <true if saved to config file, false if not>
}
\endverbatim
*
* If you requested a permanent room but a \c false value is returned
* instead, good chances are that there are permission problems.
*
* An error instead (and the same applies to all other requests, so this
* won't be repeated) would provide both an error code and a more verbose
* description of the cause of the issue:
*
\verbatim
{
"audiobridge" : "event",
"error_code" : <numeric ID, check Macros below>,
"error" : "<error description as a string>"
}
\endverbatim
*
* Notice that, in general, all users can create rooms. If you want to
* limit this functionality, you can configure an admin \c admin_key in
* the plugin settings. When configured, only "create" requests that
* include the correct \c admin_key value in an "admin_key" property
* will succeed, and will be rejected otherwise.
*
* On the other hand, \c destroy can be used to destroy an existing audio
* room, whether created dynamically or statically, and has to be
* formatted as follows:
*
\verbatim
{
"request" : "destroy",
"room" : <unique numeric ID of the room to destroy>,
"secret" : "<room secret, mandatory if configured>",
"permanent" : <true|false, whether the room should be also removed from the config file, default false>
}
\endverbatim
*
* A successful destruction procedure will result in a \c destroyed response:
*
\verbatim
{
"audiobridge" : "created",
"room" : <unique numeric ID>
}
\endverbatim
*
* You can check whether a room exists using the \c exists request,
* which has to be formatted as follows:
*
\verbatim
{
"request" : "exists",
"room" : <unique numeric ID of the room to check>
}
\endverbatim
*
* A successful request will result in a \c success response:
*
\verbatim
{
"audiobridge" : "success",
"room" : <unique numeric ID>,
"exists" : <true|false>
}
\endverbatim
*
* You can configure whether to check tokens or add/remove people who can join
* a room using the \c allowed request, which has to be formatted as follows:
*
\verbatim
{
"request" : "allowed",
"secret" : "<room secret, mandatory if configured>",
"action" : "enable|disable|add|remove",
"room" : <unique numeric ID of the room to update>,
"allowed" : [
// Array of strings (tokens users might pass in "join", only for add|remove)
]
}
\endverbatim
*
* A successful request will result in a \c success response:
*
\verbatim
{
"audiobridge" : "success",
"room" : <unique numeric ID>,
"allowed" : [
// Updated, complete, list of allowed tokens (only for enable|add|remove)
]
}
\endverbatim
*
* If you're the administrator of a room (that is, you created it and have access
* to the secret) you can kick participants using the \c kick request. Notice
* that this only kicks the user out of the room, but does not prevent them from
* re-joining: to ban them, you need to first remove them from the list of
* authorized users (see \c allowed request) and then \c kick them. The \c kick
* request has to be formatted as follows:
*
\verbatim
{
"request" : "kick",
"secret" : "<room secret, mandatory if configured>",
"room" : <unique numeric ID of the room>,
"id" : <unique numeric ID of the participant to kick>
}
\endverbatim
*
* A successful request will result in a \c success response:
*
\verbatim
{
"audiobridge" : "success",
}
\endverbatim
*
* To get a list of the available rooms (excluded those configured or
* created as private rooms) you can make use of the \c list request,
* which has to be formatted as follows:
*
\verbatim
{
"request" : "list"
}
\endverbatim
*
* A successful request will produce a list of rooms in a \c success response:
*
\verbatim
{
"audiobridge" : "success",
"rooms" : [ // Array of room objects
{ // Room #1
"room" : <unique numeric ID>,
"description" : "<Name of the room>",
"sampling_rate" : <sampling rate of the mixer>,
"record" : <true|false, whether the room is being recorded>,
"num_participants" : <count of the participants>
},
// Other rooms
]
}
\endverbatim
*
* To get a list of the participants in a specific room, instead, you
* can make use of the \c listparticipants request, which has to be
* formatted as follows:
*
\verbatim
{
"request" : "listparticipants",
"room" : <unique numeric ID of the room>
}
\endverbatim
*
* A successful request will produce a list of participants in a
* \c participants response:
*
\verbatim
{
"audiobridge" : "participants",
"room" : <unique numeric ID of the room>,
"participants" : [ // Array of participant objects
{ // Participant #1
"id" : <unique numeric ID of the participant>,
"display" : "<display name of the participant, if any; optional>",
"muted" : <true|false, whether user is muted or not>
},
// Other participants
]
}
\endverbatim
*
* To mark the Opus decoder context for the current participant as
* invalid and force it to be recreated, use the \c resetdecoder request:
*
\verbatim
{
"request" : "resetdecoder"
}
\endverbatim
*
* A successful request will produce a \c success response:
*
\verbatim
{
"audiobridge" : "success"
}
\endverbatim
*
* You can add a new RTP forwarder for an existing room exists using the
* \c rtp_forward request, which has to be formatted as follows:
*
\verbatim
{
"request" : "rtp_forward",
"room" : <unique numeric ID of the room to add the forwarder to>,
"ssrc" : <SSRC to use to use when streaming (optional: stream_id used if missing)>,
"ptype" : <payload type to use when streaming (optional: 100 used if missing)>,
"host" : "<host address to forward the RTP packets to>",
"port" : <port to forward the RTP packets to>,
"always_on" : <true|false, whether silence should be forwarded when the room is empty>
}
\endverbatim
*
* A successful request will result in a \c success response:
*
\verbatim
{
"audiobridge" : "success",
"room" : <unique numeric ID, same as request>,
"stream_id" : <unique numeric ID assigned to the new RTP forwarder>
}
\endverbatim
*
* To stop a previously created RTP forwarder and stop it, you can use
* the \c stop_rtp_forward request, which has to be formatted as follows:
*
\verbatim
{
"request" : "stop_rtp_forward",
"room" : <unique numeric ID of the room to remove the forwarder from>,
"stream_id" : <unique numeric ID of the RTP forwarder>
}
\endverbatim
*
* A successful request will result in a \c success response:
*
\verbatim
{
"audiobridge" : "success",
"room" : <unique numeric ID, same as request>,
"stream_id" : <unique numeric ID, same as request>
}
\endverbatim
*
* To get a list of the forwarders in a specific room, instead, you
* can make use of the \c listforwarders request, which has to be
* formatted as follows:
*
\verbatim
{
"request" : "listforwarders",
"room" : <unique numeric ID of the room>
}
\endverbatim
*
* A successful request will produce a list of RTP forwarders in a
* \c forwarders response:
*
\verbatim
{
"audiobridge" : "forwarders",
"room" : <unique numeric ID of the room>,
"rtp_forwarders" : [ // Array of RTP forwarder objects
{ // RTP forwarder #1
"stream_id" : <unique numeric ID of the forwarder>,
"ip" : "<IP this forwarder is streaming to>",
"port" : <port this forwarder is streaming to>,
"ssrc" : <SSRC this forwarder is using, if any>,
"ptype" : <payload type this forwarder is using, if any>
},
// Other forwarders
]
}
\endverbatim
*
* That completes the list of synchronous requests you can send to the
* AudioBridge plugin. As anticipated, though, there are also several
* asynchronous requests you can send, specifically those related to
* joining and updating one's presence as a participant in an audio room.
*
* The way you'd interact with the plugin is usually as follows:
*
* -# you use a \c join request to join an audio room, and wait for the
* \c joined event; this event will also include a list of the other
* participants, if any;
* -# you send a \c configure request attached to an audio-only JSEP offer
* to start configuring your participation in the room (e.g., join unmuted
* or muted), and wait for a \c configured event, which will be attached
* to a JSEP answer by the plugin to complete the setup of the WebRTC
* PeerConnection;
* -# you send other \c configure requests (without any JSEP-related
* attachment) to mute/unmute yourself during the audio conference;
* -# you intercept events originated by the plugin (\c joined , \c leaving )
* to notify you about users joining/leaving/muting/unmuting;
* -# you eventually send a \c leave request to leave a room; if you leave the
* PeerConnection instance intact, you can subsequently join a different
* room without requiring a new negotiation (and so just use a \c join + JSEP-less \c configure to join).
*
* Notice that there's also a \c changeroom request available: you can use
* this request to immediately leave the room you're in and join a different
* one, without requiring you to do a \c leave + \c join + \c configure
* round. Of course remember not to pass any JSEP-related payload when
* doing a \c changeroom as the same pre-existing PeerConnection will be
* re-used for the purpose.
*
* About the syntax of all the above mentioned requests, \c join has
* to be formatted as follows:
*
\verbatim
{
"request" : "join",
"room" : <numeric ID of the room to join>,
"id" : <unique ID to assign to the participant; optional, assigned by the plugin if missing>,
"pin" : "<password required to join the room, if any; optional>",
"display" : "<display name to have in the room; optional>",
"token" : "<invitation token, in case the room has an ACL; optional>",
"muted" : <true|false, whether to start unmuted or muted>,
"quality" : <0-10, Opus-related complexity to use, the higher the value, the better the quality (but more CPU); optional, default is 4>,
"volume" : <percent value, <100 reduces volume, >100 increases volume; optional, default is 100 (no volume change)>
}
\endverbatim
*
* A successful request will produce a \c joined event:
*
\verbatim
{
"audiobridge" : "joined",
"room" : <numeric ID of the room>,
"id" : <unique ID assigned to the participant>,
"display" : "<display name of the new participant>",
"participants" : [
// Array of existing participants in the room
]
}
\endverbatim
*
* The other participants in the room will be notified about the new
* participant by means of a different \c joined event, which will only
* include the \c room and the new participant as the only object in
* a \c participants array.
*
* At this point, the media-related settings of the participant can be
* modified by means of a \c configure request. The \c configure request
* has to be formatted as follows (notice that all parameters except
* \c request are optional, depending on what you want to change):
*
\verbatim
{
"request" : "configure",
"muted" : <true|false, whether to unmute or mute>,
"display" : "<new display name to have in the room>",
"quality" : <0-10, Opus-related complexity to use, lower is higher quality; optional, default is 4>,
"volume" : <percent value, <100 reduces volume, >100 increases volume; optional, default is 100 (no volume change)>,
"record": <true|false, whether to record this user's contribution to a .mjr file (mixer not involved),
"filename": "<basename of the file to record to, -audio.mjr will be added by the plugin>"
}
\endverbatim
*
* \c muted instructs the plugin to mute or unmute the participant;
* \c quality changes the complexity of the Opus encoder for the
* participant; \c record can be used to record this participant's contribution
* to a Janus .mjr file, and \c filename to provide a basename for the path to
* save the file to (notice that this is different from the recording of a whole
* room: this feature only records the packets this user is sending, and is not
* related to the mixer stuff). A successful request will result in a \c ok event:
*
\verbatim
{
"audiobridge" : "event",
"room" : <numeric ID of the room>,
"result" : "ok"
}
\endverbatim
*
* In case the \c muted property was modified, the other participants in
* the room will be notified about this by means of a \c event notification,
* which will only include the \c room and the updated participant as the
* only object in a \c participants array.
*
* As anticipated, you can leave an audio room using the \c leave request,
* which has to be formatted as follows:
*
\verbatim
{
"request" : "leave"
}
\endverbatim
*
* All the participants will receive an \c event notification with the
* ID of the participant who just left:
*
\verbatim
{
"audiobridge" : "event",
"room" : <numeric ID of the room>,
"leaving" : <numeric ID of the participant who left>
}
\endverbatim
*
* For what concerns the \c changeroom request, instead, it's pretty much
* the same as a \c join request and as such has to be formatted as follows:
*
\verbatim
{
"request" : "changeroom",
"room" : <numeric ID of the room to move to>,
"id" : <unique ID to assign to the participant; optional, assigned by the plugin if missing>,
"display" : "<display name to have in the room; optional>",
"token" : "<invitation token, in case the new room has an ACL; optional>",
"muted" : <true|false, whether to start unmuted or muted>,
"quality" : <0-10, Opus-related complexity to use, lower is higher quality; optional, default is 4>
}
\endverbatim
*
* Such a request will trigger all the above-described leaving/joined
* events to the other participants, as it is indeed wrapping a \c leave
* followed by a \c join and as such the other participants in both rooms
* need to be updated accordingly. The participant who switched room
* instead will be sent a \c roomchanged event which is pretty similar
* to what \c joined looks like:
*
* A successful request will produce a \c joined event:
*
\verbatim
{
"audiobridge" : "roomchanged",
"room" : <numeric ID of the new room>,
"id" : <unique ID assigned to the participant in the new room>,
"display" : "<display name of the new participant>",
"participants" : [
// Array of existing participants in the new room
]
}
\endverbatim
*
* \ingroup plugins
* \ref plugins
*/
#include "plugin.h"
#include <jansson.h>
#include <opus/opus.h>
#include <sys/time.h>
#include "../debug.h"
#include "../apierror.h"
#include "../config.h"
#include "../mutex.h"
#include "../rtp.h"
#include "../rtcp.h"
#include "../record.h"
#include "../sdp-utils.h"
#include "../utils.h"
/* Plugin information */
#define JANUS_AUDIOBRIDGE_VERSION 10
#define JANUS_AUDIOBRIDGE_VERSION_STRING "0.0.10"
#define JANUS_AUDIOBRIDGE_DESCRIPTION "This is a plugin implementing an audio conference bridge for Janus, mixing Opus streams."
#define JANUS_AUDIOBRIDGE_NAME "JANUS AudioBridge plugin"
#define JANUS_AUDIOBRIDGE_AUTHOR "Meetecho s.r.l."
#define JANUS_AUDIOBRIDGE_PACKAGE "janus.plugin.audiobridge"
/* Plugin methods */
janus_plugin *create(void);
int janus_audiobridge_init(janus_callbacks *callback, const char *config_path);
void janus_audiobridge_destroy(void);
int janus_audiobridge_get_api_compatibility(void);
int janus_audiobridge_get_version(void);
const char *janus_audiobridge_get_version_string(void);
const char *janus_audiobridge_get_description(void);
const char *janus_audiobridge_get_name(void);
const char *janus_audiobridge_get_author(void);
const char *janus_audiobridge_get_package(void);
void janus_audiobridge_create_session(janus_plugin_session *handle, int *error);
struct janus_plugin_result *janus_audiobridge_handle_message(janus_plugin_session *handle, char *transaction, json_t *message, json_t *jsep);
void janus_audiobridge_setup_media(janus_plugin_session *handle);
void janus_audiobridge_incoming_rtp(janus_plugin_session *handle, int video, char *buf, int len);
void janus_audiobridge_incoming_rtcp(janus_plugin_session *handle, int video, char *buf, int len);
void janus_audiobridge_hangup_media(janus_plugin_session *handle);
void janus_audiobridge_destroy_session(janus_plugin_session *handle, int *error);
json_t *janus_audiobridge_query_session(janus_plugin_session *handle);
/* Plugin setup */
static janus_plugin janus_audiobridge_plugin =
JANUS_PLUGIN_INIT (
.init = janus_audiobridge_init,
.destroy = janus_audiobridge_destroy,
.get_api_compatibility = janus_audiobridge_get_api_compatibility,
.get_version = janus_audiobridge_get_version,
.get_version_string = janus_audiobridge_get_version_string,
.get_description = janus_audiobridge_get_description,
.get_name = janus_audiobridge_get_name,
.get_author = janus_audiobridge_get_author,
.get_package = janus_audiobridge_get_package,
.create_session = janus_audiobridge_create_session,
.handle_message = janus_audiobridge_handle_message,
.setup_media = janus_audiobridge_setup_media,
.incoming_rtp = janus_audiobridge_incoming_rtp,
.incoming_rtcp = janus_audiobridge_incoming_rtcp,
.hangup_media = janus_audiobridge_hangup_media,
.destroy_session = janus_audiobridge_destroy_session,
.query_session = janus_audiobridge_query_session,
);
/* Plugin creator */
janus_plugin *create(void) {
JANUS_LOG(LOG_VERB, "%s created!\n", JANUS_AUDIOBRIDGE_NAME);
return &janus_audiobridge_plugin;
}
/* Parameter validation */
static struct janus_json_parameter request_parameters[] = {
{"request", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}
};
static struct janus_json_parameter adminkey_parameters[] = {
{"admin_key", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}
};
static struct janus_json_parameter create_parameters[] = {
{"description", JSON_STRING, 0},
{"secret", JSON_STRING, 0},
{"pin", JSON_STRING, 0},
{"is_private", JANUS_JSON_BOOL, 0},
{"allowed", JSON_ARRAY, 0},
{"sampling", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},
{"record", JANUS_JSON_BOOL, 0},
{"record_file", JSON_STRING, 0},
{"permanent", JANUS_JSON_BOOL, 0},
{"audiolevel_ext", JANUS_JSON_BOOL, 0},
{"audiolevel_event", JANUS_JSON_BOOL, 0},
{"audio_active_packets", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},
{"audio_level_average", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},
{"room", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}
};
static struct janus_json_parameter destroy_parameters[] = {
{"room", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},
{"permanent", JANUS_JSON_BOOL, 0}
};
static struct janus_json_parameter allowed_parameters[] = {
{"room", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},
{"secret", JSON_STRING, 0},
{"action", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},
{"allowed", JSON_ARRAY, 0}
};
static struct janus_json_parameter kick_parameters[] = {
{"room", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},
{"secret", JSON_STRING, 0},
{"id", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE}
};
static struct janus_json_parameter room_parameters[] = {
{"room", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE}
};
static struct janus_json_parameter join_parameters[] = {
{"room", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},
{"display", JSON_STRING, 0},
{"token", JSON_STRING, 0},
{"muted", JANUS_JSON_BOOL, 0},
{"quality", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},
{"volume", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},
{"id", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE}
};
static struct janus_json_parameter configure_parameters[] = {
{"muted", JANUS_JSON_BOOL, 0},
{"quality", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},
{"volume", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},
{"record", JANUS_JSON_BOOL, 0},
{"filename", JSON_STRING, 0},
{"display", JSON_STRING, 0}
};
static struct janus_json_parameter rtp_forward_parameters[] = {
{"room", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},
{"ssrc", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},
{"ptype", JSON_INTEGER, JANUS_JSON_PARAM_POSITIVE},
{"port", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},
{"host", JSON_STRING, JANUS_JSON_PARAM_REQUIRED},
{"always_on", JANUS_JSON_BOOL, 0}
};
static struct janus_json_parameter stop_rtp_forward_parameters[] = {
{"room", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE},
{"stream_id", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE}
};
/* Static configuration instance */
static janus_config *config = NULL;
static const char *config_folder = NULL;
static janus_mutex config_mutex;
/* Useful stuff */
static volatile gint initialized = 0, stopping = 0;
static gboolean notify_events = TRUE;
static janus_callbacks *gateway = NULL;
static GThread *handler_thread;
static GThread *watchdog;
static void *janus_audiobridge_handler(void *data);
static void janus_audiobridge_relay_rtp_packet(gpointer data, gpointer user_data);
static void *janus_audiobridge_mixer_thread(void *data);
static void *janus_audiobridge_participant_thread(void *data);
typedef struct janus_audiobridge_message {
janus_plugin_session *handle;
char *transaction;
json_t *message;
json_t *jsep;
} janus_audiobridge_message;
static GAsyncQueue *messages = NULL;
static janus_audiobridge_message exit_message;
static void janus_audiobridge_message_free(janus_audiobridge_message *msg) {
if(!msg || msg == &exit_message)
return;
msg->handle = NULL;
g_free(msg->transaction);
msg->transaction = NULL;
if(msg->message)
json_decref(msg->message);
msg->message = NULL;
if(msg->jsep)
json_decref(msg->jsep);
msg->jsep = NULL;
g_free(msg);
}
typedef struct janus_audiobridge_room {
guint64 room_id; /* Unique room ID */
gchar *room_name; /* Room description */
gchar *room_secret; /* Secret needed to manipulate (e.g., destroy) this room */
gchar *room_pin; /* Password needed to join this room, if any */
gboolean is_private; /* Whether this room is 'private' (as in hidden) or not */
uint32_t sampling_rate; /* Sampling rate of the mix (e.g., 16000 for wideband; can be 8, 12, 16, 24 or 48kHz) */
gboolean audiolevel_ext; /* Whether the ssrc-audio-level extension must be negotiated or not for new joins */
gboolean audiolevel_event; /* Whether to emit event to other users about audiolevel */
int audio_active_packets; /* amount of packets with audio level for checkup */
int audio_level_average; /* average audio level */
gboolean record; /* Whether this room has to be recorded or not */
gchar *record_file; /* Path of the recording file */
FILE *recording; /* File to record the room into */
gint64 record_lastupdate; /* Time when we last updated the wav header */
gboolean destroy; /* Value to flag the room for destruction */
GHashTable *participants; /* Map of participants */
gboolean check_tokens; /* Whether to check tokens when participants join (see below) */
GHashTable *allowed; /* Map of participants (as tokens) allowed to join */
GThread *thread; /* Mixer thread for this room */
gint64 destroyed; /* When this room has been destroyed */
janus_mutex mutex; /* Mutex to lock this room instance */
/* RTP forwarders for this room's mix */
GHashTable *rtp_forwarders; /* RTP forwarders list (as a hashmap) */
OpusEncoder *rtp_encoder; /* Opus encoder instance to use for all RTP forwarders */
janus_mutex rtp_mutex; /* Mutex to lock the RTP forwarders list */
int rtp_udp_sock; /* UDP socket to use to forward RTP packets */
} janus_audiobridge_room;
static GHashTable *rooms;
static janus_mutex rooms_mutex;
static GList *old_rooms;
static char *admin_key = NULL;
typedef struct janus_audiobridge_session {
janus_plugin_session *handle;
gpointer participant;
gboolean started;
gboolean stopping;
volatile gint hangingup;
gint64 destroyed; /* Time at which this session was marked as destroyed */
} janus_audiobridge_session;
static GHashTable *sessions;
static GList *old_sessions;
static janus_mutex sessions_mutex;
typedef struct janus_audiobridge_participant {
janus_audiobridge_session *session;
janus_audiobridge_room *room; /* Room */
guint64 user_id; /* Unique ID in the room */
gchar *display; /* Display name (opaque value, only meaningful to application) */
gboolean prebuffering; /* Whether this participant needs pre-buffering of a few packets (just joined) */
gboolean active; /* Whether this participant can receive media at all */
gboolean working; /* Whether this participant is currently encoding/decoding */
gboolean muted; /* Whether this participant is muted */
int volume_gain; /* Gain to apply to the input audio (in percentage) */
int opus_complexity; /* Complexity to use in the encoder (by default, DEFAULT_COMPLEXITY) */
/* RTP stuff */
GList *inbuf; /* Incoming audio from this participant, as an ordered list of packets */
GAsyncQueue *outbuf; /* Mixed audio for this participant */
gint64 last_drop; /* When we last dropped a packet because the imcoming queue was full */
janus_mutex qmutex; /* Incoming queue mutex */
int opus_pt; /* Opus payload type */
int extmap_id; /* Audio level RTP extension id, if any */
int dBov_level; /* Value in dBov of the audio level (last value from extension) */
int audio_active_packets; /* participants number of audio packets to accumulate */
int audio_dBov_sum; /* participants accumulated dBov value for audio level */
janus_rtp_switching_context context; /* Needed in case the participant changes room */
/* Opus stuff */
OpusEncoder *encoder; /* Opus encoder instance */
OpusDecoder *decoder; /* Opus decoder instance */
gboolean reset; /* Whether or not the Opus context must be reset, without re-joining the room */
GThread *thread; /* Encoding thread for this participant */
janus_recorder *arc; /* The Janus recorder instance for this user's audio, if enabled */
janus_mutex rec_mutex; /* Mutex to protect the recorder from race conditions */
gint64 destroyed; /* When this participant has been destroyed */
} janus_audiobridge_participant;
/* Packets we get from gstreamer and relay */
typedef struct janus_audiobridge_rtp_relay_packet {
rtp_header *data;
gint length;
uint32_t ssrc;
uint32_t timestamp;
uint16_t seq_number;
gboolean silence;
} janus_audiobridge_rtp_relay_packet;
/* RTP forwarder instance: address to send to, and current RTP header info */
typedef struct janus_audiobridge_rtp_forwarder {
struct sockaddr_in serv_addr;
uint32_t ssrc;
int payload_type;
uint16_t seq_number;
uint32_t timestamp;
gboolean always_on;
} janus_audiobridge_rtp_forwarder;
static guint32 janus_audiobridge_rtp_forwarder_add_helper(janus_audiobridge_room *room, const gchar* host, uint16_t port, uint32_t ssrc, int pt, gboolean always_on) {
if(room == NULL || host == NULL)
return 0;
janus_audiobridge_rtp_forwarder *rf = g_malloc0(sizeof(janus_audiobridge_rtp_forwarder));
/* Resolve address */
rf->serv_addr.sin_family = AF_INET;
inet_pton(AF_INET, host, &(rf->serv_addr.sin_addr));
rf->serv_addr.sin_port = htons(port);
/* Setup RTP info (we'll use the stream ID as SSRC) */
rf->ssrc = ssrc;
rf->payload_type = pt;
rf->seq_number = 0;
rf->timestamp = 0;
rf->always_on = always_on;
janus_mutex_lock(&room->rtp_mutex);
guint32 stream_id = janus_random_uint32();
while(g_hash_table_lookup(room->rtp_forwarders, GUINT_TO_POINTER(stream_id)) != NULL) {
stream_id = janus_random_uint32();
}
g_hash_table_insert(room->rtp_forwarders, GUINT_TO_POINTER(stream_id), rf);
janus_mutex_unlock(&room->rtp_mutex);
JANUS_LOG(LOG_VERB, "Added RTP forwarder to room %"SCNu64": %s:%d (ID: %"SCNu32")\n",
room->room_id, host, port, stream_id);
return stream_id;
}
/* Helper to sort incoming RTP packets by sequence numbers */
static gint janus_audiobridge_rtp_sort(gconstpointer a, gconstpointer b) {
janus_audiobridge_rtp_relay_packet *pkt1 = (janus_audiobridge_rtp_relay_packet *)a;
janus_audiobridge_rtp_relay_packet *pkt2 = (janus_audiobridge_rtp_relay_packet *)b;
if(pkt1->seq_number < 100 && pkt2->seq_number > 65000) {
/* Sequence number was probably reset, pkt2 is older */
return 1;
} else if(pkt2->seq_number < 100 && pkt1->seq_number > 65000) {
/* Sequence number was probably reset, pkt1 is older */
return -1;
}
/* Simply compare timestamps */
if(pkt1->seq_number < pkt2->seq_number)
return -1;
else if(pkt1->seq_number > pkt2->seq_number)
return 1;
return 0;
}
/* Helper struct to generate and parse WAVE headers */
typedef struct wav_header {
char riff[4];
uint32_t len;
char wave[4];
char fmt[4];
uint32_t formatsize;
uint16_t format;
uint16_t channels;
uint32_t samplerate;
uint32_t avgbyterate;
uint16_t samplebytes;
uint16_t channelbits;
char data[4];
uint32_t blocksize;
} wav_header;
/* Mixer settings */
#define DEFAULT_PREBUFFERING 6
/* Opus settings */
#define BUFFER_SAMPLES 8000
#define OPUS_SAMPLES 160
#define USE_FEC 0
#define DEFAULT_COMPLEXITY 4
/* Error codes */
#define JANUS_AUDIOBRIDGE_ERROR_UNKNOWN_ERROR 499
#define JANUS_AUDIOBRIDGE_ERROR_NO_MESSAGE 480
#define JANUS_AUDIOBRIDGE_ERROR_INVALID_JSON 481
#define JANUS_AUDIOBRIDGE_ERROR_INVALID_REQUEST 482
#define JANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT 483
#define JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT 484
#define JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM 485
#define JANUS_AUDIOBRIDGE_ERROR_ROOM_EXISTS 486
#define JANUS_AUDIOBRIDGE_ERROR_NOT_JOINED 487
#define JANUS_AUDIOBRIDGE_ERROR_LIBOPUS_ERROR 488
#define JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED 489
#define JANUS_AUDIOBRIDGE_ERROR_ID_EXISTS 490
#define JANUS_AUDIOBRIDGE_ERROR_ALREADY_JOINED 491
#define JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_USER 492
#define JANUS_AUDIOBRIDGE_ERROR_INVALID_SDP 493
/* AudioBridge watchdog/garbage collector (sort of) */
static void *janus_audiobridge_watchdog(void *data) {
JANUS_LOG(LOG_INFO, "AudioBridge watchdog started\n");
gint64 now = 0;
while(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) {
janus_mutex_lock(&sessions_mutex);
/* Iterate on all the sessions */
now = janus_get_monotonic_time();
if(old_sessions != NULL) {
GList *sl = old_sessions;
JANUS_LOG(LOG_HUGE, "Checking %d old AudioBridge sessions...\n", g_list_length(old_sessions));
while(sl) {
janus_audiobridge_session *session = (janus_audiobridge_session *)sl->data;
if(!session) {
sl = sl->next;
continue;
}
if(now-session->destroyed >= 5*G_USEC_PER_SEC) {
/* We're lazy and actually get rid of the stuff only after a few seconds */
JANUS_LOG(LOG_VERB, "Freeing old AudioBridge session\n");
GList *rm = sl->next;
old_sessions = g_list_delete_link(old_sessions, sl);
sl = rm;
session->handle = NULL;
g_free(session);
session = NULL;
continue;
}
sl = sl->next;
}
}
janus_mutex_unlock(&sessions_mutex);
janus_mutex_lock(&rooms_mutex);
if(old_rooms != NULL) {
GList *rl = old_rooms;
now = janus_get_monotonic_time();
while(rl) {
janus_audiobridge_room *audiobridge = (janus_audiobridge_room*)rl->data;
if(!initialized || stopping){
break;
}
if(!audiobridge) {
rl = rl->next;
continue;
}
if(now - audiobridge->destroyed >= 5*G_USEC_PER_SEC) {
/* Free resources */
JANUS_LOG(LOG_VERB, "Freeing old AudioBridge room %"SCNu64"\n", audiobridge->room_id);
g_free(audiobridge->room_name);
g_free(audiobridge->room_secret);
g_free(audiobridge->room_pin);
g_free(audiobridge->record_file);
g_hash_table_destroy(audiobridge->participants);
g_hash_table_destroy(audiobridge->allowed);
janus_mutex_lock(&audiobridge->rtp_mutex);
if(audiobridge->rtp_udp_sock > 0)
close(audiobridge->rtp_udp_sock);
if(audiobridge->rtp_encoder)
opus_encoder_destroy(audiobridge->rtp_encoder);
g_hash_table_destroy(audiobridge->rtp_forwarders);
janus_mutex_unlock(&audiobridge->rtp_mutex);
g_free(audiobridge);
/* Move on */
GList *rm = rl->next;
old_rooms = g_list_delete_link(old_rooms, rl);
rl = rm;
continue;
}
rl = rl->next;
}
}
janus_mutex_unlock(&rooms_mutex);
g_usleep(500000);
}
JANUS_LOG(LOG_INFO, "AudioBridge watchdog stopped\n");
return NULL;
}
/* Plugin implementation */
int janus_audiobridge_init(janus_callbacks *callback, const char *config_path) {
if(g_atomic_int_get(&stopping)) {
/* Still stopping from before */
return -1;
}
if(callback == NULL || config_path == NULL) {