4

FreeSWITCH添加自定义endpoint - Mike_Zhang

 1 year ago
source link: https://www.cnblogs.com/MikeZhang/p/fsAddEndpoint20230528.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
操作系统 :CentOS 7.6_x64     
FreeSWITCH版本 :1.10.9
日常开发过程中会遇到需要扩展FreeSWITCH对接其它系统的情况,这里记录下编写FreeSWITCH自定义endpoint的过程。

一、模块定义函数

使用FreeSWITCH自带的框架来定义模块函数,函数指针及参数列表定义如下(src/include/switch_types.h)
#define SWITCH_MODULE_LOAD_ARGS (switch_loadable_module_interface_t **module_interface, switch_memory_pool_t *pool)
#define SWITCH_MODULE_RUNTIME_ARGS (void)
#define SWITCH_MODULE_SHUTDOWN_ARGS (void)
typedef switch_status_t (*switch_module_load_t) SWITCH_MODULE_LOAD_ARGS;
typedef switch_status_t (*switch_module_runtime_t) SWITCH_MODULE_RUNTIME_ARGS;
typedef switch_status_t (*switch_module_shutdown_t) SWITCH_MODULE_SHUTDOWN_ARGS;
#define SWITCH_MODULE_LOAD_FUNCTION(name) switch_status_t name SWITCH_MODULE_LOAD_ARGS
#define SWITCH_MODULE_RUNTIME_FUNCTION(name) switch_status_t name SWITCH_MODULE_RUNTIME_ARGS
#define SWITCH_MODULE_SHUTDOWN_FUNCTION(name) switch_status_t name SWITCH_MODULE_SHUTDOWN_ARGS

1、模块加载

SWITCH_MODULE_LOAD_FUNCTION
模块加载函数,负责系统启动时或运行时加载模块,可以进行配置读取及资源初始化。

2、模块卸载

SWITCH_MODULE_SHUTDOWN_FUNCTION
模块卸载函数,负载模块卸载及相关资源回收。

3、模块运行时

SWITCH_MODULE_RUNTIME_FUNCTION
模块运行时函数,可以启动线程处理请求,监听socket等。

4、模块定义

SWITCH_MODULE_DEFINITION
相关代码:
typedef struct switch_loadable_module_function_table {
    int switch_api_version;
    switch_module_load_t load;
    switch_module_shutdown_t shutdown;
    switch_module_runtime_t runtime;
    switch_module_flag_t flags;
} switch_loadable_module_function_table_t;

#define SWITCH_MODULE_DEFINITION_EX(name, load, shutdown, runtime, flags)                   \
static const char modname[] =  #name ;                                                      \
SWITCH_MOD_DECLARE_DATA switch_loadable_module_function_table_t name##_module_interface = { \
    SWITCH_API_VERSION,                                                                     \
    load,                                                                                   \
    shutdown,                                                                               \
    runtime,                                                                                \
    flags                                                                                   \
}

#define SWITCH_MODULE_DEFINITION(name, load, shutdown, runtime)                             \
        SWITCH_MODULE_DEFINITION_EX(name, load, shutdown, runtime, SMODF_NONE) 

二、模块加载流程

FreeSWITCH使用 switch_loadable_module_load_module 或 switch_loadable_module_load_module_ex 进行模块加载,具体实现逻辑可以在 switch_loadable_module.c 中查看,这里做下简单介绍。 

1、模块加载函数

通过 switch_loadable_module_load_module 函数加载模块,函数调用链如下:
switch_loadable_module_load_module 
        => switch_loadable_module_load_module_ex  
        => switch_loadable_module_load_file
            => switch_loadable_module_process
            => switch_core_launch_thread  =>  switch_loadable_module_exec
通过 switch_dso_data_sym 根据定义的 XXX_module_interface 从动态库里面获取回调函数指针,使用 switch_loadable_module_function_table_t 数据结构进行回调函数绑定。
switch_dso_data_sym 函数实现如下(src/switch_dso.c):
void *switch_dso_data_sym(switch_dso_lib_t lib, const char *sym, char **err)
{
    void *addr = dlsym(lib, sym);
    if (!addr) {
        char *err_str = NULL;
        dlerror();

        if (!(addr = dlsym(lib, sym))) {
            err_str = (char *)dlerror();
        }

        if (err_str) {
            *err = strdup(err_str);
        }
    }
    return addr;
}
switch_loadable_module_exec函数:
static void *SWITCH_THREAD_FUNC switch_loadable_module_exec(switch_thread_t *thread, void *obj)
{


    switch_status_t status = SWITCH_STATUS_SUCCESS;
    switch_core_thread_session_t *ts = obj;
    switch_loadable_module_t *module = ts->objs[0];
    int restarts;

    switch_assert(thread != NULL);
    switch_assert(module != NULL);

    for (restarts = 0; status != SWITCH_STATUS_TERM && !module->shutting_down; restarts++) {
        status = module->switch_module_runtime();
    }
    switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_NOTICE, "Thread ended for %s\n", module->module_interface->module_name);

    if (ts->pool) {
        switch_memory_pool_t *pool = ts->pool;
        switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_DEBUG, "Destroying Pool for %s\n", module->module_interface->module_name);
        switch_core_destroy_memory_pool(&pool);
    }
    switch_thread_exit(thread, 0);
    return NULL;
}
switch_loadable_module_exec 函数为独立线程中运行,模块运行时通过 module->switch_module_runtime() 触发。

2、FreeSWITCH启动时加载模块

1)整体结构
300959-20230528125118744-670329275.png

 函数调用链如下:

main 
    => switch_core_init_and_modload 
        => switch_core_init
        => switch_loadable_module_init => switch_loadable_module_load_module
main函数在switch.c中实现。 
2)加载顺序
先加载系统核心模块:
switch_loadable_module_load_module_ex("", "CORE_SOFTTIMER_MODULE", SWITCH_FALSE, SWITCH_FALSE, &err, SWITCH_LOADABLE_MODULE_TYPE_COMMON, event_hash);
switch_loadable_module_load_module_ex("", "CORE_PCM_MODULE", SWITCH_FALSE, SWITCH_FALSE, &err, SWITCH_LOADABLE_MODULE_TYPE_COMMON, event_hash);
switch_loadable_module_load_module_ex("", "CORE_SPEEX_MODULE", SWITCH_FALSE, SWITCH_FALSE, &err, SWITCH_LOADABLE_MODULE_TYPE_COMMON, event_hash);
使用 switch_xml_open_cfg 函数(src/switch_xml.c中定义)先后加载以下文件中定义的模块:
pre_load_modules.conf
modules.conf
post_load_modules.conf
具体格式参考 conf/autoload_configs/modules.conf.xml
3)xml加载过程
 函数调用链如下:
main => switch_core_init_and_modload 
        => switch_core_init 
            => switch_xml_init 
                => switch_xml_open_root => XML_OPEN_ROOT_FUNCTION
其中 SWITCH_GLOBAL_filenames 变量定义如下(main => switch_core_set_globals):
if (!SWITCH_GLOBAL_filenames.conf_name && (SWITCH_GLOBAL_filenames.conf_name = (char *) malloc(BUFSIZE))) {
        switch_snprintf(SWITCH_GLOBAL_filenames.conf_name, BUFSIZE, "%s", "freeswitch.xml");
}
XML_OPEN_ROOT_FUNCTION实现如下(src/switch_xml.c):
static switch_xml_open_root_function_t XML_OPEN_ROOT_FUNCTION = (switch_xml_open_root_function_t)__switch_xml_open_root;

SWITCH_DECLARE_NONSTD(switch_xml_t) __switch_xml_open_root(uint8_t reload, const char **err, void *user_data)
{
    char path_buf[1024];
    uint8_t errcnt = 0;
    switch_xml_t new_main, r = NULL;

    if (MAIN_XML_ROOT) {
        if (!reload) {
            r = switch_xml_root();
            goto done;
        }
    }

    switch_snprintf(path_buf, sizeof(path_buf), "%s%s%s", SWITCH_GLOBAL_dirs.conf_dir, SWITCH_PATH_SEPARATOR, SWITCH_GLOBAL_filenames.conf_name);
    if ((new_main = switch_xml_parse_file(path_buf))) {
        *err = switch_xml_error(new_main);
        switch_copy_string(not_so_threadsafe_error_buffer, *err, sizeof(not_so_threadsafe_error_buffer));
        *err = not_so_threadsafe_error_buffer;
        if (!zstr(*err)) {
            switch_xml_free(new_main);
            new_main = NULL;
            errcnt++;
        } else {
            *err = "Success";
            switch_xml_set_root(new_main);

        }
    } else {
        *err = "Cannot Open log directory or XML Root!";
        errcnt++;
    }

    if (errcnt == 0) {
        r = switch_xml_root();
    }

 done:

    return r;
}
freeswitch.xml 为xml文件的总入口,配置的有加载各个模块的数据:
<section name="configuration" description="Various Configuration">
    <X-PRE-PROCESS cmd="include" data="autoload_configs/*.xml"/>
</section>

3、控制台动态加载

在fs_cli中可以使用load及reload加载模块,具体流程如下:
fs_cli => load ... => SWITCH_STANDARD_API(load_function) => switch_loadable_module_load_module 

fs_cli => reload ... => SWITCH_STANDARD_API(reload_function) => switch_loadable_module_unload_module 
                                                             => switch_loadable_module_load_module

三、关键数据结构

1、switch_loadable_module_t

作用:用于定义模块信息。
结构体定义:
struct switch_loadable_module {
    char *key;
    char *filename;
    int perm;
    switch_loadable_module_interface_t *module_interface;
    switch_dso_lib_t lib;
    switch_module_load_t switch_module_load;
    switch_module_runtime_t switch_module_runtime;
    switch_module_shutdown_t switch_module_shutdown;
    switch_memory_pool_t *pool;
    switch_status_t status;
    switch_thread_t *thread;
    switch_bool_t shutting_down;
    switch_loadable_module_type_t type;
};

typedef struct switch_loadable_module switch_loadable_module_t;
字段解释:
key =》 模块文件名称
filename => 模块文件路径(动态库路径)
perm =》 定义模块是否允许被卸载
module_interface =》 模块接口(由switch_module_load函数赋值)
lib =》 动态库句柄(dlopen函数返回)
switch_module_load =》 模块加载函数
switch_module_runtime =》 模块运行时函数
switch_module_shutdown =》 模块关闭(卸载)函数
pool =》 模块内存池
status =》 switch_module_shutdown 函数的返回值
shutting_down => 模块是否关闭

2、switch_loadable_module_interface

作用: 模块接口(入口)
结构体定义:
struct switch_loadable_module_interface {
    /*! the name of the module */
    const char *module_name;
    /*! the table of endpoints the module has implemented */
    switch_endpoint_interface_t *endpoint_interface;
    /*! the table of timers the module has implemented */
    switch_timer_interface_t *timer_interface;
    /*! the table of dialplans the module has implemented */
    switch_dialplan_interface_t *dialplan_interface;
    /*! the table of codecs the module has implemented */
    switch_codec_interface_t *codec_interface;
    /*! the table of applications the module has implemented */
    switch_application_interface_t *application_interface;
    /*! the table of chat applications the module has implemented */
    switch_chat_application_interface_t *chat_application_interface;
    /*! the table of api functions the module has implemented */
    switch_api_interface_t *api_interface;
    /*! the table of json api functions the module has implemented */
    switch_json_api_interface_t *json_api_interface;
    /*! the table of file formats the module has implemented */
    switch_file_interface_t *file_interface;
    /*! the table of speech interfaces the module has implemented */
    switch_speech_interface_t *speech_interface;
    /*! the table of directory interfaces the module has implemented */
    switch_directory_interface_t *directory_interface;
    /*! the table of chat interfaces the module has implemented */
    switch_chat_interface_t *chat_interface;
    /*! the table of say interfaces the module has implemented */
    switch_say_interface_t *say_interface;
    /*! the table of asr interfaces the module has implemented */
    switch_asr_interface_t *asr_interface;
    /*! the table of management interfaces the module has implemented */
    switch_management_interface_t *management_interface;
    /*! the table of limit interfaces the module has implemented */
    switch_limit_interface_t *limit_interface;
    /*! the table of database interfaces the module has implemented */
    switch_database_interface_t *database_interface;
    switch_thread_rwlock_t *rwlock;
    int refs;
    switch_memory_pool_t *pool;
};

typedef struct switch_loadable_module_interface switch_loadable_module_interface_t;
字段解释:
module_name => 模块的名称
endpoint_interface => 模块endpoint的具体实现
timer_interface => 模块timer的具体实现
dialplan_interface => 模块dialplan的具体实现
codec_interface => 模块编解码的具体实现
application_interface => 模块提供的app工具的具体实现
chat_application_interface => 模块提供的文本聊天app工具的具体实现
api_interface => 模块提供的api具体实现
json_api_interface => 模块提供的json格式api的具体实现
file_interface => 模块支持的文件格式的具体实现(比如mp4、mkv等文件格式)
speech_interface => 模块使用的speech接口实现
directory_interface => 模块使用的directory接口实现
chat_interface => 模块使用的chat接口实现
say_interface => 模块使用的say接口实现
asr_interface => 模块使用的asr接口实现
management_interface => 模块使用的管理接口实现
limit_interface => 模块使用的limit接口实现
database_interface => 模块使用的limit接口实现
rwlock => 模块使用的锁
refs => 模块锁的计数器
pool =》 模块内存池
使用 switch_loadable_module_create_module_interface 来创建 switch_loadable_module_interface_t 实例。
SWITCH_DECLARE(switch_loadable_module_interface_t *) switch_loadable_module_create_module_interface(switch_memory_pool_t *pool, const char *name)
{
    switch_loadable_module_interface_t *mod;

    mod = switch_core_alloc(pool, sizeof(switch_loadable_module_interface_t));
    switch_assert(mod != NULL);

    mod->pool = pool;

    mod->module_name = switch_core_strdup(mod->pool, name);
    switch_thread_rwlock_create(&mod->rwlock, mod->pool);
    return mod;
}
使用 switch_loadable_module_create_interface 来创建模块里面的子接口,示例如下:
*module_interface = switch_loadable_module_create_module_interface(pool, modname);

rtc_endpoint_interface = switch_loadable_module_create_interface(*module_interface, SWITCH_ENDPOINT_INTERFACE);
rtc_endpoint_interface->interface_name = "rtc";
rtc_endpoint_interface->io_routines = &rtc_io_routines;
rtc_endpoint_interface->state_handler = &rtc_event_handlers;
rtc_endpoint_interface->recover_callback = rtc_recover_callback;
具体实现如下:
SWITCH_DECLARE(void *) switch_loadable_module_create_interface(switch_loadable_module_interface_t *mod, switch_module_interface_name_t iname)
{

    switch (iname) {
    case SWITCH_ENDPOINT_INTERFACE:
        ALLOC_INTERFACE(endpoint)

    case SWITCH_TIMER_INTERFACE:
        ALLOC_INTERFACE(timer)

    case SWITCH_DIALPLAN_INTERFACE:
        ALLOC_INTERFACE(dialplan)

    case SWITCH_CODEC_INTERFACE:
        ALLOC_INTERFACE(codec)

    case SWITCH_APPLICATION_INTERFACE:
        ALLOC_INTERFACE(application)

    case SWITCH_CHAT_APPLICATION_INTERFACE:
        ALLOC_INTERFACE(chat_application)

    case SWITCH_API_INTERFACE:
        ALLOC_INTERFACE(api)

    case SWITCH_JSON_API_INTERFACE:
        ALLOC_INTERFACE(json_api)

    case SWITCH_FILE_INTERFACE:
        ALLOC_INTERFACE(file)

    case SWITCH_SPEECH_INTERFACE:
        ALLOC_INTERFACE(speech)

    case SWITCH_DIRECTORY_INTERFACE:
        ALLOC_INTERFACE(directory)

    case SWITCH_CHAT_INTERFACE:
        ALLOC_INTERFACE(chat)

    case SWITCH_SAY_INTERFACE:
        ALLOC_INTERFACE(say)

    case SWITCH_ASR_INTERFACE:
        ALLOC_INTERFACE(asr)

    case SWITCH_MANAGEMENT_INTERFACE:
        ALLOC_INTERFACE(management)

    case SWITCH_LIMIT_INTERFACE:
        ALLOC_INTERFACE(limit)

    case SWITCH_DATABASE_INTERFACE:
        ALLOC_INTERFACE(database)

    default:
        switch_log_printf(SWITCH_CHANNEL_LOG, SWITCH_LOG_WARNING, "Invalid Module Type!\n");
        return NULL;
    }
}

3、switch_endpoint_interface_t

作用:endpoint的入口
结构体定义:
struct switch_endpoint_interface {
    /*! the interface's name */
    const char *interface_name;

    /*! channel abstraction methods */
    switch_io_routines_t *io_routines;

    /*! state machine methods */
    switch_state_handler_table_t *state_handler;

    /*! private information */
    void *private_info;

    switch_thread_rwlock_t *rwlock;
    int refs;
    switch_mutex_t *reflock;

    /* parent */
    switch_loadable_module_interface_t *parent;

    /* to facilitate linking */
    struct switch_endpoint_interface *next;

    switch_core_recover_callback_t recover_callback;

};

typedef struct switch_endpoint_interface switch_endpoint_interface_t;
字段解释:
interface_name => endpoint名称,比如:"rtc"
io_routines => endpoint对应的io操作回调函数
state_handler => endpoint对应的事件处理回调函数
private_info => endpoint私有参数配置(比如编码格式、采样率等)
rwlock => endpoint锁
refs => endpoint锁的引用次数
reflock => endpoint引用锁
parent => endpoint所属模块
next => next指针
recover_callback => endpoint对应的recover回调函数

4、switch_io_routines

作用:存储io操作的回调函数
结构体定义:
struct switch_io_routines {
    /*! creates an outgoing session from given session, caller profile */
    switch_io_outgoing_channel_t outgoing_channel;
    /*! read a frame from a session */
    switch_io_read_frame_t read_frame;
    /*! write a frame to a session */
    switch_io_write_frame_t write_frame;
    /*! send a kill signal to the session's channel */
    switch_io_kill_channel_t kill_channel;
    /*! send a string of DTMF digits to a session's channel */
    switch_io_send_dtmf_t send_dtmf;
    /*! receive a message from another session */
    switch_io_receive_message_t receive_message;
    /*! queue a message for another session */
    switch_io_receive_event_t receive_event;
    /*! change a sessions channel state */
    switch_io_state_change_t state_change;
    /*! read a video frame from a session */
    switch_io_read_video_frame_t read_video_frame;
    /*! write a video frame to a session */
    switch_io_write_video_frame_t write_video_frame;
    /*! read a video frame from a session */
    switch_io_read_text_frame_t read_text_frame;
    /*! write a video frame to a session */
    switch_io_write_text_frame_t write_text_frame;
    /*! change a sessions channel run state */
    switch_io_state_run_t state_run;
    /*! get sessions jitterbuffer */
    switch_io_get_jb_t get_jb;
    void *padding[10];
};

typedef struct switch_io_routines switch_io_routines_t;
字段解释:
outgoing_channel => 创建外呼channel的回调函数
read_frame => 读session音频数据的回调函数
write_frame => 写session音频数据的回调函数
kill_channel => kill信号处理函数,用于处理channel接收的kill信号
send_dtmf => send dtmf操作的回调函数,用于处理channel接收的DTMF字符串
receive_message => 处理channel消息的回调函数,用于处理其它channel发来的消息
receive_event => 发送channel消息的回调函数,用于向目标session发送自定义事件(比如rtc session、rtmp session等)
state_change => channel状态修改的回调函数
read_video_frame => 读session视频数据的回调函数
write_video_frame => 写session视频数据的回调函数
read_text_frame => 读session文本数据的回调函数
write_text_frame => 写session文本数据的回调函数
state_run => 改变session的运行状态,目前没见到有endpoint使用过
get_jb => 获取session的jitter_buffer

5、switch_state_handler_table_t

作用:用于存储状态机的回调函数。
定义如下:
struct switch_state_handler_table {
    /*! executed when the state changes to init */
    switch_state_handler_t on_init;
    /*! executed when the state changes to routing */
    switch_state_handler_t on_routing;
    /*! executed when the state changes to execute */
    switch_state_handler_t on_execute;
    /*! executed when the state changes to hangup */
    switch_state_handler_t on_hangup;
    /*! executed when the state changes to exchange_media */
    switch_state_handler_t on_exchange_media;
    /*! executed when the state changes to soft_execute */
    switch_state_handler_t on_soft_execute;
    /*! executed when the state changes to consume_media */
    switch_state_handler_t on_consume_media;
    /*! executed when the state changes to hibernate */
    switch_state_handler_t on_hibernate;
    /*! executed when the state changes to reset */
    switch_state_handler_t on_reset;
    /*! executed when the state changes to park */
    switch_state_handler_t on_park;
    /*! executed when the state changes to reporting */
    switch_state_handler_t on_reporting;
    /*! executed when the state changes to destroy */
    switch_state_handler_t on_destroy;
    int flags;
    void *padding[10];
};


typedef struct switch_state_handler_table switch_state_handler_table_t;
参数解释:
on_init => channel进入 CS_INIT 状态的回调函数
on_routing => channel进入 CS_ROUTING 状态的回调函数
on_execute => channel进入 CS_EXECUTE 状态的回调函数,用于执行操作
on_hangup => channel进入 CS_HANGUP 状态的回调函数
on_exchange_media => channel进入 CS_EXCHANGE_MEDIA 状态的回调函数
on_soft_execute => channel进入 CS_SOFT_EXECUTE 状态的回调函数,用于从其它channel接收或发送数据
on_consume_media => channel进入 CS_CONSUME_MEDIA 状态的回调函数,
on_hibernate => channel进入 CS_HIBERNATE 状态的回调函数,sleep操作
on_reset => channel进入 CS_RESET 状态的回调函数
on_park => channel进入 CS_PARK 状态的回调函数
on_reporting => channel进入 CS_REPORTING 状态的回调函数
on_destroy => channel进入 CS_DESTROY 状态的回调函数
switch_core_state_machine.c中使用 STATE_MACRO 触发,部分触发代码如下:
case CS_ROUTING:    /* Look for a dialplan and find something to do */
    STATE_MACRO(routing, "ROUTING");
    break;
case CS_RESET:        /* Reset */
    STATE_MACRO(reset, "RESET");
    break;
    /* These other states are intended for prolonged durations so we do not signal lock for them */
case CS_EXECUTE:    /* Execute an Operation */
    STATE_MACRO(execute, "EXECUTE");
    break;
case CS_EXCHANGE_MEDIA:    /* loop all data back to source */
    STATE_MACRO(exchange_media, "EXCHANGE_MEDIA");
    break;
case CS_SOFT_EXECUTE:    /* send/recieve data to/from another channel */
    STATE_MACRO(soft_execute, "SOFT_EXECUTE");
    break;
case CS_PARK:        /* wait in limbo */
    STATE_MACRO(park, "PARK");
    break;
case CS_CONSUME_MEDIA:    /* wait in limbo */
    STATE_MACRO(consume_media, "CONSUME_MEDIA");
    break;
case CS_HIBERNATE:    /* sleep */
    STATE_MACRO(hibernate, "HIBERNATE");
    break;

四、模块编写示例 

1、编写c风格的endpoint模块

仿照mod_rtc模块编写,核心文件只有两个:
mod_rtc.c
Makefile.am
1)复制mod_artc目录
cp mod_rtc mod_ctest -r
300959-20230528125818348-515469271.png

 2)修改文件名

mv mod_rtc.c mod_ctest.c
3)修改文件内容,将rtc关键字替换成ctest
300959-20230528125900126-1094366186.png
300959-20230528125925990-780625654.png

 4)修改编译选项

文件: freeswitch-1.10.9.-release/configure.ac
仿照rtc模块,添加ctest模块内容:
src/mod/endpoints/mod_ctest/Makefile
300959-20230528125951481-677936287.png
5)开启模块编译
文件:freeswitch-1.10.9.-release/modules.conf
仿照rtc模块,添加ctest模块编译:
endpoints/mod_ctest
300959-20230528130014074-1184738580.png
6)生成Makefile
./rebootstrap.sh && ./configure
300959-20230528130037657-2096098443.png

 7)安装模块

在 freeswitch-1.10.9.-release 根目录(或mod_ctest目录)执行如下指令:
make && make install
300959-20230528130118311-1248577118.png

 8)加载模块

文件:conf/autoload_configs/modules.conf.xml
添加如下内容:
9)模块测试
控制台加载测试:
reload mod_ctest
300959-20230528130148960-1011592747.png

c风格endpoint模块编译及运行效果视频:

关注微信公众号(聊聊博文,文末可扫码)后回复 2023052801 获取。 

2、编写c++风格的endpoint模块

仿照mod_h323模块编写,目录结构、编译等参考c风格endpoint模块编写部分。
加载效果如下:
300959-20230528130643938-1614238630.png

c++风格endpoint模块编译及运行效果视频:

关注微信公众号(聊聊博文,文末可扫码)后回复 2023052802 获取。 

五、资源下载

本文涉及源码和文件,可以从如下途径获取:

关注微信公众号(聊聊博文,文末可扫码)后回复 20230528 获取。

300959-20230528130819425-511004018.png
 

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK