最近搞了个项目,各种接视频,踩了各种坑。今天记一个搞RTSP的思路,过几天有时间再记一下直接搞大华和海康SDK的思路。
需求
简单地说,用户在系统里接几路RTSP视频,需要定期(10秒左右)从RTSP抓图回来保存到本地。
技术方案
其实前期没论证其他的技术方案,上来直接磕javacv
了。但是中间遇到好多问题,每次感觉搞不定了都去搜其他的解决方案,常见的方案大概有:
1、进程调用ffmpeg
这思路太虎了,传参太费劲,控制进程也太费劲,直接pass。
2、jcodec
搞了一晚上,没搞通它的接口。也是时间紧张没来得及细读源码,单从接口参数上看只能给本地文件路径,给rtsp地址直接报错了。如果有搞过的小伙伴可以留言交流一下,搞个示例代码给我学习学习。总之查了很多jcodec的实例,没一个好使的。
3、第三个叫啥玩意儿忘了,过去时间太久。在mvnrepository上看它的最后更新好像2013年,maven引过来发现和网上示例代码完全对不上,自己尝试调用接口咣咣报错,一脑门汗。
所以最后还是接着磕javacv
吧…… 抓图源码直接放上来:
首先是javacv的引用,整个javacv全都引用过来太过巨大,打完包900多M,直接给我干蒙了。根据实际情况我们引用精简版(注意,因为在windows开发,在linux部署,所以引用两个版本,如果只引用windows的就更精简了,自己根据需要去引用)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<properties>
<javacv.version>1.5.8</javacv.version>
<system.windows64>windows-x86_64</system.windows64>
<system.linux64>linux-x86_64</system.linux64>
</properties>
<dependencies>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>${javacv.version}</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacpp-platform</artifactId>
<version>${javacv.version}</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>5.1.2-${javacv.version}</version>
<classifier>${system.windows64}</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg</artifactId>
<version>5.1.2-${javacv.version}</version>
<classifier>${system.linux64}</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>opencv</artifactId>
<version>4.6.0-${javacv.version}</version>
<classifier>${system.windows64}</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>openblas</artifactId>
<version>0.3.21-${javacv.version}</version>
<classifier>${system.windows64}</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>flycapture</artifactId>
<version>2.13.3.31-${javacv.version}</version>
<classifier>${system.windows64}</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>opencv</artifactId>
<version>4.6.0-${javacv.version}</version>
<classifier>${system.linux64}</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>openblas</artifactId>
<version>0.3.21-${javacv.version}</version>
<classifier>${system.linux64}</classifier>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>flycapture</artifactId>
<version>2.13.3.31-${javacv.version}</version>
<classifier>${system.linux64}</classifier>
</dependency>
</dependencies>
</project>
抓图代码:
public class CameraUtil {
private static Java2DFrameConverter java2DFrameConverter;
static {
java2DFrameConverter = new Java2DFrameConverter();
avutil.av_log_set_level(avutil.AV_LOG_QUIET);
}
public static void screenShot(String rtspUrl) {
FFmpegFrameGrabber grabber = null;
try {
grabber = FFmpegFrameGrabber.createDefault(rtspUrl);
grabber.setOption("rtsp_transport", "tcp");
grabber.setOption("stimeout", "3000000");
grabber.setOption("timeout", "3000000");
grabber.setImageWidth(1280);
grabber.setImageHeight(720);
grabber.startUnsafe();
String imgBase64 = null;
while (true) {
Frame frame = grabber.grabImage();
if (frame != null) {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
synchronized (CameraUtil.class) {
ImageIO.write(java2DFrameConverter.getBufferedImage(frame), "jpg", outputStream);
}
imgBase64 = Base64Utils.imageToBase64ByLocalByte(outputStream.toByteArray());
break;
}
}
} catch (Exception e){
log.error(rtspUrl);
log.error(e.getMessage(), e);
} finally {
try {
grabber.stop();
} catch (FrameGrabber.Exception e) {
log.error(e.getMessage(), e);
} finally {
try {
grabber.close();
} catch (FrameGrabber.Exception e) {
log.error(e.getMessage(), e);
}
}
}
return captureResult;
}
}
坑
- 调用程序期间,出现大量javacv日志,影响日志的正常分析了。解决办法在这一行:
avutil.av_log_set_level(avutil.AV_LOG_QUIET);
- 多线程调用screenShot接口,其中一个线程因为某种原因卡住无法及时返回的时候,其他线程全都卡住了。解决办法是:
grabber.startUnsafe();
这里有些示例代码用的是grabber.start()方法,start方法是一个同步方法,里面synchronized加锁调用startUnsafe()方法。所以如果你直接调用startUnsafe()就可以避免其他线程等待同步方法结束。
- startUnsafe() 方法卡住不动,一直不返回,也不响应,也不报错,解决办法在:
grabber.setOption("timeout", "3000000");
设置timeout参数,可以使grabber在连接超时的时候返回。
这个timeout参数有点神奇,首先他是个微秒单位,不是毫秒,所以看起来很大(1秒=110001000微秒)。
其次实际测试的时候,发现设置了3秒超时,但它实际9秒左右抛异常返回;如果设置2秒超时,实际6秒左右抛异常返回;设置4秒的时候实际12秒左右抛异常返回,总会有个3倍左右的差值不知道是因为什么。
- startUnsafe() 方法卡住后抛出异常,异常信息为avformat_open_input() error xxx : Could not open input xxx . (Has setFormat() been called?)
实际上,上面连接超时的时候就会抛出这个异常。程序运行过程中发现有很多种情况都会抛这个异常,当时没有一一去记录,但每次都尝试从异常中发现问题和解决问题。总结下来大概有这几个方向来解决这个问题:
第一是超时问题,rtsp本身或者网络原因造成的超时;
第二是一个狗屎原因,用户帮我们发现的——rtsp地址带密码,他们在密码中设置了#符号,这种的需要urlEncode转义,要不然拿过来进行连接又连不上;
第三个可能的原因,你的摄像头没有采集声音,但是在grabber这里疯狂要音轨——其实这是播放的时候可能发生的问题,而非截图的时候;
第四个可能的原因,我没遇到过,之前看有其他人说的,采样率设置得不对也会报这个问题。
时间紧张也顾不上好好写,也顾不上好好排版了。有不对的或者有问题的欢迎指正留言,共同探讨。