FreeSWITCH中如何播放阿里OSS上的文件,或录音到OSS上?

在基于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文件的延时, 打开操作实际交给线程池处理等.

暂时写这么多,以后再补充

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值