#include "janus.h" #include #include #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); }