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

在基于freeswitch进行业务开发时,一般只需要播放本地音频文件即可,如果音频文件存储在云端,比如OSS上,亦可下载到本地进行播放。但是,如果云端的音频文件内容变更了,或者业务逻辑变更上传了新的音频文件,这些都需要业务端进行同步。如果有多台freeswitch服务器,则同步操作将会比较繁琐。那有没有简单的方法呢?显然是有的。

freeswitch提供了多样的接口,我们可以定制开发一个模块,实现简单的oss音频文件的下载和播放,而且可以实现便下载边播放。具体操作如下:

 

1.去阿里下载和安装oss sdk

 

按如下步骤下载安装即可。

 

https://help.aliyun.com/document_detail/32132.html?spm=a2c4g.11186623.6.1135.711c3470A1oKJT


2.新建模块

在src/mod/application目录下新建一个模块 mod_ali_oss, 新建代码文件: mod_ali_oss.c,

 引入头文件:

1 #include <switch.h>
2 #include <switch_curl.h>
3 #include <stdlib.h>
4 
5 #include <aos_util.h>
6 #include <aos_string.h>
7 #include <aos_status.h>
8 #include <oss_auth.h>
9 #include <oss_api.h>

定义模块入口:

1 SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_ali_oss_shutdown);
2 SWITCH_MODULE_LOAD_FUNCTION(mod_ali_oss_load);
3 SWITCH_MODULE_DEFINITION(mod_ali_oss, mod_ali_oss_load, mod_ali_oss_shutdown, NULL);

模块加载时函数入口:

 1 SWITCH_MODULE_LOAD_FUNCTION(mod_ali_oss_load)
 2 {
 3     switch_file_interface_t* file_interface;
 4     *module_interface = switch_loadable_module_create_module_interface(pool, modname);
 5 
 6     switch_core_new_memory_pool(&pool);
 7 
 8     memset(&globals, 0, sizeof(oss_global_t));
 9     globals.pool = pool;
10         // 存储已经缓存到本端的OSS文件.
11     switch_core_hash_init(&globals.map_cache);
12     switch_mutex_init(&globals.map_mutex, SWITCH_MUTEX_DEFAULT, globals.pool);
13 
14     if (do_config(&globals) != SWITCH_STATUS_SUCCESS) {
15         switch_core_destroy_memory_pool(&globals.pool);
16         return SWITCH_STATUS_FALSE;
17     }
18 
19         // 初始化oss sdk
20     if (aos_http_io_initialize(NULL, 0) != AOSE_OK) {
21         switch_core_destroy_memory_pool(&globals.pool);
22         return SWITCH_STATUS_FALSE;
23     }    
24 
25         // 创建文件操作的接口
26     file_interface = switch_loadable_module_create_interface(*module_interface, SWITCH_FILE_INTERFACE);
27     file_interface->interface_name = "oss";  // 文件操作的前缀
28     file_interface->extens = oss_supported_formats;
29     file_interface->file_open = oss_file_open;
30     file_interface->file_close = oss_file_close;
31     file_interface->file_read = oss_file_read;
32     file_interface->file_write = oss_file_write;
33 ...

其中核心是  switch_loadable_module_create_interface 函数, 向freeswitch注册创建一个文件接口. 

file_interface->interface_name = "oss"; 注册文件接口名称, 我们使用oss作为接口名称

在模块被加载到freeswitch中后, 我们可以这样播放oss文件: 

1 <action application="playback" data="oss://xxxx.oss-cn-hangzhou.aliyuncs.com/myfile.wav" />

也可以这样录音到oss:

1 <action application="record_session" data="oss://xxx.oss-cn-hangzhou.aliyuncs.com/record/rec001.wav" />

其中xxx是桶(bucket)的名称, 至于访问oss需要的AK,SK则由从配置文件读入.

 

读取配置:

 1 switch_status_t do_config(oss_global_t *cache)
 2 {
 3     char *cf = "ali_oss.conf";
 4     switch_xml_t cfg, xml, param, settings;
 5     switch_status_t status = SWITCH_STATUS_SUCCESS;
 6 
 7     if (!(xml = switch_xml_open_cfg(cf, &cfg, NULL))) {
 8         switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "open of %s failed\n", cf);
 9         return SWITCH_STATUS_TERM;
10     }
11 
12     // 从文件获取配置.
13     settings = switch_xml_child(cfg, "settings");
14     if (settings) 
15     {
16         for (param = switch_xml_child(settings, "param"); param; param = param->next) {
17             char *var = (char *) switch_xml_attr_soft(param, "name");
18             char *val = (char *) switch_xml_attr_soft(param, "value");
19             
20             if (!strcasecmp(var, "oss-access-key-id")) {
21                 cache->oss_access_key_id = switch_core_strdup(cache->pool, val);
22             }
23             else if (!strcasecmp(var, "oss-secret-access-key")) {
24                 cache->oss_secret_access_key = switch_core_strdup(cache->pool, val);
25             }
26             else if (!strcasecmp(var, "oss-bucket"))
27             {
28                 cache->oss_bucket = switch_core_strdup(cache->pool, val);
29             }
30             else if (!strcasecmp(var, "oss-endpoint"))
31             {
32                 cache->oss_endpoint = switch_core_strdup(cache->pool, val);
33             }
34         }
35     }
36 
37     switch_xml_free(xml);
38 
39     // 如果缺少必要参数,则返回失败.
40     if (NULL == cache->oss_access_key_id || NULL == cache->oss_secret_access_key || NULL == cache->oss_bucket ||
41         NULL == cache->oss_endpoint) {
42         switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "missing parameter\n");
43         return SWITCH_STATUS_TERM;
44     }
45 
46     return status;
47 }

 

打开文件的操作:

 1 switch_status_t oss_file_open(switch_file_handle_t *handle, const char *object)
 2 {
 3     if (switch_test_flag(handle, SWITCH_FILE_FLAG_WRITE)) 
 4     {
 5         return oss_file_open_write(handle, object);
 6     }
 7     else
 8     {
 9         return oss_file_open_read(handle, object);
10     }
11 }

打开文件读(放音):

 1 switch_status_t oss_file_open_read(switch_file_handle_t *handle, const char *object) 
 2 {
 3     oss_playback_context_t *context = NULL;
 4     oss_cache_context_t *cc;
 5     switch_status_t status = SWITCH_STATUS_SUCCESS;
 6 
 7     switch_mutex_lock(globals.map_mutex);
 8     cc = switch_core_hash_find(globals.map_cache, object);
 9     if (NULL == cc) 
10     { 
11         cc = oss_add_cache(object); 
12     }
13 
14     if (cc) 
15     {
16         cc->recent = time(NULL);
17     }
18     switch_mutex_unlock(globals.map_mutex);
19 
20     if (NULL == cc) { return SWITCH_STATUS_FALSE; }
21 
22     // 播放的通道.
23     context = switch_core_alloc(handle->memory_pool, sizeof(oss_playback_context_t));
24 
25     if (OSS_FILE_READY == cc->status) {
26 
27         // 直接打开返回.
28         context->fh.pre_buffer_datalen = handle->pre_buffer_datalen;
29         status = switch_core_file_open(&context->fh, cc->local_path, handle->channels, handle->samplerate,
30                                        SWITCH_FILE_FLAG_READ | SWITCH_FILE_DATA_SHORT, NULL);
31 
32         if (SWITCH_STATUS_SUCCESS != status) {
33             switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "Failed to open cache file: %s, %s\n",
34                               cc->local_path, object);
35             return status;
36         }
37 
38         // fh 有效的情况下, cache设置位NULL.
39         context->cache = NULL;
40         handle->private_info = context;
41 
42         handle->samples = context->fh.samples;
43         handle->format = context->fh.format;
44         handle->sections = context->fh.sections;
45         handle->seekable = context->fh.seekable;
46         handle->speed = context->fh.speed;
47         handle->interval = context->fh.interval;
48         handle->channels = context->fh.channels;
49         handle->flags |= SWITCH_FILE_NOMUX;
50         handle->pre_buffer_datalen = 0;
51 
52         if (switch_test_flag((&context->fh), SWITCH_FILE_NATIVE)) {
53             switch_set_flag_locked(handle, SWITCH_FILE_NATIVE);
54         } else {
55             switch_clear_flag_locked(handle, SWITCH_FILE_NATIVE);
56         }
57 
58 //        switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "open cache file: %s, %s\n", cc->local_path,
59 //                          object);
60 
61         return status;
62     }
63 
64     // fh 无效的情况下, cache设置为 cc
65     context->cache = cc;
66     handle->private_info = context;
67     handle->samplerate = 8000;
68     handle->channels = 1;
69     handle->format = 0; // SF_FORMAT_RAW | SF_FORMAT_PCM_16;// config.format;
70     handle->seekable = 0;
71     handle->speed = 0;
72     handle->pre_buffer_datalen = 0;
73 
74 //    switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "waitting for download file: %s\n", object);
75 
76     return status;
77 }

 

其中 oss_add_cache 函数实现将oss文件下载且缓存到本地:

 1 oss_cache_context_t* oss_add_cache(const char* object)
 2 {
 3     // 缓存到本地内存的hash表中. 同时执行下载操作.
 4 
 5     oss_cache_context_t *context;
 6     switch_thread_data_t *td;
 7     const char *ext;
 8     char uuid[SWITCH_UUID_FORMATTED_LENGTH + 2];
 9 
10     static unsigned int seq = 0;
11 
12     context = malloc(sizeof(oss_cache_context_t));
13     if (NULL == context) return NULL;
14     memset(context, 0, sizeof(oss_cache_context_t));
15 
16     context->status = OSS_FILE_DOWNLOADING;
17     context->oss_object = strdup(object);
18     // 需要将object转换到本地文件, 使用uuid.
19     ext = strrchr(object, '.');
20     if (NULL == ext) ext = ".wav";
21     switch_uuid_str(uuid, sizeof(uuid));
22     context->local_path = switch_mprintf("%s/%02d/%s%s", globals.cache_location, (int)(seq++ % OSS_DIR_COUNT), uuid, ext);
23 
24     switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "add cache, oss object: %s\n", object);
25 
26     switch_core_hash_insert(globals.map_cache, context->oss_object, context);
27 
28     // 准备下载任务.
29     td = malloc(sizeof(switch_thread_data_t));
30     if (td) {
31         memset(td, 0, sizeof(switch_thread_data_t));
32         td->func = oss_download_thread;
33         td->obj = context;
34         td->alloc = 1;
35         switch_thread_pool_launch_thread(&td);
36     }
37 
38     return context;
39 }

 

实际的下载操作在线程中处理, 下载线程处理函数 oss_download_thread:

 1 void* SWITCH_THREAD_FUNC oss_download_thread(switch_thread_t* thread, void* param)
 2 {
 3     oss_cache_context_t* cc = (oss_cache_context_t*)param;
 4 
 5     aos_pool_t *p = NULL;
 6     aos_string_t bucket;
 7     aos_string_t object;
 8     oss_request_options_t *options = NULL;
 9     aos_table_t *headers = NULL;
10     aos_table_t *params = NULL;
11     aos_table_t *resp_headers = NULL;
12     aos_status_t *s = NULL;
13     aos_string_t file;
14 
15     aos_pool_create(&p, NULL);
16     options = oss_request_options_create(p);
17 
18     options->config = oss_config_create(options->pool);
19 
20     aos_str_set(&options->config->endpoint, globals.oss_endpoint);
21     aos_str_set(&options->config->access_key_id, globals.oss_access_key_id);
22     aos_str_set(&options->config->access_key_secret, globals.oss_secret_access_key);
23     options->config->is_cname = 0;
24     options->ctl = aos_http_controller_create(options->pool, 0);
25     aos_str_set(&bucket, globals.oss_bucket);
26     aos_str_set(&object, cc->oss_object);
27     headers = aos_table_make(p, 0);
28     aos_str_set(&file, cc->local_path);
29     params = aos_table_make(p, 0);
30     
31     switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "downloading %s, cache file: %s!\n", cc->oss_object, cc->local_path);
32 
33     s = oss_get_object_to_file(options, &bucket, &object, headers, params, &file, &resp_headers);
34     if (aos_status_is_ok(s)) {
35         cc->status = OSS_FILE_READY;
36         switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "%s downloaded!\n", cc->oss_object);
37     } 
38     else 
39     {
40         cc->status = OSS_FILE_INVALID;
41         switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING,
42                           "failed to download, code:%d, error_code:%s, error_msg:%s, request_id:%s\n", s->code,
43                           s->error_code, s->error_msg, s->req_id);
44         //switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "failed to download %s!\n", cc->oss_object);
45     }
46 
47     aos_pool_destroy(p);
48 
49     return NULL;
50 }

 

 

读文件内容:

 1 switch_status_t oss_file_read(switch_file_handle_t *handle, void *data, size_t *len)
 2 {
 3     oss_playback_context_t*context = (oss_playback_context_t*)handle->private_info;
 4 
 5     if (NULL == context->cache)
 6     {
 7         return switch_core_file_read(&context->fh, data, len);
 8     }
 9 
10     if (OSS_FILE_READY == context->cache->status)
11     {
12         switch_status_t status;
13         // 缓存再 context->fh 中, 需要设置 预缓存长度.
14         context->fh.pre_buffer_datalen = handle->pre_buffer_datalen;
15         status = switch_core_file_open(&context->fh,
16             context->cache->local_path,
17             handle->channels,
18             handle->samplerate,
19             SWITCH_FILE_FLAG_READ | SWITCH_FILE_DATA_SHORT,
20             NULL);
21         if (SWITCH_STATUS_SUCCESS != status)
22         {
23             return status;
24         }
25 
26         context->cache = NULL;
27         handle->samples = context->fh.samples;
28         handle->format = context->fh.format;
29         handle->sections = context->fh.sections;
30         handle->seekable = context->fh.seekable;
31         handle->speed = context->fh.speed;
32         handle->interval = context->fh.interval;
33         handle->channels = context->fh.channels;
34         handle->flags |= SWITCH_FILE_NOMUX;
35         // 本地句柄则不需要预缓存了. 因为根本没有数据...
36         handle->pre_buffer_datalen = 0;
37 
38         if (switch_test_flag((&context->fh), SWITCH_FILE_NATIVE)) {
39             switch_set_flag_locked(handle, SWITCH_FILE_NATIVE);
40         }
41         else {
42             switch_clear_flag_locked(handle, SWITCH_FILE_NATIVE);
43         }
44 
45         return switch_core_file_read(&context->fh, data, len);
46     }
47 
48     if (OSS_FILE_INVALID == context->cache->status)
49     {
50         return SWITCH_STATUS_FALSE;
51     }
52 
53     if (*len > 320)
54     {
55         memset(data, 0, 320 * sizeof(short));
56         *len = 320;
57     }
58     else
59     {
60         memset(data, 0, (*len) * sizeof(short));
61     }
62 
63     handle->sample_count += *len;
64     return SWITCH_STATUS_SUCCESS;
65 }

 

 

打开文件写(录音):

 1 switch_status_t oss_file_open_write(switch_file_handle_t *handle, const char *object)
 2 {
 3     oss_record_context_t *context;
 4     char uuid[SWITCH_UUID_FORMATTED_LENGTH + 2] = {0};
 5     const char *ext;
 6 
 7     context = switch_core_alloc(handle->memory_pool, sizeof(oss_record_context_t));
 8 
 9     context->fh.channels = handle->channels;
10     context->fh.native_rate = handle->native_rate;
11     context->fh.samples = handle->samples;
12     context->fh.samplerate = handle->samplerate;
13     context->fh.prefix = handle->prefix;
14     context->fh.pre_buffer_datalen = handle->pre_buffer_datalen;
15 
16     switch_uuid_str(uuid, sizeof(uuid));
17     // 上传到OSS服务器的Key.
18     context->oss_object = switch_core_strdup(handle->memory_pool, object);
19     // 本地缓存文件
20     ext = strrchr(object, '.');
21     if (NULL == ext) ext = ".wav";
22     context->local_path = switch_core_sprintf(handle->memory_pool, "%s/%s%s", globals.record_location, uuid, ext);
23 
24     if (switch_core_file_open(&context->fh, context->local_path, handle->channels, handle->samplerate, handle->flags,
25                               NULL) != SWITCH_STATUS_SUCCESS) {
26 
27         switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_ERROR, "failed to open temp record file: %s\n", context->local_path);
28         return SWITCH_STATUS_GENERR;
29     }
30 
31     switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_INFO, "temp record file: %s\n", context->local_path);
32     handle->private_info = context;
33     handle->pre_buffer_datalen = 0;
34 
35     return SWITCH_STATUS_SUCCESS;
36 }

 

录音完成后的上传操作在线程中, 如果上传失败需要存储在sqlite数据库中, 等待片刻重新上传.

 1 void *SWITCH_THREAD_FUNC oss_upload_thread(switch_thread_t *thread, void *param)
 2 {
 3     oss_upload_context_t *uc = (oss_upload_context_t *)param;
 4 
 5     aos_pool_t *p = NULL;
 6     aos_string_t bucket;
 7     aos_string_t object;
 8     aos_table_t *headers = NULL;
 9     aos_table_t *resp_headers = NULL;
10     oss_request_options_t *options = NULL;
11     aos_status_t *s = NULL;
12     aos_string_t file;
13 
14     aos_pool_create(&p, NULL);
15     options = oss_request_options_create(p);
16 
17     options->config = oss_config_create(options->pool);
18 
19     aos_str_set(&options->config->endpoint, globals.oss_endpoint);
20     aos_str_set(&options->config->access_key_id, globals.oss_access_key_id);
21     aos_str_set(&options->config->access_key_secret, globals.oss_secret_access_key);
22     options->config->is_cname = 0;
23     options->ctl = aos_http_controller_create(options->pool, 0);
24 
25     headers = aos_table_make(options->pool, 1);
26     apr_table_set(headers, OSS_CONTENT_TYPE, "audio/wav");
27     aos_str_set(&bucket, globals.oss_bucket);
28     aos_str_set(&object, uc->oss_object);
29     aos_str_set(&file, uc->local_path);
30 
31     s = oss_put_object_from_file(options, &bucket, &object, &file, headers, &resp_headers);
32     if (aos_status_is_ok(s)) {
33         // 上传成功.
34         switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "upload success, object: %s\n", uc->oss_object);
35         // 删除本地文件.
36         switch_file_remove(uc->local_path, uc->pool);
37 
38         if (uc->sqlite) {
39             // 需要从sqlite中删除记录.
40             switch_cache_db_handle_t *dbh;
41             switch_mutex_lock(globals.sql_mutex);
42             if (SWITCH_STATUS_SUCCESS == switch_cache_db_get_db_handle_dsn(&dbh, OSS_SQLITE_NAME)) {
43                 char *sql = switch_mprintf("delete from files where oss_object = '%q';", uc->oss_object);
44                 if (sql) {
45                     switch_cache_db_execute_sql(dbh, sql, NULL);
46                     switch_safe_free(sql);
47                 }
48                 switch_cache_db_release_db_handle(&dbh);
49             }
50             switch_mutex_unlock(globals.sql_mutex);
51         }
52     } else {
53         // 上传失败, 文件信息需要缓存到sqlite中.
54 
55         switch_cache_db_handle_t *dbh;
56 
57         // 缓存? 需要重新上传.
58         switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING,
59                           "failed to upload, code:%d, error_code:%s, error_msg:%s, record file:%s, object:%s\n",
60                           s->code, s->error_code, s->error_msg, uc->local_path, uc->oss_object);
61         //    switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "failed to upload\n");
62 
63         // 记录文件信息. 等候重新上传.
64         // 为什么使用sqlite, 而不是内存queue?
65         // 防止重启程序丢失未上传的录音数据.
66         if (!uc->sqlite) {
67             switch_mutex_lock(globals.sql_mutex);
68             if (SWITCH_STATUS_SUCCESS == switch_cache_db_get_db_handle_dsn(&dbh, OSS_SQLITE_NAME)) {
69                 char *sql = switch_mprintf("insert into files (oss_object, path) values('%q','%q');", uc->oss_object,
70                                            uc->local_path);
71                 if (sql) {
72                     switch_cache_db_execute_sql(dbh, sql, NULL);
73                     switch_safe_free(sql);
74 
75                     // 需要一个线程, 检查本SQLite中没有上传的数据.
76                     globals.upload_sqlite = SWITCH_TRUE;
77                 }
78                 switch_cache_db_release_db_handle(&dbh);
79             }
80             switch_mutex_unlock(globals.sql_mutex);
81         } else {
82             // 失败了, 还要重新上传.
83             switch_mutex_lock(globals.sql_mutex);
84             globals.upload_sqlite = SWITCH_TRUE;
85             switch_mutex_unlock(globals.sql_mutex);
86         }
87     }
88 
89     aos_pool_destroy(p);
90 
91     return NULL;
92 }

 

 

如上基本上oss文件的放音和录音功能基本实现, 实际代码还有其它处理, 比如, 为了降低打开oss文件的延时, 打开操作实际交给线程池处理等.

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

 

posted @ 2020-07-23 14:51  codeframer0  阅读(641)  评论(0)    收藏  举报