通过海康接口返回的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); System.err.println(fileExist); // 文件夹不存在,则新建 if (!fileExistTWo.exists()) { fileExistTWo.mkdirs(); } // 省略查询路径部分 实体-> result String codeId = fileName + ":" + code; manager.stop(codeId); // 先停止视频 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")); TaskEntity info= (TaskEntity) manager.query(codeId); System.out.println(info); int suspensionState = info.getSuspensionState(); System.out.println(suspensionState); String urlHead = "http://"; // 返回路径根据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> 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; ListfileNamesList = 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() { // 先去遍历删除文件 @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()) { ListfileNamesList = getFileNamesList(basePathAll); if (CollectionUtils.isNotEmpty(fileNamesList)) { fileNamesList.forEach(x -> { try { stopBackVideo(x, token1); } catch (IOException e) { e.printStackTrace(); } }); if (fileExist.exists()) { deleteDir(basePathAll); } } } }
拿取到所有该文件目录,下面所有的文件夹名称 集合
private ListgetFileNamesList(String path) { File file = new File(path); if (!file.exists()) { return null; } List fileNames = new ArrayList<>(); return getFileNames(file, fileNames); } /** * 得到文件名称 * * @param file 文件 * @param fileNames 文件名 * @return {@link List}<{@link String}> */ private List getFileNames(File file, List 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); } } }