经过前面两个章节的铺垫,终于要讲讲整个系统的设计了,先上张图:
核心服务是rtsp2hls,它的作用是启动并控制ffmpeg的工作,并对外提供了一套Socket接口。mod_proxy的作用是将socket接口封装成http接口,并映射了m3u8和ts文件的http访问url。
核心功能
HLS的工作原理并不复杂,在前面的第一章节已经说了通过视频库程序ffmpeg来实现,rtsp2hsl对它进行了封装,实现了以下功能:
- ffmpeg的命令行参数配置,可通过配置文件精细化配置ffmpeg的运行参数:
[transcoding]
# ffmpeg转码命令行, {1}rtsp地址 {2}输出文件
ffmpeg_params = "-rtsp_transport tcp -i {1} -fflags flush_packets -max_delay 2 -flags -global_header -hls_init_time 0.5 -hls_time 1 -hls_wrap 5 -acodec copy -vcodec copy -y {2}"
参数涉及视频专业知识,这里就不展开说了,感兴趣的童鞋可以访问ffmpeg官网获取这些参数的含义,也可以调整参数来优化HLS的性能。参数中的{1}和{2}是占位符,比如上一章节Web页面中的两个输入框:
最后传递给ffmpeg的参数就是: -rtsp_transport tcp -i rtsp://admin:Pass1234@2.36.207.50:554 -fflags flush_packets -max_delay 2 -flags -global_header -hls_init_time 0.5 -hls_time 1 -hls_wrap 5 -acodec copy -vcodec copy -y D:\hls\out\1\video.m3u8
这样的设计也就表明rtsp2hsl是支持服务并发的,比如像这几年应急、城市大脑等业务中,需要监控的摄像头成百上千,我们可以为每个摄像头定义一个唯一编码,然后按需去调用播放所需摄像头。
- 启动、关闭ffmpeg
rtsp2hls服务通过调用shell命令行方式启动ffmpeg程序,ffmpeg即成为其子进程。主子进程关系可以让rtsp2hls方便的关闭ffmpeg,下面代码是启动和关闭ffmpeg的代码片段:
- 任务保持
从HLS的工作原理我们知道,播放过程是和生产过程是通过.m3u8和.ts文件关联到一起的,ffmpeg并不知道播放这回事,用户在播放过程中可能会暴力地关闭浏览器窗口,而服务端却傻乎乎的什么也不知道,这样ffmpeg启动后会一直处于工作状态,造成资源浪费。
改善这种情况的常用方式是通过心跳机制,在浏览器端播放过程中,定期发送一个信号给服务端,rtst2hls 服务通过检查心跳信号来保持 ffmpeg 的运行,一旦超过一个内定的时间而没有收到信号,则说明浏览器端不再播放,rtst2hls 也就可以放心的 kill 掉 ffmpeg 的工作。
$.ajax({
......
success: function(rsp_json){
myVideo.src({
src: "/videoplay/"+code+"/video.m3u8" //m3u8文件url
});
myVideo.play(); //播放
heartbeat = setInterval (keepalive, 5000, code); //发送保活心跳,5秒间隔
}
});
HLS的这种松耦合的特点,也并不总是带来麻烦,它可以构建一种单一生产者和多消费者的模式,可以节省转码的资源消耗,如下图:
填坑
HLS松耦合的特点,在编程中容易掉到坑里,例如下面启动任务过程:
启动任务流程:
1)浏览器HTTP请求发起HLS任务,2) mod_proxy 转发给 rtsp2hls,3)rtsp2hls 启动 ffmpeg 子进程,4.1)接口最终反馈给浏览器,5)浏览器HTTP请求获得.m3u8等文件。
在这个流程中埋着一个坑:4.1接口返回只表示了 rtsp2hls 启动了 ffmpeg 子进程,但4.2生成第一个.ts文件和.m3u8文件需要一定的时间,而这个生成状态在 ffmpeg进程内部,并不能随接口返回给浏览器,也就是4.1和4.2是同时发生的,并没有前后关系,大多情况下第5步获取文件时是获取不到的,因为还没生产出来,结果就是播放出错。
要解决这个问题,需要在4.1返回之前等待第一个.ts文件和.m3u8文件的生成,这里需要使用一定的编程技巧:比如先阻塞4.1返回,再开一个线程来轮询输出路径,等待.ts文件和.m3u8文件生成了,反过来通知阻塞线程让4.1返回。这涉及线程、消息等一系列操作,总之很麻烦。
rtsp2hls服务是Rust语言编写的,使用了异步运行时库tokio,处理起来就简单了:
timeout函数有两个参数,第一个参数是超时时间(8秒),第二个参数是检查.ts文件和.m3u8文件生成函数。如果check_m3u8_ok参数函数检查到了文件生成则timeout函数立即返回;如果一直检测不到而时间超过8秒,timeout函数也立即返回。函数后面的.await表示timeout是异步函数,无需多线程,整个代码逻辑显得简单清晰。
接口的封装
rtsp2hls封装了以下业务接口,接口输入和输出的数据都采用了Json格式,接口对外网络协议是TCP Socket。
- 启动任务接口
输入:
{"method":"start","code":"视频编码","rtsp":"rtsp地址"}
返回(成功):
{"status":"ok"}
返回(失败):
{"status":"error"}
接口内部的阻塞机制等待.m3u8文件生成后才返回成功,或者超时返回失败。
- 任务保持/心跳接口
输入:
{"method":"keepalive","code":"视频编码"}
返回(成功):
{"status":"ok"}
返回(失败):
{"status":"error"}
返回失败表示任务不存在,或是已经被kill了。
rtsp2hls的接口形式是TCP Socket,并不能让Web前端直接访问,这里mod_proxy充当了将http和socket的转换桥梁。mod_proxy的全称是 Module Proxy,是一款有特殊能力的http反向代理服务器软件,有关它更详细的信息请访问 https://gitee.com/dyf029/module-proxy, 或 《Module Proxy》 系列文章。