linux系统移植pjsua库实现sip通话功能

一、概述
本文实现pjsua开源库的交叉编译以及通过调用pjsua库api实现sip语言双向通话功能,包括注册上线sip服务器、接收处理指令(电话邀请、挂断等)、双向音频对讲功能。
二、交叉编译pjsua库
1.解压
tar -zxvf 2.15.1.tar.gz; cd pjproject-2.15.1;
2.配置编译选项
./configure \ --host=arm-linux-gnueabihf \ --prefix=$(pwd)/_mini \ --enable-shared \ --disable-video \ --disable-pjsua2 \ --disable-sound \ --disable-ssl \ --disable-ffmpeg \ --disable-v4l2 \ CFLAGS="-Os -DNDEBUG" \ LDFLAGS="-s" \ CC=arm-linux-gnueabihf-gcc \ CXX=arm-linux-gnueabihf-g++ \ AR=arm-linux-gnueabihf-ar \ RANLIB=arm-linux-gnueabihf-ranlib
可用选项:
#禁用高质量的音频处理算法
--disable-libwebrtc
3.编译安装
make dep
make
make install
4.完成动态库移植
动态库路径在 pjproject-2.15.1/_install/lib/
5.工程项目编译可能会报错,比如大小端未定义
makefile加个选项:
CFLAGS+= -DPJ_IS_LITTLE_ENDIAN=1 -DPJ_IS_BIG_ENDIAN=0 (仅供参考)
6.解决编译警告
sip/include/pj/compat/limits.h:38:4: error: #warning "limits.h is not found or not supported. LONG_MIN and LONG_MAX " "will be defined by the library in pj/compats/limits.h and " "overridable in config_site.h" [-Werror=cpp] 38 | # warning "limits.h is not found or not supported. LONG_MIN and LONG_MAX " \ | ^~~~~~~ cc1: all warnings being treated as errors
(1)将/sip/include/pj/config_site.h的文件内容改为:

点击查看代码
#ifndef PJ_CONFIG_SITE_H
        #define PJ_CONFIG_SITE_H

        // 主动定义 LONG_MIN / LONG_MAX,绕过 PJSIP 的警告
        #if defined(__LP64__) || defined(_LP64) || defined(__arm64__) || defined(__aarch64__) \
            || defined(_M_ARM64) || defined(__x86_64__) || defined(_M_X64)
            // 64 位平台 
            #define LONG_MIN (-9223372036854775807L - 1L)
            #define LONG_MAX 9223372036854775807L
        #else
            // 32 位平台(ARM、x86 等)
            #define LONG_MIN (-2147483647L - 1L)
            #define LONG_MAX 2147483647L
        #endif
        #endif
(2)将/sip/include/pj/compat/limits.h的
点击查看代码
#  ifdef _MSC_VER
#  pragma message("limits.h is not found or not supported. LONG_MIN and "\
                 "LONG_MAX will be defined by the library in "\
                 "pj/compats/limits.h and overridable in config_site.h")
#  else
#  warning "limits.h is not found or not supported. LONG_MIN and LONG_MAX " \
           "will be defined by the library in pj/compats/limits.h and "\
           "overridable in config_site.h"
#  endif
改为:
点击查看代码
#  ifdef _MSC_VER
#  pragma message("limits.h is not found or not supported. LONG_MIN and "\
                 "LONG_MAX will be defined by the library in "\
                 "pj/compats/limits.h and overridable in config_site.h")
#  else
#  if !defined(LONG_MIN) || !defined(LONG_MAX)
#  warning "limits.h is not found or not supported. LONG_MIN and LONG_MAX " \
           "will be defined by the library in pj/compats/limits.h and "\
           "overridable in config_site.h"
#  endif
#  endif
**三、简单了解sip协议**

SIP(会话初始协议)是一个在互联网上发起、管理和结束多媒体通信(如语音通话、视频会议)的信令控制协议。本文只实现语言通话。
以下是客户端与服务器和主叫方注册,通话等交互的流程:

1.注册与认证(通话准备)
(1)用户设备(UA)向SIP服务器发送 REGISTER 请求,包含用户标识(如 sip:user@example.com)。
(2)若需认证,服务器返回 ​401 Unauthorized,要求提供凭证(如用户名、密码)。
(3)终端重新发送带认证信息的 REGISTER,服务器验证通过后回复 ​200 OK,完成注册。
(4)作用​:告知服务器终端可达地址,为后续呼叫路由提供依据。

2.呼叫建立(信令交互)
(1)主叫发起邀请
a.主叫发送 ​INVITE​ 请求,包含被叫URI(如 sip:bob@domain.com)和媒体描述(SDP协议)。
b.SDP信息包括:
媒体类型(音频/视频)
编解码格式(如G.711、Opus)
接收音频rtp数据包的IP地址及端口号
(2)被叫响应
a.被叫收到 INVITE 后返回 ​100 Trying​(处理中)。
b.若可接听,返回 ​180 Ringing​(振铃),主叫听到回铃音。
c.被叫摘机后发送 ​200 OK,携带自身SDP参数(如接收IP和端口)。
d.主叫回复 ​ACK​ 确认,呼叫正式建立

3.会话修改与终止
(1)会话修改(可选)
主叫通话中调整参数(如切换高清音频)时,发送 UPDATE​或 RE-INVITE请求,重新协商SDP
​ (2)终止通话
任一方发送 BYE​请求
对方回复 200 OK,释放媒体资源及网络承载

4.通话阶段(媒体传输)
双向RTP语音流(主叫 ↔ 被叫)​​
(1)协议​:RTP(Real-time Transport Protocol)
(2)传输内容​:
a.音频数据包(按协商的编解码格式)
b.语音连续性信息
c.时间戳(确保音频同步)

四、代码实现

点击查看代码
/*
    支持sip语音双向通话功能,实现注册上线sip服务器、接收处理指令(电话邀请、挂断等)、双向音频对讲功能
*/
#include <stdio.h>
#include <string.h>
#include <ifaddrs.h>
#include <netdb.h>
#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <time.h>
#include <linux/if.h>

#include <pjlib.h>
#include <pjmedia.h>
#include <pjsua-lib/pjsua.h>

#define SIP_PROPERTIES_LISTENER_ID "SIP_PROPERTIES_LISTENER_ID"

#define MAX_GETFRAME_COUNT  5               // 最大获取音频帧尝试次数
#define RTP_TIMEOUT_SEC     30              // RTP超时时间(秒)

// SIP通话状态
typedef enum sip_calling_status_e
{
    SIP_CALLING_STATUS_NOT_SUPPORTED = 0,           //0-Not supported
    SIP_CALLING_STATUS_NOT_CONNECTED = 1,           //1-Not connected
    SIP_CALLING_STATUS_SUCCESSFULL_CONNECTED = 2,   //2-Successfully connected
    SIP_CALLING_STATUS_CALL_IN_PROGRESS = 3,        //3-Call in progress
}sip_calling_status_e;

// 检查数据帧头是否为空,为空则填充静音帧并返回
#define CHECK_DATA_FRAME_HDR_NULL(hdr, frame) \
    do { \
        if (NULL == (hdr)) { \
            printf("talk data_frame_hdr is NULL\n"); \
            memset((frame)->buf, 0, (frame)->size); \
            (frame)->type = PJMEDIA_FRAME_TYPE_AUDIO; \
            return PJ_SUCCESS; \
        } \
    } while (0)

/**
 * 音频帧队列结构体
 * 用于存储待播放的音频帧
 */
typedef struct AudioFrameQueue
{
    unsigned char **frames;                 // 音频帧数组指针   
    int capacity;                // 队列总容量
    int front;                   // 队首索引
    int rear;                    // 队尾索引
    int count;                   // 当前帧数
    int is_destroy;              // 是否销毁标志 1-已销毁 0-未销毁
    pthread_mutex_t lock;            // 互斥锁
    pthread_cond_t not_empty;        // 非空条件变量
} AudioFrameQueue;

// 全局变量声明
static pjsua_call_id current_call_id = PJSUA_INVALID_ID;     // 当前通话ID
static pjsua_conf_port_id audio_port_id = PJSUA_INVALID_ID;  // 音频端口ID
static pjmedia_port *custom_audio_port = NULL;               // 自定义音频端口指针
static pj_pool_t *pool_t = NULL;                               // 内存池
static AudioFrameQueue audio_queue;                          // 音频帧队列
static int is_playing = 0;                                   // 播放线程运行标志 1-正在播放 0-未播放
static int g_is_run = 0;                       // sip主线程运行标记
static int g_is_speaker_open = 0;              // 喇叭打开标志 true-已打开 false-未打开
static char g_calling_status = SIP_CALLING_STATUS_NOT_CONNECTED; // SIP通话状态,默认未连接
static int g_is_pjsua_created = 0; // 是否创建了PJSUA
static os_th_t g_thSIP;

/*异常处理:还未接收到挂断电话的通知,呼叫方就挂断了电?? 被叫方就一直会在通话??,异常检测逻辑 未验证*/
static pj_time_val last_rtp_time;                            // 最后收到RTP包的时间

static void * g_audio_handle = NULL; // 取音频帧句柄
static unsigned char *g_frame_buf = NULL; // 音频帧缓冲区
static unsigned int g_frame_buf_len = 0; // 音频帧缓冲区长度


// 全局互斥锁
static pthread_mutex_t g_sip_mutex = PTHREAD_MUTEX_INITIALIZER;
// 全局条件变量
static pthread_cond_t g_sip_thread_exit_cond = PTHREAD_COND_INITIALIZER;
static int g_is_main_thread_exited = 0;

/*
    brief:获取SIP通话状态
    @return char 参考sip_calling_status_e
*/
char sip_get_calling_status(void)
{
    return g_calling_status;
}

/*
    brief:获取喇叭是否已使用
    @return true-已打开 false-未打开
*/
int sip_get_Intercom_Used(void)
{
    return g_is_speaker_open;
}

/**
 * 初始化音频帧队列
 * @param queue 队列指针
 * @param capacity 队列容量(帧数)
 * @return 成功返回0,内存分配失败返回-1
 */
static int audio_queue_init(AudioFrameQueue *queue, int capacity)
{
    queue->frames = (unsigned char **)malloc(capacity * sizeof(unsigned char *));
    if (!queue->frames)
    {
        return -1;
    }

    for (int i = 0; i < capacity; i++)
    {
        queue->frames[i] = (unsigned char *)malloc(640);  // 每帧640字节
        if (!queue->frames[i])
        {
            // 分配失败时释放已分配的内存
            for (int j = 0; j < i; j++)
            {
                free(queue->frames[j]);
            }
            free(queue->frames);
            return -1;
        }
    }

    queue->capacity = capacity;
    queue->front = 0;
    queue->rear = 0;
    queue->count = 0;
    queue->is_destroy = 0;
    pthread_mutex_init(&queue->lock, NULL);
    pthread_cond_init(&queue->not_empty, NULL);
    return 0;
}

/**
 * 销毁音频帧队列
 * @param queue 队列指针
 */
static void audio_queue_destroy(AudioFrameQueue *queue)
{
    if (!queue)
    {
        printf("queue is NULL");
        return;
    }

    if (queue->is_destroy)
    {
        printf("queue is already destroyed");
        return;
    }

    printf("audio_queue_destroy step 1\n");
    for (int i = 0; i < queue->capacity; i++)
    {
        if (queue->frames[i])
        {
            free(queue->frames[i]);
            queue->frames[i] = NULL;
        }
    }
    printf("audio_queue_destroy step 2\n");
    if (queue->frames)
    {
        free(queue->frames);
        queue->frames = NULL;
    }
    printf("audio_queue_destroy step 3\n");
    pthread_mutex_destroy(&queue->lock);
    pthread_cond_destroy(&queue->not_empty);
    printf("audio_queue_destroy step 4\n");
    queue->is_destroy = 1;
    printf("queue is destroyed");
    return;
}

/**
 * 向队列添加音频帧
 * @param queue 队列指针
 * @param frame 音频帧数据指针
 * @param size 音频帧大小
 * @return 成功返回0,队列满返回-1
 */
static int audio_queue_enqueue(AudioFrameQueue *queue, const unsigned char *frame, int size)
{
    pthread_mutex_lock(&queue->lock);
    
    if (queue->count >= queue->capacity)
    {
        pthread_mutex_unlock(&queue->lock);
        printf("队列已满 目前帧数:%d\n",queue->count);
        return -1;  // 队列已满
    }

    memcpy(queue->frames[queue->rear], frame, size);
    queue->rear = (queue->rear + 1) % queue->capacity;
    queue->count++;
    pthread_cond_signal(&queue->not_empty);
    pthread_mutex_unlock(&queue->lock);
    return 0;
}

/**
 * 从队列获取音频帧(消费者)
 * @param queue 队列指针
 * @param frame 存储音频帧的缓冲区
 * @param size 音频帧大小
 * @return 成功返回0,队列空且线程需退出返回-1
 */
static int audio_queue_dequeue(AudioFrameQueue *queue, unsigned char *frame, int size)
{
    pthread_mutex_lock(&queue->lock);
    
    // 处理队列空的情况(包括虚假唤醒)
    while (queue->count <= 0)
    {
        pthread_cond_wait(&queue->not_empty, &queue->lock);
        
        // 检查播放线程退出标志
        if (!is_playing) 
        {
            pthread_mutex_unlock(&queue->lock);
            return -1;
        }
    }
  
    // 从队列复制帧数据
    memcpy(frame, queue->frames[queue->front], size);
    queue->front = (queue->front + 1) % queue->capacity;
    queue->count--;
    pthread_mutex_unlock(&queue->lock);
    return 0;
}

/**
 * 停止音频播放
 */
static void stop_audio_playback()
{
    int ret = -1;

    // 设置播放停止标志
    is_playing = 0;
    printf("stop_audio_playback step 1\n");
    
    // 关闭喇叭
    if (g_is_speaker_open)
    {
        ret = mng_audio_out_set(0, 0, NULL);
        if (ret < 0)
        {
            printf("audio out close error");
        }
        else
        {
            printf("audio out close success");
        }
    }
    printf("stop_audio_playback step 2\n");
    // 初始化喇叭资源为未打开
    g_is_speaker_open = 0;

    // 释放音频帧缓冲区
    if (g_frame_buf)
    {
        free(g_frame_buf);
        g_frame_buf = NULL;
    }
    printf("stop_audio_playback step 3\n");
    // 销毁音频帧句柄
    if (NULL != g_audio_handle)
    {
        ret = mng_ai_media_audio_frame_destroy(g_audio_handle);
        if (ret < 0)
        {
            printf("audio frame destroy error");
        }
        else
        {
            printf("audio frame destroy success");
        }
        g_audio_handle = NULL;
    }

    printf("stop_audio_playback step 4\n");
    
    // 唤醒可能阻塞在队列上的线程
    pthread_cond_signal(&audio_queue.not_empty);
    printf("stop_audio_playback step 5\n");
    
    // 销毁音频队列
    audio_queue_destroy(&audio_queue);
    printf("stop_audio_playback step 6\n");
    
    printf("[播放停止] 音频播放已完全停止\n");
}

/**
 * 音频播放线程函数
 * @param arg 线程参数(未使用)
 * 注:符合播放要求: 25fps
 */
static void *audio_play_thread(void *arg)
{
    (void)arg;
    int ret = -1;
    unsigned char PCMAframe[640];
    unsigned char frame[320];
    struct timespec next_ts;
    clock_gettime(CLOCK_MONOTONIC, &next_ts); // 初始化起始时间

    printf("[播放线程] 音频播放线程启动\n");
    while (is_playing)
    {
        // 尝试从队列获取帧
        int has_frame = audio_queue_dequeue(&audio_queue, PCMAframe, sizeof(PCMAframe));
        if (!has_frame) 
        {
            //暂时只支持pcm格式
            if (0/*is g711a*/)
            {
                //调用g711编码函数
                ret = mng_audio_out_add_data(0, frame, sizeof(frame));
                if (ret < 0)
                {
                    printf("add audio task error");
                } 
            }
            else if (1/*is pcm*/)
            {
                //printf("audio play pcm frame");
                // 默认开启音频通道0
                ret = mng_audio_out_add_data(0, PCMAframe, sizeof(PCMAframe));
                if (ret < 0)
                {
                    printf("add audio task error");
                } 
            }
            else
            {
                printf("audio encode type error");
                break;
            }
        }else {
            // 播放静音帧(0数据)
            if (0/*is g711a*/)
            {
                memset(frame, 0, sizeof(frame));
                ret = mng_audio_out_add_data(0, frame, sizeof(frame));
                if (ret < 0)
                {
                    printf("add audio task error");
                }
            }
            else if (1/*is pcm*/)
            {
                memset(PCMAframe, 0, sizeof(PCMAframe));
                ret = mng_audio_out_add_data(0, PCMAframe, sizeof(PCMAframe));
                if (ret < 0)
                {
                    printf("add audio task error");
                }
            }
            else
            {
                printf("audio encode type error");
                break;
            }
        }
        
        // 计算下一帧目标时间(固定40ms间隔)
        next_ts.tv_nsec += 40000000;
        if (next_ts.tv_nsec >= 1000000000) 
        {
            next_ts.tv_sec++;
            next_ts.tv_nsec -= 1000000000;
        }

        // 动态补偿:测量实际耗时并调整休眠时间
        struct timespec now_ts;
        clock_gettime(CLOCK_MONOTONIC, &now_ts);
        int64_t elapsed_ns = (now_ts.tv_sec - next_ts.tv_sec) * 1000000000LL + (now_ts.tv_nsec - next_ts.tv_nsec);
        if (elapsed_ns < 40000000) 
        { // 未超时则休眠剩余时间
            clock_nanosleep(CLOCK_MONOTONIC, TIMER_ABSTIME, &next_ts, NULL);
        } 
        else 
        {
            // 超时则跳过休眠,下一帧立即追上
            clock_gettime(CLOCK_MONOTONIC, &next_ts); // 重置为当前时间
        } 
    }
    printf("[播放线程] 音频播放线程退出\n");
    return NULL;
}

/**
 * 超时检测线程函数
 * 注:挂断电话的通知以后,通话仍在运行
 */
/*
static void rtp_timeout_checker(void *arg)
{
    (void)arg;
    printf("[RTP检测] 超时检测线程启动 (阈??: %d??)\n", RTP_TIMEOUT_SEC);
    
    while (is_playing)
    {
        pj_time_val now;
        pj_gettimeofday(&now);
        
        // 手动计算时间差
               pj_time_val diff;
        diff.sec = now.sec - last_rtp_time.sec;
        diff.msec = now.msec - last_rtp_time.msec;
        
        // 规范化时间值(确保毫秒??0-999范围内)
        if (diff.msec >= 1000) 
        {
            diff.sec += diff.msec / 1000;
            diff.msec %= 1000;
        } 
        else if (diff.msec < 0) 
        {
            diff.sec -= 1;
            diff.msec += 1000;
        }
        
        // 计算总秒??
        pj_uint32_t total_sec = diff.sec;
        if (diff.msec > 0) total_sec += 1; // 有毫秒也算一??
        
        // 检查超??
        if (total_sec > RTP_TIMEOUT_SEC)
        {
            printf("[RTP超时] 超过 %d 秒未收到RTP数据,主动挂断通话\n", RTP_TIMEOUT_SEC);
            
            //主动挂断电话
            if (current_call_id != PJSUA_INVALID_ID) 
            {
                pjsua_call_hangup(current_call_id, 0, NULL, NULL);
            }
            
            // 直接停止播放线程
            stop_audio_playback();
            break;
          
        }
        
        // 每秒检查一??
        sleep(1);
    }
    
    printf("[RTP检测] 超时检测线程退出\n");
    return ;
}
*/

/**
 * @brief 初始化喇叭资源,注意关闭通话需要关闭喇叭资源
 * 
 * @return 执行返回值
 * @note   只能初始化一次,后续调用返回0
 */
static int audio_speaker_init()
{
    unsigned int audio_out_chn = 1;
    unsigned int idx = 0;
    char key[128] = {0};
    char intercom_chn[32] = {0};
    char intercomvol[32] = {0};
    int intercomvolszie[10] = {0};
    int channel = 0;
    
    memset(key, 0, sizeof(key));
    memset(intercom_chn, 0, sizeof(intercom_chn));
    memset(intercomvol, 0, sizeof(intercomvol));
    memset(intercomvolszie, 0, sizeof(intercomvolszie));

    // 判断喇叭资源是否可用
    if (0 == _jtrtp_is_speaker_available())
    {
        printf("喇叭被占用, 请先关闭JT1078对讲!!!");
        return -1;
    }

    api_properties_get("rw.base.audioconfig.audioin.intercom.channel", intercom_chn, "");
    channel =_Str2Val_API(intercom_chn, strlen(intercom_chn));
    printf("key: %s, val: %s(%d)", "rw.base.audioconfig.audioin.intercom.channel", intercom_chn, channel);

    snprintf(key, sizeof(key), "rw.base.audioconfig.ch%d.audioout.intercomvol", audio_out_chn);
    api_properties_get(key, intercomvol, "");
    snprintf(key, sizeof(key), "rw.base.audioconfig.ch%d.audioout.intercomvolsize", audio_out_chn);

    MOD_AUDIO_OUT_CFG_S pst_cfg;
    pst_cfg.ui_samplelength = 16;
    pst_cfg.ui_soundmode = 1;
    pst_cfg.ui_samplerate = 8000;
    idx =_Str2Val_API(intercomvol, strlen(intercomvol));
    if (0 == idx)
    {
        printf("intercomvol is 0, use default 5");
        idx = 5;
    }
    if (api_properties_get_2int(key, intercomvolszie) > 0)
    {
        pst_cfg.ui_volume = intercomvolszie[idx];
    }
    else
    {
        pst_cfg.ui_volume = 70;
    }
    //后续音频编码格式需要一协商为准
    pst_cfg.encode_type = AUDIO_ENCODE_PCM;//AUDIO_ENCODE_G711A
    int ret = mng_audio_out_set(audio_out_chn - 1, 1, &pst_cfg);
    if (ret < 0)
    {
        printf("creat audio output error");
        return -1;
    }

    // 初始化喇叭资源为已打开
    g_is_speaker_open = 1;

    //创建取音频的句柄
    g_audio_handle = mng_ai_media_audio_frame_reader_create(channel);
    printf("mng_ai_media_audio_frame_reader_create start, handle %p, chn %d", g_audio_handle, channel);
    if (NULL == g_audio_handle)
    {
        printf("Error in creating handle for audio frame reading.\n");
        return -1;
    }

    //分配音频帧缓冲区
    g_frame_buf_len = 2048;
    g_frame_buf = (unsigned char *)malloc(g_frame_buf_len);
    if (NULL == g_frame_buf)
    {
        printf("Error in allocating memory for audio frame buffer.\n");
        return -1;
    }

    printf("audio_speaker_init success");
    return 0;
}
/**
 * 启动音频播放线程
 * @return 成功返回0,失败返??-1
 */
static int start_audio_playback()
{
    int ret = 0;
    
    //初始化喇叭资源(只有一个喇叭,和JT1078的对讲互斥)
    if (audio_speaker_init() != 0)
    {
        printf("喇叭资源初始化失败\n");
        goto ERROR;
    }

    // 初始化音频队列(容量??4秒的数据??25??/?? × 4?? = 100帧) 50*80ms
    if (audio_queue_init(&audio_queue, 50) != 0)
    {
        printf("音频队列初始化失败\n");
        goto ERROR;
    }

    // 设置初始RTP时间
    pj_gettimeofday(&last_rtp_time);
    is_playing = 1;
    
    os_th_t play_thread;
    if (api_os_th_create(&play_thread, audio_play_thread, NULL, "audio_play_thread", 0) != 0)
    {
        printf("播放线程创建失败\n");
        audio_queue_destroy(&audio_queue);
        goto ERROR;
    }
    /*
    // 创建RTP超时检测线程
    thread_t rtp_timeout_thread;
    if (_thread_create(&rtp_timeout_thread, NULL, rtp_timeout_checker, NULL, "rtp_timeout_checker"))
    {
        printf("超时检测线程创建失败\n");
        stop_audio_playback(); // 停止播放
        return -2;
    }*/
    return 0;
ERROR:
    if (g_is_speaker_open)
    {
        // 关闭喇叭
        ret = mng_audio_out_set(0, 0, NULL);
        if (ret < 0)
        {
            printf("audio out close error");
        }
        else
        {
            printf("audio out close success");
        }
        // 初始化喇叭资源为未打开
        g_is_speaker_open = 0;
    }
    return -1;
}

#if 0 //for debug
//"/mnt/sdcard1_1/sip_audio.pcm"
static void audio_write_pcm_to_file(unsigned char *pcm_data, unsigned int pcm_size, char *filename)
{
    FILE *fp = fopen(filename, "ab");
    if (fp == NULL)
    {
        printf("open file audio.pcm failed");
        return;
    }
    fwrite(pcm_data, 1, pcm_size, fp);
    fclose(fp);

    return;
}

static void audio_read_pcm_to_frame(unsigned char *pcm_data, unsigned int *pcm_size, char *filename)
{
    // 静态变量,记录已读取的字节数,保持上次读取位置
    static long long read_offset = 0;
 
    FILE *fp = fopen(filename, "rb");
    if (fp == NULL)
    {
        printf("open file test.pcm failed");
        return;
    }
 
    // 将文件光标移动到已读取位置之后
    if (fseek(fp, read_offset, SEEK_SET) != 0)
    {
        printf("fseek file test.pcm failed");
        fclose(fp);
        return;
    }
 
    // 从当前位置读取数据
    size_t read_bytes = fread(pcm_data, 1, *pcm_size, fp);

    // 判断是否已读取到文件末尾
    if (read_bytes == 0)
    {
        printf("已读取到文件末尾");
        read_offset = 0;
        fclose(fp);
        return;
    }
 
    // 更新已读取的字节数
    read_offset += read_bytes;
 
    // 更新实际读取到的大小
    *pcm_size = (unsigned int)read_bytes;
 
    fclose(fp);

    return;
}
#endif



// 音频接收回调函数 理论??40ms调用一次接口,1s调用25次接收PCMA数据,50次则PCM数据
static pj_status_t on_audio_frame_received(pjmedia_port *port, pjmedia_frame *frame)
{
    if (frame->type == PJMEDIA_FRAME_TYPE_AUDIO)
    {
        //printf("on_audio_frame_received type %d size %d bit_info %d", frame->type, frame->size, frame->bit_info);
        pj_gettimeofday(&last_rtp_time); // 更新最后收到RTP的时间
        static unsigned char frame_buffer[960];  // 用于拼接的缓冲区
        static int buffer_offset = 0;            // 当前缓冲位置
        // 检查帧大小是否符合预期
        if (frame->size != 320) 
        {
            printf("收到 != 320字节 (%d 字节),忽略\n", frame->size);
            return PJ_SUCCESS;
        }

        #if 0 //for debug 从pcm文件读取数据,替换到当前帧
        //char filename[128] = "/mnt/sdcard1_1/test.pcm";
        //audio_read_pcm_to_frame(frame->buf, &frame->size, filename);
        #endif

        // 复制当前帧到缓冲区
        memcpy(frame_buffer + buffer_offset, frame->buf, frame->size);
        buffer_offset += frame->size;
        
        // 当缓冲区??640字节时入队列
        if (buffer_offset >= 640)
        {
            if (audio_queue_enqueue(&audio_queue, frame_buffer, 640) != 0)
            {
                printf("音频队列已满, 丢640字节数据, 帧数量已满 %d\n",audio_queue.count);
            }
            #if 0 //for debug
            char filename[128] = "/mnt/sdcard1_1/sip_recev_ez511_1.pcm";
            audio_write_pcm_to_file(frame_buffer, 640, filename);
            #endif
            buffer_offset -= 640;
            if (buffer_offset > 0)
            {
                // 将剩余数据移到缓冲区开头
                memmove(frame_buffer, frame_buffer + 640, buffer_offset);
            }
        }
        else
        {
           //printf("[音频接收] 收到320字节数据 (等待第二帧)\n");
        }
    }
    return PJ_SUCCESS;
}

short audio_decode(unsigned char alaw)  
{  
    alaw ^= 0xD5;  
    int sign = alaw & 0x80;  
    int exponent = (alaw & 0x70) >> 4;  
    int data = alaw & 0x0f;  
    data <<= 4;  
    data += 8;  
    if (exponent != 0)  
        data += 0x100;  
    if (exponent > 1)  
        data <<= (exponent - 1);  
  
    return (short)(sign == 0 ? data : -data);  
}  

/** 个人理解  
 * bitsize 应该为16, pcm 数组的宽度变为原来两倍(short *out_data = (short*)pRawData;),  
 * 通过对pBuffer(g711数据)中char解码转为两个字节的short ,后复制给out_data数组, 
 * 在使用的时候又转为char类型, 则 解码后的数据就是原来的两倍(nBufferSize*2)  
 */  
int audio_g711_decode_API(char* pDstBuf, const unsigned char* pRawData, int rawLen)  
{  
    short *out_data = (short*)pDstBuf;  
    int i;  
    for(i=0; i<rawLen; i++)  
    {  
        out_data[i] = audio_decode(pRawData[i]);  
    }  
  
    return rawLen*2;  
}  

// 音频发送回调函数
// 当前实现只支持G711A TODO:根据实际的编码取帧
static pj_status_t on_audio_frame_to_send(pjmedia_port *port, pjmedia_frame *frame) 
{
    static unsigned char pcma_buffer[640];             // G711a缓冲区
    int opt_ret = 0;
    static int pcma_remaining = 0;            // 剩余PCMA数据量
    static unsigned char *pcma_current = NULL;          // 当前PCMA位置
    int i = 0;
    unsigned char *audio_data = NULL;
    size_t bsize = 0;
    int data_len = 0, requested_size = 0, copy_size = 0, silence_bytes = 0;
    API_FS_DATA_FRAMEHDR_S *data_frame_hdr = NULL; // 读取的音视频数据头信息

    // === 1. 当需要新数据时 ===
    if (pcma_remaining <= 0) 
    {
    // 获取共享内存指针
        opt_ret = mng_ai_media_audio_frame_get(g_audio_handle, (char *)g_frame_buf, &g_frame_buf_len);
        //printf("mng_ai_media_audio_frame_get start, handle %p, buf_len %u, opt_ret %d", g_audio_handle, g_frame_buf_len, opt_ret);
        if (opt_ret < 0) {
            printf("opt_ret %d\n", opt_ret);
            memset(frame->buf, 0, frame->size);
            frame->type = PJMEDIA_FRAME_TYPE_AUDIO;
            return PJ_SUCCESS;
        }
        else if (opt_ret > 0)
        {
            printf("Number of lost audio frames: %d\n", opt_ret);
        }

        data_frame_hdr = (API_FS_DATA_FRAMEHDR_S *)g_frame_buf;
        CHECK_DATA_FRAME_HDR_NULL(data_frame_hdr, frame);

        if(data_frame_hdr->voptype != FRAME_TYPE_AUDIO){
            for (i=0; i < MAX_GETFRAME_COUNT; i++)
            {
                opt_ret = mng_ai_media_audio_frame_get(g_audio_handle, (char *)g_frame_buf, &g_frame_buf_len);
                data_frame_hdr = (API_FS_DATA_FRAMEHDR_S *)g_frame_buf;
                CHECK_DATA_FRAME_HDR_NULL(data_frame_hdr, frame);
                
                if ( data_frame_hdr->voptype == FRAME_TYPE_AUDIO ) 
                {
                    //printf("mng_ai_media_audio_frame_get start, handle %p, buf_len %u, opt_ret %d", g_audio_handle, g_frame_buf_len, opt_ret);
                    break;
                }
                printf("[SKIP] 非音频帧(类型:%d),retry_count:%d\n", data_frame_hdr->voptype,i);
                usleep(1000);
            
                if (i == MAX_GETFRAME_COUNT)
                {
                    return PJ_SUCCESS;
                }
            }
        }
        
        audio_data = g_frame_buf + sizeof(API_FS_DATA_FRAMEHDR_S);
        bsize = g_frame_buf_len - sizeof(API_FS_DATA_FRAMEHDR_S);
        //uint64_t ts_current = ((uint64_t)data_frame_hdr->time_tick.tut.time_s * 1000) + ((uint64_t)data_frame_hdr->time_tick.tut.time_us / 1000);
        //printf("bsize %d, ts_current %llu\n", bsize, ts_current);
        if(NULL == audio_data)
        {
            printf("talk audio_data is NULL\n");
            memset(frame->buf, 0, frame->size);
            frame->type = PJMEDIA_FRAME_TYPE_AUDIO;
            return PJ_SUCCESS;
        }
        
        // 解码音频数据
        data_len = audio_g711_decode_API((char *)pcma_buffer, audio_data, bsize);
        if (data_len >= 640) 
        {
            pcma_current = (unsigned char*)pcma_buffer;
            pcma_remaining = data_len;
        } 
        else{
            pcma_remaining = 0; // 无效音频数据
        }
    }

    // === 2. 填充请求数据 ===
    //printf("audio request frame size: %d, pcma_remaining %d\n", frame->size, pcma_remaining);
    requested_size = frame->size;
    copy_size = (pcma_remaining > requested_size) ? requested_size : pcma_remaining;

    memcpy(frame->buf, pcma_current, copy_size);
    pcma_current += copy_size;
    pcma_remaining -= copy_size;

    // === 3. 静音填充 ===
    if (copy_size < requested_size) 
    {
        silence_bytes = requested_size - copy_size;
        memset((char*)frame->buf + copy_size, 0, silence_bytes);
    }

    #if 0 //for debug
    //将pcm数据写入文件
    char filename[128] = "/mnt/sdcard1_1/sip_send.pcm";
    audio_write_pcm_to_file(frame->buf, requested_size, filename);
    #endif

    frame->type = PJMEDIA_FRAME_TYPE_AUDIO;
    return PJ_SUCCESS;

}

 //端口销毁回调(释放资源,避免警告)
 static pj_status_t custom_audio_on_destroy(pjmedia_port *port)
 {
    printf("[媒体端口] 自定义端口已销毁\n");
     if (port == NULL) 
        return PJ_SUCCESS;
     // 若有自定义资源(如缓存、外部设备句柄),在此释放
     
     // 重置全局变量(避免野指针)
     custom_audio_port = NULL;
     audio_port_id = PJSUA_INVALID_ID;
     return PJ_SUCCESS;
 }

 
/**
 * 创建自定义音频端口
 * @param pool 内存池
 * @param clock_rate 时钟频率(Hz)
 * @param samples_per_frame 每帧采样数
 * @return 成功返回PJ_SUCCESS,失败返回错误码
 */

/*
 * 1. 创建自定义音频端口
 * 2. 初始化端口信息
 * 3. 绑定回调函数
 * 4. 添加到PJSUA会议
 * @param pool 内存池
 * @param clock_rate 时钟频率(Hz)
 * @param samples_per_frame 每帧采样数
 * @return 成功返回PJ_SUCCESS,失败返回错误码
*/
static pj_status_t create_custom_audio_port(pj_pool_t *pool, unsigned clock_rate, unsigned samples_per_frame)
{
    pj_str_t name = pj_str( (char*)("custom_audio"));
    custom_audio_port = PJ_POOL_ZALLOC_T(pool, pjmedia_port);
    
    // 初始化端口信息
    pjmedia_port_info_init(&custom_audio_port->info, &name, 
        PJMEDIA_SIG_CLASS_PORT_AUD('C','P'),  // 音频端口标识
        clock_rate,      // 时钟频率
        1,               // 通道数(单声道)
        8,               // 每样本字节数(8位)
        samples_per_frame // 每帧采样数
    );

    // 绑定回调函数
    custom_audio_port->get_frame = &on_audio_frame_to_send;//读取待发送的音频帧
    custom_audio_port->put_frame = &on_audio_frame_received;//处理接收到的音频帧
    custom_audio_port->on_destroy = &custom_audio_on_destroy; // 绑定销毁回调

    // 检查put_frame回调
    if (custom_audio_port->put_frame != &on_audio_frame_received) {
        printf("put_frame回调绑定异常\n");
        // 清理已分配资源
        //custom_audio_port = NULL;// 无需手动释放,内存属于 pool_t
        return PJ_EINVALIDOP;
    }
    printf("## [端口初始化] put_frame回调绑定正常\n");

    // 检查get_frame回调
    if (custom_audio_port->get_frame != &on_audio_frame_to_send) {
        printf("get_frame回调绑定异常\n");
        // 清理已分配资源
        //custom_audio_port = NULL;// 无需手动释放,内存属于 pool_t
        return PJ_EINVALIDOP;
    }
    printf("## [端口初始化] get_frame回调绑定正常\n");
    
    // 添加到PJSUA会议??
    pj_status_t status = pjsua_conf_add_port(pool, custom_audio_port, &audio_port_id);
    if (status == PJ_SUCCESS)
    {
        printf("[媒体端口] 自定义音频端口创建成功 (ID=%d)\n", audio_port_id);
    }
    else
    {
        char errmsg[PJ_ERR_MSG_SIZE];
        pj_strerror(status, errmsg, sizeof(errmsg));
        printf("[媒体端口] 端口创建失败: %s\n", errmsg);
    }
    return status;
}

/**
 * 来电处理回调
 * @param acc_id 账户ID
 * @param call_id 通话ID
 * @param rdata 接收数据
 */
static void on_incoming_call(pjsua_acc_id acc_id, pjsua_call_id call_id, pjsip_rx_data *rdata)
{
    pj_sockaddr_in *remote_addr = NULL;
    char ip_str[PJ_INET6_ADDRSTRLEN] = {0};

    current_call_id = call_id;
    printf("=============on_incoming_call=========================\n");
    printf("acc_id: %d, call_id: %d\n", acc_id, call_id);

    // 回答来电
    pjsua_call_answer(call_id, 200, NULL, NULL);
    
    // 调试信息 远程地址信息(使用 pkt_info.src_addr)
    remote_addr = (pj_sockaddr_in*)&rdata->pkt_info.src_addr;
    if (remote_addr == NULL) {
        printf("remote_addr is NULL\n");
        return;
    }

    if (remote_addr->sin_family == AF_INET) {
        pj_inet_ntop(AF_INET, &remote_addr->sin_addr, ip_str, sizeof(ip_str));
    }
    
    printf("远程地址: %s:%d", ip_str, pj_sockaddr_get_port((pj_sockaddr*)remote_addr));

    return;
}

/**
 * 通话状态变更回调
 * @param call_id 通话ID
 * @param e SIP事件
 */
static void on_call_state(pjsua_call_id call_id, pjsip_event *e)
{
    pjsua_call_info ci;memset(&ci, 0, sizeof(pjsua_call_info));
    pjsua_call_get_info(call_id, &ci);
    printf("[通话状态] 状态变更: %.*s, 状态码: %d\n", 
            (int)ci.state_text.slen, ci.state_text.ptr, ci.state);

    // 通话结束时停止播放
    if (ci.state == PJSIP_INV_STATE_DISCONNECTED) 
    {
        stop_audio_playback();
        // 重置全局状态
        current_call_id = PJSUA_INVALID_ID;
        // 通话结束 更新通话状态为已连接(能操作挂断,必然是已上线服务器)
        g_calling_status = SIP_CALLING_STATUS_SUCCESSFULL_CONNECTED;
    }
    return;
}

/**
 * 媒体状态变更回调
 * @param call_id 通话ID
 */
static void on_call_media_state(pjsua_call_id call_id)
{
    pjsua_call_info ci;memset(&ci, 0, sizeof(pjsua_call_info));
    pjsua_call_get_info(call_id, &ci);
    
    printf("## [媒体状态] 媒体状态: %d\n", ci.media_status);
    if (ci.media_status == PJSUA_CALL_MEDIA_ERROR)
    {
        printf("PJSUA_CALL_MEDIA_ERROR\n");
    }

    if (ci.media_status == PJSUA_CALL_MEDIA_ACTIVE)
    {
        pj_gettimeofday(&last_rtp_time); // 初始化计时器,保证sip双方通信仍在建立
        
        // 启动音频播放线程
        if (start_audio_playback() != 0)
        {
            printf("无法启动音频播放线程\n");
            return;
        }
        else
        {
            printf("音频播放线程已启动\n");
        }
        
        // 更新通话状态为通话中
        g_calling_status = SIP_CALLING_STATUS_CALL_IN_PROGRESS;
        
        pjsua_conf_port_id call_port = pjsua_call_get_conf_port(call_id);
        printf("## [媒体端口] 通话端口ID: %d, 自定义端口ID: %d\n", call_port, audio_port_id);
        
        if (call_port == PJSUA_INVALID_ID || audio_port_id == PJSUA_INVALID_ID) 
        {
            printf("!! [错误] 无效端口ID (call_port:%d, audio_port_id:%d)\n", call_port, audio_port_id);
            return;
        }
        
        pj_status_t status = pjsua_conf_connect(call_port, audio_port_id);
        printf("## [媒体连接] 通话 -> 自定义端口: %s (状态码:%d)\n", status == PJ_SUCCESS ? "成功" : "失败", status);
        
        status = pjsua_conf_connect(audio_port_id, call_port);
        printf("## [媒体连接] 自定义端口 -> 通话: %s (状态码:%d)\n", status == PJ_SUCCESS ? "成功" : "失败", status);
    }
    return;
}

/**
 * 注册状态回调
 * @param acc_id 账户ID
 */
static void on_reg_state(pjsua_acc_id acc_id)
{
    pjsua_acc_info acc_info;
    pjsua_acc_get_info(acc_id, &acc_info);

    printf("[注册状态] 账户ID=%d | 状态码=%d | 状态文本=%.*s | 过期时间=%d秒\n",
           acc_id,
           acc_info.status,
           (int)acc_info.status_text.slen,
           acc_info.status_text.ptr,
           acc_info.expires);
    
    if (acc_info.status / 100 == 2 && acc_info.expires > 0) 
    {
        printf("[注册成功] 已成功绑定到服务器\n");
        // 更新通话状态为已连接
        g_calling_status = SIP_CALLING_STATUS_SUCCESSFULL_CONNECTED;
    } 
    else 
    {
        printf("[注册失败] 请检查网络或账户配置\n");
        // 更新通话状态为未连接
        g_calling_status = SIP_CALLING_STATUS_NOT_CONNECTED;
    }
    return;
}

static int sip_config_pjsua(void)
{
    int ret = 0;
    char errmsg[PJ_ERR_MSG_SIZE] = {0};
    pj_status_t status = PJ_SUCCESS;
    pjsua_config cfg;memset(&cfg, 0, sizeof(pjsua_config));
    pjsua_logging_config log_cfg;memset(&log_cfg, 0, sizeof(pjsua_logging_config));
    pjsua_media_config media_cfg;memset(&media_cfg, 0, sizeof(pjsua_media_config));

    pjsua_config_default(&cfg);
    cfg.cb.on_reg_state = &on_reg_state;
    cfg.cb.on_incoming_call = &on_incoming_call;
    cfg.cb.on_call_state = &on_call_state;
    cfg.cb.on_call_media_state = &on_call_media_state;

    // 赋值静态STUN地址, 用于获取设备的公网地址
    cfg.stun_srv_cnt = 2; // 2个服务器
    cfg.stun_srv[0] = pj_str("stun.l.google.com:3478"); // 第一个服务器
    cfg.stun_srv[0].slen = strlen(cfg.stun_srv[0].ptr);
    cfg.stun_srv[1] = pj_str("stun.xten.com:3478");     // 第二个(备用)
    cfg.stun_srv[1].slen = strlen(cfg.stun_srv[1].ptr);

    // (可选)配置 DNS 服务器(推荐,解析更稳定)
    cfg.nameserver_count = 2;
    cfg.nameserver[0].ptr = "114.114.114.114"; // DNS
    cfg.nameserver[0].slen = strlen(cfg.nameserver[0].ptr);

    // 第二个DNS服务器(阿里云DNS,备用)
    cfg.nameserver[1].ptr = "223.5.5.5";
    cfg.nameserver[1].slen = strlen(cfg.nameserver[1].ptr);

    /*
        重要信息 
        1.该配置项用于设置 SDP(会话描述协议)中的 NAT 类型,控制 PJSIP 在生成 SDP 时如何处理 NAT 穿透问题
        2.参数值说明
            0	PJSUA_NAT_IN_SDP_DISABLED	禁用 NAT 处理(不修改 SDP)
            1	PJSUA_NAT_IN_SDP_REMOTE	使用远程地址替换 SDP 中的地址
            2	PJSUA_NAT_IN_SDP_BOTH	同时使用远程地址和 STUN 检测地址
        3.nat_type_in_sdp = 2 会自动将 SDP 中的内网地址替换为
            (1)远程地址:从接收到的 SIP 消息中提取的来源地址
            (2)STUN 检测地址:通过 STUN 协议获取的公网地址
    */
    cfg.nat_type_in_sdp = 2;

    pjsua_logging_config_default(&log_cfg);
    log_cfg.console_level = 4;  // 日志级别

    pjsua_media_config_default(&media_cfg);

    media_cfg.clock_rate = 8000;        // 8kHz采样率
    media_cfg.audio_frame_ptime = 20;   // 20ms帧长
    media_cfg.snd_auto_close_time = 1;

    printf("[系统初始化] 初始化PJSUA...\n");
    status = pjsua_init(&cfg, &log_cfg, &media_cfg);
    if (status != PJ_SUCCESS)
    {
        pj_strerror(status, errmsg, sizeof(errmsg));
        printf("PJSUA初始化失败: %s\n", errmsg);
        ret = -1;   
    }

    return ret;
}

/*
 * @brief 配置音频编码器
 * @return 0 成功
 * @return -1 失败
 * 注:配置 PJSIP 仅使用 PCMA (G.711 A-law) 音频编解码器,禁用所有其他编解码器。
 * TODO:如果未找到 PCMA,所有编解码器都被禁用,将导致通话失败
*/
static int sip_audio_codec_config()
{
    int ret = 0;
    pj_status_t status = PJ_SUCCESS;
    pjsua_codec_info codec_list[16] = {0};
    unsigned int max_count = 16;  // 数组最大容量
    unsigned int actual_count;    // 实际枚举到的数量
    pj_str_t pcma_name;memset(&pcma_name, 0, sizeof(pj_str_t));
    pj_bool_t pcma_found = PJ_FALSE;

    /* 1.使用空音频设备,不能禁用音频设备,不然发送接收回调跑不进去*/
    pjsua_set_null_snd_dev();

    // 2. 枚举所有支持的编解码器
    status = pjsua_enum_codecs(codec_list, &max_count);
    if (status != PJ_SUCCESS) {
        PJ_LOG(2, ("Codec", "枚举编解码器失败"));
    }
    actual_count = max_count;  // 输出时,max_count 已更新为实际数量

    // 3. 遍历列表,检查是否包含 PCMA
    pcma_name = pj_str("PCMA/8000/1");
    for (unsigned int i = 0; i < actual_count; i++) {
        pjsua_codec_info *codec = &codec_list[i];
        pj_str_t tmp = pj_str(codec->codec_id.ptr);
            // 先检查指针有效性
        if (codec->codec_id.ptr == NULL) {
            printf("Warning: codec_id.ptr is NULL at index %u\n", i);
            continue;
        }
        // 检查是否为 PCMA
        if (pj_strcmp(&codec->codec_id, &pcma_name) == 0) {
            pcma_found = PJ_TRUE;
            pjsua_codec_set_priority(&tmp, 128); 
        }
        else{
            pjsua_codec_set_priority(&tmp, 0);    // 0 = 禁用
        }
        printf("codec_id %s,desc %s,priority %d\n",codec->codec_id.ptr,codec->desc.ptr,codec->priority);
    }
    // 4. 输出检查结果
    printf("PCMA 编解码器检查结果:%d", pcma_found);
    if (pcma_found) {
        PJ_LOG(2, ("PCMA Check", "PCMA 编解码器已被支持"));
        printf("PCMA 编解码器已被支持");
    } else {
        PJ_LOG(2, ("PCMA Check", "未找 PCMA 编解码器,请检查编译配置"));
        printf("未找 PCMA 编解码器,请检查编译配置");
    }

    return ret;
}

/*
 * @brief 配置网络参数
 * @return 0 成功
 * @return -1 失败
*/
static int sip_network_config()
{
    int ret = 0;
    pj_status_t status = PJ_SUCCESS;
    pjsua_transport_config trans_cfg;
    memset(&trans_cfg, 0, sizeof(pjsua_transport_config));

    pjsua_transport_config_default(&trans_cfg);

    printf("[网络配置] 创建UDP传输 ...\n");
    status = pjsua_transport_create(PJSIP_TRANSPORT_UDP, &trans_cfg, NULL);
    if (status != PJ_SUCCESS)
    {
        char errmsg[PJ_ERR_MSG_SIZE];
        pj_strerror(status, errmsg, sizeof(errmsg));
        printf("传输创建失败: %s\n", errmsg);
        ret = -1;   
    }

    return ret;
}

static unsigned int sip_account_config()
{
    unsigned int ret = 0;
    char errmsg[PJ_ERR_MSG_SIZE] = {0};
    pj_status_t status = PJ_SUCCESS;
    unsigned short SIP_PORT = 0;
    char id[128] = {0}, uri[128] = {0};
    char USER_NAME[64] = {0}, PASSWD[64] = {0};
    char SERVER_IP[32] = {0}, PORT[32] = {0};
    pjsua_acc_config acc_cfg;
    memset(&acc_cfg, 0, sizeof(pjsua_acc_config));
    pjsua_acc_id acc_id = 0;

    pjsua_acc_config_default(&acc_cfg);
    
    //获取配置
    api_properties_get(SIP_KEY_USERNAME, USER_NAME, SIP_DEFAULT_USERNAME);
    api_properties_get(SIP_KEY_PASSWORD, PASSWD, SIP_DEFAULT_PASSWORD);
    api_properties_get(SIP_KEY_IP, SERVER_IP, SIP_DEFAULT_IP);
    api_properties_get(SIP_KEY_PORT, PORT, SIP_DEFAULT_PORT);
    SIP_PORT = (unsigned short)atoi(PORT);
    printf("USER_NAME %s,PASSWD %s,SERVER_IP %s,PORT %s,SIP_PORT %d", 
        USER_NAME, PASSWD, SERVER_IP, PORT, SIP_PORT);

    snprintf(id, sizeof(id), "sip:%s@%s", USER_NAME, SERVER_IP);
    snprintf(uri, sizeof(uri), "sip:%s:%d", SERVER_IP, SIP_PORT);
    printf("id %s,uri %s", id, uri);

    acc_cfg.id = pj_str(id);
    acc_cfg.reg_uri = pj_str(uri);
    acc_cfg.cred_count = 1;
    acc_cfg.cred_info[0].realm = pj_str( (char*)("*"));
    acc_cfg.cred_info[0].username = pj_str( (char*)(USER_NAME));
    acc_cfg.cred_info[0].data_type = PJSIP_CRED_DATA_PLAIN_PASSWD;
    acc_cfg.cred_info[0].data = pj_str( (char*)(PASSWD));
    
    // 绑定 SIP 账户配置
    printf("[账户配置] 添加SIP账户 (用户=%s, 服务IP=%s)...\n", USER_NAME, SERVER_IP);
    status = pjsua_acc_add(&acc_cfg, PJ_TRUE, &acc_id);
    if (status != PJ_SUCCESS)
    {
        pj_strerror(status, errmsg, sizeof(errmsg));
        printf("账户添加失败: %s\n", errmsg);
        ret = -1;   
    }

    return ret;
}

static int sip_start_pjsua()
{
    int ret = 0;
    char errmsg[PJ_ERR_MSG_SIZE] = {0};
    pj_status_t status = PJ_SUCCESS;
    
    printf("[系统启动] 启动PJSUA...\n");
    status = pjsua_start();
    if (status != PJ_SUCCESS)
    {
        pj_strerror(status, errmsg, sizeof(errmsg));
        printf("PJSUA启动失败: %s\n", errmsg);
        return -1;// PJSUA启动失败,终止流程
    }
    pool_t = pjsua_pool_create("custom", 1000, 1000);
    if (!pool_t) {
        printf("创建内存池失败\n");
        return -1;// 内存池创建失败,终止流程
    }
    printf("[媒体配置] 创建自定义音频端口...\n");

    status = create_custom_audio_port(pool_t, 8000, 160);
    if (status != PJ_SUCCESS) {
        printf("自定义端口注册到会议桥失败(状态码: %d)\n", status);
        pjmedia_port_destroy(custom_audio_port); // 销毁未注册的端口
        pj_pool_release(pool_t);
        return -1;// 自定义音频端口注册失败,终止流程
    }
    printf("[系统就绪] 等待来电... \n");

    return ret;
}

/*
 * @brief 初始化一些全局状态
 * @return 0 成功
 * @return -1 失败
*/
static int sip_init_global_status()
{
    int ret = 0;
    pthread_mutex_lock(&g_sip_mutex);
    g_is_run = 1;
    g_is_main_thread_exited = 0;
    pthread_mutex_unlock(&g_sip_mutex);
    return ret;
}

/**
 * SIP服务主线程
 * @param args 线程参数(未使用)
 */
static void *sip_service_thread(void *args) 
{
    char errmsg[PJ_ERR_MSG_SIZE] = {0};
    pj_status_t status = PJ_SUCCESS;

    PJ_UNUSED_ARG(args);
    printf("[系统初始化] 开始创建PJSUA实例...\n");

    status = pjsua_create();
    if (status != PJ_SUCCESS)
    {
        pj_strerror(status, errmsg, sizeof(errmsg));
        printf("PJSUA创建失败: %s\n", errmsg);
        return NULL;// pjsua_create 失败,不能调用 pjsua_destroy
    }

    g_is_pjsua_created = 1; // 标记为已创建

    // 1. 配置PJSUA
    if (sip_config_pjsua() != 0)
    {
        printf("PJSUA配置失败\n");
        goto cleanup;
    }

    // 2. 配置音频编码器
    if (sip_audio_codec_config() != 0)
    {
        printf("音频编码器配置失败\n");
        goto cleanup;
    }

    // 3. 配置网络参数
    if (sip_network_config() != 0)
    {
        printf("网络配置失败\n");
        goto cleanup;
    }
    
    // 4. 配置账户
    if (sip_account_config() != 0)
    {
        printf("账户配置失败\n");
        goto cleanup;
    }
    
    // 5. 启动PJSUA
    if (sip_start_pjsua() != 0)
    {
        printf("PJSUA启动失败\n");
        goto cleanup;
    }

    // 6.1 初始化全局状态
    if (sip_init_global_status() != 0)
    {
        printf("初始化全局状态失败\n");
        goto cleanup;
    }

    // 6.2 主循环等待事件处理
    while (1)
    {
        pthread_mutex_lock(&g_sip_mutex);
        if (!g_is_run)
        {
            printf("sip server stop, break");
            pthread_mutex_unlock(&g_sip_mutex);
            break;
        }
        pthread_mutex_unlock(&g_sip_mutex);

        pjsua_handle_events(100);
    }

    printf("[PJSUA系统关闭] SIP 线程退出\n");

    pthread_mutex_lock(&g_sip_mutex);
    g_is_main_thread_exited = 1;
    pthread_cond_signal(&g_sip_thread_exit_cond);
    pthread_mutex_unlock(&g_sip_mutex);
    //正常退出销毁资源统一由Stop_SIP_Service()处理
    return NULL;

cleanup:
    // 统一清理:销毁已创建的 PJSUA 实例
    if (g_is_pjsua_created)
    {
        g_is_pjsua_created = 0;
        pjsua_destroy();
    }
    printf("[系统清理] 配置失败,已释放PJSUA资源\n");
    return NULL;
}

/*
    breif:判断参数是否合法
*/
static int sip_param_check()
{
    int ret = 0;
    char SWITCH[32] = {0}, USER_NAME[64] = {0}, PASSWD[64] = {0};
    char SERVER_IP[32] = {0}, PORT[32] = {0};
    
    //获取配置
    api_properties_get(SIP_KEY_SWITCH, SWITCH, SIP_DEFAULT_SWITCH);
    api_properties_get(SIP_KEY_USERNAME, USER_NAME, SIP_DEFAULT_USERNAME);
    api_properties_get(SIP_KEY_PASSWORD, PASSWD, SIP_DEFAULT_PASSWORD);
    api_properties_get(SIP_KEY_IP, SERVER_IP, SIP_DEFAULT_IP);
    api_properties_get(SIP_KEY_PORT, PORT, SIP_DEFAULT_PORT);

    if (strlen(SWITCH) == 0 || strlen(USER_NAME) == 0 || strlen(PASSWD) == 0 || strlen(SERVER_IP) == 0 || strlen(PORT) == 0)
    {
        printf("sip params error\n");
        ret = -1;
    }

    return ret;
}

//声明函数
int Start_SIP_Service(void);
int Stop_SIP_Service(void);
static int g_is_create_restart_th = 0; //false:未创建重启线程,true:已创建重启线程
#define MAX_WAIT_TIME (5) // 最大等待时间,单位秒

// 防止同一时刻,多个参数变化导致多次stop->start
static void * sip_restart_thread(void *args)
{
    unsigned int enter_time = SYS_SEC;
    unsigned int cur_sec = 0;
    char value[32] = {0};
    int need_stop = 0; //true:已经开启服务,需要关闭sip功能

    while (1)
    {
        cur_sec = SYS_SEC;
        if (cur_sec - enter_time >= MAX_WAIT_TIME)
        {
            printf("has %d s, restart sip service\n", MAX_WAIT_TIME);
            break;
        }
        sleep(1);
        printf("wait 1s, continue...\n");
    }

    // 1.需要先关闭sip功能,再打开
    pthread_mutex_lock(&g_sip_mutex);
    need_stop = g_is_run;
    pthread_mutex_unlock(&g_sip_mutex);
    if (1 == need_stop)
    {
        Stop_SIP_Service();
    }

    sleep(1);// 等待1s再启动, 防止启动过快功能异常
    api_properties_get(SIP_KEY_SWITCH, value, SIP_DEFAULT_SWITCH);
    if (0 == strcasecmp(value, "on"))
    {
        Start_SIP_Service();
    }
    else
    {
        printf("SIP服务 功能关闭,不启动线程\n");
    }

    pthread_mutex_lock(&g_sip_mutex);
    g_is_create_restart_th = 0;
    pthread_mutex_unlock(&g_sip_mutex);
    return NULL;
}

static void sip_properties_listener_cb(PROPERTIES_LISTENER e_action, const char *pc_key, const char *pc_value)
{
    int need_create = 0;

    if (strcmp(pc_key, SIP_KEY_SWITCH) == 0 ||
        strcmp(pc_key, SIP_KEY_USERNAME) == 0 ||
        strcmp(pc_key, SIP_KEY_PASSWORD) == 0 ||
        strcmp(pc_key, SIP_KEY_IP) == 0 ||
        strcmp(pc_key, SIP_KEY_PORT) == 0)
    {
        printf("sip properties update...pc_key %s, pc_value %s", pc_key, pc_value);
        
        // 1.判断参数是否合法
        if (0 != sip_param_check())
        {
            printf("sip params error\n");
            return;
        }

        // 2.判断是否需要创建重启线程
        pthread_mutex_lock(&g_sip_mutex);
        need_create = !g_is_create_restart_th;
        if (need_create) {
            g_is_create_restart_th = 1;
        }
        pthread_mutex_unlock(&g_sip_mutex);

        if (need_create)
        {
            os_th_t th_restart;
            if( api_os_th_create( &th_restart, sip_restart_thread, NULL, "sip_restart_thread", 0) != 0)
            {
                printf("线程创建失败\n");
                pthread_mutex_lock(&g_sip_mutex);
                g_is_create_restart_th = 0;
                pthread_mutex_unlock(&g_sip_mutex);
                return;
            }
        }
    }

    return;
}

/*
    breif:注册sip参数监听器
*/
int sip_properties_listener_reg()
{
    char id[32];
    memset(id, 0, sizeof(id));
    snprintf(id, sizeof(id), "%s", SIP_PROPERTIES_LISTENER_ID);
    return api_register_properties_listener(id, sip_properties_listener_cb);

}

/*
    breif:注销sip参数监听器
*/
int sip_properties_listener_unreg()
{
    char id[32];
    memset(id, 0, sizeof(id));
    snprintf(id, sizeof(id), "%s", SIP_PROPERTIES_LISTENER_ID);
    return api_unregister_properties_listener(id);
}

// 启动SIP服务线程
int Start_SIP_Service(void) 
{
    pthread_mutex_lock(&g_sip_mutex);
    if (g_is_run)
    {
        pthread_mutex_unlock(&g_sip_mutex);
        printf("SIP服务 已启动,不重复启动\n");
        return 0;
    }
    pthread_mutex_unlock(&g_sip_mutex);

    char value[32] = {0};

    // 1.根据配置参数判断是否启动线程
    api_properties_get(SIP_KEY_SWITCH, value, SIP_DEFAULT_SWITCH);
    if (0 == strcasecmp(value, "off"))
    {
        printf("SIP服务 功能关闭,不启动线程\n");
        return 0;
    }
    else if (0 == strcasecmp(value, "on"))
    {
        if (0 != sip_param_check())
        {
            return -1;
        }

        printf("[服务启动] 正在创建SIP服务线程...\n");
        if( api_os_th_create( &g_thSIP, sip_service_thread, NULL, "sip_service_thread", 0) != 0)
        {
            printf("线程创建失败\n");
            return -1;
        }
    }
    
    return 0;
}

/*
    停止SIP服务线程
    注:释放资源流程
    调用 Stop_SIP_Service()
            ↓
    pjsua_conf_remove_port(audio_port_id)
            ↓
    触发 custom_audio_on_destroy()
            ↓
    custom_audio_port = NULL;  // 重置全局指针
            ↓
    pj_pool_release(pool_t)     // 释放内存池(包括 custom_audio_port 内存)
            ↓
    pjsua_destroy()             // 停止 PJSUA 框架
*/
int Stop_SIP_Service(void)
{
    struct timespec timeout;memset(&timeout, 0, sizeof(struct timespec));

    pthread_mutex_lock(&g_sip_mutex);
    if (!g_is_run) {
        pthread_mutex_unlock(&g_sip_mutex);
        printf("sip server no start, return 0\n");
        return 0;
    }
    g_is_run = 0;

    printf("[服务停止] 正在停止SIP服务...\n");
    
    // 等待主线程退出(带超时)
    clock_gettime(CLOCK_REALTIME, &timeout);
    timeout.tv_sec += 5;  // 从现在起等待5秒
    while (!g_is_main_thread_exited) { // pthread_cond_timedwait 需要锁
        if (pthread_cond_timedwait(&g_sip_thread_exit_cond, &g_sip_mutex, &timeout) == ETIMEDOUT) {
            printf("主线程退出超时\n");
            break;
        }
    }
    printf("主线程已成功退出\n");
    pthread_mutex_unlock(&g_sip_mutex);

    // 1. 停止音频播放
    stop_audio_playback();

    printf("sip server stop ing, step 1");
    
    // 2. 从会议桥移除音频端口
    if (audio_port_id != PJSUA_INVALID_ID) {
        pjsua_conf_remove_port(audio_port_id);
        audio_port_id = PJSUA_INVALID_ID;
        printf("[媒体端口] 已从会议桥移除\n");
    }
    printf("sip server stop ing, step 2");
    
    // 3. 释放内存池(会自动释放 custom_audio_port)
    if (pool_t != NULL) {
        pj_pool_release(pool_t);
        pool_t = NULL;
        custom_audio_port = NULL;
        printf("[内存池] 已释放\n");
    }
    printf("sip server stop ing, step 3");
    
    // 4. 停止PJSUA
    if (g_is_pjsua_created)
    {
        g_is_pjsua_created = 0;
        pjsua_destroy();
    }
    printf("sip server stop ing, step 4");
    
    printf("[服务停止] SIP服务已完全停止\n");

    return 0;
}

五、TCP/UDP抓包数据分析
1.stun查询外网ip
stun查询外网ip

2.stun返回设备的公网ip
stun返回设备的公网ip

3.设备NAT打洞成功
设备NAT打洞成功

4.开始注册
开始注册

5.服务器应答注册
服务器应答注册

6.客户端以最新的外网端口重新注册
客户端以最新的外网端口重新注册

7.服务器应答注册成功
服务器应答注册成功

8.主叫方发起电话邀请
主叫方发起电话邀请

9.被叫方查询rtp链路外网端口
被叫方查询rtp链路外网端口

10.被叫方应答电话邀请
被叫方应答电话邀请

11.呼叫方和被叫方互相发送rtp数据包
呼叫方和被叫方互相发送rtp数据包

12.播放rtp的音频数据:wireshark软件的电话->rtp->rtp流分析
播放rtp的音频数据

posted @ 2026-07-19 16:30  今天也想躺ping  阅读(45)  评论(0)    收藏  举报