前段时间项目有一个录制rtmp流生成文件的需求,使用ffmpeg软件录制输出本地文件,再使用ffprobe读取视频文件信息保存到数据库。
当时遇到一个问题,部分文件录制完后,数据库中没有保存文件信息。经过日志分析,这部分文件在使用ffprobe读取时,抛出了Invalid data found when processing input。老司机一看就知道,八成是读取文件信息的时候,写入还没有完成。程序在读取文件时,确定录制是已经停止的,这种小问题,加个延时应该就解决了。于是在调用读取的地方,添加了一个定时。调用停止录制2000毫秒后再读取硬盘上的文件信息。
//发现录制应该结束
Process cmd = FFmpegProcessUtil.getFFmpegProcessUtil().getRecord(streamName);
cmd.destroy();
vertx.setTimer(2000,id->{
FFmpegProbeResult probe = ShellUtil.probe(config.getString("ffprobe_path"), filePath);
//保存文件信息
});
然而周冬雨眉头一皱,发现事情并不简单。【此处应有图,原谅我图床还没选好😂😂😂】
前两天部门一小哥说录制文件找不到。分析日志仍然是Invalid data found when processing input。感觉我之前的想法确实有点简单了。
而且小哥分析日志后,发现读取错误的文件其实是有共同点的。
这个record程序的流程如下。
使用一个定时器,每秒钟扫描nginx-rtmp端口。发现有数据流就加载到缓存中,提供外部接口,由上层业务系统调用接口控制录制的开始和关闭。ffmpeg录制结束有两个节点。
- 上层业务系统调用接口停止录制。
- 定时器扫描到nginx端口不存在这个数据流,停止录制。
小哥发现:所有读取错误的文件,均是第二种方式停止的录制。
经测试。在使用如下命令转存文件时,及时输入端数据已经终止,ffmpeg进程仍然存在。ffmpeg -i rtmp://<ip>/live/streamName -f mp4 test.mp4
本地调试代码后发现当定时器检测到nginx端口的流已经不存在时,调用Process.destroy()方法并不能立即终止进程。
vertx.setPeriodic(1000 * 1, new RefreshStreamHandler());
RefreshStreamHandler.javapublic void handle(Long event) { //其它未失效数据处理 loseMapKey.forEach(streamName -> { //其它处理 if(lostMap.get(streamName).getLcFile() != null){ logger.info("发现流结束时仍然有录制"); Process cmd = FFmpegProcessUtil.getFFmpegProcessUtil().getRecord(streamName); cmd.destroy(); FFmpegProbeResult probe = ShellUtil.probe(config.getString("ffprobe_path"), filePath); //保存文件信息 } //其它处理 }
当我在logger.info行添加断点调试时发现就算destroy()执行成功后。ffmpeg进程仍然存在,直到整个handle处理完成之后ffmpeg才消失。
于是对代码做了如下调整。
vertx.setPeriodic(1000 * 1, new RefreshStreamHandler());
vertx.setPeriodic(1000 * 1, new FileHandler());
public void handle(Long event) {
//其它未失效数据处理
loseMapKey.forEach(streamName -> {
//其它处理
if(lostMap.get(streamName).getLcFile() != null){
logger.info("发现流结束时仍然有录制");
Process cmd = FFmpegProcessUtil.getFFmpegProcessUtil().getRecord(streamName);
cmd.destroy();
recordList.add(filePath)
}
//其它处理
}
public void handle(Long event) { getNext(); } private void getNext() { if (recordList.size() > 0) { insertFile(recordList.get(0)); recordList.remove(0); getNext(); } }
使用另一个定时器去查找文件,由于两个定时器同时执行,当扫描端口的定时器查询到流结束时将流标识添加到list中,本次同时执行的文件扫描定时器因为已经执行完成,所以只能在下个周期处理当前周期添加到list中的任务。本地测试后放到服务器,检验OK。
代码表面的问题已经解决,但是更深入的原理的仍然困扰着我。rtmp源地址没有断的情况下,使用destroy()方法后立即查询文件详情为什么没有问题?ffmpeg为何会在handle()方法执行完成后就会停止?Runtimer执行shell的时候,Process和进程之间的关系等等。。。
要学的东西还有很多。留个坑,以后一定会补上后面三个问题