如何配置 TAPI 监控并控制 Avaya IP Office 呼叫?
结论
当前代码收不到事件,是因为客户端没有建立完整的事件订阅链路:
MyTapiEventHandler只有类定义,没有创建实例,也没有通过 COM connection point 调用Advise,因此 TAPI 没有事件接收对象。- 代码没有调用
ITTAPI::put_EventFilter订阅TE_CALLNOTIFICATION、TE_CALLSTATE等事件。 - 程序通过
CoInitialize创建了 STA COM apartment,但等待循环只有Sleep,没有处理 Windows 消息。封送到该线程的 COM 事件可能无法分派。 LINECALLSTATE_OFFERING属于 TAPI 2.x。使用 TAPI 3.x 时,应监听TE_CALLSTATE,再调用ITCallStateEvent::get_State判断状态是否为CS_OFFERING。
能够枚举所有 ITAddress,只能说明 TAPI Service Provider 已经加载,无法证明当前许可证、账号和线路允许第三方监控或控制。监控多个分机通常需要 Avaya IP Office 相应版本提供的第三方 CTI/TAPI 授权,例如 TAPI Link Pro 或 CTI Link Pro。许可证名称和权限项可能随 IP Office 版本变化,需要根据当前版本核对 Avaya 文档。
事件订阅流程
TAPI 3.x 的基本调用顺序如下:
- 初始化 COM 和 TAPI。
- 设置事件过滤器。
- 从
ITTAPI获取IConnectionPointContainer。 - 找到
IID_ITTAPIEventNotification对应的 connection point。 - 创建事件处理器并调用
Advise。 - 对目标
ITAddress调用RegisterCallNotifications。 - 保持消息循环运行。
- 退出时调用
UnregisterNotifications和Unadvise。
RegisterCallNotifications 用于注册地址和呼叫权限范围,不会自动连接 MyTapiEventHandler。
监听 TE_CALLSTATE
要识别电话进入振铃或等待接听状态,应处理 TE_CALLSTATE:
HRESULT STDMETHODCALLTYPE Event(
TAPI_EVENT tapiEvent,
IDispatch* pEvent
) override {
if (pEvent == nullptr) {
return E_POINTER;
}
if (tapiEvent == TE_CALLNOTIFICATION) {
ITCallNotificationEvent* notification = nullptr;
HRESULT hr = pEvent->QueryInterface(
IID_ITCallNotificationEvent,
reinterpret_cast<void**>(¬ification)
);
if (SUCCEEDED(hr)) {
ITCallInfo* call = nullptr;
if (SUCCEEDED(notification->get_Call(&call)) && call != nullptr) {
BSTR callerId = nullptr;
if (SUCCEEDED(call->get_CallInfoString(
CIS_CALLERIDNUMBER,
&callerId)) &&
callerId != nullptr) {
std::wcout << L"New call, caller: "
<< callerId << std::endl;
SysFreeString(callerId);
}
call->Release();
}
notification->Release();
}
}
if (tapiEvent == TE_CALLSTATE) {
ITCallStateEvent* stateEvent = nullptr;
HRESULT hr = pEvent->QueryInterface(
IID_ITCallStateEvent,
reinterpret_cast<void**>(&stateEvent)
);
if (SUCCEEDED(hr)) {
CALL_STATE state = CS_IDLE;
if (SUCCEEDED(stateEvent->get_State(&state))) {
std::wcout << L"Call state: " << state << std::endl;
if (state == CS_OFFERING) {
std::wcout << L"Incoming call is offering."
<< std::endl;
// 在这里执行路由规则,但不要阻塞回调线程。
}
}
stateEvent->Release();
}
}
return S_OK;
}
TE_CALLNOTIFICATION 表示应用程序获得了一个呼叫对象,与 LINECALLSTATE_OFFERING 含义不同。呼叫状态的变化需要从 TE_CALLSTATE 事件中读取。
注册事件接收器
ITTAPI::Initialize 成功后,设置事件过滤器并注册 connection point:
#include <windows.h>
#include <tapi3.h>
#include <ocidl.h>
#include <iostream>
ITTAPI* pTapi = nullptr;
IConnectionPoint* pConnectionPoint = nullptr;
MyTapiEventHandler* pEventHandler = nullptr;
DWORD adviseCookie = 0;
long registrationId = 0;
// 创建并初始化 pTapi 后设置事件过滤器。
HRESULT hr = pTapi->put_EventFilter(
TE_CALLNOTIFICATION |
TE_CALLSTATE |
TE_CALLINFOCHANGE
);
if (FAILED(hr)) {
std::wcerr << L"put_EventFilter failed: 0x"
<< std::hex << hr << std::endl;
// 执行清理并退出。
}
// 获取 TAPI 的 connection point。
IConnectionPointContainer* pContainer = nullptr;
hr = pTapi->QueryInterface(
IID_IConnectionPointContainer,
reinterpret_cast<void**>(&pContainer)
);
if (SUCCEEDED(hr)) {
hr = pContainer->FindConnectionPoint(
IID_ITTAPIEventNotification,
&pConnectionPoint
);
pContainer->Release();
}
if (FAILED(hr)) {
std::wcerr << L"FindConnectionPoint failed: 0x"
<< std::hex << hr << std::endl;
// 执行清理并退出。
}
// 实例必须存活到 Unadvise 完成。
pEventHandler = new MyTapiEventHandler();
hr = pConnectionPoint->Advise(
static_cast<IUnknown*>(pEventHandler),
&adviseCookie
);
if (FAILED(hr)) {
std::wcerr << L"Advise failed: 0x"
<< std::hex << hr << std::endl;
// 执行清理并退出。
}
// pSpecificAddress 是已经找到的目标 ITAddress。
hr = pTapi->RegisterCallNotifications(
pSpecificAddress,
VARIANT_TRUE, // monitor
VARIANT_FALSE, // owner,先仅监控
TAPIMEDIATYPE_AUDIO,
0,
®istrationId
);
if (FAILED(hr)) {
std::wcerr << L"RegisterCallNotifications failed: 0x"
<< std::hex << hr << std::endl;
}
排查事件接收问题时,可以先使用:
fMonitor = VARIANT_TRUE;
fOwner = VARIANT_FALSE;
这组参数只申请 monitor privilege。将 fOwner 设为 VARIANT_TRUE 会同时申请 owner privilege。如果提供程序、许可证或当前线路不支持该权限,排查时还会多出一层权限问题。先确认监控正常,再单独验证 owner privilege 和呼叫控制能力。
STA 线程需要处理消息
原代码的循环只调用 Sleep(100),无法可靠分派 COM 事件。可改为带消息处理的循环:
bool running = true;
MSG msg{};
while (running) {
DWORD result = MsgWaitForMultipleObjects(
0,
nullptr,
FALSE,
100,
QS_ALLINPUT
);
if (result == WAIT_OBJECT_0) {
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
running = false;
break;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
if (GetAsyncKeyState('Q') & 0x0001) {
running = false;
}
}
不要在事件回调中执行耗时的数据库查询、网络请求或复杂路由计算。可以复制后续任务需要的呼叫信息,并把任务投递到工作线程。需要跨线程使用 COM 接口指针时,必须按照 COM 规则进行封送,不能直接把裸指针交给另一个线程。
清理注册和资源
退出时按注册的逆序释放资源:
if (registrationId != 0) {
pTapi->UnregisterNotifications(registrationId);
registrationId = 0;
}
if (pConnectionPoint != nullptr && adviseCookie != 0) {
pConnectionPoint->Unadvise(adviseCookie);
adviseCookie = 0;
}
if (pEventHandler != nullptr) {
pEventHandler->Release();
pEventHandler = nullptr;
}
if (pConnectionPoint != nullptr) {
pConnectionPoint->Release();
pConnectionPoint = nullptr;
}
pTapi->Shutdown();
pTapi->Release();
CoUninitialize();
事件处理器需要一直存活到 Unadvise 完成。在 Advise 后过早调用 Release,或在 Unadvise 前销毁对象,会导致后续回调丢失,严重时还可能访问无效内存。
检查目标地址
当前代码使用子串匹配:
if (addressName.find(targetExtension) != std::wstring::npos)
这种写法可能选错地址。例如查找 5002 时,名称中包含 50020 的地址也会匹配。应先打印完整地址名,再根据 Avaya TSP 返回的实际格式进行精确比较:
if (addressName == targetExtension) {
found = true;
break;
}
如果地址名中还包含用户名、设备类型或其他前缀,可以建立明确的“分机号到 ITAddress”映射,避免依赖任意子串匹配。
还要检查地址能力,确认该地址支持音频呼叫、监控以及所需的呼叫控制操作。能够枚举地址,并不代表它支持 Transfer、BlindTransfer 或 owner privilege。
核对 Avaya IP Office 配置
Avaya 侧需要确认以下条件:
- TAPI/TSPI 客户端版本、Windows 位数和 C++ 程序位数相互兼容。32 位 TSP 通常只能供 32 位进程使用,64 位环境需要安装对应组件。
- TSP 配置的 IP Office 地址、账号和认证信息正确。
- 当前账号有权查看需要监控的用户或分机。
- 系统拥有第三方呼叫监控和控制所需的许可证。只有基础或单用户 TAPI 能力时,可能可以枚举地址,但无法监控任意分机。
- 目标分机确实由 IP Office 控制,测试呼叫也经过该地址,没有提前被其他队列、寻线组、SIP 终端或转发规则改道。
- IP Office、TSP 和客户端组件属于受支持的组合。
不同 IP Office 版本使用的许可证名称、管理界面和权限项不完全相同,不能仅根据“能看到全部分机”判断 CTI 权限已经配置完成。
可以先用 Avaya 随 TSP 提供的测试工具,或一个最小化 TAPI 测试程序监控同一分机。如果测试工具也收不到事件,应先检查许可证、账号权限和 TSP 配置。
按业务规则转移呼叫
收到 CS_OFFERING 后,可以从 ITCallStateEvent 获取 ITCallInfo,再查询 ITBasicCallControl:
ITCallInfo* callInfo = nullptr;
if (SUCCEEDED(stateEvent->get_Call(&callInfo)) &&
callInfo != nullptr) {
ITBasicCallControl* callControl = nullptr;
HRESULT hr = callInfo->QueryInterface(
IID_ITBasicCallControl,
reinterpret_cast<void**>(&callControl)
);
if (SUCCEEDED(hr)) {
hr = callControl->BlindTransfer(
_bstr_t(L"5003")
);
if (FAILED(hr)) {
std::wcerr << L"BlindTransfer failed: 0x"
<< std::hex << hr << std::endl;
}
callControl->Release();
}
callInfo->Release();
}
这段代码能否成功,取决于 Avaya TSP 暴露的能力、当前呼叫状态、owner privilege、号码格式和 IP Office 权限。部分环境不允许在 CS_OFFERING 阶段直接转移,可能要求先应答,也可能需要通过 PBX 的转接、寻线组或短代码等机制完成路由。调用前应先查询地址和呼叫能力,再确定合适的调用时机。
呼叫事件可能重复触发,状态也可能快速变化。路由逻辑应根据呼叫标识维护状态,防止同一呼叫在多次 TE_CALLSTATE 回调中被重复转移。
TAPI 3.x 不适用时的选择
可以根据用途考虑以下方案:
- 如果 Avaya TSP 对 TAPI 2.x 的支持更完整,可以直接使用 TAPI 2.x。此时使用
LINE_CALLSTATE和LINECALLSTATE_OFFERING,不要把 TAPI 2.x 常量与 TAPI 3.x COM 接口混用。 - 如果只需要获取呼叫活动或生成统计数据,可以评估 Avaya DevLink。它主要提供事件和状态数据,不能直接假定它具备完整的呼叫控制能力。
- 如果路由规则比较固定,可以优先在 IP Office 内配置寻线组、短代码、呼叫路由或其他 PBX 规则,以免依赖外部应用持续在线。
- 如果外部业务系统必须实时决策,应使用当前 IP Office 版本正式支持的 CTI 接口,并确认该接口允许第三方监控和呼叫控制。
排查时可以先完成一个最小闭环:只注册一个明确的 ITAddress,只监听 TE_CALLNOTIFICATION | TE_CALLSTATE,使用 monitor privilege,并在 STA 线程中运行消息循环。收到 CS_OFFERING 后,再加入 owner privilege 和转移逻辑。这样可以分别确认客户端订阅、Avaya 权限和具体呼叫控制能力是否正常。
备注:内容仅供参考。
