From 6dd1c12cbaca648688f7a6fe006c4a04bb5cabe8 Mon Sep 17 00:00:00 2001 From: meetecho Date: Tue, 14 Oct 2014 16:27:31 +0200 Subject: [PATCH] Several fixes Improved DTLS timer and retransmissions; Added a helper to parse on/off configuration values; Added API command to enable/disable streaming mountpoints; Added a (commented) H.264 sample mountpoint to the streaming configuration; Improved SSRC multiplexing in the Video MCU; Added a new way to trigger the end of trickle candidates; Some more fixes here and there --- conf/janus.plugin.audiobridge.cfg.sample | 2 +- conf/janus.plugin.streaming.cfg.sample.in | 23 +++ dtls.c | 43 +---- dtls.h | 5 - html/janus.js | 2 +- ice.c | 9 +- janus.c | 18 +- mutex.h | 1 - plugins/janus_audiobridge.c | 4 +- plugins/janus_sip.c | 6 +- plugins/janus_streaming.c | 195 ++++++++++++++++++++-- plugins/janus_videoroom.c | 88 ++++++---- sdp.h | 6 +- utils.c | 3 + utils.h | 4 + 15 files changed, 293 insertions(+), 116 deletions(-) diff --git a/conf/janus.plugin.audiobridge.cfg.sample b/conf/janus.plugin.audiobridge.cfg.sample index 56b18c37b3..83e491a6cc 100644 --- a/conf/janus.plugin.audiobridge.cfg.sample +++ b/conf/janus.plugin.audiobridge.cfg.sample @@ -2,7 +2,7 @@ ; description = This is my awesome room ; secret = ; sampling_rate = (e.g., 16000 for wideband mixing) -; record = true|false ( (whether this room should be recorded, default=false) +; record = true|false (whether this room should be recorded, default=false) ; record_file = /path/to/recording.wav (where to save the recording) [1234] diff --git a/conf/janus.plugin.streaming.cfg.sample.in b/conf/janus.plugin.streaming.cfg.sample.in index 120fc14a03..f0a8ffdfa7 100644 --- a/conf/janus.plugin.streaming.cfg.sample.in +++ b/conf/janus.plugin.streaming.cfg.sample.in @@ -8,6 +8,8 @@ ; (multiple listeners = different streaming contexts) ; id = (if missing, a random one will be generated) ; description = This is my awesome stream +; secret = ; filename = path to the local file to stream (only for live/ondemand) ; audio = yes|no (do/don't stream audio) ; video = yes|no (do/don't stream video) @@ -37,6 +39,7 @@ audiortpmap = opus/48000/2 videoport = 5004 videopt = 100 videortpmap = VP8/90000 +secret = adminpwd [file-live-sample] type = live @@ -45,6 +48,7 @@ description = a-law file source (radio broadcast) filename = @streamdir@/radio.alaw ; See install.sh audio = yes video = no +secret = adminpwd [file-ondemand-sample] type = ondemand @@ -53,3 +57,22 @@ description = mu-law file source (music) filename = @streamdir@/music.mulaw ; See install.sh audio = yes video = no +secret = adminpwd + +; +; Firefox Nightly supports H.264 through Cisco's OpenH264 plugin. The only +; supported profile is the baseline one. This is an example of how to create +; a H.264 mountpoint: you can feed it an x264enc+rtph264pay pipeline in +; gstreamer. +; +;[h264-sample] +;type = rtp +;id = 10 +;description = H.264 live stream coming from gstreamer +;audio = no +;video = yes +;videoport = 8004 +;videopt = 126 +;videortpmap = H264/90000 +;videofmtp = profile-level-id=42e01f\;packetization-mode=1 + diff --git a/dtls.c b/dtls.c index ae4e07d150..c205d7e739 100644 --- a/dtls.c +++ b/dtls.c @@ -201,8 +201,6 @@ janus_dtls_srtp *janus_dtls_srtp_create(void *ice_component, janus_dtls_role rol } /* Create SSL context, at last */ dtls->srtp_valid = 0; - dtls->dtls_last_msg = NULL; - dtls->dtls_last_len = 0; dtls->ssl = SSL_new(janus_dtls_get_ssl_ctx()); if(!dtls->ssl) { JANUS_LOG(LOG_ERR, "[%"SCNu64"] No component DTLS SSL session??\n", handle->handle_id); @@ -281,12 +279,6 @@ void janus_dtls_srtp_incoming_msg(janus_dtls_srtp *dtls, char *buf, uint16_t len 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; } - /* We just got a message, can we get rid of the last sent message? */ - if(dtls->dtls_last_msg != NULL) { - g_free(dtls->dtls_last_msg); - dtls->dtls_last_msg = NULL; - dtls->dtls_last_len = 0; - } janus_dtls_fd_bridge(dtls); int written = BIO_write(dtls->read_bio, buf, len); JANUS_LOG(LOG_HUGE, " Written %d of those bytes on the read BIO...\n", written); @@ -490,10 +482,6 @@ void janus_dtls_srtp_destroy(janus_dtls_srtp *dtls) { } /* FIXME What about dtls->remote_policy and dtls->local_policy? */ } - if(dtls->dtls_last_msg != NULL) { - g_free(dtls->dtls_last_msg); - dtls->dtls_last_msg = NULL; - } g_free(dtls); dtls = NULL; } @@ -580,20 +568,6 @@ void janus_dtls_fd_bridge(janus_dtls_srtp *dtls) { JANUS_LOG(LOG_HUGE, "[%"SCNu64"] >> >> Read %d bytes from the write_BIO...\n", handle->handle_id, pending); int bytes = nice_agent_send(handle->agent, component->stream_id, component->component_id, out, outgoing); JANUS_LOG(LOG_HUGE, "[%"SCNu64"] >> >> ... and sent %d of those bytes on the socket\n", handle->handle_id, bytes); - - /* Take note of the last sent message */ - if(dtls->dtls_last_msg != NULL) { - g_free(dtls->dtls_last_msg); - dtls->dtls_last_msg = NULL; - dtls->dtls_last_len = 0; - } - dtls->dtls_last_msg = calloc(pending, sizeof(char)); - if(dtls->dtls_last_msg == NULL) { - JANUS_LOG(LOG_FATAL, "Memory error!\n"); - return; - } - memcpy(dtls->dtls_last_msg, &outgoing, out); - dtls->dtls_last_len = out; } } @@ -654,19 +628,18 @@ gboolean janus_dtls_retry(gpointer stack) { return FALSE; if(janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_STOP)) return FALSE; - //~ struct timeval timeout; - //~ DTLSv1_get_timeout(dtls->ssl, &timeout); - //~ JANUS_LOG(LOG_VERB, "[%"SCNu64"] DTLSv1_get_timeout: %"SCNu64"\n", handle->handle_id, timeout.tv_sec*1000000+timeout.tv_usec); - //~ DTLSv1_handle_timeout(dtls->ssl); - //~ janus_dtls_fd_bridge(dtls); - JANUS_LOG(LOG_VERB, "[%"SCNu64"] A second has passed on component %d of stream %d\n", handle->handle_id, component->component_id, stream->stream_id); if(dtls->dtls_state == JANUS_DTLS_STATE_CONNECTED) { JANUS_LOG(LOG_VERB, "[%"SCNu64"] DTLS already set up, disabling retransmission timer!\n", handle->handle_id); return FALSE; } - if(dtls->dtls_last_msg != NULL) { - JANUS_LOG(LOG_VERB, "[%"SCNu64"] Retransmitting last message (len=%d)\n", handle->handle_id, dtls->dtls_last_len); - nice_agent_send(handle->agent, component->stream_id, component->component_id, dtls->dtls_last_len, dtls->dtls_last_msg); + struct timeval timeout; + DTLSv1_get_timeout(dtls->ssl, &timeout); + guint64 timeout_value = timeout.tv_sec*1000 + timeout.tv_usec/1000; + JANUS_LOG(LOG_VERB, "[%"SCNu64"] DTLSv1_get_timeout: %"SCNu64"\n", handle->handle_id, timeout_value); + if(timeout_value == 0) { + JANUS_LOG(LOG_VERB, "[%"SCNu64"] DTLS timeout on component %d of stream %d, retransmitting\n", handle->handle_id, component->component_id, stream->stream_id); + DTLSv1_handle_timeout(dtls->ssl); + janus_dtls_fd_bridge(dtls); } return TRUE; } diff --git a/dtls.h b/dtls.h index be08b106ab..52e418f94a 100644 --- a/dtls.h +++ b/dtls.h @@ -75,10 +75,6 @@ typedef struct janus_dtls_srtp { srtp_policy_t local_policy; /*! \brief Mutex to lock/unlock this libsrtp context */ janus_mutex srtp_mutex; - /*! \brief Buffer of the last message the DTLS client tried to send (needed for retransmissions) */ - char *dtls_last_msg; - /*! \brief Length of the last message the DTLS client tried to send (needed for retransmissions) */ - gint dtls_last_len; /*! \brief Whether this DTLS stack is now ready to be used for messages as well (e.g., SCTP encapsulation) */ int ready; #ifdef HAVE_SCTP @@ -148,7 +144,6 @@ void janus_dtls_notify_data(janus_dtls_srtp *dtls, char *buf, int len); /*! \brief DTLS retransmission timer * \details As libnice is going to actually send and receive data, OpenSSL cannot handle retransmissions by itself: this timed callback (g_source_set_callback) deals with this. - * \todo Improve the rough mechanics implemented here by using DTLSv1_get_timeout() and DTLSv1_handle_timeout() to handle timeout and re-transmissions. * @param[in] stack Opaque pointer to the janus_dtls_srtp instance to use * @returns true if a retransmission is still needed, false otherwise */ gboolean janus_dtls_retry(gpointer stack); diff --git a/html/janus.js b/html/janus.js index d853099532..c7a310b633 100644 --- a/html/janus.js +++ b/html/janus.js @@ -886,7 +886,7 @@ function Janus(gatewayCallbacks) { config.iceDone = true; if(config.trickle === true) { // Notify end of candidates - sendTrickleCandidate(handleId, null); + sendTrickleCandidate(handleId, {"completed": true}); } else { // No trickle, time to send the complete SDP (including all candidates) sendSDP(handleId, callbacks); diff --git a/ice.c b/ice.c index 712ef1cb77..70b3b48923 100644 --- a/ice.c +++ b/ice.c @@ -104,7 +104,7 @@ gint janus_ice_init(gchar *stun_server, uint16_t stun_port, uint16_t rtp_min_por return 0; /* No initialization needed */ if(stun_port == 0) stun_port = 3478; - /*! \todo The RTP/RTCP port range configuration may be just a placeholder: for + /*! \note The RTP/RTCP port range configuration may be just a placeholder: for * instance, libnice supports this since 0.1.0, but the 0.1.3 on Fedora fails * when linking with an undefined reference to \c nice_agent_set_port_range * so this is checked by the install.sh script in advance. */ @@ -595,7 +595,7 @@ void janus_ice_cb_component_state_changed(NiceAgent *agent, guint stream_id, gui return; } /* Create retransmission timer */ - component->source = g_timeout_source_new_seconds(1); + component->source = g_timeout_source_new(500); g_source_set_callback(component->source, janus_dtls_retry, component->dtls, NULL); guint id = g_source_attach(component->source, handle->icectx); JANUS_LOG(LOG_VERB, "[%"SCNu64"] Creating retransmission timer with ID %u\n", handle->handle_id, id); @@ -681,7 +681,7 @@ void janus_ice_cb_nice_recv(NiceAgent *agent, guint stream_id, guint component_i //~ JANUS_LOG(LOG_HUGE, "[%"SCNu64"] Got an RTP packet (%s stream)!\n", handle->handle_id, //~ janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_BUNDLE) ? "bundled" : (stream->stream_id == handle->audio_id ? "audio" : "video")); if(!component->dtls || !component->dtls->srtp_valid) { - JANUS_LOG(LOG_ERR, "[%"SCNu64"] Missing valid SRTP session, skipping...\n", handle->handle_id); + JANUS_LOG(LOG_WARN, "[%"SCNu64"] Missing valid SRTP session (packet arrived too early?), skipping...\n", handle->handle_id); } else { int buflen = len; err_status_t res = srtp_unprotect(component->dtls->srtp_in, buf, &buflen); @@ -726,7 +726,7 @@ void janus_ice_cb_nice_recv(NiceAgent *agent, guint stream_id, guint component_i JANUS_LOG(LOG_HUGE, "[%"SCNu64"] Got an RTCP packet (%s stream)!\n", handle->handle_id, janus_flags_is_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_BUNDLE) ? "bundled" : (stream->stream_id == handle->audio_id ? "audio" : "video")); if(!component->dtls || !component->dtls->srtp_valid) { - JANUS_LOG(LOG_ERR, "[%"SCNu64"] Missing valid SRTP session, skipping...\n", handle->handle_id); + JANUS_LOG(LOG_WARN, "[%"SCNu64"] Missing valid SRTP session (packet arrived too early?), skipping...\n", handle->handle_id); } else { int buflen = len; err_status_t res = srtp_unprotect_rtcp(component->dtls->srtp_in, buf, &buflen); @@ -771,7 +771,6 @@ void janus_ice_cb_nice_recv(NiceAgent *agent, guint stream_id, guint component_i pkt->control = FALSE; pkt->encrypted = TRUE; /* This was already encrypted before */ g_async_queue_push(handle->queued_packets, pkt); - //~ nice_agent_send(handle->agent, stream->stream_id, component->component_id, p->length, p->data); } } rp = rp->next; diff --git a/janus.c b/janus.c index 6325049bf0..3f0779e4dc 100644 --- a/janus.c +++ b/janus.c @@ -1336,7 +1336,7 @@ int janus_process_incoming_request(janus_request_source *source, json_t *root) { } if(candidate != NULL) { /* We got a single candidate */ - if(!json_is_object(candidate)) { + if(!json_is_object(candidate) || json_object_get(candidate, "completed") != NULL) { JANUS_LOG(LOG_INFO, "No more remote candidates for handle %"SCNu64"!\n", handle->handle_id); janus_mutex_lock(&handle->mutex); janus_flags_set(&handle->webrtc_flags, JANUS_ICE_HANDLE_WEBRTC_ALL_TRICKLES); @@ -1461,7 +1461,7 @@ int janus_process_incoming_request(janus_request_source *source, json_t *root) { size_t i = 0; for(i=0; ivalue && !strcasecmp(item->value, "no")) { + if(!item || !item->value || !janus_is_true(item->value)) { JANUS_LOG(LOG_WARN, "HTTP webserver disabled\n"); } else { int wsport = 8088; @@ -3915,7 +3915,7 @@ gint main(int argc, char *argv[]) /* Do we also have to provide an HTTPS one? */ char *cert_pem_bytes = NULL, *cert_key_bytes = NULL; item = janus_config_get_item_drilldown(config, "webserver", "https"); - if(item && item->value && !strcasecmp(item->value, "no")) { + if(!item || !item->value || !janus_is_true(item->value)) { JANUS_LOG(LOG_WARN, "HTTPS webserver disabled\n"); } else { item = janus_config_get_item_drilldown(config, "webserver", "secure_port"); @@ -4009,7 +4009,7 @@ gint main(int argc, char *argv[]) JANUS_LOG(LOG_WARN, "WebSockets support not compiled\n"); #else item = janus_config_get_item_drilldown(config, "webserver", "ws"); - if(item && item->value && !strcasecmp(item->value, "no")) { + if(!item || !item->value || !janus_is_true(item->value)) { JANUS_LOG(LOG_WARN, "WebSockets server disabled\n"); } else { int wsport = 8188; @@ -4032,7 +4032,7 @@ gint main(int argc, char *argv[]) janus_mutex_init(&wss_mutex); } item = janus_config_get_item_drilldown(config, "webserver", "ws_ssl"); - if(!item || !item->value || !strcasecmp(item->value, "no")) { + if(!item || !item->value || !janus_is_true(item->value)) { JANUS_LOG(LOG_WARN, "Secure WebSockets server disabled\n"); } else { if(wss != NULL) { @@ -4065,7 +4065,7 @@ gint main(int argc, char *argv[]) JANUS_LOG(LOG_WARN, "RabbitMQ support not compiled\n"); #else item = janus_config_get_item_drilldown(config, "rabbitmq", "enable"); - if(!item || !item->value || !strcasecmp(item->value, "no")) { + if(!item || !item->value || !janus_is_true(item->value)) { JANUS_LOG(LOG_WARN, "RammitMQ support disabled\n"); } else { /* Parse configuration */ @@ -4211,7 +4211,7 @@ gint main(int argc, char *argv[]) } } item = janus_config_get_item_drilldown(config, "admin", "admin_http"); - if(item && item->value && !strcasecmp(item->value, "no")) { + if(!item || !item->value || !janus_is_true(item->value)) { JANUS_LOG(LOG_WARN, "Admin/monitor HTTP webserver disabled\n"); } else { int wsport = 7088; @@ -4250,7 +4250,7 @@ gint main(int argc, char *argv[]) } /* Do we also have to provide an HTTPS one? */ item = janus_config_get_item_drilldown(config, "admin", "admin_https"); - if(item && item->value && !strcasecmp(item->value, "no")) { + if(!item || !item->value || !janus_is_true(item->value)) { JANUS_LOG(LOG_WARN, "Admin/monitor HTTPS webserver disabled\n"); } else { item = janus_config_get_item_drilldown(config, "admin", "admin_secure_port"); diff --git a/mutex.h b/mutex.h index e3adbd4bea..3937b9f22c 100644 --- a/mutex.h +++ b/mutex.h @@ -2,7 +2,6 @@ * \author Lorenzo Miniero * \brief Semaphors and Mutexes * \details Implementation (based on pthread) of a locking mechanism based on mutexes. - * \todo This is mostly unused right now, involve mutexes more in later versions. * * \ingroup core * \ref core diff --git a/plugins/janus_audiobridge.c b/plugins/janus_audiobridge.c index 63440d10ac..fd11b16989 100644 --- a/plugins/janus_audiobridge.c +++ b/plugins/janus_audiobridge.c @@ -26,7 +26,7 @@ description = This is my awesome room secret = sampling_rate = (e.g., 16000 for wideband mixing) -record = yes/no (if yes, a raw mix of the recording will be saved) +record = true|false (whether this room should be recorded, default=false) record_file = /path/to/recording.wav (where to save the recording) \endverbatim * @@ -309,7 +309,7 @@ int janus_audiobridge_init(janus_callbacks *callback, const char *config_path) { audiobridge->room_secret = g_strdup(secret->value); } audiobridge->record = FALSE; - if(record && record->value && !strcasecmp(record->value, "yes")) + if(record && record->value && janus_is_true(record->value)) audiobridge->record = TRUE; if(recfile && recfile->value) audiobridge->record_file = g_strdup(recfile->value); diff --git a/plugins/janus_sip.c b/plugins/janus_sip.c index 94f720b512..e1fdba68f7 100644 --- a/plugins/janus_sip.c +++ b/plugins/janus_sip.c @@ -23,9 +23,9 @@ * need to fork in the same place. This specific functionality, though, has * not been implemented as of yet. * - * \todo Only Asterisk has been tested as a SIP server (which explains why - * the plugin talks of extensions and not generic SIP URIs), and specifically - * only basic audio calls have been tested: this plugin needs a lot of work. + * \todo Only Asterisk and Kamailio have been tested as a SIP server, and + * specifically only with basic audio calls: this plugin needs some work + * to make it more stable and reliable. * * \ingroup plugins * \ref plugins diff --git a/plugins/janus_streaming.c b/plugins/janus_streaming.c index c93fbab0b9..b5913d6992 100644 --- a/plugins/janus_streaming.c +++ b/plugins/janus_streaming.c @@ -49,6 +49,8 @@ type = rtp|live|ondemand id = description = This is my awesome stream filename = path to the local file to stream (only for live/ondemand) +secret = audio = yes|no (do/don't stream audio) video = yes|no (do/don't stream video) The following options are only valid for the 'rtp' type: @@ -180,6 +182,8 @@ typedef struct janus_streaming_mountpoint { gint64 id; char *name; char *description; + char *secret; + gboolean enabled; gboolean active; janus_streaming_type streaming_type; janus_streaming_source streaming_source; @@ -261,6 +265,7 @@ typedef struct janus_streaming_rtp_relay_packet { #define JANUS_STREAMING_ERROR_INVALID_ELEMENT 454 #define JANUS_STREAMING_ERROR_NO_SUCH_MOUNTPOINT 455 #define JANUS_STREAMING_ERROR_CANT_CREATE 456 +#define JANUS_STREAMING_ERROR_UNAUTHORIZED 457 #define JANUS_STREAMING_ERROR_UNKNOWN_ERROR 470 @@ -305,6 +310,7 @@ int janus_streaming_init(janus_callbacks *callback, const char *config_path) { /* RTP live source (e.g., from gstreamer/ffmpeg/vlc/etc.) */ janus_config_item *id = janus_config_get_item(cat, "id"); janus_config_item *desc = janus_config_get_item(cat, "description"); + janus_config_item *secret = janus_config_get_item(cat, "secret"); janus_config_item *audio = janus_config_get_item(cat, "audio"); janus_config_item *video = janus_config_get_item(cat, "video"); janus_config_item *aport = janus_config_get_item(cat, "audioport"); @@ -315,8 +321,8 @@ int janus_streaming_init(janus_callbacks *callback, const char *config_path) { janus_config_item *vcodec = janus_config_get_item(cat, "videopt"); janus_config_item *vrtpmap = janus_config_get_item(cat, "videortpmap"); janus_config_item *vfmtp = janus_config_get_item(cat, "videofmtp"); - gboolean doaudio = audio && audio->value && !strcasecmp(audio->value, "yes"); - gboolean dovideo = video && video->value && !strcasecmp(video->value, "yes"); + gboolean doaudio = audio && audio->value && janus_is_true(audio->value); + gboolean dovideo = video && video->value && janus_is_true(video->value); if(!doaudio && !dovideo) { JANUS_LOG(LOG_ERR, "Can't add 'rtp' stream '%s', no audio or video have to be streamed...\n", cat->name); cat = cat->next; @@ -351,7 +357,8 @@ int janus_streaming_init(janus_callbacks *callback, const char *config_path) { } } JANUS_LOG(LOG_VERB, "Audio %s, Video %s\n", doaudio ? "enabled" : "NOT enabled", dovideo ? "enabled" : "NOT enabled"); - if(!janus_streaming_create_rtp_source( + janus_streaming_mountpoint *mp = NULL; + if((mp = janus_streaming_create_rtp_source( (id && id->value) ? atoi(id->value) : 0, (char *)cat->name, desc ? (char *)desc->value : NULL, @@ -364,14 +371,17 @@ int janus_streaming_init(janus_callbacks *callback, const char *config_path) { (vport && vport->value) ? atoi(vport->value) : 0, (vcodec && vcodec->value) ? atoi(vcodec->value) : 0, vrtpmap ? (char *)vrtpmap->value : NULL, - vfmtp ? (char *)vfmtp->value : NULL)) { + vfmtp ? (char *)vfmtp->value : NULL)) == NULL) { JANUS_LOG(LOG_ERR, "Error creating 'rtp' stream '%s'...\n", cat->name); continue; } + if(secret && secret->value) + mp->secret = g_strdup(secret->value); } else if(!strcasecmp(type->value, "live")) { /* File live source */ janus_config_item *id = janus_config_get_item(cat, "id"); janus_config_item *desc = janus_config_get_item(cat, "description"); + janus_config_item *secret = janus_config_get_item(cat, "secret"); janus_config_item *file = janus_config_get_item(cat, "filename"); janus_config_item *audio = janus_config_get_item(cat, "audio"); janus_config_item *video = janus_config_get_item(cat, "video"); @@ -380,8 +390,8 @@ int janus_streaming_init(janus_callbacks *callback, const char *config_path) { cat = cat->next; continue; } - gboolean doaudio = audio && audio->value && !strcasecmp(audio->value, "yes"); - gboolean dovideo = video && video->value && !strcasecmp(video->value, "yes"); + gboolean doaudio = audio && audio->value && janus_is_true(audio->value); + gboolean dovideo = video && video->value && janus_is_true(video->value); /* TODO We should support something more than raw a-Law and mu-Law streams... */ if(!doaudio || dovideo) { JANUS_LOG(LOG_ERR, "Can't add 'live' stream '%s', we only support audio file streaming right now...\n", cat->name); @@ -405,19 +415,23 @@ int janus_streaming_init(janus_callbacks *callback, const char *config_path) { continue; } } - if(!janus_streaming_create_file_source( + janus_streaming_mountpoint *mp = NULL; + if((mp = janus_streaming_create_file_source( (id && id->value) ? atoi(id->value) : 0, (char *)cat->name, desc ? (char *)desc->value : NULL, (char *)file->value, - TRUE, doaudio, dovideo)) { + TRUE, doaudio, dovideo)) == NULL) { JANUS_LOG(LOG_ERR, "Error creating 'live' stream '%s'...\n", cat->name); continue; } + if(secret && secret->value) + mp->secret = g_strdup(secret->value); } else if(!strcasecmp(type->value, "ondemand")) { /* mu-Law file on demand source */ janus_config_item *id = janus_config_get_item(cat, "id"); janus_config_item *desc = janus_config_get_item(cat, "description"); + janus_config_item *secret = janus_config_get_item(cat, "secret"); janus_config_item *file = janus_config_get_item(cat, "filename"); janus_config_item *audio = janus_config_get_item(cat, "audio"); janus_config_item *video = janus_config_get_item(cat, "video"); @@ -426,8 +440,8 @@ int janus_streaming_init(janus_callbacks *callback, const char *config_path) { cat = cat->next; continue; } - gboolean doaudio = audio && audio->value && !strcasecmp(audio->value, "yes"); - gboolean dovideo = video && video->value && !strcasecmp(video->value, "yes"); + gboolean doaudio = audio && audio->value && janus_is_true(audio->value); + gboolean dovideo = video && video->value && janus_is_true(video->value); /* TODO We should support something more than raw a-Law and mu-Law streams... */ if(!doaudio || dovideo) { JANUS_LOG(LOG_ERR, "Can't add 'ondemand' stream '%s', we only support audio file streaming right now...\n", cat->name); @@ -451,15 +465,18 @@ int janus_streaming_init(janus_callbacks *callback, const char *config_path) { continue; } } - if(!janus_streaming_create_file_source( + janus_streaming_mountpoint *mp = NULL; + if((mp = janus_streaming_create_file_source( (id && id->value) ? atoi(id->value) : 0, (char *)cat->name, desc ? (char *)desc->value : NULL, (char *)file->value, - FALSE, doaudio, dovideo)) { + FALSE, doaudio, dovideo)) == NULL) { JANUS_LOG(LOG_ERR, "Error creating 'ondemand' stream '%s'...\n", cat->name); continue; } + if(secret && secret->value) + mp->secret = g_strdup(secret->value); } else { JANUS_LOG(LOG_WARN, "Ignoring unknown stream type '%s' (%s)...\n", type->value, cat->name); } @@ -702,6 +719,13 @@ struct janus_plugin_result *janus_streaming_handle_message(janus_plugin_session goto error; } const char *type_text = json_string_value(type); + json_t *secret = json_object_get(root, "secret"); + if(secret && !json_is_string(secret)) { + JANUS_LOG(LOG_ERR, "Invalid element (secret should be a string)\n"); + error_code = JANUS_STREAMING_ERROR_INVALID_ELEMENT; + g_snprintf(error_cause, 512, "Invalid element (secret should be a string)"); + goto error; + } janus_streaming_mountpoint *mp = NULL; if(!strcasecmp(type_text, "rtp")) { /* RTP live source (e.g., from gstreamer/ffmpeg/vlc/etc.) */ @@ -1058,7 +1082,11 @@ struct janus_plugin_result *janus_streaming_handle_message(janus_plugin_session JANUS_LOG(LOG_ERR, "Unknown stream type '%s'...\n", type_text); error_code = JANUS_STREAMING_ERROR_INVALID_ELEMENT; g_snprintf(error_cause, 512, "Unknown stream type '%s'...\n", type_text); + goto error; } + /* Any secret? */ + if(secret) + mp->secret = g_strdup(json_string_value(secret)); /* Send info back */ json_t *response = json_object(); json_object_set_new(response, "streaming", json_string("created")); @@ -1098,6 +1126,28 @@ struct janus_plugin_result *janus_streaming_handle_message(janus_plugin_session g_snprintf(error_cause, 512, "No such mountpoint/stream %"SCNu64"", id_value); goto error; } + if(mp->secret) { + /* This action requires an authorized user */ + json_t *secret = json_object_get(root, "secret"); + if(!secret) { + JANUS_LOG(LOG_ERR, "Missing element (secret)\n"); + error_code = JANUS_STREAMING_ERROR_MISSING_ELEMENT; + g_snprintf(error_cause, 512, "Missing element (secret)"); + goto error; + } + if(!json_is_string(secret)) { + JANUS_LOG(LOG_ERR, "Invalid element (secret should be a string)\n"); + error_code = JANUS_STREAMING_ERROR_INVALID_ELEMENT; + g_snprintf(error_cause, 512, "Invalid element (secret should be a string)"); + goto error; + } + if(strcmp(mp->secret, json_string_value(secret))) { + JANUS_LOG(LOG_ERR, "Unauthorized (wrong secret)\n"); + error_code = JANUS_STREAMING_ERROR_UNAUTHORIZED; + g_snprintf(error_cause, 512, "Unauthorized (wrong secret)"); + goto error; + } + } JANUS_LOG(LOG_VERB, "Request to unmount mountpoint/stream %"SCNu64"\n", id_value); /* FIXME Should we kick the current viewers as well? */ g_hash_table_remove(mountpoints, GINT_TO_POINTER(id_value)); @@ -1163,6 +1213,28 @@ struct janus_plugin_result *janus_streaming_handle_message(janus_plugin_session g_snprintf(error_cause, 512, "Recording is only available on RTP-based live streams"); goto error; } + if(mp->secret) { + /* This action requires an authorized user */ + json_t *secret = json_object_get(root, "secret"); + if(!secret) { + JANUS_LOG(LOG_ERR, "Missing element (secret)\n"); + error_code = JANUS_STREAMING_ERROR_MISSING_ELEMENT; + g_snprintf(error_cause, 512, "Missing element (secret)"); + goto error; + } + if(!json_is_string(secret)) { + JANUS_LOG(LOG_ERR, "Invalid element (secret should be a string)\n"); + error_code = JANUS_STREAMING_ERROR_INVALID_ELEMENT; + g_snprintf(error_cause, 512, "Invalid element (secret should be a string)"); + goto error; + } + if(strcmp(mp->secret, json_string_value(secret))) { + JANUS_LOG(LOG_ERR, "Unauthorized (wrong secret)\n"); + error_code = JANUS_STREAMING_ERROR_UNAUTHORIZED; + g_snprintf(error_cause, 512, "Unauthorized (wrong secret)"); + goto error; + } + } janus_streaming_rtp_source *source = mp->source; if(!strcasecmp(action_text, "start")) { /* Start a recording for audio and/or video */ @@ -1274,6 +1346,92 @@ struct janus_plugin_result *janus_streaming_handle_message(janus_plugin_session g_free(response_text); return result; } + } else if(!strcasecmp(request_text, "enable") || !strcasecmp(request_text, "disable")) { + /* A request to enable/disable a mountpoint */ + json_t *id = json_object_get(root, "id"); + if(!id) { + JANUS_LOG(LOG_ERR, "Missing element (id)\n"); + error_code = JANUS_STREAMING_ERROR_MISSING_ELEMENT; + g_snprintf(error_cause, 512, "Missing element (id)"); + goto error; + } + if(!json_is_integer(id)) { + JANUS_LOG(LOG_ERR, "Invalid element (id should be an integer)\n"); + error_code = JANUS_STREAMING_ERROR_INVALID_ELEMENT; + g_snprintf(error_cause, 512, "Invalid element (id should be an integer)"); + goto error; + } + gint64 id_value = json_integer_value(id); + janus_mutex_lock(&mountpoints_mutex); + janus_streaming_mountpoint *mp = g_hash_table_lookup(mountpoints, GINT_TO_POINTER(id_value)); + if(mp == NULL) { + janus_mutex_unlock(&mountpoints_mutex); + JANUS_LOG(LOG_VERB, "No such mountpoint/stream %"SCNu64"\n", id_value); + error_code = JANUS_STREAMING_ERROR_NO_SUCH_MOUNTPOINT; + g_snprintf(error_cause, 512, "No such mountpoint/stream %"SCNu64"", id_value); + goto error; + } + if(mp->secret) { + /* This action requires an authorized user */ + json_t *secret = json_object_get(root, "secret"); + if(!secret) { + janus_mutex_unlock(&mountpoints_mutex); + JANUS_LOG(LOG_ERR, "Missing element (secret)\n"); + error_code = JANUS_STREAMING_ERROR_MISSING_ELEMENT; + g_snprintf(error_cause, 512, "Missing element (secret)"); + goto error; + } + if(!json_is_string(secret)) { + janus_mutex_unlock(&mountpoints_mutex); + JANUS_LOG(LOG_ERR, "Invalid element (secret should be a string)\n"); + error_code = JANUS_STREAMING_ERROR_INVALID_ELEMENT; + g_snprintf(error_cause, 512, "Invalid element (secret should be a string)"); + goto error; + } + if(strcmp(mp->secret, json_string_value(secret))) { + janus_mutex_unlock(&mountpoints_mutex); + JANUS_LOG(LOG_ERR, "Unauthorized (wrong secret)\n"); + error_code = JANUS_STREAMING_ERROR_UNAUTHORIZED; + g_snprintf(error_cause, 512, "Unauthorized (wrong secret)"); + goto error; + } + } + if(!strcasecmp(request_text, "enable")) { + /* Enable a previously disabled mountpoint */ + JANUS_LOG(LOG_INFO, "[%s] Stream enabled\n", mp->name); + mp->enabled = TRUE; + /* FIXME: Should we notify the listeners, or is this up to the controller application? */ + } else { + /* Disable a previously enabled mountpoint */ + JANUS_LOG(LOG_INFO, "[%s] Stream disabled\n", mp->name); + mp->enabled = FALSE; + /* Any recording to close? */ + janus_streaming_rtp_source *source = mp->source; + if(source->arc) { + janus_recorder_close(source->arc); + JANUS_LOG(LOG_INFO, "[%s] Closed audio recording %s\n", mp->name, source->arc->filename ? source->arc->filename : "??"); + janus_recorder *tmp = source->arc; + source->arc = NULL; + janus_recorder_free(tmp); + } + if(source->vrc) { + janus_recorder_close(source->vrc); + JANUS_LOG(LOG_INFO, "[%s] Closed video recording %s\n", mp->name, source->vrc->filename ? source->vrc->filename : "??"); + janus_recorder *tmp = source->vrc; + source->vrc = NULL; + janus_recorder_free(tmp); + } + /* FIXME: Should we notify the listeners, or is this up to the controller application? */ + } + janus_mutex_unlock(&mountpoints_mutex); + /* Send a success response back */ + json_t *response = json_object(); + json_object_set_new(response, "streaming", json_string("ok")); + char *response_text = json_dumps(response, JSON_INDENT(3) | JSON_PRESERVE_ORDER); + json_decref(response); + janus_plugin_result *result = janus_plugin_result_new(JANUS_PLUGIN_OK, response_text); + g_free(response_text); + return result; } else if(!strcasecmp(request_text, "watch") || !strcasecmp(request_text, "start") || !strcasecmp(request_text, "pause") || !strcasecmp(request_text, "stop")) { /* These messages are handled asynchronously */ @@ -1687,6 +1845,7 @@ janus_streaming_mountpoint *janus_streaming_create_rtp_source( else description = g_strdup(name ? name : tempname); live_rtp->description = description; + live_rtp->enabled = TRUE; live_rtp->active = FALSE; live_rtp->streaming_type = janus_streaming_type_live; live_rtp->streaming_source = janus_streaming_source_rtp; @@ -1769,6 +1928,7 @@ janus_streaming_mountpoint *janus_streaming_create_file_source( else description = g_strdup(name ? name : tempname); file_source->description = description; + file_source->enabled = TRUE; file_source->active = FALSE; file_source->streaming_type = live ? janus_streaming_type_live : janus_streaming_type_on_demand; file_source->streaming_source = janus_streaming_source_file; @@ -1881,7 +2041,7 @@ static void *janus_streaming_ondemand_thread(void *data) { before.tv_usec -= 1000000; } /* If not started or paused, wait some more */ - if(!session->started) + if(!session->started || !mountpoint->enabled) continue; /* Read frame from file... */ read = fread(buf + RTP_HEADER_SIZE, sizeof(char), 160, audio); @@ -1989,6 +2149,9 @@ static void *janus_streaming_filesource_thread(void *data) { before.tv_sec++; before.tv_usec -= 1000000; } + /* If paused, wait some more */ + if(!mountpoint->enabled) + continue; /* Read frame from file... */ read = fread(buf + RTP_HEADER_SIZE, sizeof(char), 160, audio); if(feof(audio)) { @@ -2107,6 +2270,9 @@ static void *janus_streaming_relay_thread(void *data) { addrlen = sizeof(remote); bytes = recvfrom(audio_fd, buffer, 1500, 0, (struct sockaddr*)&remote, &addrlen); // JANUS_LOG(LOG_VERB, "************************\nGot %d bytes on the audio channel...\n", bytes); + /* If paused, ignore this packet */ + if(!mountpoint->enabled) + continue; rtp_header *rtp = (rtp_header *)buffer; // JANUS_LOG(LOG_VERB, " ... parsed RTP packet (ssrc=%u, pt=%u, seq=%u, ts=%u)...\n", // ntohl(rtp->ssrc), rtp->type, ntohs(rtp->seq_number), ntohl(rtp->timestamp)); @@ -2148,6 +2314,9 @@ static void *janus_streaming_relay_thread(void *data) { addrlen = sizeof(remote); bytes = recvfrom(video_fd, buffer, 1500, 0, (struct sockaddr*)&remote, &addrlen); //~ JANUS_LOG(LOG_VERB, "************************\nGot %d bytes on the video channel...\n", bytes); + /* If paused, ignore this packet */ + if(!mountpoint->enabled) + continue; rtp_header *rtp = (rtp_header *)buffer; //~ JANUS_LOG(LOG_VERB, " ... parsed RTP packet (ssrc=%u, pt=%u, seq=%u, ts=%u)...\n", //~ ntohl(rtp->ssrc), rtp->type, ntohs(rtp->seq_number), ntohl(rtp->timestamp)); diff --git a/plugins/janus_videoroom.c b/plugins/janus_videoroom.c index e48958d2fa..273495d18b 100644 --- a/plugins/janus_videoroom.c +++ b/plugins/janus_videoroom.c @@ -428,14 +428,7 @@ int janus_videoroom_init(janus_callbacks *callback, const char *config_path) { if(firfreq != NULL && firfreq->value != NULL) videoroom->fir_freq = atol(firfreq->value); if(record && record->value) { - if(!strcasecmp(record->value, "true")) - videoroom->record = TRUE; - else if(!strcasecmp(record->value, "false")) - videoroom->record = FALSE; - else { - JANUS_LOG(LOG_WARN, "Invalid value '%s' for 'record', recording disabled\n", record->value); - videoroom->record = FALSE; - } + videoroom->record = janus_is_true(record->value); if(rec_dir && rec_dir->value) { videoroom->rec_dir = g_strdup(rec_dir->value); } @@ -1172,6 +1165,15 @@ void janus_videoroom_hangup_media(janus_plugin_session *handle) { participant->video_active = FALSE; participant->fir_latest = 0; participant->fir_seq = 0; + janus_mutex_lock(&participant->listeners_mutex); + while(participant->listeners) { + janus_videoroom_listener *l = (janus_videoroom_listener *)participant->listeners->data; + if(l) { + participant->listeners = g_slist_remove(participant->listeners, l); + l->feed = NULL; + } + } + janus_mutex_unlock(&participant->listeners_mutex); json_t *event = json_object(); json_object_set_new(event, "videoroom", json_string("event")); json_object_set_new(event, "room", json_integer(participant->room->room_id)); @@ -1992,9 +1994,9 @@ static void *janus_videoroom_handler(void *data) { } list = g_list_reverse(list); if(janus_videoroom_muxed_unsubscribe(listener, list, msg->transaction) < 0) { - JANUS_LOG(LOG_ERR, "Error subscribing!\n"); + JANUS_LOG(LOG_ERR, "Error unsubscribing!\n"); error_code = JANUS_VIDEOROOM_ERROR_UNKNOWN_ERROR; /* FIXME */ - g_snprintf(error_cause, 512, "Error subscribing!"); + g_snprintf(error_cause, 512, "Error unsubscribing!"); goto error; } continue; @@ -2272,7 +2274,7 @@ int janus_videoroom_muxed_subscribe(janus_videoroom_listener_muxed *muxed_listen if(!muxed_listener || !feeds) return -1; janus_mutex_lock(&muxed_listener->listeners_mutex); - JANUS_LOG(LOG_INFO, "Subscribing to %d feeds\n", g_list_length(feeds)); + JANUS_LOG(LOG_VERB, "Subscribing to %d feeds\n", g_list_length(feeds)); janus_videoroom *videoroom = muxed_listener->room; GList *ps = feeds; json_t *list = json_array(); @@ -2318,7 +2320,7 @@ int janus_videoroom_muxed_subscribe(janus_videoroom_listener_muxed *muxed_listen publisher->listeners = g_slist_append(publisher->listeners, listener); janus_mutex_unlock(&publisher->listeners_mutex); muxed_listener->listeners = g_slist_append(muxed_listener->listeners, listener); - JANUS_LOG(LOG_WARN, "Now subscribed to %d feeds\n", g_slist_length(muxed_listener->listeners)); + JANUS_LOG(LOG_VERB, "Now subscribed to %d feeds\n", g_slist_length(muxed_listener->listeners)); /* Add to feeds in the answer */ added_feeds++; json_t *f = json_object(); @@ -2347,7 +2349,7 @@ int janus_videoroom_muxed_subscribe(janus_videoroom_listener_muxed *muxed_listen int janus_videoroom_muxed_unsubscribe(janus_videoroom_listener_muxed *muxed_listener, GList *feeds, char *transaction) { janus_mutex_lock(&muxed_listener->listeners_mutex); - JANUS_LOG(LOG_INFO, "Unsubscribing from %d feeds\n", g_list_length(feeds)); + JANUS_LOG(LOG_VERB, "Unsubscribing from %d feeds\n", g_list_length(feeds)); janus_videoroom *videoroom = muxed_listener->room; GList *ps = feeds; json_t *list = json_array(); @@ -2369,7 +2371,7 @@ int janus_videoroom_muxed_unsubscribe(janus_videoroom_listener_muxed *muxed_list janus_mutex_unlock(&publisher->listeners_mutex); listener->feed = NULL; muxed_listener->listeners = g_slist_remove(muxed_listener->listeners, listener); - JANUS_LOG(LOG_WARN, "Now subscribed to %d feeds\n", g_slist_length(muxed_listener->listeners)); + JANUS_LOG(LOG_VERB, "Now subscribed to %d feeds\n", g_slist_length(muxed_listener->listeners)); /* Add to feeds in the answer */ removed_feeds++; json_t *f = json_object(); @@ -2405,12 +2407,33 @@ int janus_videoroom_muxed_offer(janus_videoroom_listener_muxed *muxed_listener, * that will translate to multiple SSRCs when merging the SDP */ int audio = 0, video = 0; char audio_muxed[1024], video_muxed[1024], temp[255]; + char sdp[2048], audio_mline[512], video_mline[512]; memset(audio_muxed, 0, 1024); memset(video_muxed, 0, 1024); + memset(audio_mline, 0, 512); + memset(video_mline, 0, 512); + /* Prepare the m-lines (FIXME this will result in an audio line even for video-only rooms, but we don't care) */ + g_snprintf(audio_mline, 512, sdp_a_template, + OPUS_PT, /* Opus payload type */ + "sendonly", /* The publisher gets a recvonly back */ + OPUS_PT); /* Opus payload type */ + g_snprintf(video_mline, 512, sdp_v_template, + VP8_PT, /* VP8 payload type */ + 0, /* Bandwidth */ + "sendonly", /* The publisher gets a recvonly back */ + VP8_PT, /* VP8 payload type */ + VP8_PT, /* VP8 payload type */ + VP8_PT, /* VP8 payload type */ + VP8_PT, /* VP8 payload type */ + VP8_PT); /* VP8 payload type */ + /* FIXME Add a fake user/SSRC just to avoid the "Failed to set max send bandwidth for video content" bug */ + g_strlcat(audio_muxed, "a=planb:mcu0 1\r\n", 1024); + g_strlcat(video_muxed, "a=planb:mcu0 2\r\n", 1024); + /* Go through all the available publishers */ GSList *ps = muxed_listener->listeners; while(ps) { janus_videoroom_listener *l = (janus_videoroom_listener *)ps->data; - if(l && l->feed && l->feed->sdp) { + if(l && l->feed) { //~ && l->feed->sdp) { if(strstr(l->feed->sdp, "m=audio")) { audio++; g_snprintf(temp, 255, "a=planb:mcu%"SCNu64" %"SCNu32"\r\n", l->feed->user_id, l->feed->audio_ssrc); @@ -2425,42 +2448,24 @@ int janus_videoroom_muxed_offer(janus_videoroom_listener_muxed *muxed_listener, ps = ps->next; } /* Also add a bandwidth SDP attribute if we're capping the bitrate in the room */ - char sdp[2048], audio_mline[256], video_mline[512]; if(audio) { - g_snprintf(audio_mline, 256, sdp_a_template, - OPUS_PT, /* Opus payload type */ - "sendonly", /* The publisher gets a recvonly back */ - OPUS_PT); /* Opus payload type */ g_strlcat(audio_mline, audio_muxed, 2048); - } else { - audio_mline[0] = '\0'; } if(video) { - g_snprintf(video_mline, 512, sdp_v_template, - VP8_PT, /* VP8 payload type */ - 0, /* Bandwidth */ - "sendonly", /* The publisher gets a recvonly back */ - VP8_PT, /* VP8 payload type */ - VP8_PT, /* VP8 payload type */ - VP8_PT, /* VP8 payload type */ - VP8_PT, /* VP8 payload type */ - VP8_PT); /* VP8 payload type */ g_strlcat(video_mline, video_muxed, 2048); - } else { - video_mline[0] = '\0'; } g_snprintf(sdp, 2048, sdp_template, janus_get_monotonic_time(), /* We need current time here */ janus_get_monotonic_time(), /* We need current time here */ muxed_listener->room->room_name, /* Video room name */ - audio_mline, /* Audio m-line, if any */ - video_mline); /* Video m-line, if any */ + audio_mline, /* Audio m-line */ + video_mline); /* Video m-line */ char *newsdp = g_strdup(sdp); if(video) { - /* Remove useless bandwidth attribute */ + /* Remove useless bandwidth attribute, if any */ newsdp = janus_string_replace(newsdp, "b=AS:0\r\n", ""); } - JANUS_LOG(LOG_WARN, "%s", newsdp); + JANUS_LOG(LOG_VERB, "%s", newsdp); /* How long will the gateway take to push the event? */ gint64 start = janus_get_monotonic_time(); int res = gateway->push_event(muxed_listener->session->handle, &janus_videoroom_plugin, transaction, event_text, "offer", newsdp); @@ -2537,6 +2542,15 @@ static void janus_videoroom_participant_free(janus_videoroom_participant *p) { janus_recorder_free(p->vrc); } + janus_mutex_lock(&p->listeners_mutex); + while(p->listeners) { + janus_videoroom_listener *l = (janus_videoroom_listener *)p->listeners->data; + if(l) { + p->listeners = g_slist_remove(p->listeners, l); + l->feed = NULL; + } + } + janus_mutex_unlock(&p->listeners_mutex); g_slist_free(p->listeners); janus_mutex_destroy(&p->listeners_mutex); diff --git a/sdp.h b/sdp.h index 114f9c7d43..0e8751d238 100644 --- a/sdp.h +++ b/sdp.h @@ -12,10 +12,8 @@ * * \todo Right now, we only support sessions with up to a single audio * and/or a single video stream (as in, a single audio and/or video - * m-line). Later versions of the gateway will add support for more - * audio and video streams in the same session. Besides, DataChannels - * are not supported as of yet either: this is something we'll start - * working in the future as well. + * m-line) plus an optional DataChannel. Later versions of the gateway + * will add support for more media streams of the same type in a session. * * \ingroup protocols * \ref protocols diff --git a/utils.c b/utils.c index 253f52b69e..f62c2a304e 100644 --- a/utils.c +++ b/utils.c @@ -19,6 +19,9 @@ gint64 janus_get_monotonic_time(void) { return (ts.tv_sec*G_GINT64_CONSTANT(1000000)) + (ts.tv_nsec/G_GINT64_CONSTANT(1000)); } +gboolean janus_is_true(const char *value) { + return value && (!strcasecmp(value, "yes") || !strcasecmp(value, "true") || !strcasecmp(value, "1")); +} void janus_flags_reset(janus_flags *flags) { if(flags != NULL) diff --git a/utils.h b/utils.h index f5837d71c4..ecafd63445 100644 --- a/utils.h +++ b/utils.h @@ -28,6 +28,10 @@ gint64 janus_get_monotonic_time(void); * @returns A pointer to the updated text string (re-allocated or just updated) */ char *janus_string_replace(char *message, const char *old_string, const char *new_string) G_GNUC_WARN_UNUSED_RESULT; +/*! \brief Helper to parse yes/no|true/false configuration values + * @param value The configuration value to parse + * @returns true if the value contains a "yes", "YES", "true", TRUE", "1", false otherwise */ +gboolean janus_is_true(const char *value); /** @name Flags helper methods */