diff --git a/daemon/main.c b/daemon/main.c index c5b7009d7..3a064b3af 100644 --- a/daemon/main.c +++ b/daemon/main.c @@ -673,6 +673,7 @@ static void options(int *argc, char ***argv, GHashTable *templates) { { "player-cache",0,0, G_OPTION_ARG_NONE, &rtpe_config.player_cache,"Cache media files for playback in memory",NULL}, { "kernel-player",0,0, G_OPTION_ARG_INT, &rtpe_config.kernel_player,"Max number of kernel media player streams","INT"}, { "kernel-player-media",0,0,G_OPTION_ARG_INT, &rtpe_config.kernel_player_media,"Max number of kernel media files","INT"}, + { "preload-media-files",0,0,G_OPTION_ARG_FILENAME_ARRAY,&rtpe_config.preload_media_files,"Preload media file(s) for playback into memory","FILE"}, { "audio-buffer-length",0,0, G_OPTION_ARG_INT,&rtpe_config.audio_buffer_length,"Length in milliseconds of audio buffer","INT"}, { "audio-buffer-delay",0,0, G_OPTION_ARG_INT,&rtpe_config.audio_buffer_delay,"Initial delay in milliseconds for buffered audio","INT"}, { "audio-player",0,0, G_OPTION_ARG_STRING, &use_audio_player, "When to enable the internal audio player","on-demand|play-media|transcoding|always"}, @@ -1479,6 +1480,9 @@ static void create_everything(void) { timeval_from_us(&tmp_tv, (long long) rtpe_config.graphite_interval*1000000); set_graphite_interval_tv(&tmp_tv); + + if (!media_player_preload_files(rtpe_config.preload_media_files)) + die("Failed to preload media files"); } diff --git a/daemon/media_player.c b/daemon/media_player.c index e93b996b2..cbfaaad89 100644 --- a/daemon/media_player.c +++ b/daemon/media_player.c @@ -83,11 +83,23 @@ struct media_player_cache_packet { long long duration; // us long long duration_ts; }; +struct media_player_media_file { + struct obj obj; // must be first + str blob; +}; static mutex_t media_player_cache_lock = MUTEX_STATIC_INIT; static GHashTable *media_player_cache; // keys and values only ever freed at shutdown +TYPED_GHASHTABLE(media_player_media_files_ht, str, struct media_player_media_file, str_hash, str_equal, + NULL, __obj_put); +static mutex_t media_player_media_files_lock = MUTEX_STATIC_INIT; +static media_player_media_files_ht media_player_media_files; + static bool media_player_read_packet(struct media_player *mp); +static mp_cached_code __media_player_add_blob_id(struct media_player *mp, + media_player_opts_t opts, + const rtp_payload_type *dst_pt); #endif static struct timerthread send_timer_thread; @@ -181,6 +193,7 @@ static void __media_player_free(struct media_player *mp) { mutex_destroy(&mp->lock); obj_put(mp->call); av_packet_free(&mp->coder.pkt); + obj_release(mp->media_file); } #endif @@ -1154,6 +1167,90 @@ static bool media_player_play_start(struct media_player *mp, const rtp_payload_t return true; } +static void media_player_media_file_free(struct media_player_media_file *fo) { + g_free(fo->blob.s); +} + +static str media_player_read_file(const char *f) { + gchar *buf = NULL; + gsize len = -1; + GError *err = NULL; + gboolean ret = g_file_get_contents(f, &buf, &len, &err); + if (!ret) { + ilog(LOG_ERR, "Failed to read media file '%s' for caching: %s", f, err->message); + g_error_free(err); + return STR_NULL; + } + if (len <= 0) { + ilog(LOG_ERR, "Media file '%s' appears to be empty", f); + g_free(buf); + return STR_NULL; + } + return STR_LEN(buf, len); +} + +static struct media_player_media_file *media_player_media_file_new(str blob) { + __auto_type fo = obj_alloc0(struct media_player_media_file, + media_player_media_file_free); + fo->blob = blob; + fo->blob.dup = call_ref; // string is allocated by reference on `fo` + return fo; +} + +static struct media_player_media_file *media_player_media_file_read_c(const char *fn) { + str blob = media_player_read_file(fn); + if (blob.len == 0) + return NULL; + return media_player_media_file_new(blob); +} + +static struct media_player_media_file *media_player_media_file_read_str(const str *fn) { + char file_s[PATH_MAX]; + snprintf(file_s, sizeof(file_s), STR_FORMAT, STR_FMT(fn)); + return media_player_media_file_read_c(file_s); +} + +static struct media_player_media_file *media_player_media_files_get(const str *fn) { + struct media_player_media_file *fo; + + { + LOCK(&media_player_media_files_lock); + if (!t_hash_table_is_set(media_player_media_files)) + return NULL; + fo = t_hash_table_lookup(media_player_media_files, fn); + if (!fo) + return NULL; + + obj_hold(fo); + } + + return fo; +} + +// lock must be held, reference will be taken over +static void media_player_media_files_insert(const str *fn, struct media_player_media_file *fo) { + if (!t_hash_table_is_set(media_player_media_files)) + media_player_media_files = media_player_media_files_ht_new(); + str *dup = str_dup(fn); + t_hash_table_insert(media_player_media_files, dup, fo); +} + +static mp_cached_code media_player_set_media_file(struct media_player *mp, + media_player_opts_t opts, + const rtp_payload_type *dst_pt, + struct media_player_media_file *fo) +{ + // release old reference if any and take over this new one + if (mp->media_file) + obj_put(mp->media_file); + mp->media_file = fo; + + // switch to blob playing + opts.file = STR_NULL; + opts.blob = fo->blob; + return __media_player_add_blob_id(mp, opts, dst_pt); +} + static void __media_player_set_opts(struct media_player *mp, media_player_opts_t opts) { mp->opts = opts; @@ -1177,6 +1274,13 @@ static mp_cached_code __media_player_add_file(struct media_player *mp, if (media_player_cache_get_entry(mp, dst_pt, opts.codec_set)) return MPC_CACHED; + // check if we have it in memory + struct media_player_media_file *fo = media_player_media_files_get(&opts.file); + if (fo) { + ilog(LOG_DEBUG, "Using cached media file for playback"); + return media_player_set_media_file(mp, opts, dst_pt, fo); + } + char file_s[PATH_MAX]; snprintf(file_s, sizeof(file_s), STR_FORMAT, STR_FMT(&opts.file)); @@ -1771,6 +1875,9 @@ void media_player_free(void) { if (media_player_cache) g_hash_table_destroy(media_player_cache); + + if (t_hash_table_is_set(media_player_media_files)) + t_hash_table_destroy(media_player_media_files); #endif timerthread_free(&send_timer_thread); } @@ -1785,3 +1892,31 @@ void send_timer_launch(void) { //ilog(LOG_DEBUG, "send_timer_loop"); timerthread_launch(&send_timer_thread, rtpe_config.scheduling, rtpe_config.priority, "media player"); } + +bool media_player_preload_files(char **files) { +#ifdef WITH_TRANSCODING + if (!files || !files[0]) + return true; + + for (char **filep = files; *filep; filep++) { + char *file = *filep; + while (*file == ' ') + file++; + + ilog(LOG_DEBUG, "Reading media file '%s' for caching", file); + + str f = STR(file); + if (t_hash_table_is_set(media_player_media_files) && t_hash_table_lookup(media_player_media_files, &f)) { + ilog(LOG_CRIT, "Duplicate entry for caching media file '%s'", file); + return false; + } + + __auto_type fo = media_player_media_file_read_c(file); + if (!fo) + return false; + media_player_media_files_insert(&f, fo); + } +#endif + + return true; +} diff --git a/docs/rtpengine.md b/docs/rtpengine.md index 75288fdfb..fd9b534a8 100644 --- a/docs/rtpengine.md +++ b/docs/rtpengine.md @@ -1144,6 +1144,21 @@ call to inject-DTMF won't be sent to __\-\-dtmf-log-dest=__ or __\-\-listen-tcp- *rtpengine* (using different kernel table IDs) running on a system using the same kernel module. Unused slots use minimal resources. +- __\-\-preload-media-files=__*FILE* + + Enables reading of media files at startup and caching them in memory for + playback. Multiple files can be specified. On the command line, the option + must be given multiple times to do so, while in the config file the option + must be given only once, with the list of files separated by semicolons. + + All listed files will be read into memory at startup and cached there for + the lifetime of the daemon. When playback of one such media file is + requested, playback will be done from the cached contents instead of + opening and reading the file. The file name given in the `play media` + request must exactly match the file name given in the config option. If the + file name differs (or an entirely different file is requested for playback) + then playback will happen from file as usual. + - __\-\-audio-buffer-length=__*INT* Set the buffer length used by the audio player (see below) in milliseconds. The diff --git a/etc/rtpengine.conf b/etc/rtpengine.conf index 98c297634..0049f99f5 100644 --- a/etc/rtpengine.conf +++ b/etc/rtpengine.conf @@ -165,6 +165,8 @@ recording-method = proc # whenever MoH is triggered. If not defined, then not in use. # moh-attr-name = rtpengine-hold +# preload-media-files = /var/media/file1.wav ; /var/media/file2.wav ; /var/media/file3.wav + # signalling templates (see key `templates` above) [templates] WebRTC = transport-protocol=UDP/TLS/RTP/SAVPF ICE=force trickle-ICE rtcp-mux=[offer require] no-rtcp-attribute SDES=off generate-mid diff --git a/include/main.h b/include/main.h index 0b9469770..27844a3d8 100644 --- a/include/main.h +++ b/include/main.h @@ -181,7 +181,8 @@ enum endpoint_learning { #define RTPE_CONFIG_CHARPP_PARAMS \ X(http_ifs) \ - X(https_ifs) + X(https_ifs) \ + X(preload_media_files) \ // these are not automatically included in rtpe_config due to different types #define RTPE_CONFIG_ENUM_PARAMS \ diff --git a/include/media_player.h b/include/media_player.h index 2871cf3aa..2338154bf 100644 --- a/include/media_player.h +++ b/include/media_player.h @@ -35,6 +35,7 @@ typedef struct { #include struct media_player_cache_entry; +struct media_player_media_file; struct media_player_content_index { enum { MP_OTHER = 0, MP_FILE = 1, MP_DB, MP_BLOB } type; @@ -77,6 +78,7 @@ struct media_player { struct media_player_cache_entry *cache_entry; unsigned int cache_read_idx; unsigned int kernel_idx; + struct media_player_media_file *media_file; struct ssrc_ctx *ssrc_out; unsigned long seq; @@ -147,6 +149,7 @@ const char * call_check_moh(struct call_monologue *from_ml, struct call_monologu void media_player_init(void); void media_player_free(void); void media_player_launch(void); +bool media_player_preload_files(char **); struct send_timer *send_timer_new(struct packet_stream *); void send_timer_push(struct send_timer *, struct codec_packet *);