Asterisk播放mp4(3)——搭建开发环境
Asterisk现有版本不支持播放视频文件(支持视频通话),无法满足发送视频通知、视频IVR等场景。本系列文章,通过学习音视频的相关知识和工具,尝试实现一个通过Asterisk播放mp4视频文件的应用。
Asterisk是一个开源SIP应用服务器,支持多种方式进行扩展。本文介绍如何利用Docker搭建Asterisk的本地开发测试环境,并且提供一些利用docker简化开发过程的技巧。因为本系列文章要通过ffmpeg提供媒体处理能力,所以会涉及如何整合Asterisk和ffmpeg的内容。
项目地址:https://github.com/jasony62/tms-asterisk-dev
准备Docker环境
下载需要版本的asterisk-13.33.0
源代码压缩包,官网下载地址。
建立Dockerfile
文件,制作基于centos7环境的镜像。为了跳过ffmpeg的安装(从源代码安装ffmpeg真的很麻烦),以jrottenberg/ffmpeg:4.2.3-centos7
为基础开始构建。
在镜像构建阶段完成asterisk的编译和安装,建立build-asterisk.sh
文件将编译和安装过程从Dockerfile中独立出来。执行过程中以menuselect
命令行方式指定安装选项。
安装过程中需要使用第三方库jansson
和pjproject
,会在编译阶段进行下载,但是因为网络问题(https://raw.githubusercontent.com
能否正常访问)会下载失败,导致无法安装。为了解决这个,先把这两个包下载到本地。然后修改asterisk源代码包中的third-party
目录中这个两个项目的Makefile.rules
文件(用环境变量HOST_DOMAIN
指定主机地址,因为是在容器中运行的,不能写成localhost
或127.0.0.1
),将下载文件的地址指向本地。为了实现在本地下载文件,执行npm i http-server -g
安装http服务器(如果需要),在存放下载文件的目录下,执行http-server -p 80
启动文件下载服务。
我们要在自定义的asterisk应用中使用ffmpeg,因此需要修改编译选项。在源代码目录中打开main/Makefile
,添加需要的动态链接库。(注意:这里修改后需要重新构建镜像。)
AST_LIBS+=-lavformat -lavcodec -lavutil -lswresample -lswscale -lavfilter
做完上述准备工作就可以开始构建镜像,运行容器了。
- 构建镜像
docker-compose -f docker-compose.13.yml build
- 启动容器
docker-compose -f docker-compose.13.yml up
- 进入容器
docker exec -it tms-asterisk_13.33.0 bash
- 结束容器(启动后复制到容器中的内容会丢失,如果直接ctrl+c关闭会保留)
docker-compose -f docker-compose.13.yml down
设置Asterisk
Asterisk支持通过配置文件定制系统的行为,例如:使用extensions.conf
指定拨号计划等。在docker-compose.13.yml
文件中,将本地配置文件和容器中的文件建立了关联,这样修改本地文件后,在容器中重启asterisk服务就可以了。
另外,在开发测试时经常需要查看日志,因此通过docker-compose.13.yml
文件,将asterisk的日志目录指向了本地,便于查看日志输出内容。
Asterisk处理媒体时需要动态提供RTP端口,因为docker在windows和mac环境中不支持设置为host
模式,需要在docker-compose.13.yml
文件中指定开放的端口(和配置文件rtp.conf
中的设置保持一致。)。
Asterisk运行在容器中,其地址无法访问,这样会导致媒体协商中指定的地址无法访问,需要在pjsip.conf
文件中将external_media_address
和external_signaling_address
指定为宿主机地址。
Asterisk常用命令
- 重启asterisk。不需要用命令
asterisk -r
进入asterisk后再执行core restart now
,两步变一步。
asterisk -rx "core restart now"
- 截断日志。asterisk输出的日志非常多,为了便于查看经常需要在输出前清空之前的日志。
asterisk -rx "logger rotate"
- 打开
pjsip
的日志。
asterisk -rx "pjsip set logger on"
开发应用
在docker-compose.13.yml
文件中,通过volumns
指令将自定义应用文件关联到Asterisk源代码的apps
目录下(/usr/src/asterisk/apps
)。每次在外部修改代码后,到容器中执行make && make install
,重启asterisk。
Asterisk在代码中提供了三种输出日志ast_log
,ast_verb
和ast_debug
的方法,通过配置文件logger.conf
进行设置。可以将通过配置将不同的信息(类型和等级)输出到不同文件。ast_verb
支持设置level
控制输出的内容,可以在启动时指定asterisk -vvv
,也可以通过命令指定core set verbose
。ast_debug
支持设置level
控制输出的内容,可以在启动时指定asterisk -ddd
,也可以通过命令指定core set debug
。level
的设置没有限制。
播放alaw裸流文件
- 设置帧类型和采样编码格式
f->frametype = AST_FRAME_VOICE;
f->subclass.format = ast_format_alaw;
- 设置采样数据
f->samples = nb_samples;
uint8_t *data;
data = AST_FRAME_GET_BUFFER(f);
memcpy(data, output_data, nb_samples);
f->datalen = nb_samples;
- 帧间添加延时。填不填有什么影响?
duration = (int)(((float)nb_samples / (float)ALAW_SAMPLE_RATE) * 1000 * 1000);
ast_debug(2, "完成第 %d 个RTP帧发送,添加延时 %d\n", nb_rtps, duration);
usleep(duration);
- 对比wireshark抓包数据和alaw文件中的数据
![](https://img.haomeiwen.com/i258497/55c6ed19c07d95e6.png)
![](https://img.haomeiwen.com/i258497/fb16c75fe6c19823.png)
可以看到数据是一致的,说明我们通过asterisk正确地发送了数据。
播放mp3文件
Asterisk不支持mp3编码,我们通过ffmpeg进行转码。
![](https://img.haomeiwen.com/i258497/a649180ced922710.png)
ffmpeg是一个媒体编处理框架,整合非常多的编解码库,并且对媒体加工过程进行了抽象,形成了一个统一的处理流程。我理解,处理流程大体上分为5个阶段:
- 初始化阶段。读取文件中的信息,准备好编码器,解码器,重采样器等。
- 解码阶段。从文件中读取编码包(AVPacket),将编码包发送解码器,从解码器中接收未压缩的原始帧(AVFrame)。
- 重采样阶段。每种音频编码格式的原始帧采样格式(sample_fmt)不一样,例如:mp3的采样格式为
fltp
,alaw的采样格式为s16
(我理解,采样格式是没有压缩的采样数据的记录方式,alaw对应的是13位数据,所以没有压缩的状态就需要用s16记录)。为了进行编码转换,首先需要编码格式的转换,这个过程叫做重采样。 - 编码阶段。将采样数据帧(AVFrame)发送给编码器,从编码器中获得编码后的包(AVPacket),通过astersik发送rtp包。
- 清理阶段。释放内存。
按照上一篇文章中对mp3文件格式的解析,可以知道sine-8k-10s.mp3
文件中包含141
帧,第1帧是Info
,所以数据帧有140
帧。数据帧的帧头为ffe3 18c4
,可知,每个数据帧包含576
个采样,每帧72字节(8kbps/8kHz*576/8=72)。
app_tms_mp3输出的日志:
读取编码包 #1 size= 72 字节
读取编码包 #1 前8个字节 ff e3 18 c4 00 0d 20 96
读取编码包 #2 size= 72 字节
读取编码包 #2 前8个字节 ff e3 18 c4 06 0e 70 f2
读取编码包 #3 size= 72 字节
读取编码包 #3 前8个字节 ff e3 18 c4 07 0d e8 a6
从音频包 #2 中读取音频帧 #1, format = fltp , sample_rate = 8000 , channels = 1 , nb_samples = 47, pts = 1016064, best_effort_timestamp = 1016064
从音频包 #3 中读取音频帧 #2, format = fltp , sample_rate = 8000 , channels = 1 , nb_samples = 576, pts = 2032128, best_effort_timestamp = 2032128
结束播放文件 /var/lib/asterisk/media/sine-8k-10s.mp3,共读取 141 个包,共 10152 字节,共生成 140 个包,共 80000 字节,共发送RTP包 140 个,采样 80000 个,耗时 10207260
![](https://img.haomeiwen.com/i258497/95b3b39e3cb46670.png)
通过对比程序输出的日志和样本文件中的数据,可以看到ffmpeg通过av_read_frame
读取的包,就是mp3文件中的帧,是文件中原始的未解压数据。mp3文件中每一帧的大小虽然一样(72字节),但是包含的采样数并不相同(第1包有27个,后续的包都是576个)。通过编码后,生成80000个采样,大小是80000个字节,和预期一致。
遗留问题
在asterisk中如何知道对端支持的编码格式?如果在ast_frame中指定了和用户端不一致的编码格式,asterisk会进行转码?
解码mp3文件时,AVFrame中包含pts
,如何理解和使用这个数据?可以替代通过采样数计算发送间隔吗?