void janus_source_destroy_session(janus_plugin_session *handle, int *error) { if (g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) { *error = -1; return; } janus_source_session *session = (janus_source_session *)handle->plugin_handle; if (!session) { JANUS_LOG(LOG_ERR, "No session associated with this handle...\n"); *error = -2; return; } JANUS_LOG(LOG_VERB, "Removing Source Plugin session...\n"); janus_source_close_session(session); janus_mutex_lock(&sessions_mutex); if (!session->destroyed) { session->destroyed = janus_get_monotonic_time(); g_hash_table_remove(sessions, handle); /* Cleaning up and removing the session is done in a lazy way */ old_sessions = g_list_append(old_sessions, session); } janus_mutex_unlock(&sessions_mutex); return; }
void janus_videocall_destroy_session(janus_plugin_session *handle, int *error) { if(g_atomic_int_get(&stopping) || !g_atomic_int_get(&initialized)) { *error = -1; return; } janus_videocall_session *session = (janus_videocall_session *)handle->plugin_handle; if(!session) { JANUS_LOG(LOG_ERR, "No VideoCall session associated with this handle...\n"); *error = -2; return; } if(session->destroyed) { JANUS_LOG(LOG_VERB, "VideoCall session already destroyed...\n"); return; } JANUS_LOG(LOG_VERB, "Removing VideoCall user %s session...\n", session->username ? session->username : "******"); janus_videocall_hangup_media(handle); if(session->username != NULL) { janus_mutex_lock(&sessions_mutex); int res = g_hash_table_remove(sessions, (gpointer)session->username); JANUS_LOG(LOG_VERB, " -- Removed: %d\n", res); janus_mutex_unlock(&sessions_mutex); } /* Cleaning up and removing the session is done in a lazy way */ session->destroyed = janus_get_monotonic_time(); janus_mutex_lock(&sessions_mutex); old_sessions = g_list_append(old_sessions, session); janus_mutex_unlock(&sessions_mutex); return; }
void *janus_echotest_watchdog(void *data) { JANUS_LOG(LOG_INFO, "Echotest watchdog started\n"); gint64 now = 0; while(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) { janus_mutex_lock(&sessions_mutex); /* Iterate on all the sessions */ now = janus_get_monotonic_time(); if(old_sessions != NULL) { GList *sl = old_sessions; JANUS_LOG(LOG_VERB, "Checking %d old sessions\n", g_list_length(old_sessions)); while(sl) { janus_echotest_session *session = (janus_echotest_session *)sl->data; if(!session) { sl = sl->next; continue; } if(now-session->destroyed >= 5*G_USEC_PER_SEC) { /* We're lazy and actually get rid of the stuff only after a few seconds */ GList *rm = sl->next; old_sessions = g_list_delete_link(old_sessions, sl); sl = rm; session->handle = NULL; g_free(session); session = NULL; continue; } sl = sl->next; } } janus_mutex_unlock(&sessions_mutex); g_usleep(2000000); } JANUS_LOG(LOG_INFO, "EchoTest watchdog stopped\n"); return NULL; }
gboolean janus_dtls_retry(gpointer stack) { janus_dtls_srtp *dtls = (janus_dtls_srtp *)stack; if(dtls == NULL) return FALSE; janus_ice_component *component = (janus_ice_component *)dtls->component; if(component == NULL) return FALSE; janus_ice_stream *stream = component->stream; if(!stream) goto stoptimer; janus_ice_handle *handle = stream->handle; if(!handle) goto stoptimer; if(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP)) goto stoptimer; if(dtls->dtls_state == JANUS_DTLS_STATE_CONNECTED) { JANUS_LOG(LOG_VERB, "[%"SCNu64"] DTLS already set up, disabling retransmission timer!\n", handle->handle_id); goto stoptimer; } if(janus_get_monotonic_time() - dtls->dtls_started >= 20*G_USEC_PER_SEC) { /* FIXME Should we really give up after 20 seconds waiting for DTLS? */ JANUS_LOG(LOG_ERR, "[%"SCNu64"] DTLS taking too much time for component %d in stream %d...\n", handle->handle_id, component->component_id, stream->stream_id); janus_ice_webrtc_hangup(handle, "DTLS timeout"); goto stoptimer; } struct timeval timeout = {0}; DTLSv1_get_timeout(dtls->ssl, &timeout); guint64 timeout_value = timeout.tv_sec*1000 + timeout.tv_usec/1000; JANUS_LOG(LOG_HUGE, "[%"SCNu64"] DTLSv1_get_timeout: %"SCNu64"\n", handle->handle_id, timeout_value); if(timeout_value == 0) { dtls->retransmissions++; JANUS_LOG(LOG_VERB, "[%"SCNu64"] DTLS timeout on component %d of stream %d, retransmitting\n", handle->handle_id, component->component_id, stream->stream_id); /* Notify event handlers */ janus_dtls_notify_state_change(dtls); /* Retransmit the packet */ DTLSv1_handle_timeout(dtls->ssl); janus_dtls_fd_bridge(dtls); } return TRUE; stoptimer: if(component->dtlsrt_source != NULL) { g_source_destroy(component->dtlsrt_source); g_source_unref(component->dtlsrt_source); component->dtlsrt_source = NULL; } return FALSE; }
void janus_dtls_srtp_handshake(janus_dtls_srtp *dtls) { if(dtls == NULL || dtls->ssl == NULL) return; if(dtls->dtls_state == JANUS_DTLS_STATE_CREATED) { /* Starting the handshake now: enforce the role */ dtls->dtls_started = janus_get_monotonic_time(); if(dtls->dtls_role == JANUS_DTLS_ROLE_CLIENT) { SSL_set_connect_state(dtls->ssl); } else { SSL_set_accept_state(dtls->ssl); } dtls->dtls_state = JANUS_DTLS_STATE_TRYING; } SSL_do_handshake(dtls->ssl); janus_dtls_fd_bridge(dtls); /* Notify event handlers */ janus_dtls_notify_state_change(dtls); }
/* Thread to handle incoming messages */ static void *janus_serial_handler(void *data) { JANUS_LOG(LOG_VERB, "Joining Serial handler thread\n"); janus_serial_message *msg = NULL; int error_code = 0; char *error_cause = calloc(512, sizeof(char)); if(error_cause == NULL) { JANUS_LOG(LOG_FATAL, "Memory error!\n"); return NULL; } json_t *root = NULL; while(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) { if(!messages || (msg = g_async_queue_try_pop(messages)) == NULL) { usleep(50000); continue; } janus_serial_session *session = (janus_serial_session *)msg->handle->plugin_handle; if(!session) { JANUS_LOG(LOG_ERR, "No session associated with this handle...\n"); janus_serial_message_free(msg); continue; } if(session->destroyed) { janus_serial_message_free(msg); continue; } /* Handle request */ error_code = 0; root = NULL; JANUS_LOG(LOG_VERB, "Handling message: %s\n", msg->message); if(msg->message == NULL) { JANUS_LOG(LOG_ERR, "No message??\n"); error_code = JANUS_SERIAL_ERROR_NO_MESSAGE; g_snprintf(error_cause, 512, "%s", "No message??"); goto error; } json_error_t error; root = json_loads(msg->message, 0, &error); if(!root) { JANUS_LOG(LOG_ERR, "JSON error: on line %d: %s\n", error.line, error.text); error_code = JANUS_SERIAL_ERROR_INVALID_JSON; g_snprintf(error_cause, 512, "JSON error: on line %d: %s", error.line, error.text); goto error; } if(!json_is_object(root)) { JANUS_LOG(LOG_ERR, "JSON error: not an object\n"); error_code = JANUS_SERIAL_ERROR_INVALID_JSON; g_snprintf(error_cause, 512, "JSON error: not an object"); goto error; } /* Parse request */ json_t *audio = json_object_get(root, "audio"); if(audio && !json_is_boolean(audio)) { JANUS_LOG(LOG_ERR, "Invalid element (audio should be a boolean)\n"); error_code = JANUS_SERIAL_ERROR_INVALID_ELEMENT; g_snprintf(error_cause, 512, "Invalid value (audio should be a boolean)"); goto error; } json_t *video = json_object_get(root, "video"); if(video && !json_is_boolean(video)) { JANUS_LOG(LOG_ERR, "Invalid element (video should be a boolean)\n"); error_code = JANUS_SERIAL_ERROR_INVALID_ELEMENT; g_snprintf(error_cause, 512, "Invalid value (video should be a boolean)"); goto error; } json_t *bitrate = json_object_get(root, "bitrate"); if(bitrate && (!json_is_integer(bitrate) || json_integer_value(bitrate) < 0)) { JANUS_LOG(LOG_ERR, "Invalid element (bitrate should be a positive integer)\n"); error_code = JANUS_SERIAL_ERROR_INVALID_ELEMENT; g_snprintf(error_cause, 512, "Invalid value (bitrate should be a positive integer)"); goto error; } json_t *record = json_object_get(root, "record"); if(record && !json_is_boolean(record)) { JANUS_LOG(LOG_ERR, "Invalid element (record should be a boolean)\n"); error_code = JANUS_SERIAL_ERROR_INVALID_ELEMENT; g_snprintf(error_cause, 512, "Invalid value (record should be a boolean)"); goto error; } json_t *recfile = json_object_get(root, "filename"); if(recfile && !json_is_string(recfile)) { JANUS_LOG(LOG_ERR, "Invalid element (filename should be a string)\n"); error_code = JANUS_SERIAL_ERROR_INVALID_ELEMENT; g_snprintf(error_cause, 512, "Invalid value (filename should be a string)"); goto error; } /* Enforce request */ if(audio) { session->audio_active = json_is_true(audio); JANUS_LOG(LOG_VERB, "Setting audio property: %s\n", session->audio_active ? "true" : "false"); } if(video) { if(!session->video_active && json_is_true(video)) { /* Send a PLI */ JANUS_LOG(LOG_VERB, "Just (re-)enabled video, sending a PLI to recover it\n"); char buf[12]; memset(buf, 0, 12); janus_rtcp_pli((char *)&buf, 12); gateway->relay_rtcp(session->handle, 1, buf, 12); } session->video_active = json_is_true(video); JANUS_LOG(LOG_VERB, "Setting video property: %s\n", session->video_active ? "true" : "false"); } if(bitrate) { session->bitrate = json_integer_value(bitrate); JANUS_LOG(LOG_VERB, "Setting video bitrate: %"SCNu64"\n", session->bitrate); if(session->bitrate > 0) { /* FIXME Generate a new REMB (especially useful for Firefox, which doesn't send any we can cap later) */ char buf[24]; memset(buf, 0, 24); janus_rtcp_remb((char *)&buf, 24, session->bitrate); JANUS_LOG(LOG_VERB, "Sending REMB\n"); gateway->relay_rtcp(session->handle, 1, buf, 24); /* FIXME How should we handle a subsequent "no limit" bitrate? */ } } if(record) { if(msg->sdp) { session->has_audio = (strstr(msg->sdp, "m=audio") != NULL); session->has_video = (strstr(msg->sdp, "m=video") != NULL); } gboolean recording = json_is_true(record); const char *recording_base = json_string_value(recfile); JANUS_LOG(LOG_VERB, "Recording %s (base filename: %s)\n", recording ? "enabled" : "disabled", recording_base ? recording_base : "not provided"); if(!recording) { /* Not recording (anymore?) */ if(session->arc) { janus_recorder_close(session->arc); JANUS_LOG(LOG_INFO, "Closed audio recording %s\n", session->arc->filename ? session->arc->filename : "??"); janus_recorder_free(session->arc); } session->arc = NULL; if(session->vrc) { janus_recorder_close(session->vrc); JANUS_LOG(LOG_INFO, "Closed video recording %s\n", session->vrc->filename ? session->vrc->filename : "??"); janus_recorder_free(session->vrc); } session->vrc = NULL; } else { /* We've started recording, send a PLI and go on */ char filename[255]; gint64 now = janus_get_monotonic_time(); if(session->has_audio) { memset(filename, 0, 255); if(recording_base) { /* Use the filename and path we have been provided */ g_snprintf(filename, 255, "%s-audio", recording_base); session->arc = janus_recorder_create(NULL, 0, filename); if(session->arc == NULL) { /* FIXME We should notify the fact the recorder could not be created */ JANUS_LOG(LOG_ERR, "Couldn't open an audio recording file for this EchoTest user!\n"); } } else { /* Build a filename */ g_snprintf(filename, 255, "serial-%p-%"SCNi64"-audio", session, now); session->arc = janus_recorder_create(NULL, 0, filename); if(session->arc == NULL) { /* FIXME We should notify the fact the recorder could not be created */ JANUS_LOG(LOG_ERR, "Couldn't open an audio recording file for this EchoTest user!\n"); } } } if(session->has_video) { memset(filename, 0, 255); if(recording_base) { /* Use the filename and path we have been provided */ g_snprintf(filename, 255, "%s-video", recording_base); session->vrc = janus_recorder_create(NULL, 1, filename); if(session->vrc == NULL) { /* FIXME We should notify the fact the recorder could not be created */ JANUS_LOG(LOG_ERR, "Couldn't open an video recording file for this EchoTest user!\n"); } } else { /* Build a filename */ g_snprintf(filename, 255, "serial-%p-%"SCNi64"-video", session, now); session->vrc = janus_recorder_create(NULL, 1, filename); if(session->vrc == NULL) { /* FIXME We should notify the fact the recorder could not be created */ JANUS_LOG(LOG_ERR, "Couldn't open an video recording file for this EchoTest user!\n"); } } /* Send a PLI */ JANUS_LOG(LOG_VERB, "Recording video, sending a PLI to kickstart it\n"); char buf[12]; memset(buf, 0, 12); janus_rtcp_pli((char *)&buf, 12); gateway->relay_rtcp(session->handle, 1, buf, 12); } } } /* Any SDP to handle? */ if(msg->sdp) { JANUS_LOG(LOG_VERB, "This is involving a negotiation (%s) as well:\n%s\n", msg->sdp_type, msg->sdp); session->has_audio = (strstr(msg->sdp, "m=audio") != NULL); session->has_video = (strstr(msg->sdp, "m=video") != NULL); } /* if(!audio && !video && !bitrate && !record && !msg->sdp) { JANUS_LOG(LOG_ERR, "No supported attributes (audio, video, bitrate, record, jsep) found\n"); error_code = JANUS_SERIAL_ERROR_INVALID_ELEMENT; g_snprintf(error_cause, 512, "Message error: no supported attributes (audio, video, bitrate, record, jsep) found"); goto error; } */ json_decref(root); /* Prepare JSON event */ json_t *event = json_object(); json_object_set_new(event, "serial", json_string("event")); json_object_set_new(event, "result", json_string("ok")); char *event_text = json_dumps(event, JSON_INDENT(3) | JSON_PRESERVE_ORDER); json_decref(event); JANUS_LOG(LOG_VERB, "Pushing event: %s\n", event_text); if(!msg->sdp) { int ret = gateway->push_event(msg->handle, &janus_serial_plugin, msg->transaction, event_text, NULL, NULL); JANUS_LOG(LOG_VERB, " >> %d (%s)\n", ret, janus_get_api_error(ret)); } else { /* Forward the same offer to the gateway, to start the echo test */ const char *type = NULL; if(!strcasecmp(msg->sdp_type, "offer")) type = "answer"; if(!strcasecmp(msg->sdp_type, "answer")) type = "offer"; /* Any media direction that needs to be fixed? */ char *sdp = g_strdup(msg->sdp); if(strstr(sdp, "a=recvonly")) { /* Turn recvonly to inactive, as we simply bounce media back */ sdp = janus_string_replace(sdp, "a=recvonly", "a=inactive"); } else if(strstr(sdp, "a=sendonly")) { /* Turn sendonly to recvonly */ sdp = janus_string_replace(sdp, "a=sendonly", "a=recvonly"); /* FIXME We should also actually not echo this media back, though... */ } /* Make also sure we get rid of ULPfec, red, etc. */ if(strstr(sdp, "ulpfec")) { sdp = janus_string_replace(sdp, "100 116 117 96", "100"); sdp = janus_string_replace(sdp, "a=rtpmap:116 red/90000\r\n", ""); sdp = janus_string_replace(sdp, "a=rtpmap:117 ulpfec/90000\r\n", ""); sdp = janus_string_replace(sdp, "a=rtpmap:96 rtx/90000\r\n", ""); sdp = janus_string_replace(sdp, "a=fmtp:96 apt=100\r\n", ""); } /* How long will the gateway take to push the event? */ gint64 start = janus_get_monotonic_time(); int res = gateway->push_event(msg->handle, &janus_serial_plugin, msg->transaction, event_text, type, sdp); JANUS_LOG(LOG_VERB, " >> Pushing event: %d (took %"SCNu64" us)\n", res, janus_get_monotonic_time()-start); g_free(sdp); } g_free(event_text); janus_serial_message_free(msg); continue; error: { if(root != NULL) json_decref(root); /* Prepare JSON error event */ json_t *event = json_object(); json_object_set_new(event, "serial", json_string("event")); json_object_set_new(event, "error_code", json_integer(error_code)); json_object_set_new(event, "error", json_string(error_cause)); char *event_text = json_dumps(event, JSON_INDENT(3) | JSON_PRESERVE_ORDER); json_decref(event); JANUS_LOG(LOG_VERB, "Pushing event: %s\n", event_text); int ret = gateway->push_event(msg->handle, &janus_serial_plugin, msg->transaction, event_text, NULL, NULL); JANUS_LOG(LOG_VERB, " >> %d (%s)\n", ret, janus_get_api_error(ret)); g_free(event_text); janus_serial_message_free(msg); } } g_free(error_cause); JANUS_LOG(LOG_VERB, "Leaving Serial handler thread\n"); return NULL; }
/* Thread to handle incoming messages */ static void *janus_streaming_handler(void *data) { JANUS_LOG(LOG_VERB, "Joining thread\n"); janus_streaming_message *msg = NULL; int error_code = 0; char *error_cause = calloc(1024, sizeof(char)); if(error_cause == NULL) { JANUS_LOG(LOG_FATAL, "Memory error!\n"); return NULL; } while(initialized && !stopping) { if(!messages || (msg = g_queue_pop_head(messages)) == NULL) { usleep(50000); continue; } janus_streaming_session *session = (janus_streaming_session *)msg->handle->plugin_handle; if(!session) { JANUS_LOG(LOG_ERR, "No session associated with this handle...\n"); janus_streaming_message_free(msg); continue; } if(session->destroy) { janus_streaming_message_free(msg); continue; } /* Handle request */ error_code = 0; JANUS_LOG(LOG_VERB, "Handling message: %s\n", msg->message); if(msg->message == NULL) { JANUS_LOG(LOG_ERR, "No message??\n"); error_code = JANUS_STREAMING_ERROR_NO_MESSAGE; sprintf(error_cause, "%s", "No message??"); goto error; } json_error_t error; json_t *root = json_loads(msg->message, 0, &error); if(!root) { JANUS_LOG(LOG_ERR, "JSON error: on line %d: %s\n", error.line, error.text); error_code = JANUS_STREAMING_ERROR_INVALID_JSON; sprintf(error_cause, "JSON error: on line %d: %s", error.line, error.text); goto error; } if(!json_is_object(root)) { JANUS_LOG(LOG_ERR, "JSON error: not an object\n"); error_code = JANUS_STREAMING_ERROR_INVALID_JSON; sprintf(error_cause, "JSON error: not an object"); goto error; } json_t *request = json_object_get(root, "request"); if(!request) { JANUS_LOG(LOG_ERR, "Missing element (request)\n"); error_code = JANUS_STREAMING_ERROR_MISSING_ELEMENT; sprintf(error_cause, "Missing element (request)"); goto error; } if(!json_is_string(request)) { JANUS_LOG(LOG_ERR, "Invalid element (request should be a string)\n"); error_code = JANUS_STREAMING_ERROR_INVALID_ELEMENT; sprintf(error_cause, "Invalid element (request should be a string)"); goto error; } const char *request_text = json_string_value(request); json_t *result = NULL; char *sdp_type = NULL, *sdp = NULL; if(!strcasecmp(request_text, "list")) { result = json_object(); json_t *list = json_array(); JANUS_LOG(LOG_VERB, "Request for the list of mountpoints\n"); /* Return a list of all available mountpoints */ GList *mountpoints_list = g_hash_table_get_values(mountpoints); GList *m = mountpoints_list; while(m) { janus_streaming_mountpoint *mp = (janus_streaming_mountpoint *)m->data; json_t *ml = json_object(); json_object_set_new(ml, "id", json_integer(mp->id)); json_object_set_new(ml, "description", json_string(mp->description)); json_object_set_new(ml, "type", json_string(mp->streaming_type == janus_streaming_type_live ? "live" : "on demand")); json_array_append_new(list, ml); m = m->next; } json_object_set_new(result, "list", list); g_list_free(mountpoints_list); } else if(!strcasecmp(request_text, "watch")) { json_t *id = json_object_get(root, "id"); if(id && !json_is_integer(id)) { JANUS_LOG(LOG_ERR, "Invalid element (id should be an integer)\n"); error_code = JANUS_STREAMING_ERROR_INVALID_ELEMENT; sprintf(error_cause, "Invalid element (id should be an integer)"); goto error; } gint64 id_value = json_integer_value(id); janus_streaming_mountpoint *mp = g_hash_table_lookup(mountpoints, GINT_TO_POINTER(id_value)); if(mp == NULL) { JANUS_LOG(LOG_VERB, "No such mountpoint/stream %"SCNu64"\n", id_value); error_code = JANUS_STREAMING_ERROR_NO_SUCH_MOUNTPOINT; sprintf(error_cause, "No such mountpoint/stream %"SCNu64"", id_value); goto error; } JANUS_LOG(LOG_VERB, "Request to watch mountpoint/stream %"SCNu64"\n", id_value); session->stopping = FALSE; session->mountpoint = mp; if(mp->streaming_type == janus_streaming_type_on_demand) { g_thread_new(session->mountpoint->name, &janus_streaming_ondemand_thread, session); } /* TODO Check if user is already watching a stream, if the video is active, etc. */ mp->listeners = g_list_append(mp->listeners, session); sdp_type = "offer"; /* We're always going to do the offer ourselves, never answer */ char sdptemp[1024]; memset(sdptemp, 0, 1024); gchar buffer[100]; memset(buffer, 0, 100); gint64 sessid = janus_get_monotonic_time(); gint64 version = sessid; /* FIXME This needs to be increased when it changes, so time should be ok */ g_sprintf(buffer, "v=0\r\no=%s %"SCNu64" %"SCNu64" IN IP4 127.0.0.1\r\n", "-", sessid, version); g_strlcat(sdptemp, buffer, 1024); g_strlcat(sdptemp, "s=Streaming Test\r\nt=0 0\r\n", 1024); if(mp->codecs.audio_pt >= 0) { /* Add audio line */ g_sprintf(buffer, "m=audio 1 RTP/SAVPF %d\r\n" "c=IN IP4 1.1.1.1\r\n", mp->codecs.audio_pt); g_strlcat(sdptemp, buffer, 1024); if(mp->codecs.audio_rtpmap) { g_sprintf(buffer, "a=rtpmap:%d %s\r\n", mp->codecs.audio_pt, mp->codecs.audio_rtpmap); g_strlcat(sdptemp, buffer, 1024); } g_strlcat(sdptemp, "a=sendonly\r\n", 1024); } if(mp->codecs.video_pt >= 0) { /* Add video line */ g_sprintf(buffer, "m=video 1 RTP/SAVPF %d\r\n" "c=IN IP4 1.1.1.1\r\n", mp->codecs.video_pt); g_strlcat(sdptemp, buffer, 1024); if(mp->codecs.video_rtpmap) { g_sprintf(buffer, "a=rtpmap:%d %s\r\n", mp->codecs.video_pt, mp->codecs.video_rtpmap); g_strlcat(sdptemp, buffer, 1024); } g_strlcat(sdptemp, "a=sendonly\r\n", 1024); } sdp = g_strdup(sdptemp); JANUS_LOG(LOG_VERB, "Going to offer this SDP:\n%s\n", sdp); result = json_object(); json_object_set_new(result, "status", json_string("preparing")); } else if(!strcasecmp(request_text, "start")) { JANUS_LOG(LOG_VERB, "Starting the streaming\n"); result = json_object(); /* We wait for the setup_media event to start: on the other hand, it may have already arrived */ json_object_set_new(result, "status", json_string(session->started ? "started" : "starting")); } else if(!strcasecmp(request_text, "pause")) { JANUS_LOG(LOG_VERB, "Pausing the streaming\n"); session->started = FALSE; result = json_object(); json_object_set_new(result, "status", json_string("pausing")); } else if(!strcasecmp(request_text, "stop")) { JANUS_LOG(LOG_VERB, "Stopping the streaming\n"); session->stopping = TRUE; session->started = FALSE; result = json_object(); json_object_set_new(result, "status", json_string("stopping")); if(session->mountpoint) { JANUS_LOG(LOG_VERB, " -- Removing the session from the mountpoint listeners\n"); if(g_list_find(session->mountpoint->listeners, session) != NULL) { JANUS_LOG(LOG_VERB, " -- -- Found!\n"); } session->mountpoint->listeners = g_list_remove_all(session->mountpoint->listeners, session); } session->mountpoint = NULL; } else { JANUS_LOG(LOG_VERB, "Unknown request '%s'\n", request_text); error_code = JANUS_STREAMING_ERROR_INVALID_REQUEST; sprintf(error_cause, "Unknown request '%s'", request_text); goto error; } /* Any SDP to handle? */ if(msg->sdp) { JANUS_LOG(LOG_VERB, "This is involving a negotiation (%s) as well (but we really don't care):\n%s\n", msg->sdp_type, msg->sdp); } json_decref(root); /* Prepare JSON event */ json_t *event = json_object(); json_object_set_new(event, "streaming", json_string("event")); if(result != NULL) json_object_set(event, "result", result); char *event_text = json_dumps(event, JSON_INDENT(3)); json_decref(event); if(result != NULL) json_decref(result); JANUS_LOG(LOG_VERB, "Pushing event: %s\n", event_text); int ret = gateway->push_event(msg->handle, &janus_streaming_plugin, msg->transaction, event_text, sdp_type, sdp); JANUS_LOG(LOG_VERB, " >> %d (%s)\n", ret, janus_get_api_error(ret)); g_free(event_text); if(sdp) g_free(sdp); janus_streaming_message_free(msg); continue; error: { if(root != NULL) json_decref(root); /* Prepare JSON error event */ json_t *event = json_object(); json_object_set_new(event, "streaming", json_string("event")); json_object_set_new(event, "error_code", json_integer(error_code)); json_object_set_new(event, "error", json_string(error_cause)); char *event_text = json_dumps(event, JSON_INDENT(3)); json_decref(event); JANUS_LOG(LOG_VERB, "Pushing event: %s\n", event_text); int ret = gateway->push_event(msg->handle, &janus_streaming_plugin, msg->transaction, event_text, NULL, NULL); JANUS_LOG(LOG_VERB, " >> %d (%s)\n", ret, janus_get_api_error(ret)); g_free(event_text); janus_streaming_message_free(msg); } } g_free(error_cause); JANUS_LOG(LOG_VERB, "Leaving thread\n"); return NULL; }
/* Thread to handle incoming events */ static void *janus_sampleevh_handler(void *data) { JANUS_LOG(LOG_VERB, "Joining SampleEventHandler handler thread\n"); json_t *event = NULL, *output = NULL; char *event_text = NULL; int count = 0, max = group_events ? 100 : 1; int retransmit = 0; while(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) { if(!retransmit) { event = g_async_queue_pop(events); if(event == NULL) continue; if(event == &exit_event) break; count = 0; output = NULL; while(TRUE) { /* Handle event: just for fun, let's see how long it took for us to take care of this */ json_t *created = json_object_get(event, "timestamp"); if(created && json_is_integer(created)) { gint64 then = json_integer_value(created); gint64 now = janus_get_monotonic_time(); JANUS_LOG(LOG_DBG, "Handled event after %"SCNu64" us\n", now-then); } /* Let's check what kind of event this is: we don't really do anything * with it in this plugin, it's just to show how you can handle * different types of events in an event handler. */ int type = json_integer_value(json_object_get(event, "type")); switch(type) { case JANUS_EVENT_TYPE_SESSION: /* This is a session related event. The only info that is * required is a name for the event itself: a "created" * event may also contain transport info, in the form of * the transport module that originated the session * (e.g., "janus.transport.http") and an internal unique * ID for the transport instance (which may be associated * to a connection or anything else within the specifics * of the transport module itself). Here's an example of * a new session being created: { "type": 1, "timestamp": 3583879627, "session_id": 2004798115, "event": { "name": "created" }, "transport": { "transport": "janus.transport.http", "id": "0x7fcb100008c0" } } */ break; case JANUS_EVENT_TYPE_HANDLE: /* This is a handle related event. The only info that is provided * are the name for the event itself and the package name of the * plugin this handle refers to (e.g., "janus.plugin.echotest"). * Here's an example of a new handled being attached in a session * to the EchoTest plugin: { "type": 2, "timestamp": 3570304977, "session_id": 2004798115, "handle_id": 3708519405, "event": { "name": "attached", "plugin: "janus.plugin.echotest" } } */ break; case JANUS_EVENT_TYPE_JSEP: /* This is a JSEP/SDP related event. It provides information * about an ongoing WebRTC negotiation, and so tells you * about the SDP being sent/received, and who's sending it * ("local" means Janus, "remote" means the user). Here's an * example, where the user originated an offer towards Janus: { "type": 8, "timestamp": 3570400208, "session_id": 2004798115, "handle_id": 3708519405, "event": { "owner": "remote", "jsep": { "type": "offer", "sdp": "v=0[..]\r\n" } } } */ break; case JANUS_EVENT_TYPE_WEBRTC: /* This is a WebRTC related event, and so the content of * the event may vary quite a bit. In fact, you may be notified * about ICE or DTLS states, or when a WebRTC PeerConnection * goes up or down. Here are some examples, in no particular order: { "type": 16, "timestamp": 3570416659, "session_id": 2004798115, "handle_id": 3708519405, "event": { "ice": "connecting", "stream_id": 1, "component_id": 1 } } * { "type": 16, "timestamp": 3570637554, "session_id": 2004798115, "handle_id": 3708519405, "event": { "selected-pair": "[..]", "stream_id": 1, "component_id": 1 } } * { "type": 16, "timestamp": 3570656112, "session_id": 2004798115, "handle_id": 3708519405, "event": { "dtls": "connected", "stream_id": 1, "component_id": 1 } } * { "type": 16, "timestamp": 3570657237, "session_id": 2004798115, "handle_id": 3708519405, "event": { "connection": "webrtcup" } } */ break; case JANUS_EVENT_TYPE_MEDIA: /* This is a media related event. This can contain different * information about the health of a media session, or about * what's going on in general (e.g., when Janus started/stopped * receiving media of a certain type, or (TODO) when some media related * statistics are available). Here's an example of Janus getting * video from the peer for the first time, or after a second * of no video at all (which would have triggered a "receiving": false): { "type": 32, "timestamp": 3571078797, "session_id": 2004798115, "handle_id": 3708519405, "event": { "media": "video", "receiving": "true" } } */ break; case JANUS_EVENT_TYPE_PLUGIN: /* This is a plugin related event. Since each plugin may * provide info in a very custom way, the format of this event * is in general very dynamic. You'll always find, though, * an "event" object containing the package name of the * plugin (e.g., "janus.plugin.echotest") and a "data" * object that contains whatever the plugin decided to * notify you about, that will always vary from plugin to * plugin. Besides, notice that "session_id" and "handle_id" * may or may not be present: when they are, you'll know * the event has been triggered within the context of a * specific handle session with the plugin; when they're * not, the plugin sent an event out of context of a * specific session it is handling. Here's an example: { "type": 64, "timestamp": 3570336031, "session_id": 2004798115, "handle_id": 3708519405, "event": { "plugin": "janus.plugin.echotest", "data": { "audio_active": "true", "video_active": "true", "bitrate": 0 } } } */ break; case JANUS_EVENT_TYPE_TRANSPORT: /* This is a transport related event (TODO). The syntax of * the common format (transport specific data aside) is * exactly the same as that of the plugin related events * above, with a "transport" property instead of "plugin" * to contain the transport package name. */ break; case JANUS_EVENT_TYPE_CORE: /* This is a core related event. This can contain different * information about the health of the Janus instance, or * more generically on some events in the Janus life cycle * (e.g., when it's just been started or when a shutdown * has been requested). Considering the heterogeneous nature * of the information being reported, the content is always * a JSON object (event). Core events are the only ones * missing a session_id. Here's an example: { "type": 256, "timestamp": 28381185382, "event": { "status": "started" } } */ break; default: JANUS_LOG(LOG_WARN, "Unknown type of event '%d'\n", type); break; } if(!group_events) { /* We're done here, we just need a single event */ output = event; break; } /* If we got here, we're grouping */ if(output == NULL) output = json_array(); json_array_append_new(output, event); /* Never group more than a maximum number of events, though, or we might stay here forever */ count++; if(count == max) break; event = g_async_queue_try_pop(events); if(event == NULL || event == &exit_event) break; } /* Since this a simple plugin, it does the same for all events: so just convert to string... */ event_text = json_dumps(output, JSON_INDENT(3) | JSON_PRESERVE_ORDER); } /* Whether we just prepared the event or this is a retransmission, send it via HTTP POST */ CURLcode res; struct curl_slist *headers = NULL; CURL *curl = curl_easy_init(); if(curl == NULL) { JANUS_LOG(LOG_ERR, "Error initializing CURL context\n"); goto done; } curl_easy_setopt(curl, CURLOPT_URL, backend); headers = curl_slist_append(headers, "Accept: application/json"); headers = curl_slist_append(headers, "Content-Type: application/json"); headers = curl_slist_append(headers, "charsets: utf-8"); curl_easy_setopt(curl, CURLOPT_HTTPHEADER, headers); curl_easy_setopt(curl, CURLOPT_POSTFIELDS, event_text); curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, janus_sampleehv_write_data); /* Any credentials? */ if(backend_user != NULL && backend_pwd != NULL) { curl_easy_setopt(curl, CURLOPT_USERNAME, backend_user); curl_easy_setopt(curl, CURLOPT_PASSWORD, backend_pwd); } /* Don't wait forever (let's say, 10 seconds) */ curl_easy_setopt(curl, CURLOPT_TIMEOUT, 10L); /* Send the request */ res = curl_easy_perform(curl); if(res != CURLE_OK) { JANUS_LOG(LOG_ERR, "Couldn't relay event to the backend: %s\n", curl_easy_strerror(res)); if(max_retransmissions > 0) { /* Retransmissions enabled, let's try again */ if(retransmit == max_retransmissions) { retransmit = 0; JANUS_LOG(LOG_WARN, "Maximum number of retransmissions reached (%d), event lost...\n", max_retransmissions); } else { int next = retransmissions_backoff * (pow(2, retransmit)); JANUS_LOG(LOG_WARN, "Retransmitting event in %d ms...\n", next); g_usleep(next*1000); retransmit++; } } else { JANUS_LOG(LOG_WARN, "Retransmissions disabled, event lost...\n"); } } else { JANUS_LOG(LOG_DBG, "Event sent!\n"); retransmit = 0; } done: /* Cleanup */ if(curl) curl_easy_cleanup(curl); if(headers) curl_slist_free_all(headers); if(!retransmit) g_free(event_text); /* Done, let's unref the event */ json_decref(output); output = NULL; } JANUS_LOG(LOG_VERB, "Leaving SampleEventHandler handler thread\n"); return NULL; }
void janus_dtls_srtp_incoming_msg(janus_dtls_srtp *dtls, char *buf, uint16_t len) { if(dtls == NULL) { JANUS_LOG(LOG_ERR, "No DTLS-SRTP stack, no incoming message...\n"); return; } janus_ice_component *component = (janus_ice_component *)dtls->component; if(component == NULL) { JANUS_LOG(LOG_ERR, "No component, no DTLS...\n"); return; } janus_ice_stream *stream = component->stream; if(!stream) { JANUS_LOG(LOG_ERR, "No stream, no DTLS...\n"); return; } janus_ice_handle *handle = stream->handle; if(!handle || !handle->agent) { JANUS_LOG(LOG_ERR, "No handle/agent, no DTLS...\n"); return; } if(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT)) { JANUS_LOG(LOG_WARN, "[%"SCNu64"] Alert already triggered, clearing up...\n", handle->handle_id); return; } if(!dtls->ssl || !dtls->read_bio) { JANUS_LOG(LOG_ERR, "[%"SCNu64"] No DTLS stuff for component %d in stream %d??\n", handle->handle_id, component->component_id, stream->stream_id); return; } janus_dtls_fd_bridge(dtls); int written = BIO_write(dtls->read_bio, buf, len); JANUS_LOG(LOG_HUGE, "[%"SCNu64"] Written %d of those bytes on the read BIO...\n", handle->handle_id, written); janus_dtls_fd_bridge(dtls); /* Try to read data */ char data[1500]; /* FIXME */ memset(&data, 0, 1500); int read = SSL_read(dtls->ssl, &data, 1500); JANUS_LOG(LOG_HUGE, "[%"SCNu64"] ... and read %d of them from SSL...\n", handle->handle_id, read); if(read < 0) { unsigned long err = SSL_get_error(dtls->ssl, read); if(err == SSL_ERROR_SSL) { /* Ops, something went wrong with the DTLS handshake */ char error[200]; ERR_error_string_n(ERR_get_error(), error, 200); JANUS_LOG(LOG_ERR, "[%"SCNu64"] Handshake error: %s\n", handle->handle_id, error); return; } } janus_dtls_fd_bridge(dtls); if(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP) || janus_is_stopping()) { /* DTLS alert triggered, we should end it here */ JANUS_LOG(LOG_VERB, "[%"SCNu64"] Forced to stop it here...\n", handle->handle_id); return; } if(!SSL_is_init_finished(dtls->ssl)) { /* Nothing else to do for now */ JANUS_LOG(LOG_HUGE, "[%"SCNu64"] Initialization not finished yet...\n", handle->handle_id); return; } if(dtls->ready) { /* There's data to be read? */ JANUS_LOG(LOG_HUGE, "[%"SCNu64"] Any data available?\n", handle->handle_id); #ifdef HAVE_SCTP if(dtls->sctp != NULL && read > 0) { JANUS_LOG(LOG_HUGE, "[%"SCNu64"] Sending data (%d bytes) to the SCTP stack...\n", handle->handle_id, read); janus_sctp_data_from_dtls(dtls->sctp, data, read); } #else if(read > 0) { JANUS_LOG(LOG_WARN, "[%"SCNu64"] Data available but Data Channels support disabled...\n", handle->handle_id); } #endif } else { JANUS_LOG(LOG_VERB, "[%"SCNu64"] DTLS established, yay!\n", handle->handle_id); /* Check the remote fingerprint */ X509 *rcert = SSL_get_peer_certificate(dtls->ssl); if(!rcert) { JANUS_LOG(LOG_ERR, "[%"SCNu64"] No remote certificate??\n", handle->handle_id); } else { unsigned int rsize; unsigned char rfingerprint[EVP_MAX_MD_SIZE]; char remote_fingerprint[160]; char *rfp = (char *)&remote_fingerprint; if(handle->remote_hashing && !strcasecmp(handle->remote_hashing, "sha-1")) { JANUS_LOG(LOG_VERB, "[%"SCNu64"] Computing sha-1 fingerprint of remote certificate...\n", handle->handle_id); X509_digest(rcert, EVP_sha1(), (unsigned char *)rfingerprint, &rsize); } else { JANUS_LOG(LOG_VERB, "[%"SCNu64"] Computing sha-256 fingerprint of remote certificate...\n", handle->handle_id); X509_digest(rcert, EVP_sha256(), (unsigned char *)rfingerprint, &rsize); } X509_free(rcert); rcert = NULL; unsigned int i = 0; for(i = 0; i < rsize; i++) { g_snprintf(rfp, 4, "%.2X:", rfingerprint[i]); rfp += 3; } *(rfp-1) = 0; JANUS_LOG(LOG_VERB, "[%"SCNu64"] Remote fingerprint (%s) of the client is %s\n", handle->handle_id, handle->remote_hashing ? handle->remote_hashing : "sha-256", remote_fingerprint); if(!strcasecmp(remote_fingerprint, handle->remote_fingerprint ? handle->remote_fingerprint : "(none)")) { JANUS_LOG(LOG_VERB, "[%"SCNu64"] Fingerprint is a match!\n", handle->handle_id); dtls->dtls_state = JANUS_DTLS_STATE_CONNECTED; dtls->dtls_connected = janus_get_monotonic_time(); } else { /* FIXME NOT a match! MITM? */ JANUS_LOG(LOG_ERR, "[%"SCNu64"] Fingerprint is NOT a match! got %s, expected %s\n", handle->handle_id, remote_fingerprint, handle->remote_fingerprint); dtls->dtls_state = JANUS_DTLS_STATE_FAILED; goto done; } if(dtls->dtls_state == JANUS_DTLS_STATE_CONNECTED) { if(component->stream_id == handle->audio_id || component->stream_id == handle->video_id) { /* Complete with SRTP setup */ unsigned char material[SRTP_MASTER_LENGTH*2]; unsigned char *local_key, *local_salt, *remote_key, *remote_salt; /* Export keying material for SRTP */ if (!SSL_export_keying_material(dtls->ssl, material, SRTP_MASTER_LENGTH*2, "EXTRACTOR-dtls_srtp", 19, NULL, 0, 0)) { /* Oops... */ JANUS_LOG(LOG_ERR, "[%"SCNu64"] Oops, couldn't extract SRTP keying material for component %d in stream %d??\n", handle->handle_id, component->component_id, stream->stream_id); goto done; } /* Key derivation (http://tools.ietf.org/html/rfc5764#section-4.2) */ if(dtls->dtls_role == JANUS_DTLS_ROLE_CLIENT) { local_key = material; remote_key = local_key + SRTP_MASTER_KEY_LENGTH; local_salt = remote_key + SRTP_MASTER_KEY_LENGTH; remote_salt = local_salt + SRTP_MASTER_SALT_LENGTH; } else { remote_key = material; local_key = remote_key + SRTP_MASTER_KEY_LENGTH; remote_salt = local_key + SRTP_MASTER_KEY_LENGTH; local_salt = remote_salt + SRTP_MASTER_SALT_LENGTH; } /* Build master keys and set SRTP policies */ /* Remote (inbound) */ crypto_policy_set_rtp_default(&(dtls->remote_policy.rtp)); crypto_policy_set_rtcp_default(&(dtls->remote_policy.rtcp)); dtls->remote_policy.ssrc.type = ssrc_any_inbound; unsigned char remote_policy_key[SRTP_MASTER_LENGTH]; dtls->remote_policy.key = (unsigned char *)&remote_policy_key; memcpy(dtls->remote_policy.key, remote_key, SRTP_MASTER_KEY_LENGTH); memcpy(dtls->remote_policy.key + SRTP_MASTER_KEY_LENGTH, remote_salt, SRTP_MASTER_SALT_LENGTH); #if HAS_DTLS_WINDOW_SIZE dtls->remote_policy.window_size = 128; dtls->remote_policy.allow_repeat_tx = 0; #endif dtls->remote_policy.next = NULL; /* Local (outbound) */ crypto_policy_set_rtp_default(&(dtls->local_policy.rtp)); crypto_policy_set_rtcp_default(&(dtls->local_policy.rtcp)); dtls->local_policy.ssrc.type = ssrc_any_outbound; unsigned char local_policy_key[SRTP_MASTER_LENGTH]; dtls->local_policy.key = (unsigned char *)&local_policy_key; memcpy(dtls->local_policy.key, local_key, SRTP_MASTER_KEY_LENGTH); memcpy(dtls->local_policy.key + SRTP_MASTER_KEY_LENGTH, local_salt, SRTP_MASTER_SALT_LENGTH); #if HAS_DTLS_WINDOW_SIZE dtls->local_policy.window_size = 128; dtls->local_policy.allow_repeat_tx = 0; #endif dtls->local_policy.next = NULL; /* Create SRTP sessions */ err_status_t res = srtp_create(&(dtls->srtp_in), &(dtls->remote_policy)); if(res != err_status_ok) { /* Something went wrong... */ JANUS_LOG(LOG_ERR, "[%"SCNu64"] Oops, error creating inbound SRTP session for component %d in stream %d??\n", handle->handle_id, component->component_id, stream->stream_id); JANUS_LOG(LOG_ERR, "[%"SCNu64"] -- %d (%s)\n", handle->handle_id, res, janus_get_srtp_error(res)); goto done; } JANUS_LOG(LOG_VERB, "[%"SCNu64"] Created inbound SRTP session for component %d in stream %d\n", handle->handle_id, component->component_id, stream->stream_id); res = srtp_create(&(dtls->srtp_out), &(dtls->local_policy)); if(res != err_status_ok) { /* Something went wrong... */ JANUS_LOG(LOG_ERR, "[%"SCNu64"] Oops, error creating outbound SRTP session for component %d in stream %d??\n", handle->handle_id, component->component_id, stream->stream_id); JANUS_LOG(LOG_ERR, "[%"SCNu64"] -- %d (%s)\n", handle->handle_id, res, janus_get_srtp_error(res)); goto done; } dtls->srtp_valid = 1; JANUS_LOG(LOG_VERB, "[%"SCNu64"] Created outbound SRTP session for component %d in stream %d\n", handle->handle_id, component->component_id, stream->stream_id); } #ifdef HAVE_SCTP if(component->stream_id == handle->data_id || (janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_BUNDLE) && janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_DATA_CHANNELS))) { /* FIXME Create SCTP association as well (5000 should be dynamic, from the SDP...) */ dtls->sctp = janus_sctp_association_create(dtls, handle->handle_id, 5000); if(dtls->sctp != NULL) { /* FIXME We need to start it in a thread, though, since it has blocking accept/connect stuff */ GError *error = NULL; g_thread_try_new("DTLS-SCTP", janus_dtls_sctp_setup_thread, dtls, &error); if(error != NULL) { /* Something went wrong... */ JANUS_LOG(LOG_ERR, "[%"SCNu64"] Got error %d (%s) trying to launch the DTLS-SCTP thread...\n", handle->handle_id, error->code, error->message ? error->message : "??"); } dtls->srtp_valid = 1; } } #endif dtls->ready = 1; } done: if(!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT) && dtls->srtp_valid) { /* Handshake successfully completed */ janus_ice_dtls_handshake_done(handle, component); } else { /* Something went wrong in either DTLS or SRTP... tell the plugin about it */ janus_dtls_callback(dtls->ssl, SSL_CB_ALERT, 0); janus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING); } } } }
char *janus_sdp_merge(janus_ice_handle *handle, const char *origsdp) { if(handle == NULL || origsdp == NULL) return NULL; sdp_session_t *anon = NULL; sdp_parser_t *parser = sdp_parse(home, origsdp, strlen(origsdp), 0); if(!(anon = sdp_session(parser))) { JANUS_LOG(LOG_ERR, "[%"SCNu64"] Error parsing/merging SDP: %s\n", handle->handle_id, sdp_parsing_error(parser)); sdp_parser_free(parser); return NULL; } /* Prepare SDP to merge */ gchar buffer[512]; memset(buffer, 0, 512); char *sdp = (char*)calloc(BUFSIZE, sizeof(char)); if(sdp == NULL) { JANUS_LOG(LOG_FATAL, "Memory error!\n"); sdp_parser_free(parser); return NULL; } sdp[0] = '\0'; /* FIXME Any Plan B to take into account? */ int planb = strstr(origsdp, "a=planb:") ? 1 : 0; if(planb) { janus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_PLAN_B); } else { janus_flags_clear(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_PLAN_B); } /* Version v= */ g_strlcat(sdp, "v=0\r\n", BUFSIZE); /* Origin o= */ if(anon->sdp_origin) { g_snprintf(buffer, 512, "o=%s %"SCNu64" %"SCNu64" IN IP4 127.0.0.1\r\n", /* FIXME Should we fix the address? */ anon->sdp_origin->o_username ? anon->sdp_origin->o_username : "******", anon->sdp_origin->o_id, anon->sdp_origin->o_version); g_strlcat(sdp, buffer, BUFSIZE); } else { gint64 sessid = janus_get_monotonic_time(); gint64 version = sessid; /* FIXME This needs to be increased when it changes, so time should be ok */ g_snprintf(buffer, 512, "o=%s %"SCNi64" %"SCNi64" IN IP4 127.0.0.1\r\n", /* FIXME Should we fix the address? */ "-", sessid, version); g_strlcat(sdp, buffer, BUFSIZE); } /* Session name s= */ if(anon->sdp_subject && strlen(anon->sdp_subject) > 0) { g_snprintf(buffer, 512, "s=%s\r\n", anon->sdp_subject); } else { g_snprintf(buffer, 512, "s=%s\r\n", "Meetecho Janus"); } g_strlcat(sdp, buffer, BUFSIZE); /* Timing t= */ g_snprintf(buffer, 512, "t=%lu %lu\r\n", anon->sdp_time ? anon->sdp_time->t_start : 0, anon->sdp_time ? anon->sdp_time->t_stop : 0); g_strlcat(sdp, buffer, BUFSIZE); /* ICE Full or Lite? */ if(janus_ice_is_ice_lite_enabled()) { /* Janus is acting in ICE Lite mode, advertize this */ g_strlcat(sdp, "a=ice-lite\r\n", BUFSIZE); } /* bundle: add new global attribute */ int audio = (strstr(origsdp, "m=audio") != NULL); int video = (strstr(origsdp, "m=video") != NULL); #ifdef HAVE_SCTP int data = (strstr(origsdp, "DTLS/SCTP") && !strstr(origsdp, " 0 DTLS/SCTP")); #else int data = 0; #endif g_strlcat(sdp, "a=group:BUNDLE", BUFSIZE); if(audio) { g_snprintf(buffer, 512, " %s", handle->audio_mid ? handle->audio_mid : "audio"); g_strlcat(sdp, buffer, BUFSIZE); } if(video) { g_snprintf(buffer, 512, " %s", handle->video_mid ? handle->video_mid : "video"); g_strlcat(sdp, buffer, BUFSIZE); } if(data) { g_snprintf(buffer, 512, " %s", handle->data_mid ? handle->data_mid : "data"); g_strlcat(sdp, buffer, BUFSIZE); } g_strlcat(sdp, "\r\n", BUFSIZE); /* msid-semantic: add new global attribute */ g_strlcat(sdp, "a=msid-semantic: WMS janus\r\n", BUFSIZE); char wms[BUFSIZE]; memset(wms, 0, BUFSIZE); g_strlcat(wms, "WMS", BUFSIZE); /* Copy other global attributes, if any */ if(anon->sdp_attributes) { sdp_attribute_t *a = anon->sdp_attributes; while(a) { if(a->a_value == NULL) { g_snprintf(buffer, 512, "a=%s\r\n", a->a_name); g_strlcat(sdp, buffer, BUFSIZE); } else { g_snprintf(buffer, 512, "a=%s:%s\r\n", a->a_name, a->a_value); g_strlcat(sdp, buffer, BUFSIZE); } a = a->a_next; } } gboolean ipv6 = strstr(janus_get_public_ip(), ":") != NULL; /* Media lines now */ if(anon->sdp_media) { int audio = 0, video = 0; #ifdef HAVE_SCTP int data = 0; #endif sdp_media_t *m = anon->sdp_media; janus_ice_stream *stream = NULL; while(m) { if(m->m_type == sdp_media_audio && m->m_port > 0) { audio++; if(audio > 1 || !handle->audio_id) { JANUS_LOG(LOG_WARN, "[%"SCNu64"] Skipping audio line (we have %d audio lines, and the id is %d)\n", handle->handle_id, audio, handle->audio_id); g_strlcat(sdp, "m=audio 0 RTP/SAVPF 0\r\n", BUFSIZE); /* FIXME Adding a c-line anyway because otherwise Firefox complains? ("c= connection line not specified for every media level, validation failed") */ g_snprintf(buffer, 512, "c=IN %s %s\r\n", ipv6 ? "IP6" : "IP4", janus_get_public_ip()); g_strlcat(sdp, buffer, BUFSIZE); m = m->m_next; continue; } /* Audio */ stream = g_hash_table_lookup(handle->streams, GUINT_TO_POINTER(handle->audio_id)); if(stream == NULL) { JANUS_LOG(LOG_WARN, "[%"SCNu64"] Skipping audio line (invalid stream %d)\n", handle->handle_id, handle->audio_id); g_strlcat(sdp, "m=audio 0 RTP/SAVPF 0\r\n", BUFSIZE); /* FIXME Adding a c-line anyway because otherwise Firefox complains? ("c= connection line not specified for every media level, validation failed") */ g_snprintf(buffer, 512, "c=IN %s %s\r\n", ipv6 ? "IP6" : "IP4", janus_get_public_ip()); g_strlcat(sdp, buffer, BUFSIZE); m = m->m_next; continue; } g_strlcat(sdp, "m=audio 1 RTP/SAVPF", BUFSIZE); } else if(m->m_type == sdp_media_video && m->m_port > 0) { video++; gint id = handle->video_id; if(id == 0 && janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_BUNDLE)) id = handle->audio_id > 0 ? handle->audio_id : handle->video_id; if(video > 1 || !id) { JANUS_LOG(LOG_WARN, "[%"SCNu64"] Skipping video line (we have %d video lines, and the id is %d)\n", handle->handle_id, video, janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_BUNDLE) ? handle->audio_id : handle->video_id); g_strlcat(sdp, "m=video 0 RTP/SAVPF 0\r\n", BUFSIZE); /* FIXME Adding a c-line anyway because otherwise Firefox complains? ("c= connection line not specified for every media level, validation failed") */ g_snprintf(buffer, 512, "c=IN %s %s\r\n", ipv6 ? "IP6" : "IP4", janus_get_public_ip()); g_strlcat(sdp, buffer, BUFSIZE); m = m->m_next; continue; } /* Video */ stream = g_hash_table_lookup(handle->streams, GUINT_TO_POINTER(id)); if(stream == NULL) { JANUS_LOG(LOG_WARN, "[%"SCNu64"] Skipping video line (invalid stream %d)\n", handle->handle_id, id); g_strlcat(sdp, "m=video 0 RTP/SAVPF 0\r\n", BUFSIZE); /* FIXME Adding a c-line anyway because otherwise Firefox complains? ("c= connection line not specified for every media level, validation failed") */ g_snprintf(buffer, 512, "c=IN %s %s\r\n", ipv6 ? "IP6" : "IP4", janus_get_public_ip()); g_strlcat(sdp, buffer, BUFSIZE); m = m->m_next; continue; } g_strlcat(sdp, "m=video 1 RTP/SAVPF", BUFSIZE); #ifdef HAVE_SCTP } else if(m->m_type == sdp_media_application) { /* Is this SCTP for DataChannels? */ if(m->m_port > 0 && m->m_proto_name != NULL && !strcasecmp(m->m_proto_name, "DTLS/SCTP") && m->m_port > 0) { /* Yep */ data++; gint id = handle->data_id; if(id == 0 && janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_BUNDLE)) id = handle->audio_id > 0 ? handle->audio_id : handle->video_id; if(data > 1 || !id) { JANUS_LOG(LOG_WARN, "[%"SCNu64"] Skipping SCTP line (we have %d SCTP lines, and the id is %d)\n", handle->handle_id, data, id); g_snprintf(buffer, 512, "m=%s 0 %s 0\r\n", m->m_type_name, m->m_proto_name); g_strlcat(sdp, buffer, BUFSIZE); /* FIXME Adding a c-line anyway because otherwise Firefox complains? ("c= connection line not specified for every media level, validation failed") */ g_snprintf(buffer, 512, "c=IN %s %s\r\n", ipv6 ? "IP6" : "IP4", janus_get_public_ip()); g_strlcat(sdp, buffer, BUFSIZE); m = m->m_next; continue; } /* SCTP */ stream = g_hash_table_lookup(handle->streams, GUINT_TO_POINTER(id)); if(stream == NULL) { JANUS_LOG(LOG_WARN, "[%"SCNu64"] Skipping SCTP line (invalid stream %d)\n", handle->handle_id, id); g_snprintf(buffer, 512, "m=%s 0 %s 0\r\n", m->m_type_name, m->m_proto_name); g_strlcat(sdp, buffer, BUFSIZE); /* FIXME Adding a c-line anyway because otherwise Firefox complains? ("c= connection line not specified for every media level, validation failed") */ g_snprintf(buffer, 512, "c=IN %s %s\r\n", ipv6 ? "IP6" : "IP4", janus_get_public_ip()); g_strlcat(sdp, buffer, BUFSIZE); m = m->m_next; continue; } g_strlcat(sdp, "m=application 1 DTLS/SCTP", BUFSIZE); } else { JANUS_LOG(LOG_WARN, "[%"SCNu64"] Skipping unsupported application media line...\n", handle->handle_id); g_snprintf(buffer, 512, "m=%s 0 %s 0\r\n", m->m_type_name, m->m_proto_name); g_strlcat(sdp, buffer, BUFSIZE); m = m->m_next; continue; } #endif } else { JANUS_LOG(LOG_WARN, "[%"SCNu64"] Skipping disabled/unsupported media line...\n", handle->handle_id); g_snprintf(buffer, 512, "m=%s 0 %s 0\r\n", m->m_type_name, m->m_proto_name); g_strlcat(sdp, buffer, BUFSIZE); /* FIXME Adding a c-line anyway because otherwise Firefox complains? ("c= connection line not specified for every media level, validation failed") */ g_snprintf(buffer, 512, "c=IN %s %s\r\n", ipv6 ? "IP6" : "IP4", janus_get_public_ip()); g_strlcat(sdp, buffer, BUFSIZE); m = m->m_next; continue; } /* Add formats now */ if(!m->m_rtpmaps) { JANUS_LOG(LOG_VERB, "[%"SCNu64"] No RTP maps?? trying formats...\n", handle->handle_id); if(!m->m_format) { JANUS_LOG(LOG_ERR, "[%"SCNu64"] No formats either?? this sucks!\n", handle->handle_id); g_strlcat(sdp, " 0", BUFSIZE); /* FIXME Won't work apparently */ } else { sdp_list_t *fmt = m->m_format; while(fmt) { g_snprintf(buffer, 512, " %s", fmt->l_text); g_strlcat(sdp, buffer, BUFSIZE); fmt = fmt->l_next; } } } else { sdp_rtpmap_t *r = m->m_rtpmaps; while(r) { g_snprintf(buffer, 512, " %d", r->rm_pt); g_strlcat(sdp, buffer, BUFSIZE); r = r->rm_next; } } g_strlcat(sdp, "\r\n", BUFSIZE); /* Media connection c= */ g_snprintf(buffer, 512, "c=IN %s %s\r\n", ipv6 ? "IP6" : "IP4", janus_get_public_ip()); g_strlcat(sdp, buffer, BUFSIZE); /* Any bandwidth? */ if(m->m_bandwidths) { g_snprintf(buffer, 512, "b=%s:%lu\r\n", /* FIXME Are we doing this correctly? */ m->m_bandwidths->b_modifier_name ? m->m_bandwidths->b_modifier_name : "AS", m->m_bandwidths->b_value); g_strlcat(sdp, buffer, BUFSIZE); } /* a=mid:(audio|video|data) */ switch(m->m_type) { case sdp_media_audio: g_snprintf(buffer, 512, "a=mid:%s\r\n", handle->audio_mid ? handle->audio_mid : "audio"); break; case sdp_media_video: g_snprintf(buffer, 512, "a=mid:%s\r\n", handle->video_mid ? handle->video_mid : "video"); break; #ifdef HAVE_SCTP case sdp_media_application: /* FIXME sctpmap and webrtc-datachannel should be dynamic */ g_snprintf(buffer, 512, "a=mid:%s\r\na=sctpmap:5000 webrtc-datachannel 16\r\n", handle->data_mid ? handle->data_mid : "data"); break; #endif default: break; } g_strlcat(sdp, buffer, BUFSIZE); if(m->m_type != sdp_media_application) { /* What is the direction? */ switch(m->m_mode) { case sdp_sendonly: g_strlcat(sdp, "a=sendonly\r\n", BUFSIZE); break; case sdp_recvonly: g_strlcat(sdp, "a=recvonly\r\n", BUFSIZE); break; case sdp_inactive: g_strlcat(sdp, "a=inactive\r\n", BUFSIZE); break; case sdp_sendrecv: default: g_strlcat(sdp, "a=sendrecv\r\n", BUFSIZE); break; } /* rtcp-mux */ g_snprintf(buffer, 512, "a=rtcp-mux\n"); g_strlcat(sdp, buffer, BUFSIZE); /* RTP maps */ if(m->m_rtpmaps) { sdp_rtpmap_t *rm = NULL; for(rm = m->m_rtpmaps; rm; rm = rm->rm_next) { g_snprintf(buffer, 512, "a=rtpmap:%u %s/%lu%s%s\r\n", rm->rm_pt, rm->rm_encoding, rm->rm_rate, rm->rm_params ? "/" : "", rm->rm_params ? rm->rm_params : ""); g_strlcat(sdp, buffer, BUFSIZE); } for(rm = m->m_rtpmaps; rm; rm = rm->rm_next) { if(rm->rm_fmtp) { g_snprintf(buffer, 512, "a=fmtp:%u %s\r\n", rm->rm_pt, rm->rm_fmtp); g_strlcat(sdp, buffer, BUFSIZE); } } } } /* ICE ufrag and pwd, DTLS fingerprint setup and connection a= */ gchar *ufrag = NULL; gchar *password = NULL; nice_agent_get_local_credentials(handle->agent, stream->stream_id, &ufrag, &password); memset(buffer, 0, 100); g_snprintf(buffer, 512, "a=ice-ufrag:%s\r\n" "a=ice-pwd:%s\r\n" "a=ice-options:trickle\r\n" "a=fingerprint:sha-256 %s\r\n" "a=setup:%s\r\n" "a=connection:new\r\n", ufrag, password, janus_dtls_get_local_fingerprint(), janus_get_dtls_srtp_role(stream->dtls_role)); if(ufrag != NULL) g_free(ufrag); ufrag = NULL; if(password != NULL) g_free(password); password = NULL; g_strlcat(sdp, buffer, BUFSIZE); /* Copy existing media attributes, if any */ if(m->m_attributes) { sdp_attribute_t *a = m->m_attributes; while(a) { if(!strcmp(a->a_name, "planb")) { /* Skip the fake planb attribute, it's for internal use only */ a = a->a_next; continue; } if(a->a_value == NULL) { g_snprintf(buffer, 512, "a=%s\r\n", a->a_name); g_strlcat(sdp, buffer, BUFSIZE); } else { g_snprintf(buffer, 512, "a=%s:%s\r\n", a->a_name, a->a_value); g_strlcat(sdp, buffer, BUFSIZE); } a = a->a_next; } } /* Add last attributes, rtcp and ssrc (msid) */ if(!planb) { /* Single SSRC */ if(m->m_type == sdp_media_audio && m->m_mode != sdp_inactive && m->m_mode != sdp_recvonly) { g_snprintf(buffer, 512, "a=ssrc:%"SCNu32" cname:janusaudio\r\n" "a=ssrc:%"SCNu32" msid:janus janusa0\r\n" "a=ssrc:%"SCNu32" mslabel:janus\r\n" "a=ssrc:%"SCNu32" label:janusa0\r\n", stream->audio_ssrc, stream->audio_ssrc, stream->audio_ssrc, stream->audio_ssrc); g_strlcat(sdp, buffer, BUFSIZE); } else if(m->m_type == sdp_media_video && m->m_mode != sdp_inactive && m->m_mode != sdp_recvonly) { g_snprintf(buffer, 512, "a=ssrc:%"SCNu32" cname:janusvideo\r\n" "a=ssrc:%"SCNu32" msid:janus janusv0\r\n" "a=ssrc:%"SCNu32" mslabel:janus\r\n" "a=ssrc:%"SCNu32" label:janusv0\r\n", stream->video_ssrc, stream->video_ssrc, stream->video_ssrc, stream->video_ssrc); g_strlcat(sdp, buffer, BUFSIZE); } } else { /* Multiple SSRCs */ char mslabel[255]; memset(mslabel, 0, 255); if(m->m_attributes) { char id[256]; uint32_t ssrc = 0; sdp_attribute_t *a = m->m_attributes; while(a) { if(a->a_name == NULL || a->a_value == NULL || strcmp(a->a_name, "planb")) { a = a->a_next; continue; } if(sscanf(a->a_value, "%255s %"SCNu32"", id, &ssrc) != 2) { JANUS_LOG(LOG_ERR, "Error parsing 'planb' attribute, skipping it...\n"); a = a->a_next; continue; } JANUS_LOG(LOG_VERB, "Parsing 'planb' attribute: %s\n", a->a_value); /* Add proper SSRC attributes */ if(m->m_type == sdp_media_audio) { g_snprintf(buffer, 512, "a=ssrc:%"SCNu32" cname:%saudio\r\n" "a=ssrc:%"SCNu32" msid:%s %sa0\r\n" "a=ssrc:%"SCNu32" mslabel:%s\r\n" "a=ssrc:%"SCNu32" label:%sa0\r\n", ssrc, id, ssrc, id, id, ssrc, id, ssrc, id); } else if(m->m_type == sdp_media_video) { g_snprintf(buffer, 512, "a=ssrc:%"SCNu32" cname:%svideo\r\n" "a=ssrc:%"SCNu32" msid:%s %sv0\r\n" "a=ssrc:%"SCNu32" mslabel:%s\r\n" "a=ssrc:%"SCNu32" label:%sv0\r\n", ssrc, id, ssrc, id, id, ssrc, id, ssrc, id); } g_strlcat(sdp, buffer, BUFSIZE); /* Add to msid-semantic, if needed */ if(!strstr(wms, id)) { g_snprintf(mslabel, 255, " %s", id); g_strlcat(wms, mslabel, BUFSIZE); } /* Go on */ a = a->a_next; } } } /* And now the candidates */ janus_ice_candidates_to_sdp(handle, sdp, stream->stream_id, 1); if(!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_RTCPMUX) && m->m_type != sdp_media_application) janus_ice_candidates_to_sdp(handle, sdp, stream->stream_id, 2); /* Next */ m = m->m_next; } } /* Do we need to update the msid-semantic attribute? */ if(planb) { sdp = janus_string_replace(sdp, "WMS janus", wms); } sdp_parser_free(parser); JANUS_LOG(LOG_VERB, " -------------------------------------------\n"); JANUS_LOG(LOG_VERB, " >> Merged (%zu --> %zu bytes)\n", strlen(origsdp), strlen(sdp)); JANUS_LOG(LOG_VERB, " -------------------------------------------\n"); JANUS_LOG(LOG_VERB, "%s\n", sdp); return sdp; }
/* Thread to handle incoming messages */ static void *janus_source_handler(void *data) { JANUS_LOG(LOG_VERB, "Joining SourcePlugin handler thread\n"); janus_source_message *msg = NULL; int error_code = 0; char *error_cause = g_malloc0(512); json_t *root = NULL; while (g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) { msg = g_async_queue_pop(messages); if (msg == NULL) continue; if (msg == &exit_message) break; if (msg->handle == NULL) { janus_source_message_free(msg); continue; } janus_source_session *session = NULL; janus_mutex_lock(&sessions_mutex); if (g_hash_table_lookup(sessions, msg->handle) != NULL) { session = (janus_source_session *)msg->handle->plugin_handle; } janus_mutex_unlock(&sessions_mutex); if (!session) { JANUS_LOG(LOG_ERR, "No session associated with this handle...\n"); janus_source_message_free(msg); continue; } if (session->destroyed) { janus_source_message_free(msg); continue; } /* Handle request */ error_code = 0; root = msg->message; if (msg->message == NULL) { JANUS_LOG(LOG_ERR, "No message??\n"); error_code = JANUS_SOURCE_ERROR_NO_MESSAGE; g_snprintf(error_cause, 512, "%s", "No message??"); goto error; } if (!json_is_object(root)) { JANUS_LOG(LOG_ERR, "JSON error: not an object\n"); error_code = JANUS_SOURCE_ERROR_INVALID_JSON; g_snprintf(error_cause, 512, "JSON error: not an object"); goto error; } /* Parse request */ const char *msg_sdp_type = json_string_value(json_object_get(msg->jsep, "type")); const char *msg_sdp = json_string_value(json_object_get(msg->jsep, "sdp")); json_t *audio = json_object_get(root, "audio"); if (audio && !json_is_boolean(audio)) { JANUS_LOG(LOG_ERR, "Invalid element (audio should be a boolean)\n"); error_code = JANUS_SOURCE_ERROR_INVALID_ELEMENT; g_snprintf(error_cause, 512, "Invalid value (audio should be a boolean)"); goto error; } json_t *video = json_object_get(root, "video"); if (video && !json_is_boolean(video)) { JANUS_LOG(LOG_ERR, "Invalid element (video should be a boolean)\n"); error_code = JANUS_SOURCE_ERROR_INVALID_ELEMENT; g_snprintf(error_cause, 512, "Invalid value (video should be a boolean)"); goto error; } json_t *bitrate = json_object_get(root, "bitrate"); if (bitrate && (!json_is_integer(bitrate) || json_integer_value(bitrate) < 0)) { JANUS_LOG(LOG_ERR, "Invalid element (bitrate should be a positive integer)\n"); error_code = JANUS_SOURCE_ERROR_INVALID_ELEMENT; g_snprintf(error_cause, 512, "Invalid value (bitrate should be a positive integer)"); goto error; } json_t *record = json_object_get(root, "record"); if (record && !json_is_boolean(record)) { JANUS_LOG(LOG_ERR, "Invalid element (record should be a boolean)\n"); error_code = JANUS_SOURCE_ERROR_INVALID_ELEMENT; g_snprintf(error_cause, 512, "Invalid value (record should be a boolean)"); goto error; } json_t *recfile = json_object_get(root, "filename"); if (recfile && !json_is_string(recfile)) { JANUS_LOG(LOG_ERR, "Invalid element (filename should be a string)\n"); error_code = JANUS_SOURCE_ERROR_INVALID_ELEMENT; g_snprintf(error_cause, 512, "Invalid value (filename should be a string)"); goto error; } json_t *id = json_object_get(root, "id"); if(id && !json_is_string(id)) { JANUS_LOG(LOG_ERR, "Invalid element (id should be a string)\n"); error_code = JANUS_SOURCE_ERROR_INVALID_ELEMENT; g_snprintf(error_cause, 512, "Invalid value (id should be positive string)"); goto error; } /* Enforce request */ if (audio) { session->audio_active = json_is_true(audio); JANUS_LOG(LOG_VERB, "Setting audio property: %s\n", session->audio_active ? "true" : "false"); } if (video) { if (!session->video_active && json_is_true(video)) { /* Send a PLI */ JANUS_LOG(LOG_VERB, "Just (re-)enabled video, sending a PLI to recover it\n"); char buf[12]; memset(buf, 0, 12); janus_rtcp_pli((char *)&buf, 12); gateway->relay_rtcp(session->handle, 1, buf, 12); } session->video_active = json_is_true(video); JANUS_LOG(LOG_VERB, "Setting video property: %s\n", session->video_active ? "true" : "false"); } if (bitrate) { session->bitrate = json_integer_value(bitrate); JANUS_LOG(LOG_VERB, "Setting video bitrate: %"SCNu64"\n", session->bitrate); if (session->bitrate > 0) { /* FIXME Generate a new REMB (especially useful for Firefox, which doesn't send any we can cap later) */ char buf[24]; memset(buf, 0, 24); janus_rtcp_remb((char *)&buf, 24, session->bitrate); JANUS_LOG(LOG_VERB, "Sending REMB\n"); gateway->relay_rtcp(session->handle, 1, buf, 24); /* FIXME How should we handle a subsequent "no limit" bitrate? */ } } if(id) { session->id = g_strdup(json_string_value(id)); } if (!audio && !video && !bitrate && !record && !id && !msg_sdp) { JANUS_LOG(LOG_ERR, "No supported attributes (audio, video, bitrate, record, id, jsep) found\n"); error_code = JANUS_SOURCE_ERROR_INVALID_ELEMENT; g_snprintf(error_cause, 512, "Message error: no supported attributes (audio, video, bitrate, record, id, jsep) found"); goto error; } /* Prepare JSON event */ json_t *event = json_object(); json_object_set_new(event, "source", json_string("event")); json_object_set_new(event, "result", json_string("ok")); if(!msg_sdp) { int ret = gateway->push_event(msg->handle, &janus_source_plugin, msg->transaction, event, NULL); JANUS_LOG(LOG_VERB, " >> %d (%s)\n", ret, janus_get_api_error(ret)); json_decref(event); } else { /* Forward the same offer to the gateway, to start the source plugin */ const char *type = NULL; if (!strcasecmp(msg_sdp_type, "offer")) type = "answer"; if (!strcasecmp(msg_sdp_type, "answer")) type = "offer"; /* Any media direction that needs to be fixed? */ char *sdp = g_strdup(msg_sdp); if (strstr(sdp, "a=recvonly")) { /* Turn recvonly to inactive, as we simply bounce media back */ sdp = janus_string_replace(sdp, "a=recvonly", "a=inactive"); } else if (strstr(sdp, "a=sendonly")) { /* Turn sendonly to recvonly */ sdp = janus_string_replace(sdp, "a=sendonly", "a=recvonly"); /* FIXME We should also actually not echo this media back, though... */ } /* Make also sure we get rid of ULPfec, red, etc. */ if (strstr(sdp, "ulpfec")) { /* FIXME This really needs some better code */ sdp = janus_string_replace(sdp, "a=rtpmap:116 red/90000\r\n", ""); sdp = janus_string_replace(sdp, "a=rtpmap:117 ulpfec/90000\r\n", ""); sdp = janus_string_replace(sdp, "a=rtpmap:96 rtx/90000\r\n", ""); sdp = janus_string_replace(sdp, "a=fmtp:96 apt=100\r\n", ""); sdp = janus_string_replace(sdp, "a=rtpmap:97 rtx/90000\r\n", ""); sdp = janus_string_replace(sdp, "a=fmtp:97 apt=101\r\n", ""); sdp = janus_string_replace(sdp, "a=rtpmap:98 rtx/90000\r\n", ""); sdp = janus_string_replace(sdp, "a=fmtp:98 apt=116\r\n", ""); sdp = janus_string_replace(sdp, " 116", ""); sdp = janus_string_replace(sdp, " 117", ""); sdp = janus_string_replace(sdp, " 96", ""); sdp = janus_string_replace(sdp, " 97", ""); sdp = janus_string_replace(sdp, " 98", ""); } json_t *jsep = json_pack("{ssss}", "type", type, "sdp", sdp); sdp = janus_source_do_codec_negotiation(session, sdp); /* How long will the gateway take to push the event? */ g_atomic_int_set(&session->hangingup, 0); gint64 start = janus_get_monotonic_time(); int res = gateway->push_event(msg->handle, &janus_source_plugin, msg->transaction, event, jsep); JANUS_LOG(LOG_VERB, " >> Pushing event: %d (took %"SCNu64" us)\n", res, janus_get_monotonic_time() - start); g_free(sdp); /* We don't need the event and jsep anymore */ json_decref(event); json_decref(jsep); } janus_source_message_free(msg); continue; error: { /* Prepare JSON error event */ json_t *event = json_object(); json_object_set_new(event, "source", json_string("event")); json_object_set_new(event, "error_code", json_integer(error_code)); json_object_set_new(event, "error", json_string(error_cause)); int ret = gateway->push_event(msg->handle, &janus_source_plugin, msg->transaction, event, NULL); JANUS_LOG(LOG_VERB, " >> %d (%s)\n", ret, janus_get_api_error(ret)); janus_source_message_free(msg); /* We don't need the event anymore */ json_decref(event); } } g_free(error_cause); JANUS_LOG(LOG_VERB, "Leaving SourcePlugin handler thread\n"); return NULL; }
void janus_dtls_srtp_incoming_msg(janus_dtls_srtp *dtls, char *buf, uint16_t len) { if(dtls == NULL) { JANUS_LOG(LOG_ERR, "No DTLS-SRTP stack, no incoming message...\n"); return; } janus_ice_component *component = (janus_ice_component *)dtls->component; if(component == NULL) { JANUS_LOG(LOG_ERR, "No component, no DTLS...\n"); return; } janus_ice_stream *stream = component->stream; if(!stream) { JANUS_LOG(LOG_ERR, "No stream, no DTLS...\n"); return; } janus_ice_handle *handle = stream->handle; if(!handle || !handle->agent) { JANUS_LOG(LOG_ERR, "No handle/agent, no DTLS...\n"); return; } if(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT)) { JANUS_LOG(LOG_WARN, "[%"SCNu64"] Alert already triggered, clearing up...\n", handle->handle_id); return; } if(!dtls->ssl || !dtls->read_bio) { JANUS_LOG(LOG_ERR, "[%"SCNu64"] No DTLS stuff for component %d in stream %d??\n", handle->handle_id, component->component_id, stream->stream_id); return; } if(dtls->dtls_started == 0) { /* Handshake not started yet: maybe we're still waiting for the answer and the DTLS role? */ return; } janus_dtls_fd_bridge(dtls); int written = BIO_write(dtls->read_bio, buf, len); if(written != len) { JANUS_LOG(LOG_WARN, "[%"SCNu64"] Only written %d/%d of those bytes on the read BIO...\n", handle->handle_id, written, len); } else { JANUS_LOG(LOG_HUGE, "[%"SCNu64"] Written %d bytes on the read BIO...\n", handle->handle_id, written); } janus_dtls_fd_bridge(dtls); /* Try to read data */ char data[1500]; /* FIXME */ memset(&data, 0, 1500); int read = SSL_read(dtls->ssl, &data, 1500); JANUS_LOG(LOG_HUGE, "[%"SCNu64"] ... and read %d of them from SSL...\n", handle->handle_id, read); if(read < 0) { unsigned long err = SSL_get_error(dtls->ssl, read); if(err == SSL_ERROR_SSL) { /* Ops, something went wrong with the DTLS handshake */ char error[200]; ERR_error_string_n(ERR_get_error(), error, 200); JANUS_LOG(LOG_ERR, "[%"SCNu64"] Handshake error: %s\n", handle->handle_id, error); return; } } janus_dtls_fd_bridge(dtls); if(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP) || janus_is_stopping()) { /* DTLS alert triggered, we should end it here */ JANUS_LOG(LOG_VERB, "[%"SCNu64"] Forced to stop it here...\n", handle->handle_id); return; } if(!SSL_is_init_finished(dtls->ssl)) { /* Nothing else to do for now */ JANUS_LOG(LOG_HUGE, "[%"SCNu64"] Initialization not finished yet...\n", handle->handle_id); return; } if(dtls->ready) { /* There's data to be read? */ JANUS_LOG(LOG_HUGE, "[%"SCNu64"] Any data available?\n", handle->handle_id); #ifdef HAVE_SCTP if(dtls->sctp != NULL && read > 0) { JANUS_LOG(LOG_HUGE, "[%"SCNu64"] Sending data (%d bytes) to the SCTP stack...\n", handle->handle_id, read); janus_sctp_data_from_dtls(dtls->sctp, data, read); } #else if(read > 0) { JANUS_LOG(LOG_WARN, "[%"SCNu64"] Data available but Data Channels support disabled...\n", handle->handle_id); } #endif } else { JANUS_LOG(LOG_VERB, "[%"SCNu64"] DTLS established, yay!\n", handle->handle_id); /* Check the remote fingerprint */ X509 *rcert = SSL_get_peer_certificate(dtls->ssl); if(!rcert) { JANUS_LOG(LOG_ERR, "[%"SCNu64"] No remote certificate?? (%s)\n", handle->handle_id, ERR_reason_error_string(ERR_get_error())); } else { unsigned int rsize; unsigned char rfingerprint[EVP_MAX_MD_SIZE]; char remote_fingerprint[160]; char *rfp = (char *)&remote_fingerprint; if(stream->remote_hashing && !strcasecmp(stream->remote_hashing, "sha-1")) { JANUS_LOG(LOG_VERB, "[%"SCNu64"] Computing sha-1 fingerprint of remote certificate...\n", handle->handle_id); X509_digest(rcert, EVP_sha1(), (unsigned char *)rfingerprint, &rsize); } else { JANUS_LOG(LOG_VERB, "[%"SCNu64"] Computing sha-256 fingerprint of remote certificate...\n", handle->handle_id); X509_digest(rcert, EVP_sha256(), (unsigned char *)rfingerprint, &rsize); } X509_free(rcert); rcert = NULL; unsigned int i = 0; for(i = 0; i < rsize; i++) { g_snprintf(rfp, 4, "%.2X:", rfingerprint[i]); rfp += 3; } *(rfp-1) = 0; JANUS_LOG(LOG_VERB, "[%"SCNu64"] Remote fingerprint (%s) of the client is %s\n", handle->handle_id, stream->remote_hashing ? stream->remote_hashing : "sha-256", remote_fingerprint); if(!strcasecmp(remote_fingerprint, stream->remote_fingerprint ? stream->remote_fingerprint : "(none)")) { JANUS_LOG(LOG_VERB, "[%"SCNu64"] Fingerprint is a match!\n", handle->handle_id); dtls->dtls_state = JANUS_DTLS_STATE_CONNECTED; dtls->dtls_connected = janus_get_monotonic_time(); /* Notify event handlers */ janus_dtls_notify_state_change(dtls); } else { /* FIXME NOT a match! MITM? */ JANUS_LOG(LOG_ERR, "[%"SCNu64"] Fingerprint is NOT a match! got %s, expected %s\n", handle->handle_id, remote_fingerprint, stream->remote_fingerprint); dtls->dtls_state = JANUS_DTLS_STATE_FAILED; /* Notify event handlers */ janus_dtls_notify_state_change(dtls); goto done; } if(dtls->dtls_state == JANUS_DTLS_STATE_CONNECTED) { /* Which SRTP profile is being negotiated? */ SRTP_PROTECTION_PROFILE *srtp_profile = SSL_get_selected_srtp_profile(dtls->ssl); if(srtp_profile == NULL) { /* Should never happen, but just in case... */ JANUS_LOG(LOG_ERR, "[%"SCNu64"] No SRTP profile selected...\n", handle->handle_id); dtls->dtls_state = JANUS_DTLS_STATE_FAILED; /* Notify event handlers */ janus_dtls_notify_state_change(dtls); goto done; } JANUS_LOG(LOG_VERB, "[%"SCNu64"] %s\n", handle->handle_id, srtp_profile->name); int key_length = 0, salt_length = 0, master_length = 0; switch(srtp_profile->id) { case SRTP_AES128_CM_SHA1_80: case SRTP_AES128_CM_SHA1_32: key_length = SRTP_MASTER_KEY_LENGTH; salt_length = SRTP_MASTER_SALT_LENGTH; master_length = SRTP_MASTER_LENGTH; break; #ifdef HAVE_SRTP_AESGCM case SRTP_AEAD_AES_256_GCM: key_length = SRTP_AESGCM256_MASTER_KEY_LENGTH; salt_length = SRTP_AESGCM256_MASTER_SALT_LENGTH; master_length = SRTP_AESGCM256_MASTER_LENGTH; break; case SRTP_AEAD_AES_128_GCM: key_length = SRTP_AESGCM128_MASTER_KEY_LENGTH; salt_length = SRTP_AESGCM128_MASTER_SALT_LENGTH; master_length = SRTP_AESGCM128_MASTER_LENGTH; break; #endif default: /* Will never happen? */ JANUS_LOG(LOG_WARN, "[%"SCNu64"] Unsupported SRTP profile %lu\n", handle->handle_id, srtp_profile->id); break; } JANUS_LOG(LOG_VERB, "[%"SCNu64"] Key/Salt/Master: %d/%d/%d\n", handle->handle_id, master_length, key_length, salt_length); /* Complete with SRTP setup */ unsigned char material[master_length*2]; unsigned char *local_key, *local_salt, *remote_key, *remote_salt; /* Export keying material for SRTP */ if(!SSL_export_keying_material(dtls->ssl, material, master_length*2, "EXTRACTOR-dtls_srtp", 19, NULL, 0, 0)) { /* Oops... */ JANUS_LOG(LOG_ERR, "[%"SCNu64"] Oops, couldn't extract SRTP keying material for component %d in stream %d?? (%s)\n", handle->handle_id, component->component_id, stream->stream_id, ERR_reason_error_string(ERR_get_error())); goto done; } /* Key derivation (http://tools.ietf.org/html/rfc5764#section-4.2) */ if(dtls->dtls_role == JANUS_DTLS_ROLE_CLIENT) { local_key = material; remote_key = local_key + key_length; local_salt = remote_key + key_length; remote_salt = local_salt + salt_length; } else { remote_key = material; local_key = remote_key + key_length; remote_salt = local_key + key_length; local_salt = remote_salt + salt_length; } /* Build master keys and set SRTP policies */ /* Remote (inbound) */ switch(srtp_profile->id) { case SRTP_AES128_CM_SHA1_80: srtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(dtls->remote_policy.rtp)); srtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(dtls->remote_policy.rtcp)); break; case SRTP_AES128_CM_SHA1_32: srtp_crypto_policy_set_aes_cm_128_hmac_sha1_32(&(dtls->remote_policy.rtp)); srtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(dtls->remote_policy.rtcp)); break; #ifdef HAVE_SRTP_AESGCM case SRTP_AEAD_AES_256_GCM: srtp_crypto_policy_set_aes_gcm_256_16_auth(&(dtls->remote_policy.rtp)); srtp_crypto_policy_set_aes_gcm_256_16_auth(&(dtls->remote_policy.rtcp)); break; case SRTP_AEAD_AES_128_GCM: srtp_crypto_policy_set_aes_gcm_128_16_auth(&(dtls->remote_policy.rtp)); srtp_crypto_policy_set_aes_gcm_128_16_auth(&(dtls->remote_policy.rtcp)); break; #endif default: /* Will never happen? */ JANUS_LOG(LOG_WARN, "[%"SCNu64"] Unsupported SRTP profile %s\n", handle->handle_id, srtp_profile->name); break; } dtls->remote_policy.ssrc.type = ssrc_any_inbound; unsigned char remote_policy_key[master_length]; dtls->remote_policy.key = (unsigned char *)&remote_policy_key; memcpy(dtls->remote_policy.key, remote_key, key_length); memcpy(dtls->remote_policy.key + key_length, remote_salt, salt_length); #if HAS_DTLS_WINDOW_SIZE dtls->remote_policy.window_size = 128; dtls->remote_policy.allow_repeat_tx = 0; #endif dtls->remote_policy.next = NULL; /* Local (outbound) */ switch(srtp_profile->id) { case SRTP_AES128_CM_SHA1_80: srtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(dtls->local_policy.rtp)); srtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(dtls->local_policy.rtcp)); break; case SRTP_AES128_CM_SHA1_32: srtp_crypto_policy_set_aes_cm_128_hmac_sha1_32(&(dtls->local_policy.rtp)); srtp_crypto_policy_set_aes_cm_128_hmac_sha1_80(&(dtls->local_policy.rtcp)); break; #ifdef HAVE_SRTP_AESGCM case SRTP_AEAD_AES_256_GCM: srtp_crypto_policy_set_aes_gcm_256_16_auth(&(dtls->local_policy.rtp)); srtp_crypto_policy_set_aes_gcm_256_16_auth(&(dtls->local_policy.rtcp)); break; case SRTP_AEAD_AES_128_GCM: srtp_crypto_policy_set_aes_gcm_128_16_auth(&(dtls->local_policy.rtp)); srtp_crypto_policy_set_aes_gcm_128_16_auth(&(dtls->local_policy.rtcp)); break; #endif default: /* Will never happen? */ JANUS_LOG(LOG_WARN, "[%"SCNu64"] Unsupported SRTP profile %s\n", handle->handle_id, srtp_profile->name); break; } dtls->local_policy.ssrc.type = ssrc_any_outbound; unsigned char local_policy_key[master_length]; dtls->local_policy.key = (unsigned char *)&local_policy_key; memcpy(dtls->local_policy.key, local_key, key_length); memcpy(dtls->local_policy.key + key_length, local_salt, salt_length); #if HAS_DTLS_WINDOW_SIZE dtls->local_policy.window_size = 128; dtls->local_policy.allow_repeat_tx = 0; #endif dtls->local_policy.next = NULL; /* Create SRTP sessions */ srtp_err_status_t res = srtp_create(&(dtls->srtp_in), &(dtls->remote_policy)); if(res != srtp_err_status_ok) { /* Something went wrong... */ JANUS_LOG(LOG_ERR, "[%"SCNu64"] Oops, error creating inbound SRTP session for component %d in stream %d??\n", handle->handle_id, component->component_id, stream->stream_id); JANUS_LOG(LOG_ERR, "[%"SCNu64"] -- %d (%s)\n", handle->handle_id, res, janus_srtp_error_str(res)); goto done; } JANUS_LOG(LOG_VERB, "[%"SCNu64"] Created inbound SRTP session for component %d in stream %d\n", handle->handle_id, component->component_id, stream->stream_id); res = srtp_create(&(dtls->srtp_out), &(dtls->local_policy)); if(res != srtp_err_status_ok) { /* Something went wrong... */ JANUS_LOG(LOG_ERR, "[%"SCNu64"] Oops, error creating outbound SRTP session for component %d in stream %d??\n", handle->handle_id, component->component_id, stream->stream_id); JANUS_LOG(LOG_ERR, "[%"SCNu64"] -- %d (%s)\n", handle->handle_id, res, janus_srtp_error_str(res)); goto done; } dtls->srtp_profile = srtp_profile->id; dtls->srtp_valid = 1; JANUS_LOG(LOG_VERB, "[%"SCNu64"] Created outbound SRTP session for component %d in stream %d\n", handle->handle_id, component->component_id, stream->stream_id); #ifdef HAVE_SCTP if(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_DATA_CHANNELS)) { /* Create SCTP association as well */ janus_dtls_srtp_create_sctp(dtls); } #endif dtls->ready = 1; } done: if(!janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALERT) && dtls->srtp_valid) { /* Handshake successfully completed */ janus_ice_dtls_handshake_done(handle, component); } else { /* Something went wrong in either DTLS or SRTP... tell the plugin about it */ janus_dtls_callback(dtls->ssl, SSL_CB_ALERT, 0); janus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_CLEANING); } } } }
/* Thread to handle incoming messages */ static void *janus_echotest_handler(void *data) { JANUS_LOG(LOG_VERB, "Joining thread\n"); janus_echotest_message *msg = NULL; int error_code = 0; char *error_cause = calloc(512, sizeof(char)); /* FIXME 512 should be enough, but anyway... */ if(error_cause == NULL) { JANUS_LOG(LOG_FATAL, "Memory error!\n"); return NULL; } json_t *root = NULL; while(g_atomic_int_get(&initialized) && !g_atomic_int_get(&stopping)) { if(!messages || (msg = g_async_queue_try_pop(messages)) == NULL) { usleep(50000); continue; } janus_echotest_session *session = (janus_echotest_session *)msg->handle->plugin_handle; if(!session) { JANUS_LOG(LOG_ERR, "No session associated with this handle...\n"); janus_echotest_message_free(msg); continue; } if(session->destroyed) { janus_echotest_message_free(msg); continue; } /* Handle request */ error_code = 0; root = NULL; JANUS_LOG(LOG_VERB, "Handling message: %s\n", msg->message); if(msg->message == NULL) { JANUS_LOG(LOG_ERR, "No message??\n"); error_code = JANUS_ECHOTEST_ERROR_NO_MESSAGE; g_snprintf(error_cause, 512, "%s", "No message??"); goto error; } json_error_t error; root = json_loads(msg->message, 0, &error); if(!root) { JANUS_LOG(LOG_ERR, "JSON error: on line %d: %s\n", error.line, error.text); error_code = JANUS_ECHOTEST_ERROR_INVALID_JSON; g_snprintf(error_cause, 512, "JSON error: on line %d: %s", error.line, error.text); goto error; } if(!json_is_object(root)) { JANUS_LOG(LOG_ERR, "JSON error: not an object\n"); error_code = JANUS_ECHOTEST_ERROR_INVALID_JSON; g_snprintf(error_cause, 512, "JSON error: not an object"); goto error; } json_t *audio = json_object_get(root, "audio"); if(audio && !json_is_boolean(audio)) { JANUS_LOG(LOG_ERR, "Invalid element (audio should be a boolean)\n"); error_code = JANUS_ECHOTEST_ERROR_INVALID_ELEMENT; g_snprintf(error_cause, 512, "Invalid value (audio should be a boolean)"); goto error; } json_t *video = json_object_get(root, "video"); if(video && !json_is_boolean(video)) { JANUS_LOG(LOG_ERR, "Invalid element (video should be a boolean)\n"); error_code = JANUS_ECHOTEST_ERROR_INVALID_ELEMENT; g_snprintf(error_cause, 512, "Invalid value (video should be a boolean)"); goto error; } json_t *bitrate = json_object_get(root, "bitrate"); if(bitrate && !json_is_integer(bitrate)) { JANUS_LOG(LOG_ERR, "Invalid element (bitrate should be an integer)\n"); error_code = JANUS_ECHOTEST_ERROR_INVALID_ELEMENT; g_snprintf(error_cause, 512, "Invalid value (bitrate should be an integer)"); goto error; } if(audio) { session->audio_active = json_is_true(audio); JANUS_LOG(LOG_VERB, "Setting audio property: %s\n", session->audio_active ? "true" : "false"); } if(video) { session->video_active = json_is_true(video); JANUS_LOG(LOG_VERB, "Setting video property: %s\n", session->video_active ? "true" : "false"); } if(bitrate) { session->bitrate = json_integer_value(bitrate); JANUS_LOG(LOG_VERB, "Setting video bitrate: %"SCNu64"\n", session->bitrate); if(session->bitrate > 0) { /* FIXME Generate a new REMB (especially useful for Firefox, which doesn't send any we can cap later) */ char buf[24]; memset(buf, 0, 24); janus_rtcp_remb((char *)&buf, 24, session->bitrate); JANUS_LOG(LOG_VERB, "Sending REMB\n"); gateway->relay_rtcp(session->handle, 1, buf, 24); /* FIXME How should we handle a subsequent "no limit" bitrate? */ } } /* Any SDP to handle? */ if(msg->sdp) { JANUS_LOG(LOG_VERB, "This is involving a negotiation (%s) as well:\n%s\n", msg->sdp_type, msg->sdp); } json_decref(root); /* Prepare JSON event */ json_t *event = json_object(); json_object_set_new(event, "echotest", json_string("event")); json_object_set_new(event, "result", json_string("ok")); char *event_text = json_dumps(event, JSON_INDENT(3) | JSON_PRESERVE_ORDER); json_decref(event); JANUS_LOG(LOG_VERB, "Pushing event: %s\n", event_text); if(!msg->sdp) { int ret = gateway->push_event(msg->handle, &janus_echotest_plugin, msg->transaction, event_text, NULL, NULL); JANUS_LOG(LOG_VERB, " >> %d (%s)\n", ret, janus_get_api_error(ret)); } else { /* Forward the same offer to the gateway, to start the echo test */ const char *type = NULL; if(!strcasecmp(msg->sdp_type, "offer")) type = "answer"; if(!strcasecmp(msg->sdp_type, "answer")) type = "offer"; /* How long will the gateway take to push the event? */ gint64 start = janus_get_monotonic_time(); int res = gateway->push_event(msg->handle, &janus_echotest_plugin, msg->transaction, event_text, type, msg->sdp); JANUS_LOG(LOG_VERB, " >> Pushing event: %d (took %"SCNu64" us)\n", res, janus_get_monotonic_time()-start); } g_free(event_text); janus_echotest_message_free(msg); continue; error: { if(root != NULL) json_decref(root); /* Prepare JSON error event */ json_t *event = json_object(); json_object_set_new(event, "echotest", json_string("event")); json_object_set_new(event, "error_code", json_integer(error_code)); json_object_set_new(event, "error", json_string(error_cause)); char *event_text = json_dumps(event, JSON_INDENT(3) | JSON_PRESERVE_ORDER); json_decref(event); JANUS_LOG(LOG_VERB, "Pushing event: %s\n", event_text); int ret = gateway->push_event(msg->handle, &janus_echotest_plugin, msg->transaction, event_text, NULL, NULL); JANUS_LOG(LOG_VERB, " >> %d (%s)\n", ret, janus_get_api_error(ret)); g_free(event_text); janus_echotest_message_free(msg); } } g_free(error_cause); JANUS_LOG(LOG_VERB, "Leaving thread\n"); return NULL; }