mirror of
https://github.com/jambonz/freeswitch-modules.git
synced 2026-01-25 02:08:27 +00:00
add mod_dub (#16)
* add mod_dub Signed-off-by: Dave Horton <daveh@beachdognet.com> * remove some locks --------- Signed-off-by: Dave Horton <daveh@beachdognet.com>
This commit is contained in:
10
mod_dub/Makefile.am
Normal file
10
mod_dub/Makefile.am
Normal file
@@ -0,0 +1,10 @@
|
||||
include $(top_srcdir)/build/modmake.rulesam
|
||||
MODNAME=mod_dub
|
||||
|
||||
mod_LTLIBRARIES = mod_dub.la
|
||||
mod_dub_la_SOURCES = file_loader.cpp audio_downloader.cpp mod_dub.c dub_glue.cpp
|
||||
mod_dub_la_CFLAGS = $(AM_CFLAGS)
|
||||
mod_dub_la_CXXFLAGS = $(AM_CXXFLAGS) -std=c++17
|
||||
|
||||
mod_dub_la_LIBADD = $(switch_builddir)/libfreeswitch.la
|
||||
mod_dub_la_LDFLAGS = -avoid-version -module -no-undefined -shared `pkg-config --libs boost` -lstdc++ -lmpg123
|
||||
885
mod_dub/audio_downloader.cpp
Normal file
885
mod_dub/audio_downloader.cpp
Normal file
@@ -0,0 +1,885 @@
|
||||
#include "audio_downloader.h"
|
||||
|
||||
#include <boost/thread.hpp>
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include <boost/pool/object_pool.hpp>
|
||||
#include <boost/bind/bind.hpp>
|
||||
#include <boost/tokenizer.hpp>
|
||||
#include <boost/foreach.hpp>
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/assign/list_of.hpp>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
#include <map>
|
||||
|
||||
#include <mpg123.h>
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
#define BUFFER_GROW_SIZE (80000)
|
||||
#define BUFFER_THROTTLE_LOW (40000)
|
||||
#define BUFFER_THROTTLE_HIGH (160000)
|
||||
|
||||
static uint16_t currDownloadId = 0;
|
||||
|
||||
typedef struct
|
||||
{
|
||||
CURLM *multi;
|
||||
int still_running;
|
||||
} GlobalInfo_t;
|
||||
static GlobalInfo_t global;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
STATUS_NONE = 0,
|
||||
STATUS_FAILED,
|
||||
STATUS_DOWNLOAD_IN_PROGRESS,
|
||||
STATUS_DOWNLOAD_PAUSED,
|
||||
STATUS_DOWNLOAD_COMPLETE,
|
||||
STATUS_AWAITING_RESTART,
|
||||
STATUS_STOPPING,
|
||||
STATUS_STOPPED
|
||||
} Status_t;
|
||||
|
||||
static const char* status2String(Status_t status)
|
||||
{
|
||||
static const char* statusStrings[] = {
|
||||
"STATUS_NONE",
|
||||
"STATUS_FAILED",
|
||||
"STATUS_DOWNLOAD_IN_PROGRESS",
|
||||
"STATUS_DOWNLOAD_PAUSED",
|
||||
"STATUS_DOWNLOAD_COMPLETE",
|
||||
"STATUS_AWAITING_RESTART",
|
||||
"STATUS_STOPPING",
|
||||
"STATUS_STOPPED"
|
||||
};
|
||||
|
||||
if (status >= 0 && status < sizeof(statusStrings) / sizeof(statusStrings[0]))
|
||||
{
|
||||
return statusStrings[status];
|
||||
}
|
||||
else
|
||||
{
|
||||
return "UNKNOWN_STATUS";
|
||||
}
|
||||
}
|
||||
|
||||
typedef struct
|
||||
{
|
||||
GlobalInfo_t *global;
|
||||
CURL *easy;
|
||||
switch_mutex_t* mutex;
|
||||
CircularBuffer_t* buffer;
|
||||
mpg123_handle *mh;
|
||||
char error[CURL_ERROR_SIZE]; // curl error buffer
|
||||
char *err_msg; // http server error message
|
||||
char* url;
|
||||
bool loop;
|
||||
int rate;
|
||||
boost::asio::deadline_timer *timer;
|
||||
Status_t status;
|
||||
downloadId_t id;
|
||||
int response_code;
|
||||
int gain;
|
||||
} ConnInfo_t;
|
||||
|
||||
typedef std::map<int32_t, ConnInfo_t *> Id2ConnMap_t;
|
||||
static Id2ConnMap_t id2ConnMap;
|
||||
|
||||
static boost::object_pool<ConnInfo_t> pool ;
|
||||
static std::map<curl_socket_t, boost::asio::ip::tcp::socket *> socket_map;
|
||||
static boost::asio::io_service io_service;
|
||||
static boost::asio::deadline_timer timer(io_service);
|
||||
static std::string fullDirPath;
|
||||
static std::thread worker_thread;
|
||||
|
||||
/* forward declarations */
|
||||
static ConnInfo_t* createDownloader(const char *url, int rate, int loop, int gain, mpg123_handle *mhm, switch_mutex_t *mutex, CircularBuffer_t *buffer);
|
||||
static CURL* createEasyHandle(void);
|
||||
static void destroyConnection(ConnInfo_t *conn);
|
||||
static void check_multi_info(GlobalInfo_t *g) ;
|
||||
static int mcode_test(const char *where, CURLMcode code);
|
||||
static void event_cb(GlobalInfo_t *g, curl_socket_t s, int action, const boost::system::error_code & error, int *fdp);
|
||||
static void setsock(int *fdp, curl_socket_t s, CURL *e, int act, int oldact, GlobalInfo_t *g);
|
||||
static void addsock(curl_socket_t s, CURL *easy, int action, GlobalInfo_t *g);
|
||||
static int sock_cb(CURL *e, curl_socket_t s, int what, void *cbp, void *sockp);
|
||||
static void threadFunc();
|
||||
static void timer_cb(const boost::system::error_code & error, GlobalInfo_t *g);
|
||||
static int multi_timer_cb(CURLM *multi, long timeout_ms, GlobalInfo_t *g);
|
||||
static std::vector<int16_t> convert_mp3_to_linear(ConnInfo_t *conn, int8_t *data, size_t len);
|
||||
static void throttling_cb(const boost::system::error_code& error, ConnInfo_t* conn) ;
|
||||
static void restart_cb(const boost::system::error_code& error, ConnInfo_t* conn);
|
||||
static size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn);
|
||||
static bool parseHeader(const std::string& str, std::string& header, std::string& value) ;
|
||||
static int extract_response_code(const std::string& input) ;
|
||||
static size_t header_callback(char *buffer, size_t size, size_t nitems, ConnInfo_t *conn);
|
||||
static curl_socket_t opensocket(void *clientp, curlsocktype purpose, struct curl_sockaddr *address);
|
||||
static int close_socket(void *clientp, curl_socket_t item);
|
||||
|
||||
/* apis */
|
||||
extern "C" {
|
||||
|
||||
switch_status_t init_audio_downloader() {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "init_audio_downloader loading..\n");
|
||||
memset(&global, 0, sizeof(GlobalInfo_t));
|
||||
global.multi = curl_multi_init();
|
||||
|
||||
if (!global.multi) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "init_audio_downloader curl_multi_init() failed, exiting!\n");
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
curl_multi_setopt(global.multi, CURLMOPT_SOCKETFUNCTION, sock_cb);
|
||||
curl_multi_setopt(global.multi, CURLMOPT_SOCKETDATA, &global);
|
||||
curl_multi_setopt(global.multi, CURLMOPT_TIMERFUNCTION, multi_timer_cb);
|
||||
curl_multi_setopt(global.multi, CURLMOPT_TIMERDATA, &global);
|
||||
curl_multi_setopt(global.multi, CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
|
||||
|
||||
if (mpg123_init() != MPG123_OK) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "init_audio_downloader: failed to initiate MPG123");
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
/* start worker thread */
|
||||
std::thread t(threadFunc) ;
|
||||
worker_thread.swap( t ) ;
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "init_audio_downloader: loaded\n");
|
||||
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
|
||||
}
|
||||
|
||||
switch_status_t deinit_audio_downloader() {
|
||||
/* stop the ASIO IO service */
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "deinit_audio_downloader: stopping io service\n");
|
||||
io_service.stop();
|
||||
|
||||
/* Join the worker thread */
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "deinit_audio_downloader: wait for worker thread to complete\n");
|
||||
if (worker_thread.joinable()) {
|
||||
worker_thread.join();
|
||||
}
|
||||
|
||||
/* cleanup curl multi handle*/
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "deinit_audio_downloader: release curl multi\n");
|
||||
curl_multi_cleanup(global.multi);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "deinit_audio_downloader: completed\n");
|
||||
|
||||
mpg123_exit();
|
||||
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
downloadId_t start_audio_download(const char* url, int rate, int loop, int gain, switch_mutex_t* mutex, CircularBuffer_t* buffer) {
|
||||
int mhError = 0;
|
||||
|
||||
/* allocate handle for mpeg decoding */
|
||||
mpg123_handle *mh = mpg123_new("auto", &mhError);
|
||||
if (!mh) {
|
||||
const char *mhErr = mpg123_plain_strerror(mhError);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error allocating mpg123 handle! %s\n", switch_str_nil(mhErr));
|
||||
return INVALID_DOWNLOAD_ID;
|
||||
}
|
||||
|
||||
if (mpg123_open_feed(mh) != MPG123_OK) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error mpg123_open_feed!\n");
|
||||
return INVALID_DOWNLOAD_ID;
|
||||
}
|
||||
|
||||
if (mpg123_format_all(mh) != MPG123_OK) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error mpg123_format_all!\n");
|
||||
return INVALID_DOWNLOAD_ID;
|
||||
}
|
||||
|
||||
if (mpg123_param(mh, MPG123_FORCE_RATE, rate, 0) != MPG123_OK) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error forcing resample to 8k!\n");
|
||||
return INVALID_DOWNLOAD_ID;
|
||||
}
|
||||
|
||||
if (mpg123_param(mh, MPG123_FLAGS, MPG123_MONO_MIX, 0) != MPG123_OK) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error forcing single channel!\n");
|
||||
return INVALID_DOWNLOAD_ID;
|
||||
}
|
||||
|
||||
ConnInfo_t* conn = createDownloader(url, rate, loop, gain, mh, mutex, buffer);
|
||||
if (!conn) {
|
||||
return INVALID_DOWNLOAD_ID;
|
||||
}
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO,
|
||||
"start_audio_download: starting download %d\n", conn->id);
|
||||
|
||||
|
||||
return conn->id;
|
||||
}
|
||||
|
||||
switch_status_t stop_audio_download(int id) {
|
||||
auto it = id2ConnMap.find(id);
|
||||
if (it == id2ConnMap.end()) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "stop_audio_download: id %d has already completed\n", id);
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO,
|
||||
"stop_audio_download: stopping download %d, status %s\n", id, status2String(it->second->status));
|
||||
|
||||
ConnInfo_t *conn = it->second;
|
||||
auto status = conn->status;
|
||||
|
||||
/* past this point I shall not access either the mutex or the buffer provided */
|
||||
conn->mutex = nullptr;
|
||||
conn->buffer = nullptr;
|
||||
|
||||
/* if download is in progress set status to cancel it during next call back */
|
||||
if (status == Status_t::STATUS_DOWNLOAD_PAUSED) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "stop_audio_download: resuming download %d so we can cancel it\n", id);
|
||||
conn->status = Status_t::STATUS_STOPPING;
|
||||
curl_easy_pause(conn->easy, CURLPAUSE_CONT);
|
||||
}
|
||||
if (status != Status_t::STATUS_DOWNLOAD_IN_PROGRESS) {
|
||||
destroyConnection(conn);
|
||||
}
|
||||
conn->status = Status_t::STATUS_STOPPING;
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
/* internal */
|
||||
ConnInfo_t* createDownloader(const char *url, int rate, int loop, int gain, mpg123_handle *mh, switch_mutex_t *mutex, CircularBuffer_t *buffer) {
|
||||
ConnInfo_t *conn = pool.malloc() ;
|
||||
CURL* easy = createEasyHandle();
|
||||
|
||||
if (!easy || !conn) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "createDownloader: failed to allocate memory\n");
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
memset(conn, 0, sizeof(ConnInfo_t));
|
||||
conn->easy = easy;
|
||||
conn->mutex = mutex;
|
||||
conn->buffer = buffer;
|
||||
conn->mh = mh;
|
||||
conn->loop = loop;
|
||||
conn->gain = gain;
|
||||
conn->rate = rate;
|
||||
conn->url = strdup(url);
|
||||
conn->global = &global;
|
||||
conn->status = Status_t::STATUS_NONE;
|
||||
conn->timer = new boost::asio::deadline_timer(io_service);
|
||||
|
||||
downloadId_t id = ++currDownloadId;
|
||||
if (id == 0) id++;
|
||||
|
||||
id2ConnMap[id] = conn;
|
||||
conn->id = id;
|
||||
|
||||
curl_easy_setopt(easy, CURLOPT_URL, url);
|
||||
curl_easy_setopt(easy, CURLOPT_HTTPGET, 1L);
|
||||
curl_easy_setopt(easy, CURLOPT_WRITEFUNCTION, write_cb);
|
||||
curl_easy_setopt(easy, CURLOPT_WRITEDATA, conn);
|
||||
curl_easy_setopt(easy, CURLOPT_ERRORBUFFER, conn->error);
|
||||
curl_easy_setopt(easy, CURLOPT_PRIVATE, conn);
|
||||
curl_easy_setopt(easy, CURLOPT_VERBOSE, 0L);
|
||||
curl_easy_setopt(easy, CURLOPT_NOPROGRESS, 1L);
|
||||
curl_easy_setopt(easy, CURLOPT_HEADERFUNCTION, header_callback);
|
||||
curl_easy_setopt(easy, CURLOPT_HEADERDATA, conn);
|
||||
|
||||
/* call this function to get a socket */
|
||||
curl_easy_setopt(easy, CURLOPT_OPENSOCKETFUNCTION, opensocket);
|
||||
|
||||
/* call this function to close a socket */
|
||||
curl_easy_setopt(easy, CURLOPT_CLOSESOCKETFUNCTION, close_socket);
|
||||
|
||||
curl_easy_setopt(easy, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
|
||||
|
||||
/* keep the speed down so we don't have to buffer large amounts*/
|
||||
curl_easy_setopt(easy, CURLOPT_MAX_RECV_SPEED_LARGE, (curl_off_t)31415);
|
||||
|
||||
auto rc = curl_multi_add_handle(global.multi, conn->easy);
|
||||
if (mcode_test("new_conn: curl_multi_add_handle", rc) < 0) {
|
||||
return nullptr;
|
||||
}
|
||||
conn->status = Status_t::STATUS_DOWNLOAD_IN_PROGRESS;
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "createDownloader: launched request, gain %d\n", conn->gain);
|
||||
return conn;
|
||||
}
|
||||
|
||||
void destroyConnection(ConnInfo_t *conn) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "destroyConnection\n");
|
||||
|
||||
/* clean up the curl handle*/
|
||||
curl_multi_remove_handle(conn->global, conn->easy);
|
||||
curl_easy_cleanup(conn->easy);
|
||||
|
||||
/* clear asio resources and free resources */
|
||||
if (conn->timer) {
|
||||
conn->timer->cancel();
|
||||
delete conn->timer;
|
||||
}
|
||||
if (conn->err_msg) {
|
||||
free(conn->err_msg);
|
||||
}
|
||||
|
||||
/* free mp3 decoder */
|
||||
if (conn->mh) {
|
||||
mpg123_close(conn->mh);
|
||||
mpg123_delete(conn->mh);
|
||||
}
|
||||
|
||||
if (conn->url) {
|
||||
free(conn->url);
|
||||
}
|
||||
|
||||
if (conn->mutex) switch_mutex_lock(conn->mutex);
|
||||
id2ConnMap.erase(conn->id);
|
||||
if (conn->mutex) switch_mutex_unlock(conn->mutex);
|
||||
|
||||
memset(conn, 0, sizeof(ConnInfo_t));
|
||||
pool.destroy(conn) ;
|
||||
}
|
||||
|
||||
CURL* createEasyHandle(void) {
|
||||
CURL* easy = curl_easy_init();
|
||||
if(!easy) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "curl_easy_init() failed!\n");
|
||||
return nullptr ;
|
||||
}
|
||||
|
||||
curl_easy_setopt(easy, CURLOPT_FOLLOWLOCATION, 1L);
|
||||
curl_easy_setopt(easy, CURLOPT_USERAGENT, "jambonz/0.8.5");
|
||||
|
||||
// set connect timeout to 3 seconds and no total timeout as files could be large
|
||||
curl_easy_setopt(easy, CURLOPT_CONNECTTIMEOUT_MS, 3000L);
|
||||
curl_easy_setopt(easy, CURLOPT_TIMEOUT, 0L); // no timeout
|
||||
|
||||
return easy ;
|
||||
}
|
||||
|
||||
/* Check for completed transfers, and remove their easy handles */
|
||||
void check_multi_info(GlobalInfo_t *g) {
|
||||
CURLMsg *msg;
|
||||
int msgs_left;
|
||||
ConnInfo_t *conn;
|
||||
CURL *easy;
|
||||
CURLcode res;
|
||||
|
||||
while((msg = curl_multi_info_read(g->multi, &msgs_left))) {
|
||||
if(msg->msg == CURLMSG_DONE) {
|
||||
long response_code;
|
||||
double namelookup=0, connect=0, total=0 ;
|
||||
char *ct = NULL ;
|
||||
|
||||
easy = msg->easy_handle;
|
||||
res = msg->data.result;
|
||||
curl_easy_getinfo(easy, CURLINFO_PRIVATE, &conn);
|
||||
curl_easy_getinfo(easy, CURLINFO_RESPONSE_CODE, &response_code);
|
||||
curl_easy_getinfo(easy, CURLINFO_CONTENT_TYPE, &ct);
|
||||
|
||||
curl_easy_getinfo(easy, CURLINFO_NAMELOOKUP_TIME, &namelookup);
|
||||
curl_easy_getinfo(easy, CURLINFO_CONNECT_TIME, &connect);
|
||||
curl_easy_getinfo(easy, CURLINFO_TOTAL_TIME, &total);
|
||||
|
||||
downloadId_t id = conn->id;
|
||||
auto mutex = conn->mutex;
|
||||
auto buffer = conn->buffer;
|
||||
auto rate = conn->rate;
|
||||
auto loop = conn->loop;
|
||||
auto gain = conn->gain;
|
||||
auto oldId = conn->id;
|
||||
bool restart = conn->loop && conn->status != Status_t::STATUS_STOPPING && response_code == 200;
|
||||
|
||||
conn->response_code = response_code;
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "curl done, response code %d, status %s\n", response_code, status2String(conn->status));
|
||||
conn->status = Status_t::STATUS_DOWNLOAD_COMPLETE;
|
||||
|
||||
curl_multi_remove_handle(g->multi, easy);
|
||||
|
||||
if (restart) {
|
||||
conn->status = Status_t::STATUS_AWAITING_RESTART;
|
||||
conn->timer->expires_from_now(boost::posix_time::millisec(1000));
|
||||
conn->timer->async_wait(boost::bind(&restart_cb, boost::placeholders::_1, conn));
|
||||
|
||||
//TODO: this seems to not be working from this callback; maybe start it from a timer callback?
|
||||
}
|
||||
else {
|
||||
destroyConnection(conn);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
int mcode_test(const char *where, CURLMcode code) {
|
||||
if(CURLM_OK != code) {
|
||||
const char *s;
|
||||
switch(code) {
|
||||
case CURLM_CALL_MULTI_PERFORM:
|
||||
s = "CURLM_CALL_MULTI_PERFORM";
|
||||
break;
|
||||
case CURLM_BAD_HANDLE:
|
||||
s = "CURLM_BAD_HANDLE";
|
||||
break;
|
||||
case CURLM_BAD_EASY_HANDLE:
|
||||
s = "CURLM_BAD_EASY_HANDLE";
|
||||
break;
|
||||
case CURLM_OUT_OF_MEMORY:
|
||||
s = "CURLM_OUT_OF_MEMORY";
|
||||
break;
|
||||
case CURLM_INTERNAL_ERROR:
|
||||
s = "CURLM_INTERNAL_ERROR";
|
||||
break;
|
||||
case CURLM_UNKNOWN_OPTION:
|
||||
s = "CURLM_UNKNOWN_OPTION";
|
||||
break;
|
||||
case CURLM_LAST:
|
||||
s = "CURLM_LAST";
|
||||
break;
|
||||
default:
|
||||
s = "CURLM_unknown";
|
||||
break;
|
||||
case CURLM_BAD_SOCKET:
|
||||
s = "CURLM_BAD_SOCKET";
|
||||
break;
|
||||
}
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "mcode_test ERROR: %s returns %s:%d\n", where, s, code);
|
||||
|
||||
return -1;
|
||||
}
|
||||
return 0 ;
|
||||
}
|
||||
|
||||
void remsock(int *f, GlobalInfo_t *g) {
|
||||
if(f) {
|
||||
free(f);
|
||||
f = NULL;
|
||||
}
|
||||
}
|
||||
|
||||
/* Called by asio when there is an action on a socket */
|
||||
void event_cb(GlobalInfo_t *g, curl_socket_t s, int action, const boost::system::error_code & error, int *fdp) {
|
||||
int f = *fdp;
|
||||
|
||||
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "event_cb socket %#X has action %d\n", s, action) ;
|
||||
|
||||
// Socket already POOL REMOVED.
|
||||
if (f == CURL_POLL_REMOVE) {
|
||||
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "event_cb socket %#X removed\n", s);
|
||||
remsock(fdp, g);
|
||||
return;
|
||||
}
|
||||
|
||||
if(socket_map.find(s) == socket_map.end()) {
|
||||
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "event_cb: socket %#X already closed\n, s");
|
||||
return;
|
||||
}
|
||||
|
||||
/* make sure the event matches what are wanted */
|
||||
if(f == action || f == CURL_POLL_INOUT) {
|
||||
if(error) {
|
||||
action = CURL_CSELECT_ERR;
|
||||
}
|
||||
CURLMcode rc = curl_multi_socket_action(g->multi, s, action, &g->still_running);
|
||||
|
||||
mcode_test("event_cb: curl_multi_socket_action", rc);
|
||||
check_multi_info(g);
|
||||
|
||||
if(g->still_running <= 0) {
|
||||
timer.cancel();
|
||||
}
|
||||
|
||||
/* keep on watching.
|
||||
* the socket may have been closed and/or fdp may have been changed
|
||||
* in curl_multi_socket_action(), so check them both */
|
||||
if(!error && socket_map.find(s) != socket_map.end() &&
|
||||
(f == action || f == CURL_POLL_INOUT)) {
|
||||
boost::asio::ip::tcp::socket *tcp_socket = socket_map.find(s)->second;
|
||||
|
||||
if(action == CURL_POLL_IN) {
|
||||
tcp_socket->async_read_some(boost::asio::null_buffers(),
|
||||
boost::bind(&event_cb, g, s,
|
||||
action, boost::placeholders::_1, fdp));
|
||||
}
|
||||
if(action == CURL_POLL_OUT) {
|
||||
tcp_socket->async_write_some(boost::asio::null_buffers(),
|
||||
boost::bind(&event_cb, g, s,
|
||||
action, boost::placeholders::_1, fdp));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* socket functions */
|
||||
void setsock(int *fdp, curl_socket_t s, CURL *e, int act, int oldact, GlobalInfo_t *g) {
|
||||
std::map<curl_socket_t, boost::asio::ip::tcp::socket *>::iterator it = socket_map.find(s);
|
||||
|
||||
if(it == socket_map.end()) {
|
||||
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "setsock: socket %#X not found\n, s");
|
||||
return;
|
||||
}
|
||||
|
||||
boost::asio::ip::tcp::socket * tcp_socket = it->second;
|
||||
|
||||
*fdp = act;
|
||||
|
||||
if(act == CURL_POLL_IN) {
|
||||
if(oldact != CURL_POLL_IN && oldact != CURL_POLL_INOUT) {
|
||||
tcp_socket->async_read_some(boost::asio::null_buffers(),
|
||||
boost::bind(&event_cb, g, s,
|
||||
CURL_POLL_IN, boost::placeholders::_1, fdp));
|
||||
}
|
||||
}
|
||||
else if(act == CURL_POLL_OUT) {
|
||||
if(oldact != CURL_POLL_OUT && oldact != CURL_POLL_INOUT) {
|
||||
tcp_socket->async_write_some(boost::asio::null_buffers(),
|
||||
boost::bind(&event_cb, g, s,
|
||||
CURL_POLL_OUT, boost::placeholders::_1, fdp));
|
||||
}
|
||||
}
|
||||
else if(act == CURL_POLL_INOUT) {
|
||||
if(oldact != CURL_POLL_IN && oldact != CURL_POLL_INOUT) {
|
||||
tcp_socket->async_read_some(boost::asio::null_buffers(),
|
||||
boost::bind(&event_cb, g, s,
|
||||
CURL_POLL_IN, boost::placeholders::_1, fdp));
|
||||
}
|
||||
if(oldact != CURL_POLL_OUT && oldact != CURL_POLL_INOUT) {
|
||||
tcp_socket->async_write_some(boost::asio::null_buffers(),
|
||||
boost::bind(&event_cb, g, s,
|
||||
CURL_POLL_OUT, boost::placeholders::_1, fdp));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void addsock(curl_socket_t s, CURL *easy, int action, GlobalInfo_t *g) {
|
||||
/* fdp is used to store current action */
|
||||
int *fdp = (int *) calloc(sizeof(int), 1);
|
||||
|
||||
setsock(fdp, s, easy, action, 0, g);
|
||||
curl_multi_assign(g->multi, s, fdp);
|
||||
}
|
||||
|
||||
int sock_cb(CURL *e, curl_socket_t s, int what, void *cbp, void *sockp) {
|
||||
GlobalInfo_t *g = &global;
|
||||
|
||||
int *actionp = (int *) sockp;
|
||||
static const char *whatstr[] = { "none", "IN", "OUT", "INOUT", "REMOVE"};
|
||||
|
||||
if(what == CURL_POLL_REMOVE) {
|
||||
*actionp = what;
|
||||
}
|
||||
else {
|
||||
if(!actionp) {
|
||||
addsock(s, e, what, g);
|
||||
}
|
||||
else {
|
||||
setsock(actionp, s, e, what, *actionp, g);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
void threadFunc() {
|
||||
/* to make sure the event loop doesn't terminate when there is no work to do */
|
||||
io_service.reset() ;
|
||||
boost::asio::io_service::work work(io_service);
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "mod_dub threadFunc - starting\n");
|
||||
|
||||
for(;;) {
|
||||
|
||||
try {
|
||||
io_service.run() ;
|
||||
break ;
|
||||
}
|
||||
catch( std::exception& e) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "mod_dub threadFunc - Error: %s\n", e.what());
|
||||
}
|
||||
}
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "mod_dub threadFunc - ending\n");
|
||||
}
|
||||
|
||||
|
||||
/* Called by asio when our timeout expires */
|
||||
void timer_cb(const boost::system::error_code & error, GlobalInfo_t *g)
|
||||
{
|
||||
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "timer_cb\n");
|
||||
|
||||
if(!error) {
|
||||
CURLMcode rc = curl_multi_socket_action(g->multi, CURL_SOCKET_TIMEOUT, 0, &g->still_running);
|
||||
mcode_test("timer_cb: curl_multi_socket_action", rc);
|
||||
check_multi_info(g);
|
||||
}
|
||||
}
|
||||
|
||||
int multi_timer_cb(CURLM *multi, long timeout_ms, GlobalInfo_t *g) {
|
||||
|
||||
/* cancel running timer */
|
||||
timer.cancel();
|
||||
|
||||
if(timeout_ms >= 0) {
|
||||
// from libcurl 7.88.1-10+deb12u4 does not allow call curl_multi_socket_action or curl_multi_perform in curl_multi callback directly
|
||||
timer.expires_from_now(boost::posix_time::millisec(timeout_ms ? timeout_ms : 1));
|
||||
timer.async_wait(boost::bind(&timer_cb, boost::placeholders::_1, g));
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::vector<int16_t> convert_mp3_to_linear(ConnInfo_t *conn, int8_t *data, size_t len) {
|
||||
std::vector<int16_t> linear_data;
|
||||
int eof = 0;
|
||||
int mp3err = 0;
|
||||
|
||||
if(mpg123_feed(conn->mh, (const unsigned char*) data, len) == MPG123_OK) {
|
||||
while(!eof) {
|
||||
size_t usedlen = 0;
|
||||
off_t frame_offset;
|
||||
unsigned char* audio;
|
||||
|
||||
int decode_status = mpg123_decode_frame(conn->mh, &frame_offset, &audio, &usedlen);
|
||||
|
||||
switch(decode_status) {
|
||||
case MPG123_NEW_FORMAT:
|
||||
continue;
|
||||
|
||||
case MPG123_OK:
|
||||
{
|
||||
size_t samples = usedlen / sizeof(int16_t);
|
||||
linear_data.insert(linear_data.end(), reinterpret_cast<int16_t*>(audio), reinterpret_cast<int16_t*>(audio) + samples);
|
||||
}
|
||||
break;
|
||||
|
||||
case MPG123_DONE:
|
||||
case MPG123_NEED_MORE:
|
||||
eof = 1;
|
||||
break;
|
||||
|
||||
case MPG123_ERR:
|
||||
default:
|
||||
if(++mp3err >= 5) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Decoder Error!\n");
|
||||
eof = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (eof)
|
||||
break;
|
||||
|
||||
mp3err = 0;
|
||||
}
|
||||
|
||||
if (conn->gain != 0) {
|
||||
switch_change_sln_volume_granular(linear_data.data(), linear_data.size(), conn->gain);
|
||||
}
|
||||
}
|
||||
|
||||
return linear_data;
|
||||
}
|
||||
|
||||
void restart_cb(const boost::system::error_code& error, ConnInfo_t* conn) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "restart_cb status is %s\n", status2String(conn->status));
|
||||
if (conn->status == Status_t::STATUS_AWAITING_RESTART) {
|
||||
auto url = strdup(conn->url);
|
||||
auto rate = conn->rate;
|
||||
auto loop = conn->loop;
|
||||
auto gain = conn->gain;
|
||||
auto mutex = conn->mutex;
|
||||
auto buffer = conn->buffer;
|
||||
auto oldId = conn->id;
|
||||
|
||||
destroyConnection(conn);
|
||||
|
||||
downloadId_t id = start_audio_download(url, rate, loop, gain, mutex, buffer);
|
||||
|
||||
/* re-use id since caller is tracking that id */
|
||||
auto * newConnection = id2ConnMap[id];
|
||||
id2ConnMap[oldId] = newConnection;
|
||||
id2ConnMap.erase(id);
|
||||
|
||||
free(url);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "restarted looped download\n");
|
||||
}
|
||||
}
|
||||
|
||||
void throttling_cb(const boost::system::error_code& error, ConnInfo_t* conn) {
|
||||
if (conn->status == Status_t::STATUS_STOPPING || !conn->mutex || !conn->buffer) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "throttling_cb: session gone, resume download so we can complete\n");
|
||||
curl_easy_pause(conn->easy, CURLPAUSE_CONT);
|
||||
return;
|
||||
}
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "throttling_cb: status is %s\n", status2String(conn->status));
|
||||
|
||||
switch_mutex_lock(conn->mutex);
|
||||
if (!error) {
|
||||
auto size = conn->buffer->size();
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "throttling_cb: size is now %ld\n", size);
|
||||
if (size < BUFFER_THROTTLE_LOW) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "throttling_cb: resuming download\n");
|
||||
curl_easy_pause(conn->easy, CURLPAUSE_CONT);
|
||||
switch_mutex_unlock(conn->mutex);
|
||||
return;
|
||||
}
|
||||
|
||||
// check back in 2 seconds
|
||||
conn->timer->expires_from_now(boost::posix_time::millisec(2000));
|
||||
conn->timer->async_wait(boost::bind(&throttling_cb, boost::placeholders::_1, conn));
|
||||
|
||||
} else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "throttling_cb: error (%d): %s\n", error.value(), error.message().c_str());
|
||||
|
||||
// Handle any errors
|
||||
}
|
||||
switch_mutex_unlock(conn->mutex);
|
||||
}
|
||||
|
||||
|
||||
/* CURLOPT_WRITEFUNCTION - here is where we receive the data */
|
||||
size_t write_cb(void *ptr, size_t size, size_t nmemb, ConnInfo_t *conn) {
|
||||
int8_t *data = (int8_t *) ptr;
|
||||
size_t bytes_received = size * nmemb;
|
||||
std::vector<int16_t> pcm_data;
|
||||
|
||||
if (conn->status == Status_t::STATUS_STOPPING || conn->status == Status_t::STATUS_STOPPED || !conn->mutex || !conn->buffer) {
|
||||
if (conn->timer) conn->timer->cancel();
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG,
|
||||
"write_cb: aborting transfer, status %s, mutex %p, buffer %p\n", status2String(conn->status), conn->mutex, conn->buffer);
|
||||
/* this will abort the transfer */
|
||||
return 0;
|
||||
}
|
||||
{
|
||||
switch_mutex_lock(conn->mutex);
|
||||
|
||||
if (conn->response_code > 0 && conn->response_code != 200) {
|
||||
std::string body((char *) ptr, bytes_received);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "write_cb: received body %s\n", body.c_str());
|
||||
conn->err_msg = strdup(body.c_str());
|
||||
conn->status = Status_t::STATUS_FAILED;
|
||||
switch_mutex_unlock(conn->mutex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* throttle after reaching high water mark */
|
||||
if (conn->buffer->size() > BUFFER_THROTTLE_HIGH) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "write_cb: throttling download, buffer size is %ld\n", conn->buffer->size());
|
||||
|
||||
// check back in 2 seconds
|
||||
conn->timer->expires_from_now(boost::posix_time::millisec(2000));
|
||||
conn->timer->async_wait(boost::bind(&throttling_cb, boost::placeholders::_1, conn));
|
||||
|
||||
conn->status = Status_t::STATUS_DOWNLOAD_PAUSED;
|
||||
switch_mutex_unlock(conn->mutex);
|
||||
return CURL_WRITEFUNC_PAUSE;
|
||||
}
|
||||
|
||||
pcm_data = convert_mp3_to_linear(conn, data, bytes_received);
|
||||
size_t bytesResampled = pcm_data.size() * sizeof(int16_t);
|
||||
|
||||
// Resize the buffer if necessary
|
||||
if (conn->buffer->capacity() - conn->buffer->size() < (bytesResampled / sizeof(int16_t))) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "write_cb growing buffer, size now %ld\n", conn->buffer->size());
|
||||
|
||||
//TODO: if buffer exceeds some max size, return CURL_WRITEFUNC_ERROR to abort the transfer
|
||||
conn->buffer->set_capacity(conn->buffer->size() + std::max((bytesResampled / sizeof(int16_t)), (size_t)BUFFER_GROW_SIZE));
|
||||
}
|
||||
|
||||
/* Push the data into the buffer */
|
||||
conn->buffer->insert(conn->buffer->end(), pcm_data.data(), pcm_data.data() + pcm_data.size());
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "write_cb: wrote data, buffer size is now %ld\n", conn->buffer->size());
|
||||
|
||||
switch_mutex_unlock(conn->mutex);
|
||||
}
|
||||
return bytes_received;
|
||||
}
|
||||
|
||||
bool parseHeader(const std::string& str, std::string& header, std::string& value) {
|
||||
std::vector<std::string> parts;
|
||||
boost::split(parts, str, boost::is_any_of(":"), boost::token_compress_on);
|
||||
|
||||
if (parts.size() != 2)
|
||||
return false;
|
||||
|
||||
header = boost::trim_copy(parts[0]);
|
||||
value = boost::trim_copy(parts[1]);
|
||||
return true;
|
||||
}
|
||||
|
||||
int extract_response_code(const std::string& input) {
|
||||
std::size_t space_pos = input.find(' ');
|
||||
if (space_pos == std::string::npos) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Invalid HTTP response format %s\n", input.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
std::size_t code_start_pos = space_pos + 1;
|
||||
std::size_t code_end_pos = input.find(' ', code_start_pos);
|
||||
if (code_end_pos == std::string::npos) {
|
||||
code_end_pos = input.length();
|
||||
}
|
||||
|
||||
std::string code_str = input.substr(code_start_pos, code_end_pos - code_start_pos);
|
||||
int response_code = std::stoi(code_str);
|
||||
return response_code;
|
||||
}
|
||||
|
||||
size_t header_callback(char *buffer, size_t size, size_t nitems, ConnInfo_t *conn) {
|
||||
size_t bytes_received = size * nitems;
|
||||
const std::string prefix = "HTTP/";
|
||||
std::string header, value;
|
||||
std::string input(buffer, bytes_received);
|
||||
if (parseHeader(input, header, value)) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "recv header: %s with value %s\n", header.c_str(), value.c_str());
|
||||
}
|
||||
else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "recv header: %s\n", input.c_str());
|
||||
if (input.rfind(prefix, 0) == 0) {
|
||||
try {
|
||||
conn->response_code = extract_response_code(input);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "parsed response code: %ld\n", conn->response_code);
|
||||
} catch (const std::invalid_argument& e) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "header_callback: invalid response code %s\n", input.substr(prefix.length()).c_str());
|
||||
}
|
||||
}
|
||||
}
|
||||
return bytes_received;
|
||||
}
|
||||
|
||||
/* CURLOPT_OPENSOCKETFUNCTION */
|
||||
curl_socket_t opensocket(void *clientp, curlsocktype purpose, struct curl_sockaddr *address) {
|
||||
curl_socket_t sockfd = CURL_SOCKET_BAD;
|
||||
|
||||
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "opensocket: %d\n", purpose);
|
||||
/* restrict to IPv4 */
|
||||
if(purpose == CURLSOCKTYPE_IPCXN && address->family == AF_INET) {
|
||||
/* create a tcp socket object */
|
||||
boost::asio::ip::tcp::socket *tcp_socket = new boost::asio::ip::tcp::socket(io_service);
|
||||
|
||||
/* open it and get the native handle*/
|
||||
boost::system::error_code ec;
|
||||
tcp_socket->open(boost::asio::ip::tcp::v4(), ec);
|
||||
|
||||
if(ec) {
|
||||
/* An error occurred */
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Couldn't open socket [%ld][%s]\n", ec, ec.message().c_str());
|
||||
}
|
||||
else {
|
||||
sockfd = tcp_socket->native_handle();
|
||||
|
||||
/* save it for monitoring */
|
||||
socket_map.insert(std::pair<curl_socket_t, boost::asio::ip::tcp::socket *>(sockfd, tcp_socket));
|
||||
}
|
||||
}
|
||||
return sockfd;
|
||||
}
|
||||
|
||||
/* CURLOPT_CLOSESOCKETFUNCTION */
|
||||
int close_socket(void *clientp, curl_socket_t item) {
|
||||
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "close_socket : %#X\n", item);
|
||||
|
||||
std::map<curl_socket_t, boost::asio::ip::tcp::socket *>::iterator it = socket_map.find(item);
|
||||
if(it != socket_map.end()) {
|
||||
delete it->second;
|
||||
socket_map.erase(it);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
18
mod_dub/audio_downloader.h
Normal file
18
mod_dub/audio_downloader.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#ifndef __AUDIO_DOWNLOADER_H__
|
||||
#define __AUDIO_DOWNLOADER_H__
|
||||
|
||||
#include <switch.h>
|
||||
#include "common.h"
|
||||
|
||||
extern "C" {
|
||||
|
||||
switch_status_t init_audio_downloader();
|
||||
switch_status_t deinit_audio_downloader();
|
||||
|
||||
int start_audio_download(const char* url, int rate, int loop, int gain, switch_mutex_t* mutex, CircularBuffer_t* buffer);
|
||||
switch_status_t stop_audio_download(int id);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
12
mod_dub/common.h
Normal file
12
mod_dub/common.h
Normal file
@@ -0,0 +1,12 @@
|
||||
#ifndef _COMMON_H_
|
||||
#define _COMMON_H_
|
||||
|
||||
#include <boost/circular_buffer.hpp>
|
||||
|
||||
|
||||
typedef boost::circular_buffer<int16_t> CircularBuffer_t;
|
||||
typedef int32_t downloadId_t;
|
||||
|
||||
#define INVALID_DOWNLOAD_ID (-1)
|
||||
|
||||
#endif
|
||||
222
mod_dub/dub_glue.cpp
Normal file
222
mod_dub/dub_glue.cpp
Normal file
@@ -0,0 +1,222 @@
|
||||
#include "mod_dub.h"
|
||||
#include "audio_downloader.h"
|
||||
#include "file_loader.h"
|
||||
|
||||
#include <string>
|
||||
|
||||
#include <switch.h>
|
||||
|
||||
#include <curl/curl.h>
|
||||
|
||||
#include <boost/circular_buffer.hpp>
|
||||
|
||||
typedef boost::circular_buffer<int16_t> CircularBuffer_t;
|
||||
#define INIT_BUFFER_SIZE (80000)
|
||||
|
||||
extern "C" {
|
||||
|
||||
void init_dub_track(dub_track_t *track, char* trackName, int sampleRate) {
|
||||
track->state = DUB_TRACK_STATE_READY;
|
||||
track->trackName = strdup(trackName);
|
||||
track->sampleRate = sampleRate;
|
||||
track->circularBuffer = new CircularBuffer_t(INIT_BUFFER_SIZE);
|
||||
}
|
||||
|
||||
switch_status_t silence_dub_track(dub_track_t *track) {
|
||||
assert(track);
|
||||
switch (track->generator) {
|
||||
case DUB_GENERATOR_TYPE_HTTP:
|
||||
stop_audio_download(track->generatorId);
|
||||
break;
|
||||
case DUB_GENERATOR_TYPE_FILE:
|
||||
stop_file_load(track->generatorId);
|
||||
break;
|
||||
case DUB_GENERATOR_TYPE_TTS:
|
||||
//TODO
|
||||
break;
|
||||
}
|
||||
CircularBuffer_t* buffer = reinterpret_cast<CircularBuffer_t*>(track->circularBuffer);
|
||||
buffer->clear();
|
||||
track->state = DUB_TRACK_STATE_READY;
|
||||
track->generator = DUB_GENERATOR_TYPE_UNKNOWN;
|
||||
track->generatorId = 0;
|
||||
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
switch_status_t remove_dub_track(dub_track_t *track) {
|
||||
assert(track);
|
||||
switch (track->generator) {
|
||||
case DUB_GENERATOR_TYPE_HTTP:
|
||||
stop_audio_download(track->generatorId);
|
||||
break;
|
||||
case DUB_GENERATOR_TYPE_FILE:
|
||||
stop_file_load(track->generatorId);
|
||||
break;
|
||||
case DUB_GENERATOR_TYPE_TTS:
|
||||
//TODO
|
||||
break;
|
||||
}
|
||||
CircularBuffer_t* buffer = reinterpret_cast<CircularBuffer_t*>(track->circularBuffer);
|
||||
if (buffer) {
|
||||
delete buffer;
|
||||
}
|
||||
if (track->trackName) {
|
||||
free(track->trackName);
|
||||
}
|
||||
memset(track, 0, sizeof(dub_track_t));
|
||||
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
switch_status_t play_dub_track(dub_track_t *track, switch_mutex_t *mutex, char* url, int loop, int gain) {
|
||||
bool isHttp = strncmp(url, "http", 4) == 0;
|
||||
if (track->state != DUB_TRACK_STATE_READY) {
|
||||
silence_dub_track(track);
|
||||
}
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "play_dub_track: starting %s download: %s\n", (isHttp ? "HTTP" : "file"), url);
|
||||
int id = isHttp ?
|
||||
start_audio_download(url, track->sampleRate, loop, gain, mutex, (CircularBuffer_t*) track->circularBuffer) :
|
||||
start_file_load(url, track->sampleRate, loop, gain, mutex, (CircularBuffer_t*) track->circularBuffer);
|
||||
|
||||
if (id == INVALID_DOWNLOAD_ID) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "play_dub_track: failed to start audio download\n");
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
track->state = DUB_TRACK_STATE_ACTIVE;
|
||||
track->generatorId = id;
|
||||
track->generator = isHttp ? DUB_GENERATOR_TYPE_HTTP : DUB_GENERATOR_TYPE_FILE;
|
||||
track->gain = gain;
|
||||
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
switch_status_t say_dub_track(dub_track_t *track, switch_mutex_t *mutex, char* text, int gain) {
|
||||
if (track->state != DUB_TRACK_STATE_READY) {
|
||||
silence_dub_track(track); // wait...shouldnt we queue says?
|
||||
}
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "say_dub_track: starting TTS\n");
|
||||
|
||||
/**
|
||||
* TODO:
|
||||
* This is not implemented yet. We can play TTS using using the playOnSay function
|
||||
* because jambonz can generate local audio files using TTS vendors.
|
||||
* However, we should probably at least implement support for elevenlabs streaming api
|
||||
* here because it is so much faster.
|
||||
*
|
||||
*/
|
||||
|
||||
/*
|
||||
track->state = DUB_TRACK_STATE_ACTIVE;
|
||||
track->generatorId = id;
|
||||
track->generator = DUB_GENERATOR_TYPE_TTS;
|
||||
track->gain = gain;
|
||||
*/
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
/* module load and unload */
|
||||
switch_status_t dub_init() {
|
||||
switch_status_t status;
|
||||
status = init_audio_downloader();
|
||||
if (status == SWITCH_STATUS_SUCCESS) {
|
||||
status = init_file_loader();
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
switch_status_t dub_cleanup() {
|
||||
switch_status_t status;
|
||||
status = deinit_audio_downloader();
|
||||
if (status == SWITCH_STATUS_SUCCESS) {
|
||||
status = deinit_file_loader();
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
switch_status_t dub_session_cleanup(switch_core_session_t *session, int channelIsClosing, switch_media_bug_t *bug) {
|
||||
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||
|
||||
if (bug) {
|
||||
struct cap_cb *cb = (struct cap_cb *) switch_core_media_bug_get_user_data(bug);
|
||||
switch_mutex_lock(cb->mutex);
|
||||
|
||||
if (!switch_channel_get_private(channel, MY_BUG_NAME)) {
|
||||
// race condition
|
||||
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "%s Bug is not attached (race).\n", switch_channel_get_name(channel));
|
||||
switch_mutex_unlock(cb->mutex);
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
switch_channel_set_private(channel, MY_BUG_NAME, NULL);
|
||||
|
||||
for (int i = 0; i < MAX_DUB_TRACKS; i++) {
|
||||
dub_track_t* track = &cb->tracks[i];
|
||||
if (track->state != DUB_TRACK_STATE_INACTIVE) {
|
||||
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_DEBUG, "dub_session_cleanup: cleared track %d:%s\n", i, track->trackName);
|
||||
remove_dub_track(track);
|
||||
}
|
||||
}
|
||||
|
||||
if (!channelIsClosing) {
|
||||
switch_core_media_bug_remove(session, &bug);
|
||||
}
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "dub_session_cleanup: removed bug and cleared tracks\n");
|
||||
switch_mutex_unlock(cb->mutex);
|
||||
}
|
||||
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "%s dub_session_cleanup: Bug is not attached.\n", switch_channel_get_name(channel));
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
switch_bool_t dub_speech_frame(switch_media_bug_t *bug, void* user_data) {
|
||||
switch_core_session_t *session = switch_core_media_bug_get_session(bug);
|
||||
struct cap_cb *cb = (struct cap_cb *) user_data;
|
||||
|
||||
if (switch_mutex_trylock(cb->mutex) == SWITCH_STATUS_SUCCESS) {
|
||||
|
||||
/* check if any tracks are actively pushing audio */
|
||||
int trackCount = 0;
|
||||
for (int i = 0; i < MAX_DUB_TRACKS; i++) {
|
||||
if (cb->tracks[i].state == DUB_TRACK_STATE_ACTIVE) trackCount++;
|
||||
}
|
||||
if (trackCount == 0 && cb->gain == 0) {
|
||||
switch_mutex_unlock(cb->mutex);
|
||||
return SWITCH_TRUE;
|
||||
}
|
||||
|
||||
switch_frame_t* rframe = switch_core_media_bug_get_write_replace_frame(bug);
|
||||
int16_t *fp = reinterpret_cast<int16_t*>(rframe->data);
|
||||
|
||||
rframe->channels = 1;
|
||||
rframe->datalen = rframe->samples * rframe->channels * sizeof(int16_t);
|
||||
|
||||
/* apply gain to audio in main channel if requested*/
|
||||
if (cb->gain != 0) {
|
||||
switch_change_sln_volume_granular(fp, rframe->samples, cb->gain);
|
||||
}
|
||||
|
||||
/* now mux in the data from tracks */
|
||||
for (int i = 0; i < rframe->samples; i++) {
|
||||
int16_t input = fp[i];
|
||||
int16_t value = input;
|
||||
for (int j = 0; j < MAX_DUB_TRACKS; j++) {
|
||||
dub_track_t* track = &cb->tracks[j];
|
||||
if (track->state == DUB_TRACK_STATE_ACTIVE) {
|
||||
CircularBuffer_t* buffer = reinterpret_cast<CircularBuffer_t*>(track->circularBuffer);
|
||||
if (buffer && !buffer->empty()) {
|
||||
int16_t sample = buffer->front();
|
||||
buffer->pop_front();
|
||||
value += sample;
|
||||
}
|
||||
}
|
||||
}
|
||||
switch_normalize_to_16bit(value);
|
||||
fp[i] = (int16_t) value;
|
||||
}
|
||||
switch_core_media_bug_set_write_replace_frame(bug, rframe);
|
||||
switch_mutex_unlock(cb->mutex);
|
||||
}
|
||||
return SWITCH_TRUE;
|
||||
}
|
||||
}
|
||||
16
mod_dub/dub_glue.h
Normal file
16
mod_dub/dub_glue.h
Normal file
@@ -0,0 +1,16 @@
|
||||
#ifndef __DUB_GLUE_H__
|
||||
#define __DUB_GLUE_H__
|
||||
|
||||
switch_status_t dub_init();
|
||||
switch_status_t dub_cleanup();
|
||||
|
||||
void init_dub_track(dub_track_t *track, char* trackName, int sampleRate);
|
||||
switch_status_t silence_dub_track(dub_track_t *track);
|
||||
switch_status_t remove_dub_track(dub_track_t *track);
|
||||
switch_status_t play_dub_track(dub_track_t *track, switch_mutex_t *mutex, char* url, int loop, int gain);
|
||||
switch_status_t say_dub_track(dub_track_t *track, switch_mutex_t *mutex, char* text, int gain);
|
||||
|
||||
switch_status_t dub_session_cleanup(switch_core_session_t *session, int channelIsClosing, switch_media_bug_t *bug);
|
||||
switch_bool_t dub_speech_frame(switch_media_bug_t *bug, void* user_data);
|
||||
|
||||
#endif
|
||||
437
mod_dub/file_loader.cpp
Normal file
437
mod_dub/file_loader.cpp
Normal file
@@ -0,0 +1,437 @@
|
||||
#include "file_loader.h"
|
||||
|
||||
#include <boost/thread.hpp>
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/asio/ssl.hpp>
|
||||
#include <boost/pool/object_pool.hpp>
|
||||
#include <boost/bind/bind.hpp>
|
||||
#include <boost/tokenizer.hpp>
|
||||
#include <boost/foreach.hpp>
|
||||
#include <boost/asio.hpp>
|
||||
#include <boost/assign/list_of.hpp>
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
#include <map>
|
||||
|
||||
#include <mpg123.h>
|
||||
|
||||
#define INIT_BUFFER_SIZE (80000)
|
||||
#define BUFFER_GROW_SIZE (80000)
|
||||
#define BUFFER_THROTTLE_LOW (40000)
|
||||
#define BUFFER_THROTTLE_HIGH (160000)
|
||||
|
||||
static uint16_t currDownloadId = 0;
|
||||
|
||||
typedef enum
|
||||
{
|
||||
STATUS_NONE = 0,
|
||||
STATUS_FAILED,
|
||||
STATUS_FILE_IN_PROGRESS,
|
||||
STATUS_FILE_PAUSED,
|
||||
STATUS_FILE_COMPLETE,
|
||||
STATUS_AWAITING_RESTART,
|
||||
STATUS_STOPPING,
|
||||
STATUS_STOPPED
|
||||
} Status_t;
|
||||
|
||||
typedef enum {
|
||||
FILE_TYPE_MP3 = 0,
|
||||
FILE_TYPE_R8
|
||||
} FileType_t;
|
||||
|
||||
static const char* status2String(Status_t status)
|
||||
{
|
||||
static const char* statusStrings[] = {
|
||||
"STATUS_NONE",
|
||||
"STATUS_FAILED",
|
||||
"STATUS_FILE_IN_PROGRESS",
|
||||
"STATUS_FILE_PAUSED",
|
||||
"STATUS_FILE_COMPLETE",
|
||||
"STATUS_AWAITING_RESTART",
|
||||
"STATUS_STOPPING",
|
||||
"STATUS_STOPPED"
|
||||
};
|
||||
|
||||
if (status >= 0 && status < sizeof(statusStrings) / sizeof(statusStrings[0]))
|
||||
{
|
||||
return statusStrings[status];
|
||||
}
|
||||
else
|
||||
{
|
||||
return "UNKNOWN_STATUS";
|
||||
}
|
||||
}
|
||||
|
||||
typedef struct
|
||||
{
|
||||
switch_mutex_t* mutex;
|
||||
CircularBuffer_t* buffer;
|
||||
mpg123_handle *mh;
|
||||
FILE* fp;
|
||||
char* path;
|
||||
bool loop;
|
||||
int rate;
|
||||
boost::asio::deadline_timer *timer;
|
||||
Status_t status;
|
||||
FileType_t type;
|
||||
downloadId_t id;
|
||||
int gain;
|
||||
} FileInfo_t;
|
||||
|
||||
typedef std::map<int32_t, FileInfo_t *> Id2FileMap_t;
|
||||
static Id2FileMap_t id2FileMap;
|
||||
|
||||
static boost::object_pool<FileInfo_t> pool ;
|
||||
static boost::asio::io_service io_service;
|
||||
static std::thread worker_thread;
|
||||
|
||||
|
||||
/* forward declarations */
|
||||
static FileInfo_t* createFileLoader(const char *path, int rate, int loop, int gain, mpg123_handle *mhm, switch_mutex_t *mutex, CircularBuffer_t *buffer);
|
||||
static void destroyFileInfo(FileInfo_t *finfo);
|
||||
static void threadFunc();
|
||||
static std::vector<int16_t> convert_mp3_to_linear(FileInfo_t *file, int8_t *data, size_t len);
|
||||
static void read_cb(const boost::system::error_code& error, FileInfo_t* finfo) ;
|
||||
|
||||
/* apis */
|
||||
extern "C" {
|
||||
|
||||
switch_status_t init_file_loader() {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "init_file_loader loading..\n");
|
||||
|
||||
if (mpg123_init() != MPG123_OK) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "init_file_loader: failed to initiate MPG123");
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
/* start worker thread */
|
||||
std::thread t(threadFunc) ;
|
||||
worker_thread.swap( t ) ;
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "init_file_loader: loaded\n");
|
||||
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
|
||||
}
|
||||
|
||||
switch_status_t deinit_file_loader() {
|
||||
/* stop the ASIO IO service */
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "deinit_file_loader: stopping io service\n");
|
||||
io_service.stop();
|
||||
|
||||
/* Join the worker thread */
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "deinit_file_loader: wait for worker thread to complete\n");
|
||||
if (worker_thread.joinable()) {
|
||||
worker_thread.join();
|
||||
}
|
||||
|
||||
mpg123_exit();
|
||||
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
downloadId_t start_file_load(const char* path, int rate, int loop, int gain, switch_mutex_t* mutex, CircularBuffer_t* buffer) {
|
||||
int mhError = 0;
|
||||
|
||||
/* we only handle mp3 or r8 files atm */
|
||||
const char *ext = strrchr(path, '.');
|
||||
if (!ext) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "start_file_load: file %s has no extension\n", path);
|
||||
return INVALID_DOWNLOAD_ID;
|
||||
}
|
||||
ext++;
|
||||
if (0 != strcmp(ext, "mp3") && 0 != strcmp(ext, "r8")) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "start_file_load: file %s has unsupported extension %s\n", path, ext);
|
||||
return INVALID_DOWNLOAD_ID;
|
||||
}
|
||||
|
||||
/* allocate handle for mpeg decoding */
|
||||
mpg123_handle *mh = mpg123_new("auto", &mhError);
|
||||
if (!mh) {
|
||||
const char *mhErr = mpg123_plain_strerror(mhError);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error allocating mpg123 handle! %s\n", switch_str_nil(mhErr));
|
||||
return INVALID_DOWNLOAD_ID;
|
||||
}
|
||||
|
||||
if (mpg123_open_feed(mh) != MPG123_OK) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error mpg123_open_feed!\n");
|
||||
return INVALID_DOWNLOAD_ID;
|
||||
}
|
||||
|
||||
if (mpg123_format_all(mh) != MPG123_OK) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error mpg123_format_all!\n");
|
||||
return INVALID_DOWNLOAD_ID;
|
||||
}
|
||||
|
||||
if (mpg123_param(mh, MPG123_FORCE_RATE, rate, 0) != MPG123_OK) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error forcing resample to 8k!\n");
|
||||
return INVALID_DOWNLOAD_ID;
|
||||
}
|
||||
|
||||
if (mpg123_param(mh, MPG123_FLAGS, MPG123_MONO_MIX, 0) != MPG123_OK) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error forcing single channel!\n");
|
||||
return INVALID_DOWNLOAD_ID;
|
||||
}
|
||||
|
||||
FileInfo_t* finfo = createFileLoader(path, rate, loop, gain, mh, mutex, buffer);
|
||||
if (!finfo) {
|
||||
return INVALID_DOWNLOAD_ID;
|
||||
}
|
||||
|
||||
/* do the initial read in the worker thread so we don't block here */
|
||||
finfo->timer->expires_from_now(boost::posix_time::millisec(1));
|
||||
finfo->timer->async_wait(boost::bind(&read_cb, boost::placeholders::_1, finfo));
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO,
|
||||
"start_file_load: starting load %d\n", finfo->id);
|
||||
|
||||
return finfo->id;
|
||||
}
|
||||
|
||||
switch_status_t stop_file_load(int id) {
|
||||
auto it = id2FileMap.find(id);
|
||||
if (it == id2FileMap.end()) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "stop_file_load: id %d has already completed\n", id);
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO,
|
||||
"stop_audio_download: stopping download %d, status %s\n", id, status2String(it->second->status));
|
||||
|
||||
FileInfo_t *finfo = it->second;
|
||||
auto status = finfo->status;
|
||||
|
||||
/* past this point I shall not access either the mutex or the buffer provided */
|
||||
finfo->mutex = nullptr;
|
||||
finfo->buffer = nullptr;
|
||||
|
||||
destroyFileInfo(finfo);
|
||||
|
||||
finfo->status = Status_t::STATUS_STOPPED;
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
}
|
||||
|
||||
/* internal */
|
||||
FileInfo_t* createFileLoader(const char *path, int rate, int loop, int gain, mpg123_handle *mh, switch_mutex_t *mutex, CircularBuffer_t *buffer) {
|
||||
FileInfo_t *finfo = pool.malloc() ;
|
||||
const char *ext = strrchr(path, '.');
|
||||
|
||||
memset(finfo, 0, sizeof(FileInfo_t));
|
||||
finfo->mutex = mutex;
|
||||
finfo->buffer = buffer;
|
||||
finfo->mh = mh;
|
||||
finfo->loop = loop;
|
||||
finfo->gain = gain;
|
||||
finfo->rate = rate;
|
||||
finfo->path = strdup(path);
|
||||
finfo->status = Status_t::STATUS_NONE;
|
||||
finfo->timer = new boost::asio::deadline_timer(io_service);
|
||||
|
||||
if (0 == strcmp(ext, "mp3")) finfo->type = FileType_t::FILE_TYPE_MP3;
|
||||
else if (0 == strcmp(ext, "r8")) finfo->type = FileType_t::FILE_TYPE_R8;
|
||||
|
||||
downloadId_t id = ++currDownloadId;
|
||||
if (id == 0) id++;
|
||||
|
||||
id2FileMap[id] = finfo;
|
||||
finfo->id = id;
|
||||
|
||||
finfo->status = Status_t::STATUS_AWAITING_RESTART;
|
||||
|
||||
finfo->fp = fopen(finfo->path, "rb");
|
||||
if (finfo->fp == NULL) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "createFileLoader: failed to open file %s\n", finfo->path);
|
||||
destroyFileInfo(finfo);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG,
|
||||
"createFileLoader: launched request, loop %s, gain %d\n", (finfo->loop ? "yes": "no"), finfo->gain);
|
||||
return finfo;
|
||||
}
|
||||
|
||||
void destroyFileInfo(FileInfo_t *finfo) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "destroyFileInfo\n");
|
||||
|
||||
/* clear asio resources and free resources */
|
||||
if (finfo->timer) {
|
||||
finfo->timer->cancel();
|
||||
delete finfo->timer;
|
||||
}
|
||||
|
||||
/* free mp3 decoder */
|
||||
if (finfo->mh) {
|
||||
mpg123_close(finfo->mh);
|
||||
mpg123_delete(finfo->mh);
|
||||
}
|
||||
|
||||
if (finfo->path) {
|
||||
free(finfo->path);
|
||||
}
|
||||
|
||||
if (finfo->mutex) switch_mutex_lock(finfo->mutex);
|
||||
id2FileMap.erase(finfo->id);
|
||||
if (finfo->mutex) switch_mutex_unlock(finfo->mutex);
|
||||
|
||||
memset(finfo, 0, sizeof(FileInfo_t));
|
||||
pool.destroy(finfo) ;
|
||||
}
|
||||
|
||||
void threadFunc() {
|
||||
/* to make sure the event loop doesn't terminate when there is no work to do */
|
||||
io_service.reset() ;
|
||||
boost::asio::io_service::work work(io_service);
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "file_loader threadFunc - starting\n");
|
||||
|
||||
for(;;) {
|
||||
|
||||
try {
|
||||
io_service.run() ;
|
||||
break ;
|
||||
}
|
||||
catch( std::exception& e) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "file_loader threadFunc - Error: %s\n", e.what());
|
||||
}
|
||||
}
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "file_loader threadFunc - ending\n");
|
||||
}
|
||||
|
||||
std::vector<int16_t> convert_mp3_to_linear(FileInfo_t *finfo, int8_t *data, size_t len) {
|
||||
std::vector<int16_t> linear_data;
|
||||
int eof = 0;
|
||||
int mp3err = 0;
|
||||
|
||||
if(mpg123_feed(finfo->mh, (const unsigned char*) data, len) == MPG123_OK) {
|
||||
while(!eof) {
|
||||
size_t usedlen = 0;
|
||||
off_t frame_offset;
|
||||
unsigned char* audio;
|
||||
|
||||
int decode_status = mpg123_decode_frame(finfo->mh, &frame_offset, &audio, &usedlen);
|
||||
|
||||
switch(decode_status) {
|
||||
case MPG123_NEW_FORMAT:
|
||||
continue;
|
||||
|
||||
case MPG123_OK:
|
||||
{
|
||||
size_t samples = usedlen / sizeof(int16_t);
|
||||
linear_data.insert(linear_data.end(), reinterpret_cast<int16_t*>(audio), reinterpret_cast<int16_t*>(audio) + samples);
|
||||
}
|
||||
break;
|
||||
|
||||
case MPG123_DONE:
|
||||
case MPG123_NEED_MORE:
|
||||
eof = 1;
|
||||
break;
|
||||
|
||||
case MPG123_ERR:
|
||||
default:
|
||||
if(++mp3err >= 5) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Decoder Error!\n");
|
||||
eof = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (eof)
|
||||
break;
|
||||
|
||||
mp3err = 0;
|
||||
}
|
||||
|
||||
if (finfo->gain != 0) {
|
||||
switch_change_sln_volume_granular(linear_data.data(), linear_data.size(), finfo->gain);
|
||||
}
|
||||
}
|
||||
|
||||
return linear_data;
|
||||
}
|
||||
|
||||
void read_cb(const boost::system::error_code& error, FileInfo_t* finfo) {
|
||||
if (finfo->status == Status_t::STATUS_STOPPING || !finfo->mutex || !finfo->buffer) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %u session gone\n", finfo->id);
|
||||
return;
|
||||
}
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %u status is %s\n", finfo->id, status2String(finfo->status));
|
||||
if (finfo->status == Status_t::STATUS_AWAITING_RESTART) {
|
||||
finfo->status = Status_t::STATUS_FILE_IN_PROGRESS;
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %u starting initial read of file\n", finfo->id);
|
||||
}
|
||||
|
||||
if (!error) {
|
||||
size_t size = 0;
|
||||
|
||||
switch_mutex_lock(finfo->mutex);
|
||||
size = finfo->buffer->size();
|
||||
switch_mutex_unlock(finfo->mutex);
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %u size is now %ld\n", finfo->id, size);
|
||||
if (size < BUFFER_THROTTLE_LOW) {
|
||||
std::vector<int16_t> pcm_data;
|
||||
int8_t buf[INIT_BUFFER_SIZE];
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %u reading data\n", finfo->id);
|
||||
|
||||
size_t bytesRead = ::fread(buf, sizeof(int8_t), INIT_BUFFER_SIZE, finfo->fp);
|
||||
if (bytesRead <= 0) {
|
||||
if (::feof(finfo->fp)) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %u eof\n", finfo->id);
|
||||
}
|
||||
else if (::ferror(finfo->fp)) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "read_cb: %u error reading file\n", finfo->id);
|
||||
}
|
||||
else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "read_cb: %u unknown error reading file\n", finfo->id);
|
||||
}
|
||||
finfo->status = Status_t::STATUS_FILE_COMPLETE;
|
||||
return;
|
||||
}
|
||||
|
||||
if (finfo->type == FileType_t::FILE_TYPE_MP3) {
|
||||
pcm_data = convert_mp3_to_linear(finfo, buf, bytesRead);
|
||||
} else {
|
||||
pcm_data = std::vector<int16_t>(reinterpret_cast<int16_t*>(buf), reinterpret_cast<int16_t*>(buf) + bytesRead / 2);
|
||||
}
|
||||
|
||||
switch_mutex_lock(finfo->mutex);
|
||||
|
||||
// Resize the buffer if necessary
|
||||
if (finfo->buffer->capacity() - finfo->buffer->size() < pcm_data.size()) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "write_cb %u growing buffer, size now %ld\n", finfo->id, finfo->buffer->size());
|
||||
finfo->buffer->set_capacity(finfo->buffer->size() + std::max(pcm_data.size(), (size_t)BUFFER_GROW_SIZE));
|
||||
}
|
||||
|
||||
/* Push the data into the buffer */
|
||||
finfo->buffer->insert(finfo->buffer->end(), pcm_data.data(), pcm_data.data() + pcm_data.size());
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %u wrote data, buffer size is now %ld\n", finfo->id, finfo->buffer->size());
|
||||
|
||||
switch_mutex_unlock(finfo->mutex);
|
||||
|
||||
if (bytesRead < INIT_BUFFER_SIZE) {
|
||||
finfo->status = Status_t::STATUS_FILE_COMPLETE;
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %u reached end of file, status is %s\n", finfo->id, status2String(finfo->status));
|
||||
}
|
||||
}
|
||||
|
||||
if (finfo->status == Status_t::STATUS_FILE_COMPLETE && finfo->loop) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %u looping\n", finfo->id);
|
||||
::fseek(finfo->fp, 0, SEEK_SET);
|
||||
finfo->status = Status_t::STATUS_AWAITING_RESTART;
|
||||
}
|
||||
|
||||
if (finfo->status != Status_t::STATUS_FILE_COMPLETE) {
|
||||
// read more in 2 seconds
|
||||
finfo->timer->expires_from_now(boost::posix_time::millisec(2000));
|
||||
finfo->timer->async_wait(boost::bind(&read_cb, boost::placeholders::_1, finfo));
|
||||
}
|
||||
else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "read_cb: %u file complete, status %s loop %s\n",
|
||||
finfo->id, status2String(finfo->status), (finfo->loop ? "yes" : "no"));
|
||||
}
|
||||
} else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "read_cb: %u error (%d): %s\n", finfo->id, error.value(), error.message().c_str());
|
||||
|
||||
// Handle any errors
|
||||
}
|
||||
}
|
||||
18
mod_dub/file_loader.h
Normal file
18
mod_dub/file_loader.h
Normal file
@@ -0,0 +1,18 @@
|
||||
#ifndef __FILE_LOADER_H__
|
||||
#define __FILE_LOADER_H__
|
||||
|
||||
#include <switch.h>
|
||||
#include "common.h"
|
||||
|
||||
extern "C" {
|
||||
|
||||
switch_status_t init_file_loader();
|
||||
switch_status_t deinit_file_loader();
|
||||
|
||||
int start_file_load(const char* path, int rate, int loop, int gain, switch_mutex_t* mutex, CircularBuffer_t* buffer);
|
||||
switch_status_t stop_file_load(int id);
|
||||
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
383
mod_dub/mod_dub.c
Normal file
383
mod_dub/mod_dub.c
Normal file
@@ -0,0 +1,383 @@
|
||||
/*
|
||||
*
|
||||
* mod_dub.c
|
||||
*
|
||||
*/
|
||||
#include "mod_dub.h"
|
||||
#include <stdlib.h>
|
||||
#include <switch.h>
|
||||
#include <switch_curl.h>
|
||||
#include "dub_glue.h"
|
||||
|
||||
/* Prototypes */
|
||||
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_dub_shutdown);
|
||||
SWITCH_MODULE_LOAD_FUNCTION(mod_dub_load);
|
||||
|
||||
SWITCH_MODULE_DEFINITION(mod_dub, mod_dub_load, mod_dub_shutdown, NULL);
|
||||
|
||||
static switch_bool_t capture_callback(switch_media_bug_t *bug, void *user_data, switch_abc_type_t type)
|
||||
{
|
||||
switch_core_session_t *session = switch_core_media_bug_get_session(bug);
|
||||
switch_bool_t ret = SWITCH_TRUE;
|
||||
|
||||
switch (type) {
|
||||
case SWITCH_ABC_TYPE_INIT:
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Got SWITCH_ABC_TYPE_INIT.\n");
|
||||
break;
|
||||
|
||||
case SWITCH_ABC_TYPE_CLOSE:
|
||||
{
|
||||
dub_session_cleanup(session, 1, bug);
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "Finished SWITCH_ABC_TYPE_CLOSE.\n");
|
||||
}
|
||||
break;
|
||||
|
||||
case SWITCH_ABC_TYPE_WRITE_REPLACE:
|
||||
ret = dub_speech_frame(bug, user_data);
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
return ret;
|
||||
}
|
||||
|
||||
static switch_status_t dub_set_gain(switch_core_session_t *session, int gain) {
|
||||
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||
switch_media_bug_t *bug = NULL;
|
||||
struct cap_cb *cb = NULL;
|
||||
|
||||
if (switch_channel_pre_answer(channel) != SWITCH_STATUS_SUCCESS) {
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
if (!(bug = (switch_media_bug_t*) switch_channel_get_private(channel, MY_BUG_NAME))) {
|
||||
cb =(struct cap_cb *) switch_core_session_alloc(session, sizeof(struct cap_cb));
|
||||
memset(cb, 0, sizeof(struct cap_cb));
|
||||
switch_mutex_init(&cb->mutex, SWITCH_MUTEX_NESTED, switch_core_session_get_pool(session));
|
||||
}
|
||||
else {
|
||||
cb = (struct cap_cb *) switch_core_media_bug_get_user_data(bug);
|
||||
}
|
||||
cb->gain = gain;
|
||||
|
||||
/* check again under lock */
|
||||
switch_mutex_lock(cb->mutex);
|
||||
if (!(bug = (switch_media_bug_t*) switch_channel_get_private(channel, MY_BUG_NAME))) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "dub_set_gain: adding bug so we can set gain on main channel\n");
|
||||
if (switch_core_media_bug_add(session, MY_BUG_NAME, NULL, capture_callback, (void *) cb, 0, SMBF_WRITE_REPLACE, &bug) != SWITCH_STATUS_SUCCESS) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "dub_set_gain: error adding bug!\n");
|
||||
switch_mutex_unlock(cb->mutex);
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
switch_channel_set_private(channel, MY_BUG_NAME, bug);
|
||||
}
|
||||
else {
|
||||
cb = (struct cap_cb *) switch_core_media_bug_get_user_data(bug);
|
||||
cb->gain = gain;
|
||||
}
|
||||
switch_mutex_unlock(cb->mutex);
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "dub_set_gain: setting gain to %d\n", gain);
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
static switch_status_t dub_add_track(switch_core_session_t *session, char* trackName) {
|
||||
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||
switch_media_bug_t *bug = NULL;
|
||||
struct cap_cb *cb = NULL;
|
||||
int offset = 0;
|
||||
int samples_per_second;
|
||||
|
||||
switch_codec_implementation_t write_impl = { 0 };
|
||||
|
||||
if (switch_channel_pre_answer(channel) != SWITCH_STATUS_SUCCESS) {
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
|
||||
if (!(bug = (switch_media_bug_t*) switch_channel_get_private(channel, MY_BUG_NAME))) {
|
||||
/* allocate per-session memory and use track 0 */
|
||||
cb =(struct cap_cb *) switch_core_session_alloc(session, sizeof(struct cap_cb));
|
||||
memset(cb, 0, sizeof(struct cap_cb));
|
||||
switch_mutex_init(&cb->mutex, SWITCH_MUTEX_NESTED, switch_core_session_get_pool(session));
|
||||
offset = 0;
|
||||
}
|
||||
else {
|
||||
/* retrieve the bug and search for an empty track */
|
||||
cb = (struct cap_cb *) switch_core_media_bug_get_user_data(bug);
|
||||
while (offset < MAX_DUB_TRACKS && cb->tracks[offset].state != DUB_TRACK_STATE_INACTIVE) {
|
||||
offset++;
|
||||
}
|
||||
if (offset == MAX_DUB_TRACKS) {
|
||||
/* all tracks are in use */
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "dub_add_track: no available tracks\n");
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
}
|
||||
|
||||
switch_core_session_get_write_impl(session, &write_impl);
|
||||
samples_per_second = !strcasecmp(write_impl.iananame, "g722") ? write_impl.actual_samples_per_second : write_impl.samples_per_second;
|
||||
|
||||
init_dub_track(&cb->tracks[offset], trackName, samples_per_second);
|
||||
|
||||
if (!bug) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "dub_add_track: adding bug for track %s\n", trackName);
|
||||
if (switch_core_media_bug_add(session, MY_BUG_NAME, NULL, capture_callback, (void *) cb, 0, SMBF_WRITE_REPLACE, &bug) != SWITCH_STATUS_SUCCESS) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "dub_add_track: error adding bug!\n");
|
||||
return SWITCH_STATUS_FALSE;
|
||||
}
|
||||
switch_channel_set_private(channel, MY_BUG_NAME, bug);
|
||||
}
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "dub_add_track: added track %s at offset %d\n", trackName, offset);
|
||||
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
static switch_status_t dub_remove_track(switch_core_session_t *session, char* trackName) {
|
||||
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||
switch_media_bug_t *bug = (switch_media_bug_t*) switch_channel_get_private(channel, MY_BUG_NAME);
|
||||
switch_status_t status = SWITCH_STATUS_FALSE;
|
||||
dub_track_t *track = NULL;
|
||||
|
||||
if (bug) {
|
||||
struct cap_cb *cb =(struct cap_cb *) switch_core_media_bug_get_user_data(bug);
|
||||
switch_mutex_lock(cb->mutex);
|
||||
for (int i = 0; i < MAX_DUB_TRACKS && track == NULL; i++) {
|
||||
if (cb->tracks[i].state != DUB_TRACK_STATE_INACTIVE && strcmp(cb->tracks[i].trackName, trackName) == 0) {
|
||||
track = &cb->tracks[i];
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "dub_remove_track: removing track %s at offset %d\n", trackName, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (track) {
|
||||
int count = 0;
|
||||
|
||||
remove_dub_track(track);
|
||||
|
||||
/* check if this is the last bug */
|
||||
for (int i = 0; i < MAX_DUB_TRACKS; i++) {
|
||||
if (cb->tracks[i].state != DUB_TRACK_STATE_INACTIVE) count++;
|
||||
}
|
||||
|
||||
if (count == 0) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "dub_remove_track: removing bug after removing last track\n");
|
||||
dub_session_cleanup(session, 0, bug);
|
||||
}
|
||||
status = SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "dub_remove_track: track %s not found\n", trackName);
|
||||
}
|
||||
switch_mutex_unlock(cb->mutex);
|
||||
}
|
||||
else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "dub_remove_track: bug not found\n");
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
static switch_status_t dub_silence_track(switch_core_session_t *session, char* trackName) {
|
||||
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||
switch_media_bug_t *bug = (switch_media_bug_t*) switch_channel_get_private(channel, MY_BUG_NAME);
|
||||
switch_status_t status = SWITCH_STATUS_FALSE;
|
||||
dub_track_t *track = NULL;
|
||||
|
||||
if (bug) {
|
||||
struct cap_cb *cb =(struct cap_cb *) switch_core_media_bug_get_user_data(bug);
|
||||
switch_mutex_lock(cb->mutex);
|
||||
for (int i = 0; i < MAX_DUB_TRACKS; i++) {
|
||||
if (cb->tracks[i].state != DUB_TRACK_STATE_INACTIVE && strcmp(cb->tracks[i].trackName, trackName) == 0) {
|
||||
track = &cb->tracks[i];
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "dub_silence_track: silencing track %s at offset %d\n", trackName, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (track) {
|
||||
silence_dub_track(track);
|
||||
status = SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
switch_mutex_unlock(cb->mutex);
|
||||
}
|
||||
else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "dub_silence_track: bug not found\n");
|
||||
}
|
||||
|
||||
return status;
|
||||
}
|
||||
|
||||
static switch_status_t dub_play_on_track(switch_core_session_t *session, char* trackName, char* url, int loop, int gain) {
|
||||
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||
switch_media_bug_t *bug = (switch_media_bug_t*) switch_channel_get_private(channel, MY_BUG_NAME);
|
||||
switch_status_t status = SWITCH_STATUS_FALSE;
|
||||
dub_track_t *track = NULL;
|
||||
|
||||
if (bug) {
|
||||
struct cap_cb *cb =(struct cap_cb *) switch_core_media_bug_get_user_data(bug);
|
||||
switch_mutex_lock(cb->mutex);
|
||||
for (int i = 0; i < MAX_DUB_TRACKS; i++) {
|
||||
if (cb->tracks[i].state != DUB_TRACK_STATE_INACTIVE && strcmp(cb->tracks[i].trackName, trackName) == 0) {
|
||||
track = &cb->tracks[i];
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO,
|
||||
"dub_play_on_track: playing %s on track %s at offset %d with gain %d\n", url, trackName, i, gain);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (track) {
|
||||
status = play_dub_track(track, cb->mutex, url, loop, gain);
|
||||
}
|
||||
switch_mutex_unlock(cb->mutex);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
static switch_status_t dub_say_on_track(switch_core_session_t *session, char* trackName, char* text, int gain) {
|
||||
switch_channel_t *channel = switch_core_session_get_channel(session);
|
||||
switch_media_bug_t *bug = (switch_media_bug_t*) switch_channel_get_private(channel, MY_BUG_NAME);
|
||||
switch_status_t status = SWITCH_STATUS_FALSE;
|
||||
dub_track_t *track = NULL;
|
||||
|
||||
if (bug) {
|
||||
struct cap_cb *cb =(struct cap_cb *) switch_core_media_bug_get_user_data(bug);
|
||||
switch_mutex_lock(cb->mutex);
|
||||
for (int i = 0; i < MAX_DUB_TRACKS; i++) {
|
||||
if (cb->tracks[i].state != DUB_TRACK_STATE_INACTIVE && strcmp(cb->tracks[i].trackName, trackName) == 0) {
|
||||
track = &cb->tracks[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (track) {
|
||||
status = say_dub_track(track, cb->mutex, text, gain);
|
||||
}
|
||||
switch_mutex_unlock(cb->mutex);
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
#define DUB_API_SYNTAX "<uuid> [addTrack|removeTrack|silenceTrack|playOnTrack|sayOnTrack|setGain] track [url|text|gain] [gain] [loop]"
|
||||
SWITCH_STANDARD_API(dub_function)
|
||||
{
|
||||
char *mycmd = NULL, *argv[6] = { 0 };
|
||||
int argc = 0;
|
||||
int error_written = 0;
|
||||
switch_status_t status = SWITCH_STATUS_FALSE;
|
||||
|
||||
if (!zstr(cmd) && (mycmd = strdup(cmd))) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "dub_function: %s\n", mycmd);
|
||||
argc = switch_separate_string(mycmd, ' ', argv, (sizeof(argv) / sizeof(argv[0])));
|
||||
}
|
||||
|
||||
if (zstr(cmd) || argc < 3 || zstr(argv[0]) || zstr(argv[1]) || zstr(argv[2])) {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error with command %s.\n", cmd);
|
||||
}
|
||||
else {
|
||||
switch_core_session_t *session = NULL;
|
||||
if ((session = switch_core_session_locate(argv[0]))) {
|
||||
char* action = argv[1];
|
||||
char* track = argv[2];
|
||||
|
||||
if (0 == strcmp(action, "setGain")) {
|
||||
int gain = atoi(track);
|
||||
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "setGain %d\n", gain);
|
||||
status = dub_set_gain(session, gain);
|
||||
}
|
||||
else if (0 == strcmp(action, "addTrack")) {
|
||||
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "addTrack %s\n", track);
|
||||
status = dub_add_track(session, track);
|
||||
}
|
||||
else if (0 == strcmp(action, "removeTrack")) {
|
||||
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "removeTrack %s\n", track);
|
||||
status = dub_remove_track(session, track);
|
||||
}
|
||||
else if (0 == strcmp(action, "silenceTrack")) {
|
||||
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "silenceTrack %s\n", track);
|
||||
status = dub_silence_track(session, track);
|
||||
}
|
||||
else if (0 == strcmp(action, "playOnTrack")) {
|
||||
if (argc < 4) {
|
||||
stream->write_function(stream, "-USAGE: %s\n", DUB_API_SYNTAX);
|
||||
error_written = 1;
|
||||
}
|
||||
else {
|
||||
char* url = argv[3];
|
||||
int loop = argc > 4 && 0 == strcmp(argv[4], "loop");
|
||||
int gain = argc > 5 ? atoi(argv[5]) : 0;
|
||||
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO,
|
||||
"playOnTrack %s, %s gain %d\n", url, loop ? "loop" : "once", gain);
|
||||
status = dub_play_on_track(session, track, url, loop, gain);
|
||||
}
|
||||
}
|
||||
else if (0 == strcmp(action, "sayOnTrack")) {
|
||||
if (argc < 4) {
|
||||
stream->write_function(stream, "-USAGE: %s\n", DUB_API_SYNTAX);
|
||||
error_written = 1;
|
||||
}
|
||||
else {
|
||||
char* text = argv[3];
|
||||
int gain = argc > 4 ? atoi(argv[4]) : 0;
|
||||
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_INFO, "sayOnTrack %s gain %d\n", text, gain);
|
||||
status = dub_say_on_track(session, track, text, gain);
|
||||
}
|
||||
}
|
||||
else {
|
||||
switch_log_printf(SWITCH_CHANNEL_SESSION_LOG(session), SWITCH_LOG_ERROR, "Error with command %s %s %s.\n", cmd, argv[0], argv[1]);
|
||||
stream->write_function(stream, "-USAGE: %s\n", DUB_API_SYNTAX);
|
||||
error_written = 1;
|
||||
}
|
||||
switch_core_session_rwunlock(session);
|
||||
}
|
||||
else {
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Error locating session for %s.\n", cmd);
|
||||
stream->write_function(stream, "-ERR Invalid session\n");
|
||||
error_written = 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (status == SWITCH_STATUS_SUCCESS) {
|
||||
stream->write_function(stream, "+OK Success\n");
|
||||
} else if (!error_written){
|
||||
stream->write_function(stream, "-ERR Operation Failed\n");
|
||||
}
|
||||
|
||||
switch_safe_free(mycmd);
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
SWITCH_MODULE_LOAD_FUNCTION(mod_dub_load)
|
||||
{
|
||||
switch_api_interface_t *api_interface;
|
||||
|
||||
*module_interface = switch_loadable_module_create_module_interface(pool, modname);
|
||||
|
||||
dub_init();
|
||||
|
||||
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "mod_dub loaded\n");
|
||||
|
||||
SWITCH_ADD_API(api_interface, "uuid_dub", "dub mp3 track over channel audio", dub_function, DUB_API_SYNTAX);
|
||||
switch_console_set_complete("add uuid_dub addTrack <trackname>");
|
||||
switch_console_set_complete("add uuid_dub removeTrack <trackname>");
|
||||
switch_console_set_complete("add uuid_dub silenceTrack <trackname>");
|
||||
switch_console_set_complete("add uuid_dub playOnTrack <trackname> <url|file> [loop|once] [gain]");
|
||||
switch_console_set_complete("add uuid_dub setGain <gain>");
|
||||
switch_console_set_complete("add uuid_dub stop ");
|
||||
|
||||
/* indicate that the module should continue to be loaded */
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
/*
|
||||
Called when the system shuts down
|
||||
Macro expands to: switch_status_t mod_dub_shutdown() */
|
||||
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_dub_shutdown)
|
||||
{
|
||||
dub_cleanup();
|
||||
return SWITCH_STATUS_SUCCESS;
|
||||
}
|
||||
|
||||
59
mod_dub/mod_dub.h
Normal file
59
mod_dub/mod_dub.h
Normal file
@@ -0,0 +1,59 @@
|
||||
#ifndef __MOD_DUB_H__
|
||||
#define __MOD_DUB_H__
|
||||
|
||||
#include <switch.h>
|
||||
#include <speex/speex_resampler.h>
|
||||
|
||||
#include <mpg123.h>
|
||||
|
||||
#include <unistd.h>
|
||||
|
||||
#define MY_BUG_NAME "_dub_"
|
||||
#define MAX_SESSION_ID (256)
|
||||
#define MAX_BUG_LEN (64)
|
||||
#define MAX_URL_LEN (1024)
|
||||
#define MAX_DUB_TRACKS (2)
|
||||
|
||||
/* per-channel data */
|
||||
typedef void (*responseHandler_t)(switch_core_session_t* session, const char* json, const char* bugname, const char* details);
|
||||
|
||||
typedef enum {
|
||||
DUB_TRACK_STATE_INACTIVE = 0,
|
||||
DUB_TRACK_STATE_READY,
|
||||
DUB_TRACK_STATE_ACTIVE,
|
||||
DUB_TRACK_STATE_PAUSED
|
||||
} dub_state_t;
|
||||
|
||||
typedef enum {
|
||||
DUB_GENERATOR_TYPE_UNKNOWN = 0,
|
||||
DUB_GENERATOR_TYPE_HTTP,
|
||||
DUB_GENERATOR_TYPE_FILE,
|
||||
DUB_GENERATOR_TYPE_TTS
|
||||
} dub_generator_t;
|
||||
|
||||
typedef enum {
|
||||
DUB_TRACK_EVENT_PLAY = 0,
|
||||
DUB_TRACK_EVENT_STOP,
|
||||
DUB_TRACK_EVENT_PAUSE,
|
||||
DUB_TRACK_EVENT_RESUME
|
||||
} dub_event_t;
|
||||
|
||||
|
||||
typedef struct dub_track {
|
||||
dub_state_t state;
|
||||
dub_generator_t generator;
|
||||
char* trackName;
|
||||
int sampleRate;
|
||||
int gain;
|
||||
void* circularBuffer;
|
||||
int generatorId;
|
||||
} dub_track_t;
|
||||
|
||||
struct cap_cb {
|
||||
switch_mutex_t *mutex;
|
||||
int gain;
|
||||
dub_track_t tracks[MAX_DUB_TRACKS];
|
||||
};
|
||||
|
||||
|
||||
#endif
|
||||
Reference in New Issue
Block a user