Grandstream SDK
面向 HT813 的 Python SDK,以 Asterisk ARI 通话控制和双向语音流 为核心,同时提供声明式设备配置、XML provisioning 和轻量 HTTP 服务。
模拟电话 / PSTN ↔ HT813 FXS / FXO ↔ SIP/RTP ↔ Asterisk
↕ ARI REST + 事件 WebSocket
↕ 独立媒体 WebSocket
Python Call / AudioStream
Python 应用处理带采样率的 PCM16LE 音频;Asterisk 负责 SIP、电话编解码和媒体桥接。ARI、音频模块可独立使用,HT813 通过 endpoint 注册表为通话附加设备和端口信息。
安装
Python 支持 >=3.10,<3.13,开发环境使用 Poetry 和 Python 3.10:
poetry env use python3.10
poetry install -E provisioning
poetry run grandstream --help
provisioning 是 HTTP 服务可选依赖;XML 生成、ARI 和音频功能属于核心包。依赖版本记录在 poetry.lock。SDK 导入时不会启动服务、连接网络或修改日志配置。
本项目参考 sip-stub 的媒体链路设计,独立实现通话与音频抽象,不依赖其网页、数据库、ASR、LLM 或 TTS 服务。
连接 Asterisk
首版在 Asterisk 23.5.0 / Linux 上验证。需要 chan_websocket、res_http_websocket、ARI channels/events/bridges 模块以及对应编解码器。媒体协议使用 JSON、MARK_MEDIA 和 FLUSH_MEDIA。JSON 控制和 transport_data 自 20.18.0、22.8.0、23.2.0 加入,其他版本尚未纳入本项目实测矩阵。官方媒体协议
复制 .env.example 后填写 ARI 凭据。SDK 和 CLI 不隐式加载 .env;可在 Shell 导出变量,VS Code 调试配置通过 envFile 加载。
export GRANDSTREAM_ARI_URL=http://127.0.0.1:8088/ari
export GRANDSTREAM_ARI_USERNAME=grandstream
read -rsp 'ARI password: ' GRANDSTREAM_ARI_PASSWORD
export GRANDSTREAM_ARI_PASSWORD
poetry run grandstream doctor
doctor 只读取 ARI 状态、版本和已加载模块;媒体收发能力由后文的模拟通话验证。ARI 密码与网关 SIP 密码、Web 管理密码是三个不同的配置。
若媒体 WebSocket 位于不同反向代理路径,可设置 GRANDSTREAM_MEDIA_URL=https://pbx.example/media;默认由 ARI URL 的 /ari 后缀推导 /media。HTTP/HTTPS 对应 WS/WSS,客户端默认不使用环境中的 HTTP 代理。
最小来电应用
把进入目标 dialplan context 的来电交给应用:
[grandstream-incoming]
exten => s,1,Stasis(grandstream)
same => n,Hangup()
以下程序接听来电并回送原始采样率的音频:
import asyncio
from grandstream import AriApplication, AriConfig, Call
app = AriApplication("grandstream", ari=AriConfig.from_env())
@app.incoming_call
async def incoming(call: Call):
await call.answer()
async for frame in call.audio.receive():
await call.audio.write(frame)
asyncio.run(app.run())
CLI 也提供相同的回声应用:
poetry run grandstream audio echo --app grandstream
# 显式将接收音频转换到 16 kHz,输出自动适配媒体连接
poetry run grandstream audio echo --app grandstream --sample-rate 16000
一个 Stasis 应用名同时由一个 SDK 进程持有;多通电话在该进程中独立运行。请为不同程序使用不同应用名。收到 ApplicationReplaced 后应用停止,不反复争抢订阅。
音频接口与采样率
AudioFrame(pcm: bytes, sample_rate: int) 表示单声道、有符号 16 位小端 PCM。frame.duration 给出秒数,块长度可以不同,但必须包含完整采样。
| 接口 | 行为 |
|---|---|
call.audio.receive() / async for frame in call.audio |
保留媒体源采样率,不默认升到 16 kHz |
call.audio.receive(sample_rate=16000) |
用同一个 soxr 流在线转换到指定采样率 |
await call.audio.write(frame) |
按 frame 声明的源采样率自动适配媒体连接 |
await call.audio.drain() |
刷新重采样尾部、补齐末帧,并等待 Asterisk 播放标记 |
await call.audio.play(source) |
消费异步 AudioFrame 流,结束时自动 drain |
await call.audio.clear() |
取消当前 play、清理缓存并 flush Asterisk 播放队列;接收继续 |
媒体连接采样率通过 AriApplication(..., media_sample_rate=...) 配置。HT813 默认是 8000,对应 slin;也支持 Asterisk 的 slin12/16/24/32/44/48/96/192 格式。Python 接收目标采样率独立于连接格式。PSTN 的原始窄带信息不会因为升采样而增加。
转换使用 soxr.ResampleStream、HQ 模式,与参考项目同款。每个连续输出流维持转换状态;drain() 或 clear() 后可开始新的源采样率。连续输出中直接更换源采样率会报错。同率输入直接透传。
流只允许一个接收者和一个输出生产者;两者可以并发。应用应持续消费接收流,即使仅做播放也应启动接收任务丢弃不需要的音频。接收积压默认限制为 2 秒,超限抛出 AudioOverflow 并结束通话;默认不合成接收静音。发送窗口默认为约 200 ms,write() 会等待容量。drain() 的确认来自 Asterisk,不能证明远端听筒实际播放。
接入任意 TTS 的方式是将其分块输出包装为音频帧:
from grandstream import AudioFrame
async def tts_frames(chunks):
async for chunk in chunks:
yield AudioFrame(chunk.pcm, sample_rate=chunk.sample_rate)
# 在正在消费接收流的通话中:
# await call.audio.play(tts_frames(tts_chunks))
WavSource(path) 按 WAV 元数据读入单声道 PCM16;WavSink(path) 使用首帧采样率写出 WAV,磁盘 I/O 放在工作线程。完整示例见 examples/record_call/app.py 和 examples/outbound_wav/app.py。
应用集成(0.2.0)
AriApplication.start(wait_connected=False) 启动后台连接任务后立即返回,适用于需要在 Asterisk 离线时仍提供管理页面的应用。默认 start() 仍等待首次连接并遵循超时;后台模式首次失败后也会退避重试。connected 和 error 提供当前状态,重连成功清除错误,ApplicationReplaced 仍会停止订阅。
@app.connection_changed
def connection_changed(connected, error):
# 更新健康状态或通知浏览器;error 不包含连接凭据。
pass
@app.caller_changed
def caller_changed(call):
# SDK 已更新同一个 Call,业务可用 call.id 定位会话。
save_caller(call.id, call.caller)
await app.start(wait_connected=False)
# 应用退出时 await app.close()
两个状态回调均为同步、非阻塞通知;只更新状态或入队。通知异常被记录,不中断其他通知或通话处理。
接收静默与播放归档
async for frame in call.audio.receive(sample_rate=16000, silence_timeout=0.1):
enqueue_asr(frame.pcm)
def played(frame, acknowledged_at):
enqueue_recording(frame, acknowledged_at)
await call.audio.play(tts_frames(), on_played=played)
silence_timeout默认为None,保持不补静音;显式设置正数后,在没有音频的间隔补对应时长的静音,统一通过同一个重采样流转换。流控与播放确认消息不会重置音频时钟;关闭后停止补音。on_played(frame, acknowledged_at)是可选的同步回调。frame为该次play()输入的源采样率 PCM,按已确认媒体时长分段;acknowledged_at使用time.monotonic(),可与会话起始时间对齐。- SDK 保存有限窗口内的源音频映射,排除媒体末帧补零;重采样舍入留下的源音频尾部在最终确认后交付。未产生任何媒体采样的极短输入不会产生确认。
clear()丢弃未确认片段,旧标记不会触发后续播放的回调;drain()完成前已交付确认。回调只应入队,不执行磁盘 I/O、阻塞操作或媒体控制;回调失败以MediaError结束媒体流。- 带确认回调的
play()必须从已 drain/clear 的输出流开始。确认表示 Asterisk 处理了播放标记,不证明远端听筒播放。
会话数据库、VAD、ASR/LLM/TTS、双轨时间对齐与分段归档由应用负责;SDK 不依赖这些业务模块。
HT813 与通话设备信息
from grandstream import (
HT813, FxoPort, FxoInbound, FxoOutbound, SipAccount, NetworkConfig,
)
device = HT813(
mac="000b82123456",
address="192.168.1.36",
network=NetworkConfig(dhcp=True),
fxo=FxoPort(
endpoint="fxo1", # 可绑定已有 Asterisk endpoint
local_sip_port=5062,
random_sip_port=False,
sip=SipAccount(
server="192.168.1.25",
username="fxo1",
password="请替换为 SIP 密码",
registration=True,
transport="udp",
codecs=("alaw", "ulaw"),
dtmf="rfc4733",
),
inbound=FxoInbound(
destination="s", server="192.168.1.25", port=5060,
rings=2, ring_through_fxs=False,
),
outbound=FxoOutbound(stage_method=1),
),
)
将设备通过 devices=[device] 传给应用后,可使用 @app.incoming_call(from_port="fxo") 过滤来电,并读取 call.device.model、call.port.type、call.port.index。HT813 的 capabilities 声明 1 个 FXS、1 个 FXO、2 个 SIP profile 和 XML provisioning。
默认 endpoint 名称为 ht813-<mac>-fxs1 / ht813-<mac>-fxo1。显式 endpoint 用于接入现有配置。SDK 优先读取生成的 Stasis endpoint 参数,其次解析 PJSIP channel 名;不会把主叫号码当成设备账号。未登记 endpoint 的通话仍可使用 ARI 和音频,device、port 为 None。
主动外呼、DTMF 和生命周期
from grandstream import AriApplication, AriConfig
async def dial_and_send(device):
app = AriApplication("grandstream-outbound", ari=AriConfig.from_env(), devices=[device])
async with app:
async with await app.dial(device.fxo, number="12345678", timeout=30) as call:
await call.send_dtmf("1#")
# 可并发消费 call.audio.receive() 和调用 call.audio.play(...)
dial(device.fxo, number=...) 要求 stage_method=1,已应用到设备;dial(device.fxs) 呼叫模拟电话,无需号码。dial() 返回时 SIP 已进入 Stasis 且媒体准备完成。FXO 网关可能提前建立 SIP,PSTN 远端应答取决于线路与设备检测,不能仅据此判断用户已接听。
@app.dtmf("1")
async def stop_playback(call):
await call.audio.clear()
Call 提供 answer()、hangup()、send_dtmf()、wait_closed() 和 caller、state、error。号码晚到时会更新同一个 Call。首个匹配的来电处理器负责该通话,处理器返回或抛出异常后释放通话;外呼由调用者或异步上下文管理释放。DTMF 处理器独立调度,超过并发容量时结束该通话。
接收和播放任务应响应 asyncio 取消。正常关闭会唤醒等待者、取消受管理任务、关闭媒体并删除 SDK 拥有的通道和 bridge。ARI 事件断线后终止现有会话,以 1 至 30 秒退避重新连接来接收新通话,不恢复旧通话。若 ARI 本身不可达,远端资源删除只能记录失败;恢复后应检查 Asterisk 残留通道。
公开异常位于 grandstream.errors,包括 AriError(含 HTTP status)、ConnectionLost、DialFailed、CallClosed、MediaError、AudioOverflow 和 PlaybackInterrupted。请求错误不返回凭据或服务器响应正文。
XML provisioning 与 Asterisk 配置生成
from grandstream import ProvisioningStore, AsteriskConfig, render_asterisk
store = ProvisioningStore("outputs/provisioning")
xml_path = store.publish(device)
files = render_asterisk(
[device], AsteriskConfig(ari_password="请替换为 ARI 密码"),
)
# files 是 {文件名: 内容},由调用方审阅后保存、合并。
publish() 原子替换 cfg<mac>.xml,包括完整的 gs_provision/mac/config 结构和 XML 转义。发布成功表示配置可下载,不表示网关已经应用配置。HT813.render_xml() 可直接获得 XML bytes。Grandstream 官方 XML 格式
配置模型中 None 表示不修改相应设备参数。配置 SIP account 时始终输出服务器、用户、密码;省略 auth_id 时使用 username。静态 IPv4 地址按官方的四个 P-value 序列输出,DHCP 下 DNS 使用 Preferred DNS 参数。
P-value 映射依据 Grandstream 官方配置模板下载页 的 ht813_config_1.0.19.6.txt。该文件头仍写着 1.0.17.3;代码保留这个版本差异,仅支持已明确映射的字段,不宣称通用于所有型号和固件。未提供 Web UI 登录自动化。
poetry run grandstream provision serve \
--directory outputs/provisioning --host 0.0.0.0 --port 8000
首次在设备上把 Config Server Path 指向 Asterisk/SDK 主机的局域网地址,例如 192.168.1.25:8000,选择 HTTP,再手动触发重同步或重启。注意 DHCP provisioning override、配置文件前后缀以及设备使用的 MAC。首版不发送 SIP NOTIFY,也不提供 TFTP/binary 配置。
服务只支持 GET/HEAD、允许的 XML 文件名,不列目录,不允许符号链接跳出目录或 HTTP 写入。未知 MAC 返回 404,只有显式调用 publish_defaults(..., model="HT813") 或 publish_defaults(...) 后才提供 cfght813.xml / cfg.xml。这些配置含 SIP 凭据;默认监听回环地址,LAN 部署使用受控网络,HTTPS 可由反向代理提供。
render_asterisk() 面向同一 LAN 的注册型 UDP HT813,输出 PJSIP、dialplan、ARI、HTTP、chan_websocket 配置片段。FXS/FXO 使用不同 SIP 来源端口 identify;REGISTER AOR 使用 SIP 用户名,保留来电真实号码。设备 IP 应固定或使用 DHCP 地址保留,并关闭随机 SIP 端口。Asterisk 来源端口识别
生成器不读取或修改 /etc/asterisk。合并时将 endpoint/context 片段 include 到现有配置,HTTP/ARI 的 [general] 参数合并到已有节;需要 reload/restart 的项目由部署方处理。密码包含 Asterisk 配置语法的换行、分号、方括号、反斜杠时生成器会明确拒绝,XML 本身允许的转义字符不受此限制。
配置生成示例 演示完整生成,通过命令行传入参数:
poetry run python examples/provision_ht813/app.py \
--mac 000b82abcdef --address 192.168.1.50 --server 192.168.1.25
密码未通过参数传入时,会提示隐藏输入。每个 demo 都有独立目录、入口和说明,不读取环境变量;录音、外呼和回声示例通过 --ari-url、--ari-username、--ari-password 指定 ARI 连接。
输出目录为 outputs/provisioning 和 outputs/asterisk,均忽略于 Git。示例选择两次响铃给来电显示采集留出时间;实际响铃次数、来显制式、断线检测和回声参数应按线路调整。
验证与开发
poetry run pytest -q
poetry run ruff check src examples tests
poetry run ruff format --check src examples tests
poetry check
poetry build
# 真实 Asterisk 的模拟回声测试:独立应用名,不拨打 PSTN,不修改 dialplan
poetry run python examples/smoke_asterisk/app.py --ari-url http://127.0.0.1:8088/ari
poetry run python examples/smoke_asterisk/app.py --media-sample-rate 16000 --receive-sample-rate 24000
自动测试涵盖配置隔离与转义、SIP 双端口识别、PCM/WAV、重采样尾部、流控、播放打断、迟到事件、来电/外呼竞态、忙线/超时、媒体初始化失败和断线清理。模拟回声脚本会创建并清理自己的 ARI 资源,输出收到的音频字节数。
2026-09-07 已在本机 Asterisk 23.5.0 验证:24 kHz 音源适配 8 kHz 媒体并完成双向回声;16 kHz SDK 媒体连接、24 kHz 接收目标的链路也通过。实际 HT813 配置应用、真实 PSTN 外呼与收听效果尚未执行验证。
真实线路验收顺序:发布并应用 XML → 检查两个 PJSIP contacts → FXS/FXO 来电进入独立应用 → 检查号码、端口绑定和双向声音 → 测试 DTMF/打断 → 外呼指定测试号码 → 任一侧挂机后检查通道与 bridge 释放。请使用新应用名接入,避免占用正在运行的 sip-stub 应用。
模块边界
src/grandstream/
├── ari/ # REST、事件、Application、Call 生命周期
├── audio/ # AudioFrame、双向媒体流、soxr、WAV
├── models/ # HT813、能力、网络与端口配置
├── sip/ # PJSIP 与 Asterisk 配置生成
├── provisioning/ # HT813 P-value 映射、XML、原子发布、HTTP
└── commands/ # Typer CLI
后续型号应扩展设备能力与参数映射,ARI 和音频核心保持独立。旧版 RTP 后端、自动 resync、麦克风/扬声器及 ASR/TTS 服务适配不在首版范围。
0.3.0 外呼失败信息
DialFailed.reason 为 busy、no_answer、timeout 或 failed;cause 保留 Asterisk 提供的挂机原因整数,未提供时为 None。业务无需解析异常文本。PSTN 线路可能先建立 SIP 再拨号,因此这些结果仅反映 SDK 收到的信令,不能推断真人已接听。
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file grandstream-0.3.0.tar.gz.
File metadata
- Download URL: grandstream-0.3.0.tar.gz
- Upload date:
- Size: 36.3 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via:
poetry/2.4.3 CPython/3.12.3 Linux/7.0.0-30-generic
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
808cb1c0162ee748df24b297074cf23a794f94cb2cc9b32ecbcfe3c322a036f8
|
|
| MD5 |
55e0100fdf18f51c2ed21dd9e331a94f
|
|
| BLAKE2b-256 |
5695ad134bfed27ead4800c325063038da4956ddce3994d91459b1b04efadf37
|
File details
Details for the file grandstream-0.3.0-py3-none-any.whl.
File metadata
- Download URL: grandstream-0.3.0-py3-none-any.whl
- Upload date:
- Size: 37.3 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via:
poetry/2.4.3 CPython/3.12.3 Linux/7.0.0-30-generic
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
49bc85643a954e1c17783105819c937f657e73cc3eed8ca79c484fff309d49a0
|
|
| MD5 |
7a315190d68281352ef6931aacb867bc
|
|
| BLAKE2b-256 |
dbe73f82e765204d1504dab16f753862176082fea9752986f5ac6606ed5a5e01
|