From 116b1beeae97bda08ef48fe957157c5811a46d8a Mon Sep 17 00:00:00 2001 From: Lorenzo Miniero Date: Mon, 20 Apr 2020 12:48:09 +0200 Subject: [PATCH] Add support for playback of audio files in AudioBridge --- Makefile.am | 6 +- conf/janus.plugin.audiobridge.jcfg.sample | 2 + plugins/janus_audiobridge.c | 535 +++++++++++++++++++++- 3 files changed, 515 insertions(+), 28 deletions(-) diff --git a/Makefile.am b/Makefile.am index 6c0f5f9200..21ca5ec4a9 100644 --- a/Makefile.am +++ b/Makefile.am @@ -407,9 +407,9 @@ plugins_libadd = \ if ENABLE_PLUGIN_AUDIOBRIDGE plugin_LTLIBRARIES += plugins/libjanus_audiobridge.la plugins_libjanus_audiobridge_la_SOURCES = plugins/janus_audiobridge.c -plugins_libjanus_audiobridge_la_CFLAGS = $(plugins_cflags) $(OPUS_CFLAGS) -plugins_libjanus_audiobridge_la_LDFLAGS = $(plugins_ldflags) $(OPUS_LDFLAGS) $(OPUS_LIBS) -plugins_libjanus_audiobridge_la_LIBADD = $(plugins_libadd) $(OPUS_LIBADD) +plugins_libjanus_audiobridge_la_CFLAGS = $(plugins_cflags) $(OPUS_CFLAGS) $(OGG_CFLAGS) +plugins_libjanus_audiobridge_la_LDFLAGS = $(plugins_ldflags) $(OPUS_LDFLAGS) $(OPUS_LIBS) $(OGG_LDFLAGS) $(OGG_LIBS) +plugins_libjanus_audiobridge_la_LIBADD = $(plugins_libadd) $(OPUS_LIBADD) $(OGG_LIBADD) conf_DATA += conf/janus.plugin.audiobridge.jcfg.sample EXTRA_DIST += conf/janus.plugin.audiobridge.jcfg.sample endif diff --git a/conf/janus.plugin.audiobridge.jcfg.sample b/conf/janus.plugin.audiobridge.jcfg.sample index cd875b4010..c5f2359ddb 100644 --- a/conf/janus.plugin.audiobridge.jcfg.sample +++ b/conf/janus.plugin.audiobridge.jcfg.sample @@ -34,6 +34,8 @@ general: { # if this key is provided in the request #lock_rtp_forward = true # Whether the admin_key above should be # enforced for RTP forwarding requests too + #lock_play_file = true # Whether the admin_key above should be + # enforced for playing .opus files too #record_tmp_ext = "tmp" # Optional temporary extension to add to filenames # while recording: e.g., setting "tmp" would mean # .wav --> .wav.tmp until the file is closed diff --git a/plugins/janus_audiobridge.c b/plugins/janus_audiobridge.c index e40e50681e..8aa8c4dba7 100644 --- a/plugins/janus_audiobridge.c +++ b/plugins/janus_audiobridge.c @@ -659,6 +659,9 @@ room-: { #include #include +#ifdef HAVE_LIBOGG +#include +#endif #include #include @@ -838,6 +841,14 @@ static struct janus_json_parameter rtp_forward_parameters[] = { static struct janus_json_parameter stop_rtp_forward_parameters[] = { {"stream_id", JSON_INTEGER, JANUS_JSON_PARAM_REQUIRED | JANUS_JSON_PARAM_POSITIVE} }; +static struct janus_json_parameter play_file_parameters[] = { + {"filename", JSON_STRING, JANUS_JSON_PARAM_REQUIRED}, + {"file_id", JSON_STRING, 0}, + {"loop", JANUS_JSON_BOOL, 0} +}; +static struct janus_json_parameter stop_file_parameters[] = { + {"file_id", JSON_STRING, JANUS_JSON_PARAM_REQUIRED} +}; /* Static configuration instance */ static janus_config *config = NULL; @@ -890,6 +901,7 @@ typedef struct janus_audiobridge_room { gint64 record_lastupdate; /* Time when we last updated the wav header */ gboolean destroy; /* Value to flag the room for destruction */ GHashTable *participants; /* Map of participants */ + GHashTable *anncs; /* Map of announcements */ gboolean check_tokens; /* Whether to check tokens when participants join (see below) */ GHashTable *allowed; /* Map of participants (as tokens) allowed to join */ GThread *thread; /* Mixer thread for this room */ @@ -906,6 +918,7 @@ static GHashTable *rooms; static janus_mutex rooms_mutex = JANUS_MUTEX_INITIALIZER; static char *admin_key = NULL; static gboolean lock_rtpfwd = FALSE; +static gboolean lock_playfile = FALSE; typedef struct janus_audiobridge_session { janus_plugin_session *handle; @@ -920,6 +933,158 @@ typedef struct janus_audiobridge_session { static GHashTable *sessions; static janus_mutex sessions_mutex = JANUS_MUTEX_INITIALIZER; +#ifdef HAVE_LIBOGG +/* Helper struct to handle the playout of Opus files */ +typedef struct janus_audiobridge_file { + char *id; + char *filename; + FILE *file; + ogg_sync_state sync; + ogg_stream_state stream; + ogg_page page; + ogg_packet pkt; + char *oggbuf; + gboolean started, loop; + gint state, headers; +} janus_audiobridge_file; +/* Helper method to open an Opus file, and make sure it's valid */ +static int janus_audiobridge_file_init(janus_audiobridge_file *ctx) { + if(ctx == NULL || ctx->file == NULL) + return -1; + fseek(ctx->file, 0, SEEK_SET); + ogg_stream_clear(&ctx->stream); + ogg_sync_clear(&ctx->sync); + if(ogg_sync_init(&ctx->sync) < 0) { + JANUS_LOG(LOG_ERR, "[%s] Error re-initializing Ogg sync state...\n", ctx->id); + return -1; + } + ctx->headers = 0; + return 0; +} +/* Helper method to check if an Ogg page begins with an Ogg stream */ +static gboolean janus_audiobridge_ogg_is_opus(ogg_page *page) { + ogg_stream_state state; + ogg_packet pkt; + ogg_stream_init(&state, ogg_page_serialno(page)); + ogg_stream_pagein(&state, page); + if(ogg_stream_packetout(&state, &pkt) == 1) { + if(pkt.bytes >= 19 && !memcmp(pkt.packet, "OpusHead", 8)) { + ogg_stream_clear(&state); + return 1; + } + } + ogg_stream_clear(&state); + return FALSE; +} +/* Helper method to traverse the Opus file until we get a packet we can send */ +static int janus_audiobridge_file_read(janus_audiobridge_file *ctx, OpusDecoder *decoder, opus_int16 *buffer, int length) { + if(ctx == NULL || ctx->file == NULL || decoder == NULL || buffer == NULL) + return -1; + /* Check our current state in processing the Ogg file */ + int read = 0; + if(ctx->state == 0) { + /* Prepare a buffer, and read from the Ogg file... */ + ctx->oggbuf = ogg_sync_buffer(&ctx->sync, 8192); + if(ctx->oggbuf == NULL) { + JANUS_LOG(LOG_ERR, "[%s] ogg_sync_buffer failed...\n", ctx->id); + return -2; + } + read = fread(ctx->oggbuf, 1, 8192, ctx->file); + if(feof(ctx->file)) { + /* Check if we should rewind, or be done */ + if(!ctx->loop) { + /* We're done */ + return 0; + } + /* Rewind */ + JANUS_LOG(LOG_VERB, "[%s] Rewind! (%s)\n", ctx->id, ctx->filename); + if(janus_audiobridge_file_init(ctx) < 0) + return -3; + return janus_audiobridge_file_read(ctx, decoder, buffer, length); + } + if(ogg_sync_wrote(&ctx->sync, read) < 0) { + JANUS_LOG(LOG_ERR, "[%s] ogg_sync_wrote failed...\n", ctx->id); + return -4; + } + /* Next state: sync pageout */ + ctx->state = 1; + } + if(ctx->state == 1) { + /* Prepare an ogg_page out of the buffer */ + while((read = ogg_sync_pageout(&ctx->sync, &ctx->page)) == 1) { + /* Let's look for an Opus stream, first of all */ + if(ctx->headers == 0) { + if(janus_audiobridge_ogg_is_opus(&ctx->page)) { + /* This is the start of an Opus stream */ + if(ogg_stream_init(&ctx->stream, ogg_page_serialno(&ctx->page)) < 0) { + JANUS_LOG(LOG_ERR, "[%s] ogg_stream_init failed...\n", ctx->id); + return -5; + } + ctx->headers++; + } else if(!ogg_page_bos(&ctx->page)) { + /* No Opus stream? */ + JANUS_LOG(LOG_ERR, "[%s] No Opus stream...\n", ctx->id); + return -6; + } else { + /* Still waiting for an Opus stream */ + return janus_audiobridge_file_read(ctx, decoder, buffer, length); + } + } + /* Submit the page for packetization */ + if(ogg_stream_pagein(&ctx->stream, &ctx->page) < 0) { + JANUS_LOG(LOG_ERR, "[%s] ogg_stream_pagein failed...\n", ctx->id); + return -7; + } + /* Time to start reading packets */ + ctx->state = 2; + break; + } + if(read != 1) { + /* Go back to reading from the file */ + ctx->state = 0; + return janus_audiobridge_file_read(ctx, decoder, buffer, length); + } + } + if(ctx->state == 2) { + /* Read and process available packets */ + if(ogg_stream_packetout(&ctx->stream, &ctx->pkt) != 1) { + /* Go back to reading pages */ + ctx->state = 1; + return janus_audiobridge_file_read(ctx, decoder, buffer, length); + } else { + /* Skip header packets */ + if(ctx->headers == 1 && ctx->pkt.bytes >= 19 && !memcmp(ctx->pkt.packet, "OpusHead", 8)) { + ctx->headers++; + return janus_audiobridge_file_read(ctx, decoder, buffer, length); + } + if(ctx->headers == 2 && ctx->pkt.bytes >= 16 && !memcmp(ctx->pkt.packet, "OpusTags", 8)) { + ctx->headers++; + return janus_audiobridge_file_read(ctx, decoder, buffer, length); + } + /* Decode the audio */ + length = opus_decode(decoder, ctx->pkt.packet, ctx->pkt.bytes, + (opus_int16 *)buffer, length, 0); + return length; + } + } + /* If we got here, continue with the iteration */ + return -9; +} +/* Helper method to cleanup an Opus context */ +static void janus_audiobridge_file_free(janus_audiobridge_file *ctx) { + if(ctx == NULL) + return; + g_free(ctx->id); + g_free(ctx->filename); + if(ctx->file) + fclose(ctx->file); + if(ctx->headers > 0) + ogg_stream_clear(&ctx->stream); + ogg_sync_clear(&ctx->sync); + g_free(ctx); +} +#endif + typedef struct janus_audiobridge_participant { janus_audiobridge_session *session; janus_audiobridge_room *room; /* Room */ @@ -957,6 +1122,9 @@ typedef struct janus_audiobridge_participant { gboolean reset; /* Whether or not the Opus context must be reset, without re-joining the room */ GThread *thread; /* Encoding thread for this participant */ janus_recorder *arc; /* The Janus recorder instance for this user's audio, if enabled */ +#ifdef HAVE_LIBOGG + janus_audiobridge_file *annc; /* In case this is a fake participant, a playable file */ +#endif janus_mutex rec_mutex; /* Mutex to protect the recorder from race conditions */ volatile gint destroyed; /* Whether this room has been destroyed */ janus_refcount ref; /* Reference counter for this participant */ @@ -1013,6 +1181,7 @@ static void janus_audiobridge_participant_free(const janus_refcount *participant } g_async_queue_unref(participant->outbuf); } + janus_audiobridge_file_free(participant->annc); g_free(participant); } @@ -1052,6 +1221,7 @@ static void janus_audiobridge_room_free(const janus_refcount *audiobridge_ref) { g_free(audiobridge->room_pin); g_free(audiobridge->record_file); g_hash_table_destroy(audiobridge->participants); + g_hash_table_destroy(audiobridge->anncs); g_hash_table_destroy(audiobridge->allowed); if(audiobridge->rtp_udp_sock > 0) close(audiobridge->rtp_udp_sock); @@ -1654,6 +1824,9 @@ int janus_audiobridge_init(janus_callbacks *callback, const char *config_path) { janus_config_item *lrf = janus_config_get(config, config_general, janus_config_type_item, "lock_rtp_forward"); if(admin_key && lrf != NULL && lrf->value != NULL) lock_rtpfwd = janus_is_true(lrf->value); + janus_config_item *lpf = janus_config_get(config, config_general, janus_config_type_item, "lock_play_file"); + if(admin_key && lpf != NULL && lpf->value != NULL) + lock_playfile = janus_is_true(lpf->value); janus_config_item *ext = janus_config_get(config, config_general, janus_config_type_item, "record_tmp_ext"); if(ext != NULL && ext->value != NULL) rec_tempext = g_strdup(ext->value); @@ -1805,6 +1978,8 @@ int janus_audiobridge_init(janus_callbacks *callback, const char *config_path) { audiobridge->participants = g_hash_table_new_full( string_ids ? g_str_hash : g_int64_hash, string_ids ? g_str_equal : g_int64_equal, (GDestroyNotify)g_free, (GDestroyNotify)janus_audiobridge_participant_unref); + audiobridge->anncs = g_hash_table_new_full(g_str_hash, g_str_equal, + (GDestroyNotify)g_free, (GDestroyNotify)janus_audiobridge_participant_unref); audiobridge->check_tokens = FALSE; /* Static rooms can't have an "allowed" list yet, no hooks to the configuration file */ audiobridge->allowed = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free, NULL); g_atomic_int_set(&audiobridge->destroyed, 0); @@ -1852,7 +2027,7 @@ int janus_audiobridge_init(janus_callbacks *callback, const char *config_path) { GHashTableIter iter; gpointer value; g_hash_table_iter_init(&iter, rooms); - while (g_hash_table_iter_next(&iter, NULL, &value)) { + while(g_hash_table_iter_next(&iter, NULL, &value)) { janus_audiobridge_room *ar = value; JANUS_LOG(LOG_VERB, " ::: [%s][%s] %"SCNu32" (%s be recorded)\n", ar->room_id_str, ar->room_name, ar->sampling_rate, ar->record ? "will" : "will NOT"); @@ -1937,7 +2112,7 @@ const char *janus_audiobridge_get_package(void) { static janus_audiobridge_session *janus_audiobridge_lookup_session(janus_plugin_session *handle) { janus_audiobridge_session *session = NULL; - if (g_hash_table_contains(sessions, handle)) { + if(g_hash_table_contains(sessions, handle)) { session = (janus_audiobridge_session *)handle->plugin_handle; } return session; @@ -1989,7 +2164,7 @@ static void janus_audiobridge_notify_participants(janus_audiobridge_participant GHashTableIter iter; gpointer value; g_hash_table_iter_init(&iter, participant->room->participants); - while (!participant->room->destroyed && g_hash_table_iter_next(&iter, NULL, &value)) { + while(!participant->room->destroyed && g_hash_table_iter_next(&iter, NULL, &value)) { janus_audiobridge_participant *p = value; if(p && p->session && p != participant) { JANUS_LOG(LOG_VERB, "Notifying participant %s (%s)\n", p->user_id_str, p->display ? p->display : "??"); @@ -2256,7 +2431,10 @@ static json_t *janus_audiobridge_process_synchronous_request(janus_audiobridge_s audiobridge->record_file = g_strdup(json_string_value(recfile)); audiobridge->recording = NULL; audiobridge->destroy = 0; - audiobridge->participants = g_hash_table_new_full(g_int64_hash, g_int64_equal, + audiobridge->participants = g_hash_table_new_full( + string_ids ? g_str_hash : g_int64_hash, string_ids ? g_str_equal : g_int64_equal, + (GDestroyNotify)g_free, (GDestroyNotify)janus_audiobridge_participant_unref); + audiobridge->anncs = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free, (GDestroyNotify)janus_audiobridge_participant_unref); audiobridge->allowed = g_hash_table_new_full(g_str_hash, g_str_equal, (GDestroyNotify)g_free, NULL); if(allowed != NULL) { @@ -2585,7 +2763,7 @@ static json_t *janus_audiobridge_process_synchronous_request(janus_audiobridge_s GHashTableIter iter; gpointer value; g_hash_table_iter_init(&iter, audiobridge->participants); - while (g_hash_table_iter_next(&iter, NULL, &value)) { + while(g_hash_table_iter_next(&iter, NULL, &value)) { janus_audiobridge_participant *p = value; if(p && p->session) { if(p->room) { @@ -2955,7 +3133,7 @@ static json_t *janus_audiobridge_process_synchronous_request(janus_audiobridge_s GHashTableIter iter; gpointer value; g_hash_table_iter_init(&iter, audiobridge->participants); - while (g_hash_table_iter_next(&iter, NULL, &value)) { + while(g_hash_table_iter_next(&iter, NULL, &value)) { janus_audiobridge_participant *p = value; JANUS_LOG(LOG_VERB, "Notifying participant %s (%s)\n", p->user_id_str, p->display ? p->display : "??"); int ret = gateway->push_event(p->session->handle, &janus_audiobridge_plugin, NULL, pub, NULL); @@ -3071,7 +3249,7 @@ static json_t *janus_audiobridge_process_synchronous_request(janus_audiobridge_s GHashTableIter iter; gpointer value; g_hash_table_iter_init(&iter, audiobridge->participants); - while (g_hash_table_iter_next(&iter, NULL, &value)) { + while(g_hash_table_iter_next(&iter, NULL, &value)) { janus_audiobridge_participant *p = value; JANUS_LOG(LOG_VERB, "Notifying participant %s (%s)\n", p->user_id_str, p->display ? p->display : "??"); int ret = gateway->push_event(p->session->handle, &janus_audiobridge_plugin, NULL, event, NULL); @@ -3136,7 +3314,7 @@ static json_t *janus_audiobridge_process_synchronous_request(janus_audiobridge_s GHashTableIter iter; gpointer value; g_hash_table_iter_init(&iter, audiobridge->participants); - while (!g_atomic_int_get(&audiobridge->destroyed) && g_hash_table_iter_next(&iter, NULL, &value)) { + while(!g_atomic_int_get(&audiobridge->destroyed) && g_hash_table_iter_next(&iter, NULL, &value)) { janus_audiobridge_participant *p = value; json_t *pl = json_object(); json_object_set_new(pl, "id", string_ids ? json_string(p->user_id_str) : json_integer(p->user_id)); @@ -3327,7 +3505,7 @@ static json_t *janus_audiobridge_process_synchronous_request(janus_audiobridge_s goto prepare_response; } - if (janus_audiobridge_create_udp_socket_if_needed(audiobridge)) { + if(janus_audiobridge_create_udp_socket_if_needed(audiobridge)) { janus_mutex_unlock(&audiobridge->mutex); janus_mutex_unlock(&rooms_mutex); error_code = JANUS_AUDIOBRIDGE_ERROR_UNKNOWN_ERROR; @@ -3335,7 +3513,7 @@ static json_t *janus_audiobridge_process_synchronous_request(janus_audiobridge_s goto prepare_response; } - if (janus_audiobridge_create_opus_encoder_if_needed(audiobridge)) { + if(janus_audiobridge_create_opus_encoder_if_needed(audiobridge)) { janus_mutex_unlock(&audiobridge->mutex); janus_mutex_unlock(&rooms_mutex); error_code = JANUS_AUDIOBRIDGE_ERROR_LIBOPUS_ERROR; @@ -3398,7 +3576,6 @@ static json_t *janus_audiobridge_process_synchronous_request(janus_audiobridge_s } guint32 stream_id = json_integer_value(json_object_get(root, "stream_id")); /* Update room */ - /* Update room */ janus_mutex_lock(&rooms_mutex); janus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms, string_ids ? (gpointer)room_id_str : (gpointer)&room_id); @@ -3517,6 +3694,258 @@ static json_t *janus_audiobridge_process_synchronous_request(janus_audiobridge_s json_object_set_new(response, "room", string_ids ? json_string(room_id_str) : json_integer(room_id)); json_object_set_new(response, "rtp_forwarders", list); goto prepare_response; + } else if(!strcasecmp(request_text, "play_file")) { +#ifndef HAVE_LIBOGG + JANUS_LOG(LOG_VERB, "Playing files unsupported in this instance\n"); + error_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_REQUEST; + g_snprintf(error_cause, 512, "Playing files unsupported in this instance"); + goto prepare_response; +#else + if(!string_ids) { + JANUS_VALIDATE_JSON_OBJECT(root, room_parameters, + error_code, error_cause, TRUE, + JANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT); + } else { + JANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters, + error_code, error_cause, TRUE, + JANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT); + } + if(error_code != 0) + goto prepare_response; + JANUS_VALIDATE_JSON_OBJECT(root, play_file_parameters, + error_code, error_cause, TRUE, + JANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT); + if(error_code != 0) + goto prepare_response; + if(lock_playfile && admin_key != NULL) { + /* An admin key was specified: make sure it was provided, and that it's valid */ + JANUS_VALIDATE_JSON_OBJECT(root, adminkey_parameters, + error_code, error_cause, TRUE, + JANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT); + if(error_code != 0) + goto prepare_response; + JANUS_CHECK_SECRET(admin_key, root, "admin_key", error_code, error_cause, + JANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED); + if(error_code != 0) + goto prepare_response; + } + /* Parse parameters */ + json_t *room = json_object_get(root, "room"); + guint64 room_id = 0; + char room_id_num[30], *room_id_str = NULL; + if(!string_ids) { + room_id = json_integer_value(room); + g_snprintf(room_id_num, sizeof(room_id_num), "%"SCNu64, room_id); + room_id_str = room_id_num; + } else { + room_id_str = (char *)json_string_value(room); + } + /* Update room */ + janus_mutex_lock(&rooms_mutex); + janus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms, + string_ids ? (gpointer)room_id_str : (gpointer)&room_id); + if(audiobridge == NULL) { + janus_mutex_unlock(&rooms_mutex); + JANUS_LOG(LOG_ERR, "No such room (%s)\n", room_id_str); + error_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM; + g_snprintf(error_cause, 512, "No such room (%s)", room_id_str); + goto prepare_response; + } + /* A secret may be required for this action */ + JANUS_CHECK_SECRET(audiobridge->room_secret, root, "secret", error_code, error_cause, + JANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED); + if(error_code != 0) { + janus_mutex_unlock(&rooms_mutex); + goto prepare_response; + } + janus_mutex_lock(&audiobridge->mutex); + if(audiobridge->destroyed) { + janus_mutex_unlock(&audiobridge->mutex); + janus_mutex_unlock(&rooms_mutex); + JANUS_LOG(LOG_ERR, "No such room (%s)\n", room_id_str); + error_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM; + g_snprintf(error_cause, 512, "No such room (%s)", room_id_str); + goto prepare_response; + } + /* Check if an announcement ID has been provided, or generate a random one */ + json_t *id = json_object_get(root, "file_id"); + char *file_id = (char *)json_string_value(id); + gboolean file_id_allocated = FALSE; + if(file_id == NULL) { + /* Generate a random ID */ + while(file_id == NULL) { + file_id = janus_random_uuid(); + if(g_hash_table_lookup(audiobridge->anncs, file_id) != NULL) { + /* ID already taken, try another one */ + g_clear_pointer(&file_id, g_free); + } + } + file_id_allocated = TRUE; + JANUS_LOG(LOG_VERB, " -- Announcement ID: %s\n", file_id); + } + if(g_hash_table_lookup(audiobridge->anncs, file_id) != NULL) { + /* ID already taken */ + janus_mutex_unlock(&audiobridge->mutex); + janus_mutex_unlock(&rooms_mutex); + JANUS_LOG(LOG_ERR, "File ID exists (%s)\n", file_id); + error_code = JANUS_AUDIOBRIDGE_ERROR_ID_EXISTS; + g_snprintf(error_cause, 512, "File ID exists (%s)", file_id); + goto prepare_response; + } + /* We "abuse" the participant struct for announcements too */ + janus_audiobridge_participant *p = g_malloc0(sizeof(janus_audiobridge_participant)); + janus_refcount_init(&p->ref, janus_audiobridge_participant_free); + p->user_id_str = g_strdup(file_id); + p->codec = JANUS_AUDIOCODEC_OPUS; + p->volume_gain = 100; + /* Open the file and check it's usable */ + p->annc = g_malloc0(sizeof(janus_audiobridge_file)); + p->annc->id = g_strdup(file_id); + p->room = audiobridge; + const char *filename = json_string_value(json_object_get(root, "filename")); + p->annc->filename = g_strdup(filename); + p->annc->file = fopen(filename, "rb"); + if(p->annc->file == NULL || janus_audiobridge_file_init(p->annc) < 0) { + janus_mutex_unlock(&audiobridge->mutex); + janus_mutex_unlock(&rooms_mutex); + if(file_id_allocated) + g_free(file_id); + janus_refcount_decrease(&p->ref); + JANUS_LOG(LOG_ERR, "Error opening file\n"); + error_code = JANUS_AUDIOBRIDGE_ERROR_UNKNOWN_ERROR; + g_snprintf(error_cause, 512, "Error opening file"); + goto prepare_response; + + } + p->annc->loop = json_is_true(json_object_get(root, "loop")); + /* Setup the opus decoder */ + int opuserror = 0; + p->decoder = opus_decoder_create(audiobridge->sampling_rate, 1, &opuserror); + if(opuserror != OPUS_OK) { + janus_mutex_unlock(&audiobridge->mutex); + janus_mutex_unlock(&rooms_mutex); + if(file_id_allocated) + g_free(file_id); + janus_refcount_decrease(&p->ref); + JANUS_LOG(LOG_ERR, "Error creating Opus decoder\n"); + error_code = JANUS_AUDIOBRIDGE_ERROR_LIBOPUS_ERROR; + g_snprintf(error_cause, 512, "Error creating Opus decoder"); + goto prepare_response; + } + /* We're done, add the announcement to the room */ + janus_refcount_increase(&p->ref); + g_hash_table_insert(audiobridge->anncs, g_strdup(p->user_id_str), p); + janus_mutex_unlock(&audiobridge->mutex); + janus_mutex_unlock(&rooms_mutex); + + /* Done, prepare response */ + response = json_object(); + json_object_set_new(response, "audiobridge", json_string("success")); + json_object_set_new(response, "room", string_ids ? json_string(room_id_str) : json_integer(room_id)); + json_object_set_new(response, "file_id", json_string(file_id)); + if(file_id_allocated) + g_free(file_id); + goto prepare_response; +#endif + } else if(!strcasecmp(request_text, "stop_file")) { +#ifndef HAVE_LIBOGG + JANUS_LOG(LOG_VERB, "Playing files unsupported in this instance\n"); + error_code = JANUS_AUDIOBRIDGE_ERROR_INVALID_REQUEST; + g_snprintf(error_cause, 512, "Playing files unsupported in this instance"); + goto prepare_response; +#else + if(!string_ids) { + JANUS_VALIDATE_JSON_OBJECT(root, room_parameters, + error_code, error_cause, TRUE, + JANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT); + } else { + JANUS_VALIDATE_JSON_OBJECT(root, roomstr_parameters, + error_code, error_cause, TRUE, + JANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT); + } + if(error_code != 0) + goto prepare_response; + JANUS_VALIDATE_JSON_OBJECT(root, stop_file_parameters, + error_code, error_cause, TRUE, + JANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT); + if(error_code != 0) + goto prepare_response; + if(lock_playfile && admin_key != NULL) { + /* An admin key was specified: make sure it was provided, and that it's valid */ + JANUS_VALIDATE_JSON_OBJECT(root, adminkey_parameters, + error_code, error_cause, TRUE, + JANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT); + if(error_code != 0) + goto prepare_response; + JANUS_CHECK_SECRET(admin_key, root, "admin_key", error_code, error_cause, + JANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED); + if(error_code != 0) + goto prepare_response; + } + /* Parse parameters */ + json_t *room = json_object_get(root, "room"); + guint64 room_id = 0; + char room_id_num[30], *room_id_str = NULL; + if(!string_ids) { + room_id = json_integer_value(room); + g_snprintf(room_id_num, sizeof(room_id_num), "%"SCNu64, room_id); + room_id_str = room_id_num; + } else { + room_id_str = (char *)json_string_value(room); + } + /* Update room */ + janus_mutex_lock(&rooms_mutex); + janus_audiobridge_room *audiobridge = g_hash_table_lookup(rooms, + string_ids ? (gpointer)room_id_str : (gpointer)&room_id); + if(audiobridge == NULL) { + janus_mutex_unlock(&rooms_mutex); + JANUS_LOG(LOG_ERR, "No such room (%s)\n", room_id_str); + error_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM; + g_snprintf(error_cause, 512, "No such room (%s)", room_id_str); + goto prepare_response; + } + /* A secret may be required for this action */ + JANUS_CHECK_SECRET(audiobridge->room_secret, root, "secret", error_code, error_cause, + JANUS_AUDIOBRIDGE_ERROR_MISSING_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_INVALID_ELEMENT, JANUS_AUDIOBRIDGE_ERROR_UNAUTHORIZED); + if(error_code != 0) { + janus_mutex_unlock(&rooms_mutex); + goto prepare_response; + } + janus_mutex_lock(&audiobridge->mutex); + if(audiobridge->destroyed) { + janus_mutex_unlock(&audiobridge->mutex); + janus_mutex_unlock(&rooms_mutex); + JANUS_LOG(LOG_ERR, "No such room (%s)\n", room_id_str); + error_code = JANUS_AUDIOBRIDGE_ERROR_NO_SUCH_ROOM; + g_snprintf(error_cause, 512, "No such room (%s)", room_id_str); + goto prepare_response; + } + /* Get rid of the announcement: a notification will be sent by the mixer, if needed */ + json_t *id = json_object_get(root, "file_id"); + char *file_id = (char *)json_string_value(id); + janus_audiobridge_participant *p = g_hash_table_lookup(audiobridge->anncs, file_id); + gboolean started = (p && p->annc && p->annc->started); + if(g_hash_table_remove(audiobridge->anncs, file_id) && started) { + /* Send a notification that this announcement is over */ + JANUS_LOG(LOG_INFO, "[%s] Announcement stopped (%s)\n", audiobridge->room_id_str, p->user_id_str); + json_t *event = json_object(); + json_object_set_new(event, "audiobridge", json_string("announcement-stopped")); + json_object_set_new(event, "room", + string_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id)); + json_object_set_new(event, "file_id", json_string(p->user_id_str)); + janus_audiobridge_notify_participants(p, event); + json_decref(event); + } + janus_mutex_unlock(&audiobridge->mutex); + janus_mutex_unlock(&rooms_mutex); + + /* Done, prepare response */ + response = json_object(); + json_object_set_new(response, "audiobridge", json_string("success")); + json_object_set_new(response, "room", string_ids ? json_string(room_id_str) : json_integer(room_id)); + json_object_set_new(response, "file_id", json_string(file_id)); + goto prepare_response; +#endif } else { /* Not a request we recognize, don't do anything */ return NULL; @@ -3728,7 +4157,7 @@ void janus_audiobridge_setup_media(janus_plugin_session *handle) { GHashTableIter iter; gpointer value; g_hash_table_iter_init(&iter, audiobridge->participants); - while (g_hash_table_iter_next(&iter, NULL, &value)) { + while(g_hash_table_iter_next(&iter, NULL, &value)) { janus_audiobridge_participant *p = value; if(p == participant) { continue; /* Skip the new participant itself */ @@ -4108,7 +4537,7 @@ static void janus_audiobridge_hangup_media_internal(janus_plugin_session *handle GHashTableIter iter; gpointer value; g_hash_table_iter_init(&iter, audiobridge->participants); - while (g_hash_table_iter_next(&iter, NULL, &value)) { + while(g_hash_table_iter_next(&iter, NULL, &value)) { janus_audiobridge_participant *p = value; if(p == participant) { continue; /* Skip the leaving participant itself */ @@ -4544,7 +4973,7 @@ static void *janus_audiobridge_handler(void *data) { GHashTableIter iter; gpointer value; g_hash_table_iter_init(&iter, audiobridge->participants); - while (g_hash_table_iter_next(&iter, NULL, &value)) { + while(g_hash_table_iter_next(&iter, NULL, &value)) { janus_audiobridge_participant *p = value; if(p == participant) { continue; @@ -4557,7 +4986,7 @@ static void *janus_audiobridge_handler(void *data) { /* Return a list of all available participants for the new participant now */ json_t *list = json_array(); g_hash_table_iter_init(&iter, audiobridge->participants); - while (g_hash_table_iter_next(&iter, NULL, &value)) { + while(g_hash_table_iter_next(&iter, NULL, &value)) { janus_audiobridge_participant *p = value; if(p == participant) { continue; @@ -4705,7 +5134,7 @@ static void *janus_audiobridge_handler(void *data) { GHashTableIter iter; gpointer value; g_hash_table_iter_init(&iter, audiobridge->participants); - while (g_hash_table_iter_next(&iter, NULL, &value)) { + while(g_hash_table_iter_next(&iter, NULL, &value)) { janus_audiobridge_participant *p = value; if(p == participant) { continue; /* Skip the new participant itself */ @@ -5022,7 +5451,7 @@ static void *janus_audiobridge_handler(void *data) { GHashTableIter iter; gpointer value; g_hash_table_iter_init(&iter, old_audiobridge->participants); - while (g_hash_table_iter_next(&iter, NULL, &value)) { + while(g_hash_table_iter_next(&iter, NULL, &value)) { janus_audiobridge_participant *p = value; if(p == participant) { continue; /* Skip the new participant itself */ @@ -5085,7 +5514,7 @@ static void *janus_audiobridge_handler(void *data) { json_array_append_new(newuserlist, pl); json_object_set_new(newuser, "participants", newuserlist); g_hash_table_iter_init(&iter, audiobridge->participants); - while (g_hash_table_iter_next(&iter, NULL, &value)) { + while(g_hash_table_iter_next(&iter, NULL, &value)) { janus_audiobridge_participant *p = value; if(p == participant) { continue; @@ -5098,7 +5527,7 @@ static void *janus_audiobridge_handler(void *data) { /* Return a list of all available participants for the new participant now */ json_t *list = json_array(); g_hash_table_iter_init(&iter, audiobridge->participants); - while (g_hash_table_iter_next(&iter, NULL, &value)) { + while(g_hash_table_iter_next(&iter, NULL, &value)) { janus_audiobridge_participant *p = value; if(p == participant) { continue; @@ -5160,7 +5589,7 @@ static void *janus_audiobridge_handler(void *data) { GHashTableIter iter; gpointer value; g_hash_table_iter_init(&iter, audiobridge->participants); - while (g_hash_table_iter_next(&iter, NULL, &value)) { + while(g_hash_table_iter_next(&iter, NULL, &value)) { janus_audiobridge_participant *p = value; if(p == participant) { continue; /* Skip the new participant itself */ @@ -5446,7 +5875,7 @@ static void *janus_audiobridge_mixer_thread(void *data) { /* Loop */ int i=0; - int count = 0, rf_count = 0, prev_count = 0; + int count = 0, rf_count = 0, pf_count = 0, prev_count = 0; while(!g_atomic_int_get(&stopping) && !g_atomic_int_get(&audiobridge->destroyed)) { /* See if it's time to prepare a frame */ gettimeofday(&now, NULL); @@ -5471,19 +5900,20 @@ static void *janus_audiobridge_mixer_thread(void *data) { janus_mutex_lock_nodebug(&audiobridge->mutex); count = g_hash_table_size(audiobridge->participants); rf_count = g_hash_table_size(audiobridge->rtp_forwarders); - if((count+rf_count) == 0) { + pf_count = g_hash_table_size(audiobridge->anncs); + if((count+rf_count+pf_count) == 0) { janus_mutex_unlock_nodebug(&audiobridge->mutex); /* No participant and RTP forwarders, do nothing */ if(prev_count > 0) { - JANUS_LOG(LOG_VERB, "Last user/forwarder just left room %s, going idle...\n", audiobridge->room_id_str); + JANUS_LOG(LOG_VERB, "Last user/forwarder/file just left room %s, going idle...\n", audiobridge->room_id_str); prev_count = 0; } continue; } if(prev_count == 0) { - JANUS_LOG(LOG_VERB, "First user/forwarder just joined room %s, waking it up...\n", audiobridge->room_id_str); + JANUS_LOG(LOG_VERB, "First user/forwarder/file just joined room %s, waking it up...\n", audiobridge->room_id_str); } - prev_count = count+rf_count; + prev_count = count+rf_count+pf_count; /* Update RTP header information */ seq++; ts += OPUS_SAMPLES; @@ -5534,6 +5964,61 @@ static void *janus_audiobridge_mixer_thread(void *data) { janus_mutex_unlock(&p->qmutex); ps = ps->next; } + /* If there are announcements playing, mix those too */ + GList *anncs_list = g_hash_table_get_values(audiobridge->anncs); + if(anncs_list != NULL) { + ps = anncs_list; + while(ps) { + janus_audiobridge_participant *p = (janus_audiobridge_participant *)ps->data; + if(p->annc == NULL || g_atomic_int_get(&p->destroyed)) { + ps = ps->next; + continue; + } + int read = janus_audiobridge_file_read(p->annc, p->decoder, resampled, sizeof(resampled)); + if(read <= 0) { + /* Playback over or broken */ + if(p->annc->started) { + /* Send a notification that this announcement is over */ + JANUS_LOG(LOG_INFO, "[%s] Announcement stopped (%s)\n", audiobridge->room_id_str, p->user_id_str); + janus_mutex_lock_nodebug(&audiobridge->mutex); + json_t *event = json_object(); + json_object_set_new(event, "audiobridge", json_string("announcement-stopped")); + json_object_set_new(event, "room", + string_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id)); + json_object_set_new(event, "file_id", json_string(p->user_id_str)); + janus_audiobridge_notify_participants(p, event); + json_decref(event); + /* Remove the announcement */ + g_hash_table_remove(audiobridge->anncs, p->user_id_str); + janus_mutex_unlock_nodebug(&audiobridge->mutex); + } + ps = ps->next; + continue; + } + if(!p->annc->started) { + /* This announcement just started, notify the participants */ + p->annc->started = TRUE; + JANUS_LOG(LOG_INFO, "[%s] Announcement started (%s)\n", audiobridge->room_id_str, p->user_id_str); + janus_mutex_lock_nodebug(&audiobridge->mutex); + json_t *event = json_object(); + json_object_set_new(event, "audiobridge", json_string("announcement-started")); + json_object_set_new(event, "room", + string_ids ? json_string(audiobridge->room_id_str) : json_integer(audiobridge->room_id)); + json_object_set_new(event, "file_id", json_string(p->user_id_str)); + janus_audiobridge_notify_participants(p, event); + json_decref(event); + janus_mutex_unlock_nodebug(&audiobridge->mutex); + } + for(i=0; ivolume_gain == 100) { + buffer[i] += resampled[i]; + } else { + buffer[i] += (resampled[i]*p->volume_gain)/100; + } + } + ps = ps->next; + } + } /* Are we recording the mix? (only do it if there's someone in, though...) */ if(audiobridge->recording != NULL && g_list_length(participants_list) > 0) { for(i=0; i