在基于freeswitch进行业务开发时,一般只需要播放本地音频文件即可,如果音频文件存储在云端,比如OSS上,亦可下载到本地进行播放。但是,如果云端的音频文件内容变更了,或者业务逻辑变更上传了新的音频文件,这些都需要业务端进行同步。如果有多台freeswitch服务器,则同步操作将会比较繁琐。那有没有简单的方法呢?显然是有的。
freeswitch提供了多样的接口,我们可以定制开发一个模块,实现简单的oss音频文件的下载播放和录音上传:
首先去阿里下载和安装oss sdk
按如下步骤下载安装即可。
https://help.aliyun.com/document_detail/32132.html?spm=a2c4g.11186623.6.1135.711c3470A1oKJT
开发freeswitch模块
在src/mod/application目录下新建一个模块 mod_ali_oss, 新建代码文件: mod_ali_oss.c,
引入头文件:
#include <switch.h>
#include <switch_curl.h>
#include <stdlib.h>
#include <aos_util.h>
#include <aos_string.h>
#include <aos_status.h>
#include <oss_auth.h>
#include <oss_api.h>
定义模块入口:
SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_ali_oss_shutdown);
SWITCH_MODULE_LOAD_FUNCTION(mod_ali_oss_load);
SWITCH_MODULE_DEFINITION(mod_ali_oss, mod_ali_oss_load, mod_ali_oss_shutdown, NULL);
模块加载时函数入口:
SWITCH_MODULE_LOAD_FUNCTION(mod_ali_oss_load)
{
switch_file_interface_t* file_interface;
*module_interface = switch_loadable_module_create_module_interface(pool, modname);
switch_core_new_memory_pool(&pool);
memset(&globals, 0, sizeof(oss_global_t));
globals.pool = pool;
// 存储已经缓存到本端的OSS文件.
switch_core_hash_init(&globals.map_cache);
switch_mutex_init(&globals.map_mutex, SWITCH_MUTEX_DEFAULT, globals.pool);
if (do_config(&globals) != SWITCH_STATUS_SUCCESS) {
switch_core_destroy_memory_pool(&globals.pool);
return SWITCH_STATUS_FALSE;
}
// 初始化oss sdk
if (aos_http_io_initialize(NULL, 0) != AOSE_OK) {
switch_core_destroy_memory_pool(&globals.pool);
return SWITCH_STATUS_FALSE;
}
// 创建文件操作的接口
file_interface = switch_loadable_module_create_interface(*module_interface, SWITCH_FILE_INTERFACE);
file_interface->interface_name = "oss"; // 文件操作的前缀
file_interface->extens = oss_supported_formats;
file_interface->file_open = oss_file_open;
file_interface->file_close = oss_file_close;
file_interface->file_read = oss_file_read;
file_interface->file_write = oss_file_write;
...
其中核心是 switch_loadable_module_create_interface 函数, 向freeswitch注册创建一个文件接口.
file_interface->interface_name = "oss"; 注册文件接口名称, 我们使用oss作为接口名称
在模块被加载到freeswitch中后, 我们可以这样播放oss文件:
<action application="playback" data="oss://xxxx.oss-cn-hangzhou.aliyuncs.com/myfile.wav" />
也可以这样录音到oss:
<action application="record_session" data="oss://xxx.oss-cn-hangzhou.aliyuncs.com/record/rec001.wav" />
其中xxx是桶(bucket)的名称, 至于访问oss需要的AK,SK则由从配置文件读入.
读取配置:
switch_status_t do_config(oss_global_t *cache)
{
char *cf = "ali_oss.conf";
switch_xml_t cfg, xml, param, settings;
switch_status_t status = SWITCH_STATUS_SUCCESS;
if (!(xml = switch_xml_open_cfg(cf, &cfg, NULL))) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "open of %s failed\n", cf);
return SWITCH_STATUS_TERM;
}
// 从文件获取配置.
settings = switch_xml_child(cfg, "settings");
if (settings)
{
for (param = switch_xml_child(settings, "param"); param; param = param->next) {
char *var = (char *) switch_xml_attr_soft(param, "name");
char *val = (char *) switch_xml_attr_soft(param, "value");
if (!strcasecmp(var, "oss-access-key-id")) {
cache->oss_access_key_id = switch_core_strdup(cache->pool, val);
}
else if (!strcasecmp(var, "oss-secret-access-key")) {
cache->oss_secret_access_key = switch_core_strdup(cache->pool, val);
}
else if (!strcasecmp(var, "oss-bucket"))
{
cache->oss_bucket = switch_core_strdup(cache->pool, val);
}
else if (!strcasecmp(var, "oss-endpoint"))
{
cache->oss_endpoint = switch_core_strdup(cache->pool, val);
}
}
}
switch_xml_free(xml);
// 如果缺少必要参数,则返回失败.
if (NULL == cache->oss_access_key_id || NULL == cache->oss_secret_access_key || NULL == cache->oss_bucket ||
NULL == cache->oss_endpoint) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "missing parameter\n");
return SWITCH_STATUS_TERM;
}
return status;
}
打开文件的操作:
switch_status_t oss_file_open(switch_file_handle_t *handle, const char *object)
{
if (switch_test_flag(handle, SWITCH_FILE_FLAG_WRITE))
{
return oss_file_open_write(handle, object);
}
else
{
return oss_file_open_read(handle, object);
}
}
其中 oss_file_open_read 打开文件读(放音):
switch_status_t oss_file_open_read(switch_file_handle_t *handle, const char *object)
{
oss_playback_context_t *context = NULL;
oss_cache_context_t *cc;
switch_status_t status = SWITCH_STATUS_SUCCESS;
switch_mutex_lock(globals.map_mutex);
cc = switch_core_hash_find(globals.map_cache, object);
if (NULL == cc)
{
cc = oss_add_cache(object);
}
if (cc)
{
cc->recent = time(NULL);
}
switch_mutex_unlock(globals.map_mutex);
if (NULL == cc) { return SWITCH_STATUS_FALSE; }
// 播放的通道.
context = switch_core_alloc(handle->memory_pool, sizeof(oss_playback_context_t));
if (OSS_FILE_READY == cc->status) {
// 直接打开返回.
context->fh.pre_buffer_datalen = handle->pre_buffer_datalen;
status = switch_core_file_open(&context->fh, cc->local_path, handle->channels, handle->samplerate,
SWITCH_FILE_FLAG_READ | SWITCH_FILE_DATA_SHORT, NULL);
if (SWITCH_STATUS_SUCCESS != status) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Failed to open cache file: %s, %s\n",
cc->local_path, object);
return status;
}
// fh 有效的情况下, cache设置位NULL.
context->cache = NULL;
handle->private_info = context;
handle->samples = context->fh.samples;
handle->format = context->fh.format;
handle->sections = context->fh.sections;
handle->seekable = context->fh.seekable;
handle->speed = context->fh.speed;
handle->interval = context->fh.interval;
handle->channels = context->fh.channels;
handle->flags |= SWITCH_FILE_NOMUX;
handle->pre_buffer_datalen = 0;
if (switch_test_flag((&context->fh), SWITCH_FILE_NATIVE)) {
switch_set_flag_locked(handle, SWITCH_FILE_NATIVE);
} else {
switch_clear_flag_locked(handle, SWITCH_FILE_NATIVE);
}
// switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "open cache file: %s, %s\n", cc->local_path,
// object);
return status;
}
// fh 无效的情况下, cache设置为 cc
context->cache = cc;
handle->private_info = context;
handle->samplerate = 8000;
handle->channels = 1;
handle->format = 0; // SF_FORMAT_RAW | SF_FORMAT_PCM_16;// config.format;
handle->seekable = 0;
handle->speed = 0;
handle->pre_buffer_datalen = 0;
// switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "waitting for download file: %s\n", object);
return status;
}
函数 oss_add_cache 函数实现将oss文件下载且缓存到本地:
oss_cache_context_t* oss_add_cache(const char* object)
{
// 缓存到本地内存的hash表中. 同时执行下载操作.
oss_cache_context_t *context;
switch_thread_data_t *td;
const char *ext;
char uuid[SWITCH_UUID_FORMATTED_LENGTH + 2];
static unsigned int seq = 0;
context = malloc(sizeof(oss_cache_context_t));
if (NULL == context) return NULL;
memset(context, 0, sizeof(oss_cache_context_t));
context->status = OSS_FILE_DOWNLOADING;
context->oss_object = strdup(object);
// 需要将object转换到本地文件, 使用uuid.
ext = strrchr(object, '.');
if (NULL == ext) ext = ".wav";
switch_uuid_str(uuid, sizeof(uuid));
context->local_path = switch_mprintf("%s/%02d/%s%s", globals.cache_location, (int)(seq++ % OSS_DIR_COUNT), uuid, ext);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "add cache, oss object: %s\n", object);
switch_core_hash_insert(globals.map_cache, context->oss_object, context);
// 准备下载任务.
td = malloc(sizeof(switch_thread_data_t));
if (td) {
memset(td, 0, sizeof(switch_thread_data_t));
td->func = oss_download_thread;
td->obj = context;
td->alloc = 1;
switch_thread_pool_launch_thread(&td);
}
return context;
}
实际的下载操作在线程中处理, 下载线程处理函数 oss_download_thread:
void* SWITCH_THREAD_FUNC oss_download_thread(switch_thread_t* thread, void* param)
{
oss_cache_context_t* cc = (oss_cache_context_t*)param;
aos_pool_t *p = NULL;
aos_string_t bucket;
aos_string_t object;
oss_request_options_t *options = NULL;
aos_table_t *headers = NULL;
aos_table_t *params = NULL;
aos_table_t *resp_headers = NULL;
aos_status_t *s = NULL;
aos_string_t file;
aos_pool_create(&p, NULL);
options = oss_request_options_create(p);
options->config = oss_config_create(options->pool);
aos_str_set(&options->config->endpoint, globals.oss_endpoint);
aos_str_set(&options->config->access_key_id, globals.oss_access_key_id);
aos_str_set(&options->config->access_key_secret, globals.oss_secret_access_key);
options->config->is_cname = 0;
options->ctl = aos_http_controller_create(options->pool, 0);
aos_str_set(&bucket, globals.oss_bucket);
aos_str_set(&object, cc->oss_object);
headers = aos_table_make(p, 0);
aos_str_set(&file, cc->local_path);
params = aos_table_make(p, 0);
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "downloading %s, cache file: %s!\n", cc->oss_object, cc->local_path);
s = oss_get_object_to_file(options, &bucket, &object, headers, params, &file, &resp_headers);
if (aos_status_is_ok(s)) {
cc->status = OSS_FILE_READY;
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "%s downloaded!\n", cc->oss_object);
}
else
{
cc->status = OSS_FILE_INVALID;
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING,
"failed to download, code:%d, error_code:%s, error_msg:%s, request_id:%s\n", s->code,
s->error_code, s->error_msg, s->req_id);
//switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "failed to download %s!\n", cc->oss_object);
}
aos_pool_destroy(p);
return NULL;
}
播放音频文件时读文件内容的入口函数:
switch_status_t oss_file_read(switch_file_handle_t *handle, void *data, size_t *len)
{
oss_playback_context_t*context = (oss_playback_context_t*)handle->private_info;
if (NULL == context->cache)
{
return switch_core_file_read(&context->fh, data, len);
}
if (OSS_FILE_READY == context->cache->status)
{
switch_status_t status;
// 缓存再 context->fh 中, 需要设置 预缓存长度.
context->fh.pre_buffer_datalen = handle->pre_buffer_datalen;
status = switch_core_file_open(&context->fh,
context->cache->local_path,
handle->channels,
handle->samplerate,
SWITCH_FILE_FLAG_READ | SWITCH_FILE_DATA_SHORT,
NULL);
if (SWITCH_STATUS_SUCCESS != status)
{
return status;
}
context->cache = NULL;
handle->samples = context->fh.samples;
handle->format = context->fh.format;
handle->sections = context->fh.sections;
handle->seekable = context->fh.seekable;
handle->speed = context->fh.speed;
handle->interval = context->fh.interval;
handle->channels = context->fh.channels;
handle->flags |= SWITCH_FILE_NOMUX;
// 本地句柄则不需要预缓存了. 因为根本没有数据...
handle->pre_buffer_datalen = 0;
if (switch_test_flag((&context->fh), SWITCH_FILE_NATIVE)) {
switch_set_flag_locked(handle, SWITCH_FILE_NATIVE);
}
else {
switch_clear_flag_locked(handle, SWITCH_FILE_NATIVE);
}
return switch_core_file_read(&context->fh, data, len);
}
if (OSS_FILE_INVALID == context->cache->status)
{
return SWITCH_STATUS_FALSE;
}
if (*len > 320)
{
memset(data, 0, 320 * sizeof(short));
*len = 320;
}
else
{
memset(data, 0, (*len) * sizeof(short));
}
handle->sample_count += *len;
return SWITCH_STATUS_SUCCESS;
}
录音时, 以写方式打开文件:
switch_status_t oss_file_open_write(switch_file_handle_t *handle, const char *object)
{
oss_record_context_t *context;
char uuid[SWITCH_UUID_FORMATTED_LENGTH + 2] = {0};
const char *ext;
context = switch_core_alloc(handle->memory_pool, sizeof(oss_record_context_t));
context->fh.channels = handle->channels;
context->fh.native_rate = handle->native_rate;
context->fh.samples = handle->samples;
context->fh.samplerate = handle->samplerate;
context->fh.prefix = handle->prefix;
context->fh.pre_buffer_datalen = handle->pre_buffer_datalen;
switch_uuid_str(uuid, sizeof(uuid));
// 上传到OSS服务器的Key.
context->oss_object = switch_core_strdup(handle->memory_pool, object);
// 本地缓存文件
ext = strrchr(object, '.');
if (NULL == ext) ext = ".wav";
context->local_path = switch_core_sprintf(handle->memory_pool, "%s/%s%s", globals.record_location, uuid, ext);
if (switch_core_file_open(&context->fh, context->local_path, handle->channels, handle->samplerate, handle->flags,
NULL) != SWITCH_STATUS_SUCCESS) {
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to open temp record file: %s\n", context->local_path);
return SWITCH_STATUS_GENERR;
}
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "temp record file: %s\n", context->local_path);
handle->private_info = context;
handle->pre_buffer_datalen = 0;
return SWITCH_STATUS_SUCCESS;
}
录音完成后的上传操作在线程中, 如果上传失败需要存储在sqlite数据库中, 等待片刻重新上传.
void *SWITCH_THREAD_FUNC oss_upload_thread(switch_thread_t *thread, void *param)
{
oss_upload_context_t *uc = (oss_upload_context_t *)param;
aos_pool_t *p = NULL;
aos_string_t bucket;
aos_string_t object;
aos_table_t *headers = NULL;
aos_table_t *resp_headers = NULL;
oss_request_options_t *options = NULL;
aos_status_t *s = NULL;
aos_string_t file;
aos_pool_create(&p, NULL);
options = oss_request_options_create(p);
options->config = oss_config_create(options->pool);
aos_str_set(&options->config->endpoint, globals.oss_endpoint);
aos_str_set(&options->config->access_key_id, globals.oss_access_key_id);
aos_str_set(&options->config->access_key_secret, globals.oss_secret_access_key);
options->config->is_cname = 0;
options->ctl = aos_http_controller_create(options->pool, 0);
headers = aos_table_make(options->pool, 1);
apr_table_set(headers, OSS_CONTENT_TYPE, "audio/wav");
aos_str_set(&bucket, globals.oss_bucket);
aos_str_set(&object, uc->oss_object);
aos_str_set(&file, uc->local_path);
s = oss_put_object_from_file(options, &bucket, &object, &file, headers, &resp_headers);
if (aos_status_is_ok(s)) {
// 上传成功.
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "upload success, object: %s\n", uc->oss_object);
// 删除本地文件.
switch_file_remove(uc->local_path, uc->pool);
if (uc->sqlite) {
// 需要从sqlite中删除记录.
switch_cache_db_handle_t *dbh;
switch_mutex_lock(globals.sql_mutex);
if (SWITCH_STATUS_SUCCESS == switch_cache_db_get_db_handle_dsn(&dbh, OSS_SQLITE_NAME)) {
char *sql = switch_mprintf("delete from files where oss_object = '%q';", uc->oss_object);
if (sql) {
switch_cache_db_execute_sql(dbh, sql, NULL);
switch_safe_free(sql);
}
switch_cache_db_release_db_handle(&dbh);
}
switch_mutex_unlock(globals.sql_mutex);
}
} else {
// 上传失败, 文件信息需要缓存到sqlite中.
switch_cache_db_handle_t *dbh;
// 缓存? 需要重新上传.
switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING,
"failed to upload, code:%d, error_code:%s, error_msg:%s, record file:%s, object:%s\n",
s->code, s->error_code, s->error_msg, uc->local_path, uc->oss_object);
// switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "failed to upload\n");
// 记录文件信息. 等候重新上传.
// 为什么使用sqlite, 而不是内存queue?
// 防止重启程序丢失未上传的录音数据.
if (!uc->sqlite) {
switch_mutex_lock(globals.sql_mutex);
if (SWITCH_STATUS_SUCCESS == switch_cache_db_get_db_handle_dsn(&dbh, OSS_SQLITE_NAME)) {
char *sql = switch_mprintf("insert into files (oss_object, path) values('%q','%q');", uc->oss_object,
uc->local_path);
if (sql) {
switch_cache_db_execute_sql(dbh, sql, NULL);
switch_safe_free(sql);
// 需要一个线程, 检查本SQLite中没有上传的数据.
globals.upload_sqlite = SWITCH_TRUE;
}
switch_cache_db_release_db_handle(&dbh);
}
switch_mutex_unlock(globals.sql_mutex);
} else {
// 失败了, 还要重新上传.
switch_mutex_lock(globals.sql_mutex);
globals.upload_sqlite = SWITCH_TRUE;
switch_mutex_unlock(globals.sql_mutex);
}
}
aos_pool_destroy(p);
return NULL;
}
如上基本上oss文件的放音和录音功能基本实现, 实际代码还有其它处理, 比如, 为了降低打开oss文件的延时, 打开操作实际交给线程池处理等.
暂时写这么多,以后再补充