通过海康接口返回的rtsp视频接口,转换成.m3u8格式文件,逻辑如下
1、采用ffmpeg实时转化rtsp链接视频,转化为m3u8,存放服务器固定地址
2、采用nginx代理视频成.m3u8视频
3、采用token+redis方式处理视频播放和删除过程,开启视频录像,并将token或者自定义文件夹存入redis,将用户token解析部分(我解析的是jwt的token最后一个点后面内容,作为当前用户的开始视频存放的文件夹A),视频摄像头唯一编码作为下面一个子文件夹B,A+B作为ffmpeg开启的key
4、停止某个视频,通过A+B停止ffmpeg视频转化,并删除B下面所有资源,包含B所有文件夹
5、退出登录,停止并删除A下面的所有视频资源转化,并删除A文件夹
6、redis中的token过期,回调方法返回过期的key,对key解析,拿到token最后一个点后面内容,也是就是文件A,对第五步进行操作

1、nginx转码配置及ffmpeg转化,我参考的下面博客
https://www.freesion.com/article/5775913700/
, 注意,java的ffmpeg部分,我自定义了一个文件夹ffmpeg,


1、我的调用ffmpeg的start方法开始转视频流,注意转流的文件路径要先创建,fileExistTWo.mkdirs();
@Autowired CommandManager manager;public String toHls(String fileName, String code, String url) { //.m3u8文件路径 String basePath = rootPath + fileName + File.separator + code + File.separator + code + File.separator; //文件夹路径 String basePathTWo = rootPath + fileName + File.separator + code + File.separator; File fileExist = new File(basePath); File fileExistTWo = new File(basePathTWo); // 文件夹不存在,则新建 if (!fileExistTWo.exists()) { fileExistTWo.mkdirs(); } //ffmPeg的唯一编码codeId String codeId = fileName + ":" + code; // 先停止视频 manager.stop(codeId); // 调用ffmPeg开启新的转流方法 manager.start(codeId, CommandBuidlerFactory.createBuidler() .add("`ffmpeg`") .add("-rtsp_transport", "tcp") .add("-i", url) // 取videoUrl .add("-c", "copy") .add("-f", "hls") .add("-hls_time", "2.0") .add("-hls_list_size", "2") .add("-hls_flags", "2") .add(fileExist + ".m3u8")); String urlHead = "http://"; // CommonUtil.getIpv4IP()我自定义的获取公网IP方法 // 返回路径根据ffmpeg存放视频路径+nginx代理灵活配置 String urlTwo = urlHead + CommonUtil.getIpv4IP().trim() + ":" + 1011 + "/videoCache/" + fileName + "/" + code + "/" + code + ".m3u8"; return urlTwo; }
获取公网IP方法
public static String getIpv4IP() {
StringBuilder result = new StringBuilder();
BufferedReader in = null;
try {
URL realUrl = new URL("https://www.taobao.com/help/getip.php");
// 打开和URL之间的连接
URLConnection connection = realUrl.openConnection();
// 设置通用的请求属性
connection.setRequestProperty("accept", "*/*");
connection.setRequestProperty("connection", "Keep-Alive");
connection.setRequestProperty("user-agent",
"Mozilla/4.0 (compatible; MSIE 6.0; Windows NT 5.1;SV1)");
// 建立实际的连接
connection.connect();
// 获取所有响应头字段
Map<String, List<String>> map = connection.getHeaderFields();
// 定义 BufferedReader输入流来读取URL的响应
in = new BufferedReader(new InputStreamReader(
connection.getInputStream()));
String line;
while ((line = in.readLine()) != null) {
result.append(line);
}
} catch (Exception e) {
// log.error("获取ipv4公网地址异常");
e.printStackTrace();
} finally {
try {
if (in != null) {
in.close();
}
} catch (Exception e2) {
e2.printStackTrace();
}
}
String str = result.toString().replace("ipCallback({ip:", "");
String ipStr = str.replace("})", "");
return ipStr.replace('"', ' ');
}
2、停止视频方法
token值如下
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzUxMiJ9.eyJpc3MiOiJpc3N1c2VyIiwiYXVkIjoiYXVkaWVuY2UiLCJ0ZW5hbnRfaWQiOiIwMDAwMDAiLCJyb2xlX25hbWUiOiJhZG1pbmlzdHJhdG9yIiwicG9zdF9pZCI6IjExMjM1OTg4MTc3Mzg2NzUyMDEiLCJ1c2VyX2lkIjoiMTEyMzU5ODgyMTczODY3NTIwMSIsInJvbGVfaWQiOiIxMTIzNTk4ODE2NzM4Njc1MjAxIiwidXNlcl9uYW1lIjoiYWRtaW4iLCJuaWNrX25hbWUiOiLnrqHnkIblkZgiLCJkZXRhaWwiOnsidHlwZSI6IndlYiIsInByb2plY3RJZCI6MTU4MjAwNTQ3Mzk1NTEzMTM5MywiVXNlckNhdGVnb3J5IjoiMCIsIldzSWQiOjE0MzgzMzIwNjQ1NjExNDM4MDl9LCJ0b2tlbl90eXBlIjoiYWNjZXNzX3Rva2VuIiwiZGVwdF9pZCI6IjExMjM1OTg4MTM3Mzg2NzUyMDEiLCJhY2NvdW50IjoiYWRtaW4iLCJjbGllbnRfaWQiOiJzYWJlciIsImV4cCI6MTY3MDU2OTAyMSwibmJmIjoxNjcwNTY1NDIxfQ.NHZiaWqrCIRukfvAqChkDNAAH34Pffm_PvQIEfqAU0SdKkS9ZNhxnB354demmkAJ2l8m3OXWIkeSkeHHGNuzEg
停止的方法如下
public R stopBackVideo(String code, String token) throws IOException {
//解析jwt的token值,拿到最后面一截,这个也是不会重复
String fileName = StringUtils.split(token, ".")[2];
String basePath = rootPath + fileName + File.separator + code + File.separator;
String basePathAll = rootPath + fileName + File.separator;
List<String> fileNamesList = getFileNamesList(basePathAll);
fileNamesList.forEach(x -> {
System.err.println("文件名称:" + x);
});
System.err.println(basePathAll);
File fileExist = new File(basePath);
String codeId = fileName + ":" + code;
//停止ffmpeg转码
manager.stop(codeId);
manager.start(codeId, CommandBuidlerFactory.createBuidler()
.add("rm -rf", fileExist.toString()));
//对文件夹进行删除操作
if (fileExist.exists()) {
deleteDir(basePath);
}
return R.data("删除成功");
}
删除文件夹下面所有文件
public void deleteDir(String basePath) throws IOException {
Path path = Paths.get(basePath);
Files.walkFileTree(path,
new SimpleFileVisitor<Path>() {
// 先去遍历删除文件
@Override
public FileVisitResult visitFile(Path file,
BasicFileAttributes attrs) throws IOException {
Files.delete(file);
System.out.printf("文件被删除 : %s%n", file);
return FileVisitResult.CONTINUE;
}
// 再去遍历删除目录
@Override
public FileVisitResult postVisitDirectory(Path dir,
IOException exc) throws IOException {
Files.delete(dir);
System.out.printf("文件夹被删除: %s%n", dir);
return FileVisitResult.CONTINUE;
}
}
);
}
3、停止并删除当前用户的所有视频,及记录
public void removeAllVideoByToken(String token1) throws IOException {
String fileName = StringUtils.split(token1, ".")[2];
String basePathAll = rootPath + fileName + File.separator;
File fileExist = new File(basePathAll);
// 文件夹文件夹存在,则停止后删除
if (fileExist.exists()) {
List<String> fileNamesList = getFileNamesList(basePathAll);
if (CollectionUtils.isNotEmpty(fileNamesList)) {
fileNamesList.forEach(x -> {
try {
stopBackVideo(x, token1);
} catch (IOException e) {
e.printStackTrace();
}
});
if (fileExist.exists()) {
deleteDir(basePathAll);
}
}
}
}
拿取到所有该文件目录,下面所有的文件夹名称 集合
private List<String> getFileNamesList(String path) {
File file = new File(path);
if (!file.exists()) {
return null;
}
List<String> fileNames = new ArrayList<>();
return getFileNames(file, fileNames);
}
/**
* 得到文件名称
*
* @param file 文件
* @param fileNames 文件名
* @return {@link List}<{@link String}>
*/
private List<String> getFileNames(File file, List<String> fileNames) {
File[] files = file.listFiles();
for (File f : files) {
if (f.isDirectory()) {
fileNames.add(f.getName());
}
}
//所有文件
// if (f.isDirectory()) {
// getFileNames(f, fileNames);
// } else {
// fileNames.add(f.getName());
// }
// }
return fileNames;
}
4、监听redis中token失效回调方法,并停止用户的没有关闭的视频流,删除文件,减少资源占用
redis.conf将 notify-keyspace-events修改 成Ex
notify-keyspace-events Ex
RedisMessageListenerContainer 加入容器
@Bean
RedisMessageListenerContainer container(RedisConnectionFactory connectionFactory) {
RedisMessageListenerContainer container = new RedisMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
return container;
}
我是放在application下面

监听key变化,key过期则对视频流进行清理操作
@Component
@Slf4j
public class RedisKeyExpirationListener extends KeyExpirationEventMessageListener {
public RedisKeyExpirationListener(RedisMessageListenerContainer listenerContainer) {
super(listenerContainer);
}
@Autowired
CameraSecretInfoService cameraSecretInfoService;
/**
* key过期触发的事件
*/
@SneakyThrows
@Override
public void onMessage(Message message, byte[] pattern) {
String channel = new String(message.getChannel(), StandardCharsets.UTF_8);
String key = new String(message.getBody(), StandardCharsets.UTF_8);
boolean contains = key.contains("bladeFile:fileName:");
if (contains) {
log.info("redis key 过期:pattern={},channel={},key={}", new String(pattern), channel, key);
String token = key.substring(19);
cameraSecretInfoService.removeAllVideoByToken(token);
}
}
}



















