最近在openwrt上搭建了Asterisk,配合fxo语音网关,内线转外线,外线转内线,十分方便。但在微信这么流行,长途也取消漫游费的情况下,这个完全成了屠龙术,无用武之地。偶然发现Asterisk的语音信箱系统十分完善,给家里的固定电话加个语音留言功能,这个还算有些使用场景,折腾了几天把一个语音信箱系统搭建起来了,支持电话无人接听转语音信箱,支持密码,支持不同的家庭成员用不同的号码收听语音留言,以下是过程纪录:
先要在voicemail中配置一个账号,类似于sip.conf中配置号码一样
[myvm]
home =>, HOME,,,tz=eastern
然后把中文语音包替换原始的,中国人当然要听中文,感谢翻译人员的默付出
本文附件中的是根据上述翻译包与openwrt默认的语音文件合并后,绝大部分语音都是中文,点击下载》》》》》》下载链接
asterisk默认的语音留言目录是var/spool/asterisk,这个目录是软链接,实际整个var目录都是映射到了/tmp目录,重启就丢失语言留言数据了,真坑。
需要修改如下两处:
1) /etc/init.d/asterisk
dbdir=/var/lib/asterisk/astdb
logdir=/var/log/asterisk
cdrcsvdir=$logdir/cdr-csv
rundir=/var/run/asterisk
spooldir=/mnt/sdb/asterisk ##修改为自己的目录
varlibdir=/var/lib/asterisk
2)/etc/asterisk/asterisk.conf
[directories]
astcachedir => /tmp
astetcdir => /etc/asterisk
astmoddir => /usr/lib/asterisk/modules
astvarlibdir => /var/lib/asterisk
astdbdir => /var/lib/asterisk
astkeydir => /var/lib/asterisk
astdatadir => /usr/share/asterisk
astagidir => /usr/share/asterisk/agi-bin
astspooldir => /mnt/sdb/asterisk ;修改为自己的目录
astrundir => /var/run/asterisk
astlogdir => /var/log/asterisk
astsbindir => /usr/sbin
最后加个extension,设置呼叫多长时间转语音信箱,内外线用不同的号码访问语音信箱,收听留言
/etc/asterisk/extensions.conf
;for fix phone, redirct to voicemail for 40s timeout
exten => 1002,1,Answer()
exten => 1002,n,Dial(SIP/1002,40,tr) ;拨打1002分机40秒无人接听就转语音留言
exten => 1002,n,VoiceMail(home@myvm)
exten => 1002,n,Hangup
;for reading voicemail
exten => 8,1,Answer()
exten => 8,n,VoiceMailMain(home@myvm);拨打分机8,即可收听语音信箱的留言
exten => 8,n,Hangup()
这些都做完后,试试看吧,外线进来的电话,没人接听就语音提示留言,拨打分机8即可收听留言,还会提示有多少个新留言,多少个旧留言,还是挺方便的,这种业务在运营商那都是收费的。遥想当年大学宿舍八个兄弟,大冬天熄灯躲被窝后,来电话谁也不想下床接,要有个这玩意,接个音箱听着妹子柔柔的声音说找哪个哥哥,要多fasion有多fasion。
就这样,跑了两天,发现一个bug,收听语音信箱时,妹子播报的时间不对,差了8个小时,尽管不太影响什么,但有点强迫症的,就想搞明白为什么,因为ubuntu上同样的方式搭建的同版本asterisk就没问题。
一番搜索,尝试打开这个文件,发现时间是UTC时间,是对的,那基本就想到是时区的问题
/var/spool/asterisk\voicemail\xxx\xxx\Old\msg0000.txt
;
; Message Information file
;
[message]
origmailbox=home
context=public
macrocontext=
exten=5
rdnis=unknown
priority=6
callerchan=SIP/1005-00000000
callerid=1005
origdate=Tue Nov 9 04:01:09 PM UTC 2021
origtime=1636473669
category=
msg_id=1636473669-00000000
flag=
duration=4
openwrt的时区在如下文件中配置,检查过了,也是对的,那就奇怪了
/etc/config/system
config system
option ttylogin '0'
option log_size '64'
option urandom_seed '0'
option hostname 'xxxxx'
option log_proto 'udp'
option conloglevel '8'
option cronloglevel '8'
option zonename 'Asia/Shanghai'
option timezone 'CST-8'
config timeserver 'ntp'
option enabled '1'
list server 'ntp.aliyun.com'
list server 'time1.cloud.tencent.com'
list server 'time.ustc.edu.cn'
list server 'cn.pool.ntp.org'
查看asterisk CLI的输出,有如下把文本转乘语音播放的过程,看样子,原始的时间就是错的,导致播放出来的时间就是错误的
-- Executing [8@public:2] VoiceMailMain("SIP/1005-00000001", "home@myvm") in new stack
-- <SIP/1005-00000001> Playing 'vm-youhave.slin' (language 'en')
-- <SIP/1005-00000001> Playing 'digits/1.slin' (language 'en')
-- <SIP/1005-00000001> Playing 'vm-INBOX.slin' (language 'en')
-- <SIP/1005-00000001> Playing 'vm-and.slin' (language 'en')
-- <SIP/1005-00000001> Playing 'digits/11.slin' (language 'en')
> 0x15304c0 -- Strict RTP learning complete - Locking on source address 36.112.201.111:22155
-- <SIP/1005-00000001> Playing 'vm-Old.slin' (language 'en')
-- <SIP/1005-00000001> Playing 'vm-messages.slin' (language 'en')
-- <SIP/1005-00000001> Playing 'vm-onefor.slin' (language 'en')
-- <SIP/1005-00000001> Playing 'vm-INBOX.slin' (language 'en')
-- <SIP/1005-00000001> Playing 'vm-messages.slin' (language 'en')
-- <SIP/1005-00000001> Playing 'vm-opts.slin' (language 'en')
-- <SIP/1005-00000001> Playing 'vm-first.slin' (language 'en')
-- <SIP/1005-00000001> Playing 'vm-message.slin' (language 'en')
-- <SIP/1005-00000001> Playing 'vm-received.slin' (language 'en')
-- <SIP/1005-00000001> Playing 'digits/today.slin' (language 'en')
-- <SIP/1005-00000001> Playing 'digits/at.slin' (language 'en')
-- <SIP/1005-00000001> Playing 'digits/3.slin' (language 'en')
-- <SIP/1005-00000001> Playing 'digits/30.slin' (language 'en')
-- <SIP/1005-00000001> Playing 'digits/7.slin' (language 'en')
-- <SIP/1005-00000001> Playing 'digits/p-m.slin' (language 'en')
-- <SIP/1005-00000001> Playing '/var/spool/asterisk/voicemail/myvm/home/INBOX/msg0000.slin' (language 'en')
-- <SIP/1005-00000001> Playing 'vm-advopts.slin' (language 'en')
-- <SIP/1005-00000001> Playing 'vm-repeat.slin' (language 'en')
-- <SIP/1005-00000001> Playing 'vm-delete.slin' (language 'en')
其中时间相关的如下:
查看asterisk的代码发现,在app_voicemail.c中实现
static int play_message(struct ast_channel *chan, struct ast_vm_user *vmu, struct vm_state *vms)
这个函数里面包含了从/var/spool/asterisk/voicemail/xxxx/xxx/INBOX/msg0000.txt中取“origtime”字段的时间,然后调用下面的函数,传入origtime
static int play_message_datetime(struct ast_channel *chan, struct ast_vm_user *vmu, const char *origtime, const char *filename)
vmu 是voicemail user的意思;可以用CLI命令:voicemail show users命令查看
obana*CLI> voicemail show users
Context Mbox User Zone NewMsg
default 1234 Example Mailbox eastern 0
myaliases 1234@devices eastern 0
other 1234 Company2 User eastern 0
myvm xxxx xxxx eastern 0
myvm xxxx xxxx eastern 0
5 voicemail users configured.
其中eastern指向了voicemail.conf中的[zonemessages]配置,可以通过如下命令查看
obana*CLI> voicemail show zones
Zone Timezone Message Format
european Europe/Copenhagen 'vm-received' a d b 'digits/at' HM
military Zulu 'vm-received' q 'digits/at' H N 'hours' 'phonetic/z_p'
central24 America/Chicago 'vm-received' q 'digits/at' H N 'hours'
central America/Chicago 'vm-received' Q 'digits/at' IMp
eastern Asia/Shanghai 'vm-received' Q 'digits/at' IMp
我配置的是Asia/Shanghai,貌似也没问题,接着往下分析:
进一步看代码,播放这个时间的逻辑如下:
static int say_date_with_format(struct ast_channel *chan, time_t t, const char *ints, const char *lang, const char *format, const char *tzone)
进一步调用
int ast_say_date_with_format_en(struct ast_channel *chan, time_t t, const char *ints, const char *lang, const char *format, const char *tzone)
里面有个关键的函数:
ast_localtime(&when, &tm, tzone);
分析ast_localtime这个函数在localtime.c中实现,其功能就是把UTC时间转成本地时间,里面关键的产生就是第三个tzone,在我的Asteris上,这个参数是正确的,那毫无疑问,这个函数内部出问题了,基本应该是找到了根源,进一步分析:
最终会走到这几行代码中,tzload是加载sp,sp是关键的时间转换函数,跟zone是强相关的
if (tzload(zone, sp, TRUE) != 0) {
if (zone[0] == ':' || tzparse(zone, sp, FALSE) != 0)
(void) gmtload(sp);
}
再往下看
static int tzload(const char *name, struct state * const sp, const int doextend)
这个函数就是从linux中加载时区信息,用到了tzhead数据结构,相信这个函数逻辑不会有问题,毕竟Asterisk也发展多年,所以一眼就看到了如下代码:
if (!doaccess) {
if ((p = TZDIR) == NULL)
return -1;
if ((strlen(p) + strlen(name) + 1) >= sizeof fullname)
return -1;
(void) strcpy(fullname, p);
(void) strcat(fullname, "/");
(void) strcat(fullname, name);
上述代码实现了拼接fullname,fullname指向了linux上的时区文件,正常的应该如下:
/usr/share/zoneinfo/Asia/Shanghai
我的openwrt上居然没有这个文件
tzfile.h定义如下:看样子是linux标准的
#ifndef TZDIR
#ifdef SOLARIS
#define TZDIR "/usr/share/lib/zoneinfo"
#else
#define TZDIR "/usr/share/zoneinfo"
#endif /* defined SOLARIS */
#endif /* !defined TZDIR */
ubuntu18.04上发现有这个文件,centos上也有,armbian上也有,ESXI上没有
本想拷贝一份到openwrt上,怕格式不兼容,搜索了一番,安装个包就行了
opkg install zoneinfo-asia
到这,貌似就解决了,重启了asterisk,居然不行,果断重启openwrt,一切正常了,分析结束。
经验:openwrt近几年很活跃,但asterisk多年前的产物了,估计当年跑openwrt的设备CPU能力还不足以跑asterisk,作者也没太考虑arm设备,但随着arm处理器的提升,搞个家庭、小型企业用途的开源sip服务,asterisk还是挺胜任的,但目前openwrt上的asterisk还是有不少问题,可以考虑移植搞个小系统。