项目RTP
一个 RTP 节点加通,提供处理 RTP 数据流并将其混合的功能. 所有表演任务都用Rust(一个sami-rs本土模块)执行,每个频道的tokio演员模型用于高通量.
ProjectRTP旨在被规模化为多个服务器,服务于其他信号服务器. RTP和信号传递应该分开,这种架构允许这样. 它得到一个代理服务器和客户端,以允许远程节点.
ProjectRTP旨在提供PBX所需的所有功能,包括通道连接,录音,回放和IVR所有必要功能.
特征
- 2+ 通道混合
- CODEC 转换编码( 介于以下所列支持的CODECs之间)
- 电话录音(WAV)
- 每个频道有多个录音机
- 基于电源检测或命令的启动、暂停和结束
- WAV 使用音汤描述播放从部件播放复杂句子的回放
- 混合 play+record( playrecord) 用于0- gap 即时录入可选驳入
- 列车模式:能量
interrupt(粗放的入门切入快取),或concurrent(在提示持续播放时记录答案)stopplay()一旦答案可以接受,即以内容为主,而不是以能量为主)
- 列车模式:能量
- DTMF(RFC 2833) - 发送、接收和接通
- DTLS SRTP (WebRTC) (中文(简体) )
- 高度可缩放 - 服务器/节点解决方案以缩放媒体节点
版本编号
我们使用语义版本编号.
由于版本号为 MAJOR.MINOR.PATCH, 递增:
- 当您进行不兼容的 API 修改时, maJOR 版本,
- MINOR 版本,当您以反向兼容的方式添加功能时,以及
- PATCH 版本,当您向后兼容错误修正时 .
作为这项工作的一部分,我们维护jsdoc文件,以记录我们与这一方案的公共界面.
释放过程
在 GitHub 动作上管理发布. 它建立和测试任何版本.
夹克
Amd64 和 arm64 的公用嵌入器图像可用 多克枢纽。 。 。 .
测试
测试需要本地模块( libilbc + libspandsp), 因此最简单的路径是 Docker test 阶段。 当地, npm run build 生产 build/Release/projectrtp.node 从一箱起 npm test 用它来管理套房 libilbc 财务报告和审定财务报表 libspandsp 在主机上.
构建测试图像
docker build --target test -t projectrtp-test .
运行所有测试
docker run --rm projectrtp-test
图像的默认命令运行 mocha test/interface/*.js test/unit/*.js --exit。 。 。 .
注:套间用途 test/interface/*.js 财务报告和审定财务报表 test/unit/*.js -没有 test/**/*.js - 因为.. test/basictests.js 财务报告和审定财务报表 test/codectests.js 是独立的脚本(不是摩卡测试),如果被摩卡加载,会被悬挂.
运行特定测试
大部分测试文件都依赖于服务器测试( I)projectrtpserver.js)首先启用本地模块,因此在运行单个文件时包含:
docker run --rm projectrtp-test \
./node_modules/mocha/bin/_mocha \
test/interface/projectrtpserver.js test/interface/projectrtpplayrecord.js \
--exit --timeout 20000
测试本地编辑
挂载 test/ 财务报告和审定财务报表 lib/ 这样您就可以编辑和重运行而不重建本地模块 :
docker run --rm \
-v ./test:/usr/src/projectrtp/test \
-v ./lib:/usr/src/projectrtp/lib \
projectrtp-test \
./node_modules/mocha/bin/_mocha \
test/interface/projectrtpserver.js test/interface/projectrtpplayrecord.js \
--exit --timeout 20000
如果您更改了 Rust 源文件, 请重建图像 .
粗口测试
箱子本身的单元测试是通过货物进行的:
npm run rust:test # or: cd rust && cargo test --lib
压力测试
在荷载下的随机时间打开并关闭频道. 设计用于货币和赛跑条件测试——没有通关/故障,但会在不良行为上崩溃或产生出意料的输出.
docker run --rm projectrtp-test sh -c \
"cd /usr/src/projectrtp && npm run stress"
Mic 测试工具
test/tools/mictest.js 是一个CLI工具,用于手动测试真麦克风和扬声器的播放记录。 它需要当地土建(npm run build和 sox 在主机上 – 完整的构建依赖列表请参见 Dockerfile.
node test/tools/mictest.js [--prompt <file>] [--output <file>] [--duration <ms>] [--interrupt] [--bargeinpower <n>]
播放一个提示(或生成的测试语气),记录您的声音,实时打印事件,并保存到 /tmp/mictest_recording.wav。 。 。 .
示例脚本
示例文件夹包含使用此库的两个脚本 。 简单的节点可以在不需要特殊功能的地方被使用. 标准 projectp docker 图像指向此 - 因此启动一个 docker 实例会作为节点运行并尝试连接到一个中央控制服务器.
高级节点.js是一个包含前处理和后处理的范例——可以使用,例如下载录音,执行文字到演讲或完成后上传录音.
运行
环境变量HOST应包含控制服务器的主机地址-其默认为127.0.0.1. 可变 PORT 有端口号码可以通信 - 默认为9002.
环境变量 PA 采取以下形式——地址被传递回控制服务器,从而可以在SDP中发布——即允许在不同IP地址上设置多个RTP节点. 如果您不通过, 默认脚本将会通过调用来确定您的公共 IP 地址 https://checkip.amazonaws.com (中文(简体) ) 获取你的IP.
docker run -- restart= always --net=host -d -it --env PA=127.0.0.1 --name=prtp projectrtp:latest 互联网档案馆的存檔,存档日期2013-12-02
编解码器
ProjectRTP支持以下 CODEC 的转码
- 澳门
- PCMU 电脑
- G722集团
- iLBC (英语)
使用量
2个示例脚本可见于示例文件夹. simplenode.js作为主条目被多克容器使用.
环境变量
这些在简单节点.js的例子中解析.
- PORT - 要连接的端口
- HOST - 要连接到的主机
- PA - 这个主机的公开地址 - 它被传回到控制套接字上, 以便发布给客户端 - 如果没有脚本民调 https://checkip.amazonaws.com (中文(简体) ) 我们的公开演讲.
小示例
这个工程包含所有功能,可以用作本地资源或多个节点处理媒体. 在两个模式之间切换是无缝的. 在节点模式下使用时,它有一个简单的协议,可以在服务器和节点之间进行通信.
每个活动节点连接到主服务器,向主服务器提供服务. 然后主服务器打开任何可用的 RTP 频道(WebRTC 或普通 RTP). 所使用的协议可以在 /lib 文件中查看.
服务器示例
服务器通常在打开媒体端口之前运行应用程序逻辑.
const prtp = require( "@babblevoice/projectrtp" ).projectrtp
/* This switches this server to a central
server and requires nodes to connect to us
to provide worker nodes */
await prtp.server.listen()
const codec = 9 /* G722 */
/*
When you have a node configured and connected to this server...
Now open our channel:
The remote comes from your client (web browser?)
including options for WebRTC if it is a web browser.
*/
let channela = await prtp.openchannel( {
"remote": { address, port, codec }
} )
let channelb = await prtp.openchannel( {
"remote": { "address": otheraddress, "port": otherport, codec }
} )
/*
Offer both of these channels to the remote clients (convert to SDP?)
*/
/* Ask projectrtp to mix the audio of both channels */
channela.mix( channelb )
/* Keep calling the mix function with other channels to create a conference */
我们可以换个方式 即指示控制服务器连接节点 我们可以增加多个节点,也可以使用一个道克星团作为一个发布多个节点.
const prtp = require( "@babblevoice/projectrtp" ).projectrtp
const port = 9002
const host = "192.168.0.100"
prtp.server.addnode( { host, port } )
/*
When we need a channel now, the library will
make that request to one of our nodes.
*/
let channela = await prtp.openchannel( {
"remote": { address, port, codec }
} )
节点示例
节点是网络的工作马. 连接到服务器并等待指令 。 更多实例请参见示例文件夹,例如如何连接到事件中来履行诸如从存储中下载这样的S3等功能.
const prtp = require( "@babblevoice/projectrtp" ).projectrtp
async function go() {
prtp.run()
prtp.node.listen( "0.0.0.0", 9002 )
const pa = await wgets( "https://checkip.amazonaws.com" )
prtp.setaddress( pa )
}
listen()
状态
当我们从我们的节点(或者如果站立龙只是我们自己)接收到一个对象时,该对象包含关于: s州. 它包括开放频道数量,MOS质量分数等项目.
MOS只包括我们收到的测量数据.
滴答时间是处理RTP数据在发送前需要的时间. 我们做一些工作 当我们发送和接收 RTP数据,但大多数工作都是用打勾完成的,这表明了负荷和能力.
测量是在US(即我们在命名公约中)。 在以上的例子中,平均时间为84uS。 在溪流的ptime(我们只用20mS的ptime)上发生一滴.
如果有多个频道被混合,它们都会得到与同一频道被混合的一样的打勾时间,这代表了所有频道的总时间.
播放
播放一个声音"soup"。 这是一个要播放的文件列表,可能包括要播放的文件的起止时间. 项目RTP 支持 wav 文件 。 一个例子汤:
channel.play( {
"soup": {
"loop": true,
"files": [
{ "wav": "filename.wav", "start": 1000, "stop": 4000 }
]
}
} )
- 循环可以是真实的(连续的- well- INT MAX) 也可以是数字
- 循环可以是主汤(即通过所有文件循环),也可以是在一个文件(即通过这个文件循环了3次)上
- 文件名可以在param"wav","pcma","pcmu","l168k","l1616k","ilbc","g722"中出现. 见下文关于它们用途的说明,所有文件都载有该格式的文件名称(尚未完成).
- 启动 - 从( 可选的) 开始播放的 mS 数量
- stop - 停止播放的 mS 数( 可选)
起止参数允许很多片段包含在同一文件中.
文件名可以包含在不同的参数中 。 如果您提供了参数, RTP 服务器将假设文件存在, 并且格式正确 。 使用此参数的目标是在播放一个文件时减少任何转码的CPU间接费用.
其他汤的例子
队列通知
channel.play( {
"loop": true,
"files": [
{ "wav": "ringing.wav", "loop": 6 },
{ "wav": "youare.wav" },
{ "wav": "first.wav" },
{ "wav": "inline.wav" }
]
} )
我们也许还合并了一些疯狂的文件:
channel.play( {
"loop": true,
"files": [
{ "wav": "ringing.wav", "loop": 6 },
{ "wav": "youare.wav" },
{ "wav": "ordinals.wav", "start": 3000, "stop": 4500 },
{ "wav": "inline.wav" }
]
} )
记录
记录到一个文件。 ProjectRTP目前支持2个频道的PCM.
可以根据 projectrtp 选择: 文件 = - 要保存为 uuid= - 频道 uuuid 要记录
以秒为多至 MA 最大尺寸( 5 秒? ) , 默认为 1 秒 RMS 功率由每包计算,然后使用移动平均滤波器进行平均计算. 平均电源包=将平均电源窗口移到平均电源上
一定已经开始了 如果被忽略了 才刚刚开始 启动功率 =
何时完成 - 如果低于这一数额 完成以下功率 =
与完后功率结合使用 - 用于有功率阈值的相接 即在这个 mS 数通过之前, 功率低于完成的下限 心电图 = < int > mseconds
一定超乎意料 并设定了录音的尺寸限制 最大长度 = < int > mseconds
必须是一或二 数字通道 = < int > 计数
channel.record( {
"file": "/voicemail/greeting_1.wav",
"maxduration": 10 * 1000 /* mS */,
"numchannels": 1
} )
现场音频—— channel.createReadStream
返回标准节点 Readable 当频道接收/发送时, 发送解码音频缓冲器 。 使用此功能将音频输入到任何需要流出的东西中——STT,翻译,标题,WebSocket转发器,上磁盘抓取——而无需通过记录器/文件路径.
每个被排放块的字节形状固定在阅读器的寿命,并作为可读器的属性被曝光:
const reader = channel.createReadStream( {
direction: "in", // "in" (default) | "out" | "both"
format: "l16", // "l16" (default) | "pcma" | "pcmu" | "g722" | "ilbc"
samplerate: 8000, // 8000 (default) | 16000 — only meaningful for l16
numchannels: 1 // 1 (default) | 2 — stereo interleaves L=in R=out
} )
reader.format // "l16"
reader.samplerate // 8000
reader.numchannels // 1
reader.direction // "in"
reader.readerId // monotonic id (handy for log correlation)
reader.on( "data", ( buf ) => { /* Buffer, one 20 ms frame */ } )
reader.on( "end", () => {} ) // fires on channel close or reader.destroy()
reader.pipe( wsStream ) // standard Node stream composition
reader.destroy() // explicit teardown
方向
"in"- 音频对等端发送我们,后解码。 你想要的STT/语音生物鉴别仪 在呼叫器."out"——音频此频道正在发送(玩家,回声,或混合输出). 同行所闻."both"——立体声,相接出,L=入,R=出. 要求数numchannels: 2。 。 。 .
用于独立的每语源流( 如 diarized 标题), 创建 两个读取器在同一频道 一个 direction: "in",一个 direction: "out"两道单流,两道独立的STT管线,它们之间没有接合.
格式
"l16" 按所选样本率给出线性PCM-16 LE——每个STT/翻译API接受的通用格式. 线条格式( E)"pcma", (中文(简体) ), "pcmu", (中文(简体) ), "g722", (中文(简体) ), "ilbc")提供原始的编码字节,当你想在别处转发一条腿而不重新编码时有用.
后压和后降
输油管从不会堵住20毫秒的RTP滴答. 如果消费者(或节点事件回路,或凝胶队列)落后,框被丢弃. 在实践中,只有在消费者真的被卡住的情况下才咬伤——典型的STT/WebSocket消费者舒适地跟上了20ms帧.
示例——将一条腿插入WebSocket STT服务:
const reader = channel.createReadStream( { direction: "in", format: "l16", samplerate: 16000 } )
const ws = new WebSocket( "wss://stt.example.com/stream?rate=16000&format=l16" )
reader.on( "data", ( buf ) => ws.send( buf ) )
ws.on( "message", ( transcript ) => console.log( "→", transcript.toString() ) )
/* When the call ends the reader emits `end` and the ws.send loop quiets. */
reader.on( "end", () => ws.close() )
示例 – 双声道呼叫录制分出每个频道 :
const caller = channel.createReadStream( { direction: "in" } )
const agent = channel.createReadStream( { direction: "out" } )
caller.pipe( fs.createWriteStream( "/tmp/caller.pcm" ) )
agent.pipe( fs.createWriteStream( "/tmp/agent.pcm" ) )
现场音频—— channel.createWriteStream
对称对应到 createReadStream返回节点 Writable 将活音注入频道外出行走的腿上——管子放入TTS流中,活口翻译入音,一瓶子合成出的声音,任何产生PCM的东西.
const writer = channel.createWriteStream( {
format: "l16", // v1: l16 only
samplerate: 8000, // v1: 8000 only
numchannels: 1 // v1: mono only
} )
writer.format // "l16"
writer.samplerate // 8000
writer.numchannels // 1
writer.writerId // monotonic id
writer.write( pcmBuffer ) // accepts any chunk size — framed internally
writer.end() // flushes the tail then reverts to silence
writer.destroy() // immediate teardown, discards buffered samples
ttsStream.pipe( writer ) // standard Node stream composition
与 play
一个作家和 channel.play 共享频道上的单个出源槽。 开始一个写入器取代任何活动播放器( emits) play/end reason=new页:1 channel.play 取代活动作者。 最多有一个外出源;当两者都不在场时,信道发出沉默.
结构
你们可以 .write 任意块大小 —— Rust 侧缓冲器会用字节来框起它们,为勾选设置了20ms的插槽. 8 kHz L16 流需要 ~ 16,000 字节/秒( 每 20 ms 帧 有 320 字节) , 因此是典型的 .pipe 从一个WebSocket或HTTP身体保持舒适.
后压
Rust-side缓冲器以1s(50×20ms)为界. 如果消费者写得比滴水管快, _write 将其召回延后到空格空出——标准节点 Writable 后压踢入并 .write 返回时 false。在下行中(没有写入一个勾选符),该频道为该槽发出静音.
期末
writer.end() 放下JS的发件人 鲁斯特 AudioWriter 看到关闭,冲出任何部分框架 加上沉默,然后从频道退休。 A 类 play/end reason=completed 事件起火——与基于文件的对称 play 自然结束.
例子——将 TTS 响应输入活调:
const writer = channel.createWriteStream()
const res = await fetch( "https://tts.example.com/say", {
method: "POST",
body: JSON.stringify( { text: "Your call is important to us." } ),
} )
// res.body is a Readable web stream of raw PCM-16 @ 8 kHz mono.
// Pipe straight through; backpressure is preserved end-to-end.
res.body.pipe( writer )
用户
调音器生成
为了让RTP尽可能可扩展,我们不会支持飞音生成. 目前磁盘空间比CPU资源便宜得多. 在8K取样的1S瓦夫数据为16K. 使用声音,我们可以为每个支持的可解码器提供wav文件,并方便地循环,这需要极少的CPU上浮.
我们需要提供的是生成 wav 文件的有用工具, 生成用于电信(即振铃、 DTMF等) 的音量.
const prtp = require( "@babblevoice/projectrtp" ).projectrtp
let filename = "/some/file.wav"
prtp.tone.generate( "350+440*0.5:1000", filename )
格式试图密切遵循格式 https://www.itu.int/dms pub/itu-t/opb/sp/T-SP-E.180-2010-PDF-E.pdf 互联网档案馆的存檔,存档日期2014-12-21 - 虽然该标准在某些方面不是最明确的.
我们目前支持
- + 共增加1个或更多频率
- '*' 减小振幅( 即能力乘以此值, 0. 75 是 0. 75 x 最大值)
- ':"结肠后值为cadences,即调子移动到下个调子前的持续时间
- '~' start~ end, 例如, 这可以用于频率或振幅, 350+440* 0.5~0: 100 将产生从 anp 0. 5 到 0 到 1000 mS 期间的音调
实例
英国拨号:
prtp.tone.generate( "350+440*0.5:1000" "dialtone.wav" )
联合王国
prtp.tone.generate( "400+450*0.5/0/400+450*0.5/0:400/200/400/2000" "ringing.wav" )
自动取款机
| 1209赫兹 | 1336赫兹 | 1477赫兹 | 1633Hz (英语) | |
|---|---|---|---|---|
| 697赫兹 | 1个 | 2个 | 3个 | A 类 |
| 770赫兹 | 页:1 | 页:1 | 6个 | 页:1 |
| 852赫兹 | 第7条 | 第8条 | 第9条 | C级 |
| 941赫兹 | * 联合国 | 0 个 | {\fn方正粗倩简体\fs12\an8\1cHFFFF00\b0} | D级 |
例1将混合1209Hz和697Hz
prtp.tone.generate( "697+1209*0.5:400" "dtmf1.wav" )
prtp.tone.generate( "697+1209*0.5/0/697+1336*0.5/0/697+1477*0.5/0:400/100", "dtmf1-3.wav" )
开始
- Wav文件类型(l16, rate pcmu, pcma等)之间的格式转换.
- 犯罪时保持平整(在CI中强制):
cd rust && cargo clippy --all-targets -- -D warnings