MacroSIP
EnglishРусский中文

架构

MacroSIP 是一个 SwiftPM 包(Package.swift,Swift 6,macOS 14+),没有 Xcode 项目。Swift 代码从不包含 PJSIP 头文件:一切都通过 CPJSUA C 目标访问 PJSIP,而 UI 完全不导入 CPJSUA。本页的权威来源是 docs/ENGINE.md、docs/DIRECTORY.md、docs/DEVELOPMENT.md 以及 docs/adr/ 中的 ADR。

+----------------------------------------------------------------------+
|  MacroSIPApp  (SwiftUI + AppKit)                                     |
|  AppModel and its extensions · Views · Platform · Mock engine, demos |
+----------------------------------------------------------------------+
|  MacroSIPCore  (Swift, no UI imports)                                |
|  Engine actor · Models · Storage · Recording · Directory service     |
+-----------------------------------+----------------------------------+
|  CPJSUA  (C shim, umbrella header) |  CLDAP  (C shim over libldap)   |
+-----------------------------------+----------------------------------+
|  PJSIP 2.17 + OpenSSL 3.5 + Opus   |  libldap from the macOS SDK     |
|  static, universal archives        |                                 |
+-----------------------------------+----------------------------------+
   macrosip-cli -> MacroSIPCore      pjsip-smoke -> CPJSUA
   Tests/MacroSIPCoreTests -> MacroSIPCore

目标

目标 路径 内容
CPJSUA Sources/CPJSUA 伞头文件 CPJSUA.h(定义 PJ_AUTOCONF=1,包含 pjsua.h)和 shim.c,后者包含 Swift 无法表达的辅助函数(线程注册、录音器销毁并等待、音调和增益端口、DTMF、IP 变化和域名服务器辅助函数)。链接 PJSIP、OpenSSL 和 Opus 归档。
CLDAP Sources/CLDAP 系统 OpenLDAP 客户端(libldap)之上的不透明句柄封装,使 Swift 从不导入 <ldap.h>。
MacroSIPCore Sources/MacroSIPCore 引擎(Engine/)、模型(Model/)、JSON 存储和钥匙串(Storage/)、录音转换(Recording/)、目录层(Directory/)、Product.swift。
MacroSIPApp Sources/MacroSIPApp 应用程序:Model/(AppModel)、Views/、Platform/、Engine/(工厂、模拟引擎、模拟目录)以及包含 en.lproj 和 ru.lproj 的 Resources/。依赖 Sparkle 2.7.3(ADR 0013)。
macrosip-cli Sources/macrosip-cli 基于 MacroSIPCore 的命令行电话:用于对 PBX 调试的交互模式,以及由 scripts/selftest-call.sh 驱动的 --selftest。包含 SIPStub,一个用于自测的小型 UDP 注册服务器。
pjsip-smoke Sources/pjsip-smoke 证明 Swift 能链接静态构建:创建 pjsua,打印版本、编解码器和音频设备,以 0 退出。
MacroSIPCoreTests Tests/MacroSIPCoreTests Core 的 Swift Testing 测试套件(见 测试)。

引擎 (MacroSIPCore)

单个 actor。Engine 是一个运行在串行 DispatchQueue("macrosip.engine") 上的 Swift actor;每个 pjsua_* 调用都在其中运行。PJSIP 要求调用线程已注册,而 GCD 可能在不同的线程池线程上运行该队列,因此每个入口点都调用 cpjsua_thread_register()(线程局部的 pj_thread_desc,注册后再调用为空操作)。pjsua_config.thread_cnt = 1;PJSIP 的工作线程轮询事件。

**回调与事件流。**pjsua 回调是没有上下文的 C 函数指针,因此它们是全局 Swift 函数,通过 EngineBridge.shared(一个受锁保护的类)访问引擎。回调将最少的数据复制到 EngineEvent 中,将其送入 Engine.events(一个单消费者的 AsyncStream)并立即返回;引擎必须完成的工作被调度为 actor 上的 Task,并且是幂等的。应用在主 actor 上迭代该流。

通话快照。CallInfo 是不可变的值:id、账户、方向、状态、远端、开始时间、静音/保持/录音/会议标志、编解码器、isSecure、isReconnecting。CallID 是 (slot, serial):pjsua 复用槽位,序列号永不重复,过期的 id 抛出 .noSuchCall。UI 在 .incomingCall 和 .callChanged 时更新或插入行,在 .callEnded 时移除。媒体连线是对所有活动通话和录音的一次期望状态调和(reconcileMedia())。

账户。AccountMapping 将 AccountConfig 转换为 pjsua_acc_config(传输、SRTP、注册服务器、代理、认证 id、注册间隔、发布、RTP 端口窗口)。密码不在 AccountConfig 中;它们来自钥匙串。已停用的账户根本不会添加到 pjsua;registerWithServer 关闭表示仅外拨。TLS 有两个监听器:默认监听器根据导出到 ca-roots.pem 的 macOS 系统根证书验证服务器(TLSRoots.swift);另一个服务于关闭了验证的账户。

重新连接与退避(Engine+Network.swift)。NWPathMonitor 将每条路径归约为其主路由;路由变化、恢复在线或唤醒会运行一个“回合”:pjsua_handle_ip_change 重新注册账户,已确认的通话收到 re-INVITE,从而在 IP 变化后存续。临时的 REGISTER 失败变为 .reconnecting(attempt:nextRetryIn:lastError:),延迟为 2、4、8、16、32,然后 60 秒;永久失败(401、403、TLS 证书错误)变为 .failed。媒体看门狗用对话内 OPTIONS 探测静默的通话,并在收到 408 或 481 时以“连接已断开”结束它。

质量模型(Engine+Quality.swift、CallQualityModel.swift)。quality(of:) 读取自上次采样以来窗口内的流 RTCP 统计(应用每 2 秒轮询一次),并根据编解码器、丢包、抖动和 RTT 计算简化的 ITU-T G.107 MOS:3.8 起为良好,3.0 起为一般,否则为差。

在线状态。subscribePresence(uri:) 添加一个以令牌为键的 pjsua buddy;NOTIFY 正文映射到 PresenceStatus(available、away、dnd、ringing、offline),包括 Asterisk 的“on the phone”和“Ringing”备注。PresenceRegistry 是一个引用计数的关注者表:联系人和快捷键对同一目标共享一个引擎订阅;最后一个所有者取消订阅。

自动接听是一个纯函数 AutoAnswerPolicy.decide(...):勿扰模式优先,存在另一路通话时绝不接听,Call-Info: answer-after=N 仅在来自账户自己的服务器时遵从,然后按模式 all / contactsOnly / off 和配置的延迟处理。

拨号字符串。DialString.parse(raw, account:) 产生完整的 URI:sip:/sips: 原样保留,tel: 数字转为 sip:<digits>@<domain>,普通号码去除空格、括号和连字符,任何包含 @ 的内容加上 sip: 前缀;TCP/TLS 账户会添加传输参数。

**通话选项与拒接规则。**一个引擎范围的 CallOptions(在 start 时设置,通过 setCallOptions 实时更改):incomingWhileOnCall(呼叫等待提示音、仅窗口或 486 忙)、dtmfMethod(自动、RFC 2833、SIP INFO、带内)、doNotDisturb(立即 486)和 denyIncoming(everyone 发送 603,notInContacts 检查 knownNumbers,anonymous 回答 433)。这些规则在 PJSIP 线程上的 on_incoming_call 内、自动 180 之前求值,因此不会有任何响铃,决定也不依赖应用是否响应。应用只看到 .callEnded 并写入通话记录注释。

应用程序 (MacroSIPApp)

AppModel(@MainActor @Observable,Model/AppModel.swift)是 UI 的唯一事实来源:它拥有引擎(any SIPEngine)、存储、钥匙串保管库和可观察状态。其扩展按领域拆分行为:+Engine(引导、事件处理、编解码器和网络应用)、+Accounts、+Calls、+LiveCalls、+AutoAnswer、+DND、+Presence、+Quality、+QuickKeys、+Directory、+Presentation。RecordingCoordinator 将完成的 WAV 转换为 M4A。引擎调用采用 Task { try await engine.x },错误以临时横幅显示。

SIPEngine(Engine/SIPEngine.swift)是真实的 Engine 和 MockEngine 都满足的协议;EngineFactory.make(useMock:) 选择其一。LaunchOptions.swift 解析 --mock-engine、--demo-state、--data-dir、--snapshot、--snapshot-after、--appearance 和 --log-level;DemoState.swift 植入示例数据并将模拟引擎驱动到命名状态(见 测试)。

Views/ 遵循 docs/design/SCREENS.md,每个界面一个文件夹(Main/、Dialpad/、Call/、Contacts/、CallLog/、Incoming/、QuickKeys/、Directory/、每个面板一个文件的 Settings/、FirstLaunch/),加上共享的 Components/(Theme、表单组件、@Local 包装器)。Platform/ 存放 AppKit 和系统部分:AppDelegate、StatusItemController(菜单栏)、IncomingCallPanelController(浮动窗口)、CallNotifications、AudioTransport、RecordingPlayer、QuickKeysMenu、Updater(Sparkle)、AppLog 和 AppInfo。

存储

除密码外的一切都是 ~/Library/Application Support/MacroSIP/ 中的 JSON(ADR 0004):accounts.json、contacts.json、calllog.json、settings.json、quickkeys.json。JSONStore<T> 原子写入;损坏的文件被重命名为 .broken-<timestamp>.json 并使用默认值。联系人照片是 photos/<id>.jpg 中的 JPEG 文件。密码存放在钥匙串中,服务名 com.hsmm.macrosip,账户 = 账户 UUID(Storage/Keychain.swift);目录机密通过 DirectoryCredentials(LDAP 绑定密码;Google 令牌在 com.hsmm.macrosip.google 下)。录音默认在 ~/Music/MacroSIP Recordings,日志文件在 ~/Library/Logs/MacroSIP/MacroSIP.log。

本地化

ADR 0011:Sources/MacroSIPApp/Resources/{en,ru}.lproj 下的经典 Localizable.strings 和 Localizable.stringsdict,因为 .xcstrings 目录需要 Xcode 的 xcstringstool。所有查找都通过 Model/Strings.swift 中的 L;make-app.sh 将 .lproj 文件夹拷贝到 Contents/Resources,scripts/check-strings.sh 保持代码与两种语言同步。麦克风和通讯录的权限提示在 packaging/InfoPlist/<lang>.lproj/InfoPlist.strings 中。

目录层

DirectoryService(MacroSIPCore/Directory/ 中的一个 actor)为每个来源持有一个 DirectoryProvider:MacContactsProvider(Contacts 框架)、LDAPDirectoryProvider(通过 CLDAP 使用 libldap)、URLDirectoryProvider(CSV 或 JSON 源)和一个始终报告“需要登录”的 GoogleDirectoryProvider 存根。搜索同时询问所有已启用的来源,在全部应答后或 3 秒后返回;来电者查询向仅搜索的来源询问最多 1 秒;LDAP 请求有 3 秒的截止时间(导入为 30 秒)。状态通过 AsyncStream<DirectoryEvent> 流动。在应用中,AppModel+Directory.swift 运行搜索、查询、导入和计划,Engine/LiveDirectory.swift 包装该服务,MockDirectory.swift 服务于演示启动,Model/DirectoryMerge.swift 按号码将导入合并到联系人:由来源创建的名片跟随目录,用户的名片只接收自己缺少的部分。

决策记录

0001 平台与 UI 技术栈 ·
0002 PJSIP 构建 ·
0003 产品范围 ·
0004 存储 ·
0005 账户与通话 ·
0006 质量与测试 ·
0007 许可证与分发 ·
0008 开发流程 ·
0009 UI 决策 ·
0010 版本号 ·
0011 本地化文件 ·
0012 通用构建 ·
0013 应用内更新

另见 构建、测试、发布、参与贡献。

其他页面:首页 · Home (English)