You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
rtpengine/daemon/janus.c

1976 lines
56 KiB

#include "janus.h"
#include <json-glib/json-glib.h>
#include <stdbool.h>
#include "websocket.h"
#include "log.h"
#include "main.h"
#include "obj.h"
#include "call.h"
#include "sdp.h"
#include "call_interfaces.h"
#include "rtplib.h"
#include "ice.h"
struct janus_session { // "login" session
struct obj obj;
uint64_t id;
mutex_t lock;
time_t last_act;
GHashTable *websockets; // controlling transports, websocket_conn -> websocket_conn
GHashTable *handles; // handle ID -> 0x1. handle ID owned by janus_handles
};
struct janus_handle { // corresponds to a conference participant
uint64_t id;
struct janus_session *session; // holds a reference
uint64_t room;
};
struct janus_room {
uint64_t id;
str call_id;
int num_publishers;
uint64_t handle_id; // controlling handle which created the room
GHashTable *publishers; // handle ID -> feed ID
GHashTable *subscribers; // handle ID -> subscribed feed ID
GHashTable *feeds; // feed ID -> handle ID
};
static mutex_t janus_lock;
static GHashTable *janus_tokens; // auth tokens, currently mostly unused
static GHashTable *janus_sessions; // session ID -> session. holds a session reference
static GHashTable *janus_handles; // handle ID -> handle
static GHashTable *janus_rooms; // room ID -> room
static void __janus_session_free(void *p) {
struct janus_session *s = p;
if (g_hash_table_size(s->websockets) != 0)
ilog(LOG_WARN, "Janus session is leaking %i WS references", g_hash_table_size(s->websockets));
g_hash_table_destroy(s->websockets);
if (g_hash_table_size(s->handles) != 0)
ilog(LOG_WARN, "Janus session is leaking %i handle references", g_hash_table_size(s->handles));
g_hash_table_destroy(s->handles);
mutex_destroy(&s->lock);
}
// XXX we have several hash tables that hold references to objs - unify all these
static struct janus_session *janus_get_session(uint64_t id) {
mutex_lock(&janus_lock);
struct janus_session *ret = g_hash_table_lookup(janus_sessions, &id);
if (ret)
obj_hold(ret);
mutex_unlock(&janus_lock);
if (!ret)
return NULL;
mutex_lock(&ret->lock);
ret->last_act = rtpe_now.tv_sec;
mutex_unlock(&ret->lock);
return ret;
}
static uint64_t *uint64_dup(uint64_t u) {
uint64_t *ret = g_malloc(sizeof(*ret));
*ret = u;
return ret;
}
INLINE uint64_t janus_random(void) {
return ssl_random() & 0x7ffffffffffffLL;
}
static uint64_t jr_str_int(JsonReader *r) {
uint64_t ret = json_reader_get_int_value(r);
if (ret)
return ret;
const char *s = json_reader_get_string_value(r);
if (!s || !*s)
return 0;
char *ep;
ret = strtoull(s, &ep, 10);
if (*ep)
return 0;
return ret;
}
static struct call_monologue *janus_get_monologue(uint64_t handle_id, struct call *call,
struct call_monologue *(*fn)(struct call *, const str *))
{
AUTO_CLEANUP_GBUF(handle_buf);
handle_buf = g_strdup_printf("%" PRIu64, handle_id);
str handle_str = STR_INIT(handle_buf);
return fn(call, &handle_str);
}
// frees 'builder'
// sends a single final response message to a received websocket message. requires a response code
static void janus_send_json_sync_response(struct websocket_message *wm, JsonBuilder *builder, int code) {
char *result = glib_json_print(builder);
if (wm->method == M_WEBSOCKET)
websocket_write_text(wm->wc, result, true);
else {
websocket_http_response(wm->wc, code, "application/json", strlen(result));
websocket_write_http(wm->wc, result, true);
}
g_free(result);
}
// frees 'builder'
// sends an asynchronous notification to all websockets connected to a session
// session must be locked already
static void janus_send_json_async(struct janus_session *session, JsonBuilder *builder) {
char *result = glib_json_print(builder);
GHashTableIter iter;
gpointer value;
g_hash_table_iter_init(&iter, session->websockets);
while (g_hash_table_iter_next(&iter, NULL, &value)) {
struct websocket_conn *wc = value;
// lock order constraint: janus_session lock first, websocket_conn lock second
websocket_write_text(wc, result, true);
}
g_free(result);
}
// session is locked
static void janus_send_ack(struct websocket_message *wm, const char *transaction, struct janus_session *session) {
// build and send an early ack
JsonBuilder *ack = json_builder_new();
json_builder_begin_object(ack); // {
json_builder_set_member_name(ack, "janus");
json_builder_add_string_value(ack, "ack");
json_builder_set_member_name(ack, "transaction");
json_builder_add_string_value(ack, transaction);
json_builder_set_member_name(ack, "session_id");
json_builder_add_int_value(ack, session->id);
json_builder_end_object(ack); // }
janus_send_json_async(session, ack);
}
// returns g_malloc'd string
INLINE char *janus_call_id(uint64_t room_id) {
return g_strdup_printf("janus %" PRIu64, room_id);
}
// global janus_lock is held
static const char *janus_videoroom_create(struct janus_session *session, struct janus_handle *handle,
JsonBuilder *builder, JsonReader *reader, int *retcode)
{
*retcode = 436;
if (handle->room != 0)
return "User already exists in a room";
// create new videoroom
struct janus_room *room = g_slice_alloc0(sizeof(*room));
if (json_reader_read_member(reader, "publishers"))
room->num_publishers = jr_str_int(reader);
json_reader_end_member(reader);
if (room->num_publishers <= 0)
room->num_publishers = 3;
room->handle_id = handle->id; // controlling handle
// XXX optimise for 64-bit archs
room->publishers = g_hash_table_new_full(g_int64_hash, g_int64_equal, g_free, g_free);
room->subscribers = g_hash_table_new_full(g_int64_hash, g_int64_equal, g_free, g_free);
room->feeds = g_hash_table_new_full(g_int64_hash, g_int64_equal, g_free, g_free);
uint64_t room_id = 0;
if (json_reader_read_member(reader, "room")) {
room_id = jr_str_int(reader);
if (!room_id)
return "Invalid room ID requested";
}
json_reader_end_member(reader);
if (room_id) {
*retcode = 512;
if (g_hash_table_lookup(janus_rooms, &room_id))
return "Requested room already exists";
}
while (1) {
if (!room_id)
room_id = janus_random();
room->id = room_id;
if (g_hash_table_lookup(janus_rooms, &room->id))
continue;
room->call_id.s = janus_call_id(room_id);
room->call_id.len = strlen(room->call_id.s);
struct call *call = call_get_or_create(&room->call_id, true);
if (!call) {
ilog(LOG_WARN, "Call with reserved Janus ID '" STR_FORMAT
"' already exists", STR_FMT(&room->call_id));
g_free(room->call_id.s);
continue;
}
if (!call->created_from)
call->created_from = "janus";
g_hash_table_insert(janus_rooms, &room->id, room);
rwlock_unlock_w(&call->master_lock);
obj_put(call);
break;
}
handle->room = room_id;
ilog(LOG_INFO, "Created new videoroom with ID %" PRIu64, room_id);
json_builder_set_member_name(builder, "videoroom");
json_builder_add_string_value(builder, "created");
json_builder_set_member_name(builder, "room");
json_builder_add_int_value(builder, room_id);
json_builder_set_member_name(builder, "permanent");
json_builder_add_boolean_value(builder, false);
return NULL;
}
// global janus_lock is held
static const char *janus_videoroom_exists(struct janus_session *session,
JsonBuilder *builder, uint64_t room_id)
{
struct janus_room *room = NULL;
bool exists = false;
if (room_id)
room = g_hash_table_lookup(janus_rooms, &room_id);
if (room) {
struct call *call = call_get(&room->call_id);
if (call) {
exists = true;
rwlock_unlock_w(&call->master_lock);
obj_put(call);
}
}
json_builder_set_member_name(builder, "videoroom");
json_builder_add_string_value(builder, "success");
json_builder_set_member_name(builder, "room");
json_builder_add_int_value(builder, room_id);
json_builder_set_member_name(builder, "exists");
json_builder_add_boolean_value(builder, exists);
return NULL;
}
// global janus_lock is held
static const char *janus_videoroom_destroy(struct janus_session *session,
JsonBuilder *builder, int *retcode, uint64_t room_id)
{
struct janus_room *room = NULL;
if (room_id)
g_hash_table_steal_extended(janus_rooms, &room_id, NULL, (void **) &room);
*retcode = 426;
if (!room)
return "No such room";
ilog(LOG_INFO, "Destroying videoroom with ID %" PRIu64, room_id);
struct call *call = call_get(&room->call_id);
// XXX if call is destroyed separately, room persist -> room should be destroyed too
if (call) {
rwlock_unlock_w(&call->master_lock);
call_destroy(call);
obj_put(call);
}
g_free(room->call_id.s);
g_hash_table_destroy(room->publishers);
g_hash_table_destroy(room->subscribers);
g_hash_table_destroy(room->feeds);
g_slice_free1(sizeof(*room), room);
//XXX notify?
json_builder_set_member_name(builder, "videoroom");
json_builder_add_string_value(builder, "destroyed");
json_builder_set_member_name(builder, "room");
json_builder_add_int_value(builder, room_id);
json_builder_set_member_name(builder, "permanent");
json_builder_add_boolean_value(builder, false);
return NULL;
}
// adds fields "streams": [...] and "audio_codec" etc into the builder at the current position
static void janus_add_publisher_details(JsonBuilder *builder, struct call_monologue *ml) {
json_builder_set_member_name(builder, "streams");
json_builder_begin_array(builder);
const char *a_codec = NULL, *v_codec = NULL;
for (unsigned int i = 0; i < ml->medias->len; i++) {
struct call_media *media = ml->medias->pdata[i];
if (!media)
continue;
const char *codec = NULL;
for (GList *k = media->codecs.codec_prefs.head; k; k = k->next) {
struct rtp_payload_type *pt = k->data;
codec = pt->encoding.s;
// XXX check codec support?
break;
}
json_builder_begin_object(builder);
json_builder_set_member_name(builder, "type");
json_builder_add_string_value(builder, media->type.s);
json_builder_set_member_name(builder, "mindex");
json_builder_add_int_value(builder, media->index - 1);
json_builder_set_member_name(builder, "mid");
if (media->media_id.s)
json_builder_add_string_value(builder, media->media_id.s);
else
json_builder_add_null_value(builder);
if (!MEDIA_ISSET2(media, SEND, RECV)) {
json_builder_set_member_name(builder, "disabled");
json_builder_add_boolean_value(builder, true);
}
else if (codec) {
json_builder_set_member_name(builder, "codec");
json_builder_add_string_value(builder, codec);
if (media->type_id == MT_AUDIO && !a_codec)
a_codec = codec;
else if (media->type_id == MT_VIDEO && !v_codec)
v_codec = codec;
}
json_builder_end_object(builder);
}
json_builder_end_array(builder);
if (a_codec) {
json_builder_set_member_name(builder, "audio_codec");
json_builder_add_string_value(builder, a_codec);
}
if (v_codec) {
json_builder_set_member_name(builder, "video_codec");
json_builder_add_string_value(builder, v_codec);
}
// TODO add "display"
}
static void janus_publishers_list(JsonBuilder *builder, struct call *call, struct janus_room *room,
uint64_t feed_id)
{
json_builder_begin_array(builder); // [
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, room->publishers);
while (g_hash_table_iter_next(&iter, &key, &value)) {
uint64_t *feed_id_ptr = value;
if (*feed_id_ptr == feed_id) // skip self
continue;
// get monologue
uint64_t *handle_id_ptr = key;
struct call_monologue *ml = janus_get_monologue(*handle_id_ptr, call, call_get_monologue);
if (!ml)
continue;
json_builder_begin_object(builder); // {
json_builder_set_member_name(builder, "id");
json_builder_add_int_value(builder, *feed_id_ptr);
janus_add_publisher_details(builder, ml);
json_builder_end_object(builder); // }
}
json_builder_end_array(builder); // ]
}
// global janus_lock is held
static const char *janus_videoroom_join_sub(struct janus_handle *handle, struct janus_room *room, int *retcode,
uint64_t feed_id, struct call *call, GQueue *srcs)
{
// does the feed actually exist? get the feed handle
*retcode = 512;
uint64_t *feed_handle = g_hash_table_lookup(room->feeds, &feed_id);
if (!feed_handle)
return "No such feed exists";
if (!g_hash_table_lookup(room->publishers, feed_handle))
return "No such feed handle exists";
// handle ID points to the subscribed feed
g_hash_table_insert(room->subscribers, uint64_dup(handle->id), uint64_dup(feed_id));
// add the subscription
struct call_monologue *source_ml = janus_get_monologue(*feed_handle, call, call_get_monologue);
if (!source_ml)
return "Feed not found";
struct call_subscription *cs = g_slice_alloc0(sizeof(*cs));
cs->monologue = source_ml;
g_queue_push_tail(srcs, cs);
return NULL;
}
static void janus_clear_ret_streams(GQueue *q) {
uint64_t *id;
while ((id = g_queue_pop_head(q)))
g_slice_free1(sizeof(*id), id);
}
static int g_int64_cmp(gconstpointer a, gconstpointer b) {
const uint64_t *A = a, *B = b;
return !(*A == *B);
}
// global janus_lock is held
static const char *janus_videoroom_join(struct websocket_message *wm, struct janus_session *session,
const char *transaction,
struct janus_handle *handle, JsonBuilder *builder, JsonReader *reader, const char **successp,
int *retcode,
char **jsep_type_out, str *jsep_sdp_out,
uint64_t room_id)
{
janus_send_ack(wm, transaction, session);
*retcode = 456;
if (!json_reader_read_member(reader, "ptype"))
return "JSON object does not contain 'message.ptype' key";
const char *ptype = json_reader_get_string_value(reader);
if (!ptype)
return "JSON object does not contain 'message.ptype' key";
json_reader_end_member(reader);
bool plain_offer = false;
if (json_reader_read_member(reader, "plain"))
plain_offer = json_reader_get_boolean_value(reader);
json_reader_end_member(reader);
*retcode = 436;
if (handle->room != 0 && handle->room != room_id)
return "User already exists in a different room";
*retcode = 430;
bool is_pub = false;
if (!strcmp(ptype, "publisher"))
is_pub = true;
else if (!strcmp(ptype, "subscriber") || !strcmp(ptype, "listener"))
is_pub = false;
else
return "Invalid 'ptype'";
struct janus_room *room = NULL;
if (room_id)
room = g_hash_table_lookup(janus_rooms, &room_id);
*retcode = 426;
if (!room)
return "No such room";
AUTO_CLEANUP_NULL(struct call *call, call_unlock_release);
*retcode = 426;
call = call_get(&room->call_id);
if (!call)
return "No such room";
*retcode = 436;
if (!is_pub && g_hash_table_lookup(room->subscribers, &handle->id))
return "User already exists in the room as a subscriber";
if (is_pub && g_hash_table_lookup(room->publishers, &handle->id))
return "User already exists in the room as a publisher";
uint64_t feed_id = 0; // set for single feed IDs, otherwise remains 0
AUTO_CLEANUP_INIT(GString *feed_ids, __g_string_free, g_string_new("feeds ")); // for log output
AUTO_CLEANUP(GQueue ret_streams, janus_clear_ret_streams) = G_QUEUE_INIT; // return list for multiple subs
if (is_pub) {
if (json_reader_read_member(reader, "id")) {
feed_id = jr_str_int(reader);
if (!feed_id)
return "Invalid feed ID requested";
if (g_hash_table_lookup(room->feeds, &feed_id))
return "Feed already exists";
}
json_reader_end_member(reader);
// random feed ID?
while (!feed_id) {
feed_id = janus_random();
if (feed_id && g_hash_table_lookup(room->feeds, &feed_id))
feed_id = 0;
}
// feed ID points to the handle
g_hash_table_insert(room->feeds, uint64_dup(feed_id), uint64_dup(handle->id));
// handle ID points to the feed
g_hash_table_insert(room->publishers, uint64_dup(handle->id), uint64_dup(feed_id));
}
else {
// subscriber
AUTO_CLEANUP(GQueue srcs, call_subscriptions_clear) = G_QUEUE_INIT;
// get single feed ID if there is one
if (json_reader_read_member(reader, "feed")) {
*retcode = 456;
feed_id = jr_str_int(reader);
if (!feed_id)
return "JSON object contains invalid 'message.feed' key";
const char *ret = janus_videoroom_join_sub(handle, room, retcode, feed_id,
call, &srcs);
if (ret)
return ret;
}
json_reader_end_member(reader);
// handle list of subscriptions if given
if (json_reader_read_member(reader, "streams")) {
*retcode = 456;
if (!json_reader_is_array(reader))
return "Invalid 'message.streams' key (not an array)";
int eles = json_reader_count_elements(reader);
if (eles < 0)
return "Invalid 'message.streams' key (invalid array)";
for (int i = 0; i < eles; i++) {
if (!json_reader_read_element(reader, i))
return "Invalid 'message.streams' key (cannot read element)";
if (!json_reader_is_object(reader))
return "Invalid 'message.streams' key (contains not an object)";
if (!json_reader_read_member(reader, "feed"))
return "Invalid 'message.streams' key (doesn't contain 'feed')";
uint64_t fid = jr_str_int(reader); // leave `feed_id` zero
if (!fid)
return "Invalid 'message.streams' key (contains invalid 'feed')";
// check for duplicate feed IDs. the "streams" list actually contains one
// element for each media section ("streams":[{"feed":74515332221,"mid":"0"},
// {"feed":74515332221,"mid":"1"}]) but this isn't supported right now.
// instead always expect all media sections to be subscribed to, in order,
// and so simply honour each unique feed ID given.
// TODO: fix this up
if (!g_queue_find_custom(&ret_streams, &fid, g_int64_cmp)) {
const char *ret = janus_videoroom_join_sub(handle, room, retcode, fid,
call, &srcs);
if (ret)
return ret;
g_string_append_printf(feed_ids, "%" PRIu64 ", ", fid);
uint64_t *fidp = g_slice_alloc(sizeof(*fidp));
*fidp = fid;
g_queue_push_tail(&ret_streams, fidp);
}
json_reader_end_member(reader);
json_reader_end_element(reader);
}
}
json_reader_end_member(reader);
*retcode = 456;
if (!srcs.length)
return "No feeds to subscribe to given";
struct call_monologue *dest_ml = janus_get_monologue(handle->id, call,
call_get_or_create_monologue);
AUTO_CLEANUP(struct sdp_ng_flags flags, call_ng_free_flags);
call_ng_flags_init(&flags, OP_REQUEST);
flags.generate_mid = 1;
flags.rtcp_mirror = 1;
if (!plain_offer) {
// set all WebRTC-specific attributes
flags.transport_protocol = &transport_protocols[PROTO_UDP_TLS_RTP_SAVPF];
flags.ice_option = ICE_FORCE;
flags.trickle_ice = 1;
flags.rtcp_mux_offer = 1;
flags.rtcp_mux_require = 1;
flags.no_rtcp_attr = 1;
flags.sdes_off = 1;
}
else {
flags.transport_protocol = &transport_protocols[PROTO_RTP_AVP];
flags.ice_option = ICE_REMOVE;
flags.rtcp_mux_demux = 1;
}
int ret = monologue_subscribe_request(&srcs, dest_ml, &flags);
if (ret)
return "Subscribe error";
// create SDP: if there's only one subscription, we can use the original
// SDP, otherwise we generate a new one
if (srcs.length == 1) {
struct call_subscription *cs = srcs.head->data;
struct call_monologue *source_ml = cs->monologue;
struct sdp_chopper *chopper = sdp_chopper_new(&source_ml->last_in_sdp);
ret = sdp_replace(chopper, &source_ml->last_in_sdp_parsed, dest_ml, &flags);
sdp_chopper_destroy_ret(chopper, jsep_sdp_out);
}
else
ret = sdp_create(jsep_sdp_out, dest_ml, &flags);
if (!dest_ml->janus_session)
dest_ml->janus_session = obj_get(session);
dequeue_sdp_fragments(dest_ml);
if (ret)
return "Error generating SDP";
*jsep_type_out = "offer";
}
handle->room = room_id;
// single or multiple feed IDs?
if (feed_id)
g_string_printf(feed_ids, "feed %" PRIu64, feed_id);
else if (feed_ids->len >= 2) // truncate trailing ", "
g_string_truncate(feed_ids, feed_ids->len - 2);
ilog(LOG_INFO, "Handle %" PRIu64 " has joined room %" PRIu64 " as %s (%s)",
handle->id, room_id,
is_pub ? "publisher" : "subscriber", feed_ids->str);
*successp = "event";
if (is_pub) {
json_builder_set_member_name(builder, "videoroom");
json_builder_add_string_value(builder, "joined");
json_builder_set_member_name(builder, "room");
json_builder_add_int_value(builder, room_id);
json_builder_set_member_name(builder, "id");
json_builder_add_int_value(builder, feed_id);
json_builder_set_member_name(builder, "publishers");
janus_publishers_list(builder, call, room, feed_id);
}
else {
// subscriber
json_builder_set_member_name(builder, "videoroom");
json_builder_add_string_value(builder, "attached");
json_builder_set_member_name(builder, "room");
json_builder_add_int_value(builder, room_id);
// output format: single feed ID or multiple?
if (feed_id) {
json_builder_set_member_name(builder, "id");
json_builder_add_int_value(builder, feed_id);
}
else {
json_builder_set_member_name(builder, "streams");
json_builder_begin_array(builder);
uint64_t idx = 0;
for (GList *l = ret_streams.head; l; l = l->next) {
uint64_t *fidp = l->data;
json_builder_begin_object(builder);
json_builder_set_member_name(builder, "mindex");
json_builder_add_int_value(builder, idx++);
json_builder_set_member_name(builder, "feed_id");
json_builder_add_int_value(builder, *fidp);
json_builder_end_object(builder);
}
json_builder_end_array(builder);
}
}
return NULL;
}
// callback function for janus_notify_publishers()
static void janus_notify_publishers_joined(JsonBuilder *event, void *ptr, uint64_t u64, struct janus_room *room,
uint64_t publisher_feed)
{
json_builder_set_member_name(event, "publishers");
janus_publishers_list(event, ptr, room, publisher_feed);
}
// callback function for janus_notify_publishers()
static void janus_notify_publishers_unpublished(JsonBuilder *event, void *ptr, uint64_t u64,
struct janus_room *room, uint64_t publisher_feed)
{
json_builder_set_member_name(event, "unpublished");
json_builder_add_int_value(event, u64);
}
// callback function for janus_notify_publishers()
static void janus_notify_publishers_leaving(JsonBuilder *event, void *ptr, uint64_t u64, struct janus_room *room,
uint64_t publisher_feed)
{
json_builder_set_member_name(event, "leaving");
json_builder_add_int_value(event, u64);
}
// global janus_lock is held
static void janus_notify_publishers(uint64_t room_id, uint64_t except, void *ptr, uint64_t u64,
void (*callback)(JsonBuilder *event, void *ptr, uint64_t u64, struct janus_room *room,
uint64_t publisher_feed))
{
struct janus_room *room = g_hash_table_lookup(janus_rooms, &room_id);
if (!room)
return;
GHashTableIter iter;
gpointer key, value;
g_hash_table_iter_init(&iter, room->publishers);
while (g_hash_table_iter_next(&iter, &key, &value)) {
uint64_t *handle_id = key;
if (*handle_id == except)
continue;
// look up the handle and determine which session it belongs to
struct janus_handle *handle = g_hash_table_lookup(janus_handles, handle_id);
if (!handle)
continue;
if (!handle->session)
continue;
// send to the handle's session
uint64_t *feed_id = value;
JsonBuilder *event = json_builder_new();
json_builder_begin_object(event); // {
json_builder_set_member_name(event, "janus");
json_builder_add_string_value(event, "event");
json_builder_set_member_name(event, "session_id");
json_builder_add_int_value(event, handle->session->id);
json_builder_set_member_name(event, "sender");
json_builder_add_int_value(event, handle->id); // destination of notification
json_builder_set_member_name(event, "plugindata");
json_builder_begin_object(event); // {
json_builder_set_member_name(event, "plugin");
json_builder_add_string_value(event, "janus.plugin.videoroom");
json_builder_set_member_name(event, "data");
json_builder_begin_object(event); // {
json_builder_set_member_name(event, "videoroom");
json_builder_add_string_value(event, "event");
json_builder_set_member_name(event, "room");
json_builder_add_int_value(event, room_id);
callback(event, ptr, u64, room, *feed_id);
json_builder_end_object(event); // }
json_builder_end_object(event); // }
json_builder_end_object(event); // }
janus_send_json_async(handle->session, event);
}
}
// global janus_lock is held
static const char *janus_videoroom_configure(struct websocket_message *wm, struct janus_session *session,
const char *jsep_type, const char *jsep_sdp,
const char *transaction,
struct janus_handle *handle, JsonBuilder *builder, JsonReader *reader, const char **successp,
int *retcode,
char **jsep_type_out, str *jsep_sdp_out,
uint64_t room_id)
{
janus_send_ack(wm, transaction, session);
*retcode = 456;
if (!room_id)
room_id = handle->room;
if (!room_id)
return "JSON object does not contain 'message.room' key";
int has_audio = -1; // tri-state -1/0/1
if (json_reader_read_member(reader, "audio"))
has_audio = !!json_reader_get_boolean_value(reader); // 0/1
json_reader_end_member(reader);
int has_video = -1; // tri-state -1/0/1
if (json_reader_read_member(reader, "video"))
has_video = !!json_reader_get_boolean_value(reader); // 0/1
json_reader_end_member(reader);
// exit "body"
json_reader_end_member(reader);
*retcode = 512;
if (handle->room != room_id)
return "Not in the room";
AUTO_CLEANUP_NULL(struct call *call, call_unlock_release);
struct janus_room *room = g_hash_table_lookup(janus_rooms, &room_id);
*retcode = 426;
if (!room)
return "No such room";
call = call_get(&room->call_id);
// XXX if call is destroyed separately, room persists -> room should be destroyed too
if (!call)
return "No such room";
*retcode = 512;
if (!g_hash_table_lookup(room->publishers, &handle->id))
return "Not a publisher";
struct call_monologue *ml = NULL;
if (jsep_type && jsep_sdp) {
if (strcmp(jsep_type, "offer"))
return "Not an offer";
AUTO_CLEANUP(str sdp_in, str_free_dup) = STR_INIT_DUP(jsep_sdp);
AUTO_CLEANUP(struct sdp_ng_flags flags, call_ng_free_flags);
AUTO_CLEANUP(GQueue parsed, sdp_free) = G_QUEUE_INIT;
AUTO_CLEANUP(GQueue streams, sdp_streams_free) = G_QUEUE_INIT;
call_ng_flags_init(&flags, OP_PUBLISH);
*retcode = 512;
if (sdp_parse(&sdp_in, &parsed, &flags))
return "Failed to parse SDP";
if (sdp_streams(&parsed, &streams, &flags))
return "Incomplete SDP specification";
ml = janus_get_monologue(handle->id, call, call_get_or_create_monologue);
// accept unsupported codecs if necessary
flags.accept_any = 1;
int ret = monologue_publish(ml, &streams, &flags);
if (ret)
return "Publish error";
// XXX check there's only one audio and one video stream?
AUTO_CLEANUP(str sdp_out, str_free_dup) = STR_NULL;
ret = sdp_create(&sdp_out, ml, &flags);
if (ret)
return "Publish error";
if (!ml->janus_session)
ml->janus_session = obj_get(session);
save_last_sdp(ml, &sdp_in, &parsed, &streams);
*jsep_sdp_out = sdp_out;
sdp_out = STR_NULL; // ownership passed to output
dequeue_sdp_fragments(ml);
*jsep_type_out = "answer";
}
else {
// reconfigure existing publisher
ml = janus_get_monologue(handle->id, call, call_get_monologue);
if (!ml)
return "Not an existing publisher";
}
*successp = "event";
json_builder_set_member_name(builder, "videoroom");
json_builder_add_string_value(builder, "event");
json_builder_set_member_name(builder, "room");
json_builder_add_int_value(builder, room_id);
json_builder_set_member_name(builder, "configured");
json_builder_add_string_value(builder, "ok");
// apply audio/video bool flags
for (unsigned int i = 0; i < ml->medias->len; i++) {
struct call_media *media = ml->medias->pdata[i];
if (!media)
continue;
if (media->type_id == MT_AUDIO) {
if (has_audio == 0)
MEDIA_CLEAR(media, RECV);
else if (has_audio == 1)
MEDIA_SET(media, RECV);
}
else if (media->type_id == MT_VIDEO) {
if (has_video == 0)
MEDIA_CLEAR(media, RECV);
else if (has_video == 1)
MEDIA_SET(media, RECV);
}
}
janus_add_publisher_details(builder, ml);
janus_notify_publishers(room_id, handle->id, call, 0, janus_notify_publishers_joined);
return NULL;
}
// global janus_lock is held
static const char *janus_videoroom_start(struct websocket_message *wm, struct janus_session *session,
const char *jsep_type, const char *jsep_sdp,
const char *transaction,
struct janus_handle *handle, JsonBuilder *builder, JsonReader *reader, const char **successp,
int *retcode,
uint64_t room_id)
{
janus_send_ack(wm, transaction, session);
*retcode = 456;
if (!room_id)
return "JSON object does not contain 'message.room' key";
json_reader_end_member(reader);
if (handle->room != room_id)
return "Not in the room";
if (!jsep_type || !jsep_sdp)
return "No SDP";
if (strcmp(jsep_type, "answer"))
return "Not an answer";
AUTO_CLEANUP(str sdp_in, str_free_dup) = STR_INIT_DUP(jsep_sdp);
AUTO_CLEANUP(struct sdp_ng_flags flags, call_ng_free_flags);
AUTO_CLEANUP(GQueue parsed, sdp_free) = G_QUEUE_INIT;
AUTO_CLEANUP(GQueue streams, sdp_streams_free) = G_QUEUE_INIT;
call_ng_flags_init(&flags, OP_PUBLISH);
*retcode = 512;
if (sdp_parse(&sdp_in, &parsed, &flags))
return "Failed to parse SDP";
if (sdp_streams(&parsed, &streams, &flags))
return "Incomplete SDP specification";
AUTO_CLEANUP_NULL(struct call *call, call_unlock_release);
struct janus_room *room = g_hash_table_lookup(janus_rooms, &room_id);
*retcode = 426;
if (!room)
return "No such room";
call = call_get(&room->call_id);
if (!call)
return "No such room";
*retcode = 456;
uint64_t *feed_id = g_hash_table_lookup(room->subscribers, &handle->id);
if (!feed_id)
return "Not a subscriber";
*retcode = 512;
uint64_t *feed_handle = g_hash_table_lookup(room->feeds, feed_id);
if (!feed_handle)
return "No such feed exists";
struct call_monologue *source_ml = janus_get_monologue(*feed_handle, call, call_get_monologue);
if (!source_ml)
return "Feed not found";
// XXX verify that dest_ml is subscribed to source_ml
struct call_monologue *dest_ml = janus_get_monologue(handle->id, call, call_get_monologue);
if (!dest_ml)
return "Subscriber not found";
int ret = monologue_subscribe_answer(dest_ml, &flags, &streams);
if (ret)
return "Failed to process subscription answer";
*successp = "event";
json_builder_set_member_name(builder, "videoroom");
json_builder_add_string_value(builder, "event");
json_builder_set_member_name(builder, "room");
json_builder_add_int_value(builder, room_id);
json_builder_set_member_name(builder, "started");
json_builder_add_string_value(builder, "ok");
return NULL;
}
// global janus_lock is held
static const char *janus_videoroom_unpublish(struct websocket_message *wm, struct janus_session *session,
const char *transaction,
struct janus_handle *handle, JsonBuilder *builder, const char **successp,
int *retcode)
{
janus_send_ack(wm, transaction, session);
// get all our info
uint64_t room_id = handle->room;
*retcode = 512;
if (!room_id)
return "Not in any room";
struct janus_room *room = NULL;
if (room_id)
room = g_hash_table_lookup(janus_rooms, &room_id);
*retcode = 426;
if (!room)
return "No such room";
AUTO_CLEANUP_NULL(struct call *call, call_unlock_release);
call = call_get(&room->call_id);
if (!call)
return "No such room";
uint64_t *feed_id = g_hash_table_lookup(room->publishers, &handle->id);
*retcode = 512;
if (!feed_id)
return "Not a publisher";
// all is ok
// notify other publishers
janus_notify_publishers(room_id, handle->id, NULL, *feed_id, janus_notify_publishers_unpublished);
struct call_monologue *ml = janus_get_monologue(handle->id, call, call_get_monologue);
if (ml)
monologue_destroy(ml);
*successp = "event";
json_builder_set_member_name(builder, "videoroom");
json_builder_add_string_value(builder, "event");
json_builder_set_member_name(builder, "room");
json_builder_add_int_value(builder, room_id);
json_builder_set_member_name(builder, "unpublished");
json_builder_add_string_value(builder, "ok");
return NULL;
}
// global janus_lock is held
// TODO: more granular locking
static const char *janus_videoroom(struct websocket_message *wm, struct janus_session *session,
const char *jsep_type, const char *jsep_sdp,
const char *transaction,
struct janus_handle *handle, JsonBuilder *builder, JsonReader *reader, const char **successp,
int *retcodep, char **jsep_type_out, str *jsep_sdp_out)
{
uint64_t room_id = 0;
if (json_reader_read_member(reader, "room"))
room_id = jr_str_int(reader);
json_reader_end_member(reader);
int retcode = 456;
const char *err = "JSON object does not contain 'message.request' key";
if (!json_reader_read_member(reader, "request"))
goto err;
const char *req = json_reader_get_string_value(reader);
if (!req)
goto err;
str req_str = STR_INIT((char*) req);
json_reader_end_member(reader);
switch (__csh_lookup(&req_str)) {
case CSH_LOOKUP("create"):
err = janus_videoroom_create(session, handle, builder, reader, &retcode);
break;
case CSH_LOOKUP("exists"):
err = janus_videoroom_exists(session, builder, room_id);
break;
case CSH_LOOKUP("destroy"):
err = janus_videoroom_destroy(session, builder, &retcode, room_id);
break;
case CSH_LOOKUP("join"):
err = janus_videoroom_join(wm, session, transaction, handle, builder, reader, successp,
&retcode, jsep_type_out, jsep_sdp_out, room_id);
break;
case CSH_LOOKUP("configure"):
err = janus_videoroom_configure(wm, session, jsep_type, jsep_sdp, transaction,
handle, builder, reader, successp, &retcode, jsep_type_out, jsep_sdp_out,
room_id);
break;
case CSH_LOOKUP("start"):
err = janus_videoroom_start(wm, session, jsep_type, jsep_sdp, transaction,
handle, builder, reader, successp,
&retcode, room_id);
break;
case CSH_LOOKUP("unpublish"):
err = janus_videoroom_unpublish(wm, session, transaction,
handle, builder, successp,
&retcode);
break;
default:
retcode = 423;
err = "Unknown videoroom request";
break;
}
err:
if (err)
*retcodep = retcode;
return err;
}
static const char *janus_add_token(JsonReader *reader, JsonBuilder *builder, bool authorised, int *retcode) {
*retcode = 403;
if (!authorised)
return "Janus 'admin_secret' key not provided or incorrect";
const char *token = NULL;
if (json_reader_read_member(reader, "token"))
token = json_reader_get_string_value(reader);
json_reader_end_member(reader);
*retcode = 456;
if (!token)
return "JSON object does not contain 'token' key";
time_t *now = g_malloc(sizeof(*now));
*now = rtpe_now.tv_sec;
mutex_lock(&janus_lock);
g_hash_table_replace(janus_tokens, g_strdup(token), now);
mutex_unlock(&janus_lock);
json_builder_set_member_name(builder, "data");
json_builder_begin_object(builder); // {
json_builder_set_member_name(builder, "plugins");
json_builder_begin_array(builder); // [
json_builder_add_string_value(builder, "janus.plugin.videoroom");
json_builder_end_array(builder); // ]
json_builder_end_object(builder); // }
return NULL;
}
static const char *janus_create(JsonReader *reader, JsonBuilder *builder, struct websocket_message *wm) {
if (wm->method != M_WEBSOCKET)
return "Unsupported transport protocol";
uint64_t session_id = 0;
if (json_reader_read_member(reader, "id"))
session_id = jr_str_int(reader);
json_reader_end_member(reader);
struct janus_session *session = obj_alloc0("janus_session", sizeof(*session), __janus_session_free);
mutex_init(&session->lock);
mutex_lock(&session->lock); // not really necessary but Coverity complains
session->last_act = rtpe_now.tv_sec;
session->websockets = g_hash_table_new(g_direct_hash, g_direct_equal);
session->handles = g_hash_table_new(g_int64_hash, g_int64_equal);
g_hash_table_insert(session->websockets, wm->wc, wm->wc);
do {
while (!session_id)
session_id = janus_random();
mutex_lock(&janus_lock);
if (g_hash_table_lookup(janus_sessions, &session_id))
session_id = 0; // pick a random one
else {
session->id = session_id;
g_hash_table_insert(janus_sessions, &session->id, obj_get(session));
}
mutex_unlock(&janus_lock);
}
while (!session_id);
mutex_unlock(&session->lock);
ilog(LOG_INFO, "Created new Janus session with ID %" PRIu64, session_id);
websocket_conn_add_session(wm->wc, obj_get(session));
json_builder_set_member_name(builder, "data");
json_builder_begin_object(builder); // {
json_builder_set_member_name(builder, "id");
json_builder_add_int_value(builder, session_id);
json_builder_end_object(builder); // }
return NULL;
}
void janus_detach_websocket(struct janus_session *session, struct websocket_conn *wc) {
LOCK(&session->lock);
g_hash_table_remove(session->websockets, wc);
}
// call is locked in some way
void janus_rtc_up(struct call_monologue *ml) {
struct janus_session *session = ml->janus_session;
if (!session)
return;
// the monologue tag is the handle ID
uint64_t handle = str_to_ui(&ml->tag, 0);
if (!handle)
return;
// build json
JsonBuilder *builder = json_builder_new();
json_builder_begin_object(builder); // {
json_builder_set_member_name(builder, "janus");
json_builder_add_string_value(builder, "webrtcup");
json_builder_set_member_name(builder, "session_id");
json_builder_add_int_value(builder, session->id);
json_builder_set_member_name(builder, "sender");
json_builder_add_int_value(builder, handle);
json_builder_end_object(builder); // }
LOCK(&session->lock);
janus_send_json_async(session, builder);
}
// call is locked in some way
void janus_media_up(struct call_media *media) {
struct call_monologue *ml = media->monologue;
if (!ml)
return;
struct janus_session *session = ml->janus_session;
if (!session)
return;
// the monologue tag is the handle ID
uint64_t handle = str_to_ui(&ml->tag, 0);
if (!handle)
return;
// build json
JsonBuilder *builder = json_builder_new();
json_builder_begin_object(builder); // {
json_builder_set_member_name(builder, "janus");
json_builder_add_string_value(builder, "media");
json_builder_set_member_name(builder, "session_id");
json_builder_add_int_value(builder, session->id);
json_builder_set_member_name(builder, "sender");
json_builder_add_int_value(builder, handle);
json_builder_set_member_name(builder, "mid");
if (media->media_id.s)
json_builder_add_string_value(builder, media->media_id.s);
else
json_builder_add_null_value(builder);
json_builder_set_member_name(builder, "type");
json_builder_add_string_value(builder, media->type.s);
json_builder_set_member_name(builder, "receiving");
json_builder_add_boolean_value(builder, true);
json_builder_end_object(builder); // }
LOCK(&session->lock);
janus_send_json_async(session, builder);
}
static const char *janus_attach(JsonReader *reader, JsonBuilder *builder, struct janus_session *session,
int *retcode)
{
*retcode = 458;
if (!session)
return "Session ID not found";
// verify the plugin
*retcode = 456;
if (!json_reader_read_member(reader, "plugin"))
return "No plugin given";
const char *plugin = json_reader_get_string_value(reader);
if (!plugin)
return "No plugin given";
*retcode = 460;
if (strcmp(plugin, "janus.plugin.videoroom"))
return "Unsupported plugin";
json_reader_end_member(reader);
struct janus_handle *handle = g_slice_alloc0(sizeof(*handle));
mutex_lock(&janus_lock);
handle->session = obj_get(session);
uint64_t handle_id = 0;
while (1) {
handle_id = handle->id = janus_random();
if (g_hash_table_lookup(janus_handles, &handle->id))
continue;
g_hash_table_insert(janus_handles, &handle->id, handle);
break;
}
mutex_unlock(&janus_lock);
mutex_lock(&session->lock);
assert(g_hash_table_lookup(session->handles, &handle_id) == NULL);
g_hash_table_insert(session->handles, &handle->id, (void *) 0x1);
mutex_unlock(&session->lock);
json_builder_set_member_name(builder, "data");
json_builder_begin_object(builder); // {
json_builder_set_member_name(builder, "id");
json_builder_add_int_value(builder, handle_id);
json_builder_end_object(builder); // }
return NULL;
}
static void janus_destroy_handle(struct janus_handle *handle) {
uint64_t room_id = handle->room;
uint64_t handle_id = handle->id;
// destroy handle
if (handle->session)
obj_put(handle->session);
g_slice_free1(sizeof(*handle), handle);
if (!room_id)
return;
struct janus_room *room = g_hash_table_lookup(janus_rooms, &room_id);
if (!room)
return;
uint64_t *feed = g_hash_table_lookup(room->publishers, &handle_id);
if (feed) {
// was a publisher - send notifies
janus_notify_publishers(room_id, handle_id, NULL, *feed, janus_notify_publishers_unpublished);
janus_notify_publishers(room_id, handle_id, NULL, *feed, janus_notify_publishers_leaving);
struct call *call = call_get(&room->call_id);
if (call) {
// remove publisher monologue
struct call_monologue *ml = janus_get_monologue(handle_id, call, call_get_monologue);
if (ml)
monologue_destroy(ml);
rwlock_unlock_w(&call->master_lock);
obj_put(call);
}
g_hash_table_remove(room->publishers, &handle_id);
feed = NULL;
}
if (g_hash_table_remove(room->subscribers, &handle_id)) {
// was a subscriber
struct call *call = call_get(&room->call_id);
if (call) {
// remove subscriber monologue
struct call_monologue *ml = janus_get_monologue(handle_id, call, call_get_monologue);
if (ml)
monologue_destroy(ml);
rwlock_unlock_w(&call->master_lock);
obj_put(call);
}
}
}
static const char *janus_detach(struct websocket_message *wm, JsonReader *reader, JsonBuilder *builder,
struct janus_session *session,
uint64_t handle_id, int *retcode)
{
*retcode = 458;
if (!session)
return "Session ID not found";
*retcode = 457;
if (!handle_id)
return "Unhandled request method";
// remove handle from session first as the handle ID in the hash is owned by the
// janus_handle object, which is owned by janus_handles
{
LOCK(&session->lock);
bool exists = g_hash_table_remove(session->handles, &handle_id);
*retcode = 463;
if (!exists)
return "Could not detach handle from plugin";
}
LOCK(&janus_lock);
struct janus_handle *handle = NULL;
g_hash_table_steal_extended(janus_handles, &handle_id, NULL, (void **) &handle);
*retcode = 463;
if (!handle)
return "Could not detach handle from plugin";
if (handle->session != session) {
g_hash_table_insert(janus_handles, &handle->id, handle);
return "Invalid session/handle association";
}
janus_destroy_handle(handle);
return NULL;
}
// janus_lock must be held
static void janus_session_cleanup(struct janus_session *session) {
GHashTableIter iter;
g_hash_table_iter_init(&iter, session->handles);
gpointer key;
while (g_hash_table_iter_next(&iter, &key, NULL)) {
uint64_t *handle_id = key;
struct janus_handle *handle = NULL;
g_hash_table_steal_extended(janus_handles, handle_id, NULL, (void **) &handle);
if (!handle) // bug?
continue;
janus_destroy_handle(handle);
}
}
static const char *janus_destroy(struct websocket_message *wm, JsonReader *reader, JsonBuilder *builder,
struct janus_session *session,
int *retcode)
{
*retcode = 458;
if (!session)
return "Session ID not found";
LOCK(&janus_lock);
struct janus_session *ht_session = NULL;
g_hash_table_steal_extended(janus_sessions, &session->id, NULL, (void **) &ht_session);
if (ht_session != session)
return "Sesssion ID not found"; // already removed/destroyed
janus_session_cleanup(session);
obj_put(session);
return NULL;
}
static const char *janus_message(struct websocket_message *wm, JsonReader *reader, JsonBuilder *builder,
struct janus_session *session,
const char *transaction,
uint64_t handle_id,
const char **successp,
int *retcode)
{
// we only pretend to support one plugin so ignore the handle
// and just go straight to the message
*retcode = 458;
if (!session)
return "Session ID not found";
*retcode = 457;
if (!handle_id)
return "No plugin handle given";
const char *jsep_type = NULL, *jsep_sdp = NULL;
if (json_reader_read_member(reader, "jsep")) {
if (json_reader_read_member(reader, "type"))
jsep_type = json_reader_get_string_value(reader);
json_reader_end_member(reader);
if (json_reader_read_member(reader, "sdp"))
jsep_sdp = json_reader_get_string_value(reader);
json_reader_end_member(reader);
}
json_reader_end_member(reader);
*retcode = 456;
if (!json_reader_read_member(reader, "body"))
return "JSON object does not contain 'body' key";
json_builder_set_member_name(builder, "plugindata");
json_builder_begin_object(builder); // {
json_builder_set_member_name(builder, "plugin");
json_builder_add_string_value(builder, "janus.plugin.videoroom");
json_builder_set_member_name(builder, "data");
json_builder_begin_object(builder); // {
char *jsep_type_out = NULL;
str jsep_sdp_out = STR_NULL;
LOCK(&janus_lock);
struct janus_handle *handle = g_hash_table_lookup(janus_handles, &handle_id);
const char *err = NULL;
if (!handle || handle->session != session) {
*retcode = 457;
err = "No plugin handle given or invalid handle";
}
else
err = janus_videoroom(wm, session, jsep_type, jsep_sdp, transaction, handle,
builder, reader, successp, retcode, &jsep_type_out,
&jsep_sdp_out);
json_builder_end_object(builder); // }
json_builder_end_object(builder); // }
if (jsep_type_out && jsep_sdp_out.len) {
json_builder_set_member_name(builder, "jsep");
json_builder_begin_object(builder); // {
json_builder_set_member_name(builder, "type");
json_builder_add_string_value(builder, jsep_type_out);
json_builder_set_member_name(builder, "sdp");
json_builder_add_string_value(builder, jsep_sdp_out.s);
json_builder_end_object(builder); // }
}
str_free_dup(&jsep_sdp_out);
return err;
}
static const char *janus_trickle(JsonReader *reader, struct janus_session *session, uint64_t handle_id,
const char **successp, int *retcode)
{
*retcode = 458;
if (!session)
return "Session ID not found";
*retcode = 457;
if (!handle_id)
return "Unhandled request method";
*retcode = 456;
if (!json_reader_read_member(reader, "candidate"))
return "JSON object does not contain 'candidate' key";
const char *candidate = NULL;
if (json_reader_read_member(reader, "candidate"))
candidate = json_reader_get_string_value(reader);
json_reader_end_member(reader);
if (!candidate) {
if (json_reader_read_member(reader, "completed")) {
*successp = "ack";
return NULL;
}
return "ICE candidate string missing";
}
const char *ufrag = NULL;
if (json_reader_read_member(reader, "usernameFragment"))
ufrag = json_reader_get_string_value(reader);
json_reader_end_member(reader);
const char *sdp_mid = NULL;
int64_t sdp_m_line = -1;
if (json_reader_read_member(reader, "sdpMid"))
sdp_mid = json_reader_get_string_value(reader);
json_reader_end_member(reader);
if (json_reader_read_member(reader, "sdpMLineIndex")) {
// make sure what we're reading is an int
JsonNode *node = json_reader_get_value(reader);
if (node && json_node_get_value_type(node) == G_TYPE_INT64)
sdp_m_line = json_node_get_int(node);
}
json_reader_end_member(reader);
json_reader_end_member(reader);
if (!sdp_mid && sdp_m_line < 0)
return "Neither sdpMid nor sdpMLineIndex given";
// fetch call
AUTO_CLEANUP_GBUF(call_id);
AUTO_CLEANUP_NULL(struct call *call, call_unlock_release);
{
LOCK(&janus_lock);
struct janus_handle *handle = g_hash_table_lookup(janus_handles, &handle_id);
if (!handle || !handle->room || handle->session != session)
return "Unhandled request method";
call_id = janus_call_id(handle->room);
struct janus_room *room = g_hash_table_lookup(janus_rooms, &handle->room);
if (room)
call = call_get(&room->call_id);
}
// set up "streams" structures to use an trickle ICE update. these must be
// allocated in case of delayed trickle ICE updates. it's using a refcounted
// ng_buffer as storage.
*successp = "ack";
// top-level structures first, with auto cleanup
AUTO_CLEANUP(GQueue streams, sdp_streams_free) = G_QUEUE_INIT;
AUTO_CLEANUP(struct ng_buffer *ngbuf, ng_buffer_auto_release) = ng_buffer_new(NULL);
AUTO_CLEANUP(struct sdp_ng_flags flags, call_ng_free_flags);
call_ng_flags_init(&flags, OP_OTHER);
// then the contained structures, and add them in
struct stream_params *sp = g_slice_alloc0(sizeof(*sp));
g_queue_push_tail(&streams, sp);
struct ice_candidate *cand = g_slice_alloc0(sizeof(*cand));
g_queue_push_tail(&sp->ice_candidates, cand);
// allocate and parse candidate
str cand_str;
bencode_strdup_str(&ngbuf->buffer, &cand_str, candidate);
str_shift_cmp(&cand_str, "candidate:"); // skip prefix
if (!cand_str.len) // end of candidates
return NULL;
*retcode = 466;
int ret = sdp_parse_candidate(cand, &cand_str);
if (ret < 0)
return "Failed to parse trickle candidate";
if (ret > 0)
return NULL; // unsupported candidate type, accept and ignore it
// set required signalling flags
flags.fragment = 1;
AUTO_CLEANUP_GBUF(handle_buf);
handle_buf = g_strdup_printf("%" PRIu64, handle_id);
bencode_strdup_str(&ngbuf->buffer, &flags.from_tag, handle_buf);
bencode_strdup_str(&ngbuf->buffer, &flags.call_id, call_id);
// populate and allocate a=mid
if (sdp_mid)
bencode_strdup_str(&ngbuf->buffer, &sp->media_id, sdp_mid);
// check m= line index
if (sdp_m_line >= 0)
sp->index = sdp_m_line + 1;
// ufrag can be given in-line or separately
sp->ice_ufrag = cand->ufrag;
if (!sp->ice_ufrag.len && ufrag)
bencode_strdup_str(&ngbuf->buffer, &sp->ice_ufrag, ufrag);
// finally do the update
trickle_ice_update(ngbuf, call, &flags, &streams);
return NULL;
}
static const char *janus_server_info(JsonBuilder *builder) {
json_builder_set_member_name(builder, "name");
json_builder_add_string_value(builder, "rtpengine Janus interface");
json_builder_set_member_name(builder, "version_string");
json_builder_add_string_value(builder, RTPENGINE_VERSION);
json_builder_set_member_name(builder, "plugins");
json_builder_begin_object(builder); // {
json_builder_set_member_name(builder, "janus.plugin.videoroom");
json_builder_begin_object(builder); // {
json_builder_set_member_name(builder, "name");
json_builder_add_string_value(builder, "rtpengine Janus videoroom");
json_builder_end_object(builder); // }
json_builder_end_object(builder); // }
return "server_info";
}
static void janus_finish_response(JsonBuilder *builder, const char *success, const char *err, int retcode) {
json_builder_set_member_name(builder, "janus");
if (err) {
json_builder_add_string_value(builder, "error");
json_builder_set_member_name(builder, "error");
json_builder_begin_object(builder); // {
json_builder_set_member_name(builder, "code");
json_builder_add_int_value(builder, retcode);
json_builder_set_member_name(builder, "reason");
json_builder_add_string_value(builder, err);
json_builder_end_object(builder); // }
ilog(LOG_WARN, "Janus processing returning error (code %i): %s", retcode, err);
}
else
json_builder_add_string_value(builder, success);
}
static const char *websocket_janus_process_json(struct websocket_message *wm,
uint64_t session_id, uint64_t handle_id)
{
JsonParser *parser = NULL;
JsonReader *reader = NULL;
const char *err = NULL;
int retcode = 200;
const char *transaction = NULL;
const char *success = "success";
struct janus_session *session = NULL;
ilog(LOG_DEBUG, "Processing Janus message: '%.*s'", (int) wm->body->len, wm->body->str);
// prepare response
JsonBuilder *builder = json_builder_new();
json_builder_begin_object(builder); // {
// start parsing message
parser = json_parser_new();
retcode = 454;
err = "Failed to parse JSON";
if (!json_parser_load_from_data(parser, wm->body->str, wm->body->len, NULL))
goto err;
reader = json_reader_new(json_parser_get_root(parser));
if (!reader)
goto err;
retcode = 455;
err = "JSON string is not an object";
if (!json_reader_is_object(reader))
goto err;
retcode = 456;
err = "JSON object does not contain 'janus' key";
if (!json_reader_read_member(reader, "janus"))
goto err;
const char *janus_cmd = json_reader_get_string_value(reader);
err = "'janus' key does not contain a string";
if (!janus_cmd)
goto err;
json_reader_end_member(reader);
retcode = 456;
err = "JSON object does not contain 'transaction' key";
if (!json_reader_read_member(reader, "transaction"))
goto err;
transaction = json_reader_get_string_value(reader);
err = "'transaction' key does not contain a string";
json_reader_end_member(reader);
bool authorised = false;
if (json_reader_read_member(reader, "admin_secret")) {
const char *admin_secret = json_reader_get_string_value(reader);
if (janus_cmd && rtpe_config.janus_secret && !strcmp(admin_secret, rtpe_config.janus_secret))
authorised = true;
}
json_reader_end_member(reader);
if (json_reader_read_member(reader, "session_id"))
session_id = jr_str_int(reader);
json_reader_end_member(reader);
if (session_id)
session = janus_get_session(session_id);
if (json_reader_read_member(reader, "handle_id"))
handle_id = jr_str_int(reader);
json_reader_end_member(reader);
ilog(LOG_DEBUG, "Processing '%s' type Janus message", janus_cmd);
str janus_cmd_str = STR_INIT((char*) janus_cmd);
err = NULL;
switch (__csh_lookup(&janus_cmd_str)) {
case CSH_LOOKUP("add_token"):
err = janus_add_token(reader, builder, authorised, &retcode);
break;
case CSH_LOOKUP("ping"):
success = "pong";
break;
case CSH_LOOKUP("keepalive"):
if (!session) {
retcode = 458;
err = "Session ID not found";
}
else
success = "ack";
break;
case CSH_LOOKUP("info"):
success = janus_server_info(builder);
break;
case CSH_LOOKUP("get_status"):
// dummy output
json_builder_set_member_name(builder, "status");
json_builder_begin_object(builder);
json_builder_set_member_name(builder, "token_auth");
json_builder_add_boolean_value(builder, false);
json_builder_end_object(builder);
break;
case CSH_LOOKUP("list_sessions"):
// dummy output
json_builder_set_member_name(builder, "sessions");
json_builder_begin_array(builder);
json_builder_end_array(builder);
break;
case CSH_LOOKUP("create"): // create new session
err = janus_create(reader, builder, wm);
session_id = 0; // don't add it to the reply
break;
case CSH_LOOKUP("attach"): // attach to a plugin, obtains handle
err = janus_attach(reader, builder, session, &retcode);
break;
case CSH_LOOKUP("detach"):
err = janus_detach(wm, reader, builder, session, handle_id, &retcode);
break;
case CSH_LOOKUP("destroy"): // destroy session
err = janus_destroy(wm, reader, builder, session, &retcode);
break;
case CSH_LOOKUP("message"):
err = janus_message(wm, reader, builder, session, transaction, handle_id, &success,
&retcode);
break;
case CSH_LOOKUP("trickle"):
err = janus_trickle(reader, session, handle_id, &success, &retcode);
handle_id = 0; // don't include sender
break;
default:
retcode = 457;
err = "Unhandled request method";
goto err;
}
// done
err:
janus_finish_response(builder, success, err, retcode);
if (transaction) {
json_builder_set_member_name(builder, "transaction");
json_builder_add_string_value(builder, transaction);
}
if (session_id) {
json_builder_set_member_name(builder, "session_id");
json_builder_add_int_value(builder, session_id);
}
if (handle_id) {
json_builder_set_member_name(builder, "sender");
json_builder_add_int_value(builder, handle_id);
}
json_builder_end_object(builder); // }
janus_send_json_sync_response(wm, builder, 200);
if (reader)
g_object_unref(reader);
if (parser)
g_object_unref(parser);
if (session)
obj_put(session);
return NULL;
}
const char *websocket_janus_process(struct websocket_message *wm) {
return websocket_janus_process_json(wm, 0, 0);
}
const char *websocket_janus_get(struct websocket_message *wm) {
str uri = STR_INIT(wm->uri);
ilog(LOG_DEBUG, "Processing Janus GET: '%s'", wm->uri);
JsonBuilder *builder = json_builder_new();
json_builder_begin_object(builder); // {
int retcode = 200;
const char *success = "success";
const char *err = NULL;
switch (__csh_lookup(&uri)) {
case CSH_LOOKUP("/admin/info"):
success = janus_server_info(builder);
break;
default:
retcode = 457;
err = "Unhandled request method";
break;
}
janus_finish_response(builder, success, err, retcode);
json_builder_end_object(builder); // }
janus_send_json_sync_response(wm, builder, 200);
return NULL;
}
const char *websocket_janus_post(struct websocket_message *wm) {
str uri = STR_INIT(wm->uri);
ilog(LOG_DEBUG, "Processing Janus POST: '%s'", wm->uri);
uint64_t session_id = 0;
uint64_t handle_id = 0;
str_shift_cmp(&uri, "/");
// parse out session ID and handle ID if given
str s;
if (str_token_sep(&s, &uri, '/'))
goto done;
if (str_cmp(&s, "janus"))
goto done;
if (str_token_sep(&s, &uri, '/'))
goto done;
session_id = str_to_ui(&s, 0);
if (str_token_sep(&s, &uri, '/'))
goto done;
handle_id = str_to_ui(&s, 0);
done:
return websocket_janus_process_json(wm, session_id, handle_id);
}
void janus_init(void) {
mutex_init(&janus_lock);
janus_tokens = g_hash_table_new_full(g_str_hash, g_str_equal, g_free, g_free);
janus_sessions = g_hash_table_new(g_int64_hash, g_int64_equal);
janus_handles = g_hash_table_new(g_int64_hash, g_int64_equal);
janus_rooms = g_hash_table_new(g_int64_hash, g_int64_equal);
// XXX timer thread to clean up orphaned sessions
}
void janus_free(void) {
mutex_destroy(&janus_lock);
g_hash_table_destroy(janus_tokens);
g_hash_table_destroy(janus_sessions);
g_hash_table_destroy(janus_handles);
g_hash_table_destroy(janus_rooms);
}