FFmpeg转封装rtsp到rtmp(无需转码,低资源消耗)

Stella981
• 阅读 1103

FFmpeg转封装rtsp到rtmp(无需转码,低资源消耗)

发表于 2019-12-31 | 分类于 FFmpeg | 0 | 阅读数 247

参考:javaCV开发详解之8:转封装在rtsp转rtmp流中的应用(无须转码,更低的资源消耗)

用到的技术:FFmpeg、JavaCV、ngingx
项目背景:将海康摄像头的rtsp流转为rtmp流,配合video.js实现web端播放。
[注]:该项目中的一些处理是为了满足公司项目需求添加完善的,如果需要改造扩展只需要在原来的基础上进行扩充或者剥离即可。最基本的核心操作在CameraPush.java这个类中,或者参考上述链接原作者的代码。

该项目需要搭配使用的nginx服务器下载地址:http://cdn.banmajio.com/nginx.rar
下载后解压该文件,点击nginx.exe(闪退是正常的,可以通过任务管理器查看是否存在nginx进程,存在则说明启动成功了)启动nginx服务。
nginx的配置文件存放在conf目录下的nginx.conf,根据需要修改。项目中的rtmp地址就是根据这个配置文件来的。

待优化之处:
1.如果服务部署在Docker环境下,本机ip是动态的,并非固定为127.0.0.1,所以需要动态获取nginx域名解析为ip,rtmp推送地址才能生效,可以使用InetAddress.getByName(www.baidu.com).getHostAddress();这样的方式获取解析到的ip地址。
2.目前出现的一个bug尚未解决,如果传入的设备ip填写错误,在JavaCV的FFmpegFrameGrabber构造器在调用start()方法是会出现阻塞现象,导致构造器无法释放,后续推流工作无法继续。

项目github地址:https://github.com/banmajio/RTSPtoRTMP
CSDN主页:banmajio’s csdn

目录结构

FFmpeg转封装rtsp到rtmp(无需转码,低资源消耗)

1.com.junction包里的类为SpringBoot项目启动类。
2.com.junction.cache包里的类为保存推流信息的缓存类。
3.com.junction.controller包里的类为项目controller API接口。
4.com.junction.pojo包里的类为相机信息和配置文件映射的bean。
5.com,junction.thread包里的类为线程池管理类。
6.com.junction.util包里的类为拉流推流业务处理类和定时任务Timer类。
7.application.yml为项目配置文件。

添加依赖,编写配置文件

1.添加依赖,引入javacpp和ffmpeg的jar包。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

org.bytedeco javacv 1.5.1 org.bytedeco ffmpeg-platform 4.1.3-1.5.1 org.springframework.boot spring-boot-configuration-processor true

2.pom中引入的spring-boot-configuration-processor是为了将配置文件映射为bean,方便项目中使用配置文件中的值

1 2 3 4 5 6 7 8 9 10 11 12

server: port: 8082 servlet: context-path: /camera

config: #直播流保活时间(分钟) keepalive: 5 #nginx推送地址 push_ip: 127.0.0.1 #nginx推送端口 push_port: 1935

创建Bean

1.CameraPojo(相机信息)

1 2 3 4 5 6 7 8 9 10 11 12

private String username;// 摄像头账号 private String password;// 摄像头密码 private String ip;// 摄像头ip private String channel;// 摄像头通道号 private String stream;// 摄像头码流(main为主码流、sub为子码流) private String rtsp;// rtsp地址 private String rtmp;// rtmp地址 private String startTime;// 回放开始时间 private String endTime;// 回放结束时间 private String openTime;// 打开时间 private int count = 0;// 使用人数 private String token;//唯一标识token

2.Config(读取配置文件的bean)

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42

package com.junction.pojo;

import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component;

/** * @Title ConfigPojo.java * @description 读取配置文件的bean * @time 2019年12月25日 下午5:11:21 * @author wuguodong **/ @Component //读取application.yml中config层级下的配置项 @ConfigurationProperties(prefix = "config") public class Config { private String keepalive;//保活时长(分钟) private String push_ip;//推送地址 private String push_port;//推送端口

public String getKeepalive() { return keepalive; } public void setKeepalive(String keepalive) { this.keepalive = keepalive; } public String getPush_ip() { return push_ip; } public void setPush_ip(String push_ip) { this.push_ip = push_ip; } public String getPush_port() { return push_port; } public void setPush_port(String push_port) { this.push_port = push_port; } @Override public String toString() { return "Config [keepalive=" + keepalive + ", push_ip=" + push_ip + ", push_port=" + push_port + "]"; } }

创建缓存Cache

保存推流信息,与服务启动的时间。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

/** * @Title CacheUtil.java * @description 推流缓存信息 * @time 2019年12月17日 下午3:12:45 * @author wuguodong **/ public final class CacheUtil { /* * 保存已经开始推的流 */ public static Map<String, CameraPojo> STREAMMAP = new ConcurrentHashMap<String, CameraPojo>();

/* * 保存服务启动时间 */ public static long STARTTIME; }

修改启动类

项目启动时,将启动时间存入缓存中;项目结束时,销毁线程池和定时器,释放资源。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31

package com.junction;

import java.util.Date;

import javax.annotation.PreDestroy;

import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;

import com.junction.cache.CacheUtil; import com.junction.thread.CameraThread; import com.junction.util.TimerUtil;

@SpringBootApplication public class CameraServerApplication {

public static void main(String[] args) { //将服务启动时间存入缓存 CacheUtil.STARTTIME = new Date().getTime(); SpringApplication.run(CameraServerApplication.class, args); }

@PreDestroy public void destory() { System.err.println("释放空间..."); // 关闭线程池 CameraThread.MyRunnable.es.shutdownNow(); // 销毁定时器 TimerUtil.timer.cancel(); } }

拉流、推流、转封装

1.两个重要构造器FFmpegFrameGrabberFFmpegFrameRecorder
2.转封装不涉及转码,所以资源占用很低。

什么是转封装?为什么转封装比转码消耗更少?为什么转封装无法改动视频尺寸?
先举个栗子:假设视频格式(mp4,flv,avi等)是盒子,里面的视频编码数据(h264,hevc)是苹果,我们把这个苹果从盒子里取出来放到另一个盒子里,盒子是变了,苹果是没有变动的,因此视频相关的尺寸数据是没有改动的,这个就是转封装的概念。
有了上面这个例子,我们可以把“转码”理解为:把这个盒子里的苹果(hevc)拿出来削皮切块后再加工成樱桃(h264)后再装到另一个盒子里,多了一步对苹果(hevc)转换为樱桃(h264)的操作,自然比直接把苹果拿到另一个盒子(转封装)要消耗更多机器性能。

com.junction.util;

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156

import static org.bytedeco.ffmpeg.global.avcodec.av_packet_unref;

import org.bytedeco.ffmpeg.avcodec.AVPacket; import org.bytedeco.ffmpeg.avformat.AVFormatContext; import org.bytedeco.javacv.FFmpegFrameGrabber; import org.bytedeco.javacv.FFmpegFrameRecorder;

import com.junction.pojo.CameraPojo;

/** * @Title CameraPush.java * @description 拉流推流 * @time 2019年12月16日 上午9:34:41 * @author wuguodong **/ public class CameraPush { protected FFmpegFrameGrabber grabber = null;// 解码器 protected FFmpegFrameRecorder record = null;// 编码器 int width;// 视频像素宽 int height;// 视频像素高

// 视频参数 protected int audiocodecid; protected int codecid; protected double framerate;// 帧率 protected int bitrate;// 比特率

// 音频参数 // 想要录制音频,这三个参数必须有:audioChannels > 0 && audioBitrate > 0 && sampleRate > 0 private int audioChannels; private int audioBitrate; private int sampleRate;

// 设备信息 private CameraPojo cameraPojo;

public CameraPush(CameraPojo cameraPojo) { this.cameraPojo = cameraPojo; } /** * 选择视频源 * * @author wuguodong * @throws Exception */ public CameraPush from() throws Exception { // 采集/抓取器 System.out.println(cameraPojo.getRtsp()); grabber = new FFmpegFrameGrabber(cameraPojo.getRtsp()); if (cameraPojo.getRtsp().indexOf("rtsp") >= 0) { grabber.setOption("rtsp_transport", "tcp");// tcp用于解决丢包问题 } // 设置采集器构造超时时间 grabber.setOption("stimeout", "2000000"); grabber.start();// 开始之后ffmpeg会采集视频信息,之后就可以获取音视频信息 width = grabber.getImageWidth(); height = grabber.getImageHeight(); // 若视频像素值为0,说明采集器构造超时,程序结束 if (width == 0 && height == 0) { System.err.println("[ERROR] 拉流超时..."); return null; } // 视频参数 audiocodecid = grabber.getAudioCodec(); System.err.println("音频编码:" + audiocodecid); codecid = grabber.getVideoCodec(); framerate = grabber.getVideoFrameRate();// 帧率 bitrate = grabber.getVideoBitrate();// 比特率 // 音频参数 // 想要录制音频,这三个参数必须有:audioChannels > 0 && audioBitrate > 0 && sampleRate > 0 audioChannels = grabber.getAudioChannels(); audioBitrate = grabber.getAudioBitrate(); if (audioBitrate < 1) { audioBitrate = 128 * 1000;// 默认音频比特率 } return this; } /** * 选择输出 * * @author wuguodong * @throws Exception */ public CameraPush to() throws Exception { // 录制/推流器 record = new FFmpegFrameRecorder(cameraPojo.getRtmp(), width, height); record.setVideoOption("crf", "28");// 画面质量参数,051;1828是一个合理范围 record.setGopSize(2); record.setFrameRate(framerate); record.setVideoBitrate(bitrate);

record.setAudioChannels(audioChannels); record.setAudioBitrate(audioBitrate); record.setSampleRate(sampleRate); AVFormatContext fc = null; if (cameraPojo.getRtmp().indexOf("rtmp") >= 0 || cameraPojo.getRtmp().indexOf("flv") > 0) { // 封装格式flv record.setFormat("flv"); record.setAudioCodecName("aac"); record.setVideoCodec(codecid); fc = grabber.getFormatContext(); } record.start(fc); return this; }

/** * 转封装 * * @author wuguodong * @throws org.bytedeco.javacv.FrameGrabber.Exception * @throws org.bytedeco.javacv.FrameRecorder.Exception * @throws InterruptedException */ public CameraPush go(Thread nowThread) throws org.bytedeco.javacv.FrameGrabber.Exception, org.bytedeco.javacv.FrameRecorder.Exception { long err_index = 0;// 采集或推流导致的错误次数 // 连续五次没有采集到帧则认为视频采集结束,程序错误次数超过5次即中断程序 for (int no_frame_index = 0; no_frame_index < 5 || err_index < 5;) { try { // 用于中断线程时,结束该循环 nowThread.sleep(1); AVPacket pkt = null; // 获取没有解码的音视频帧 pkt = grabber.grabPacket(); if (pkt == null || pkt.size() <= 0 || pkt.data() == null) { // 空包记录次数跳过 no_frame_index++; err_index++; continue; } // 不需要编码直接把音视频帧推出去 err_index += (record.recordPacket(pkt) ? 0 : 1); av_packet_unref(pkt); } catch (InterruptedException e) { // 当需要结束推流时,调用线程中断方法,中断推流的线程。当前线程for循环执行到 // nowThread.sleep(1);这行代码时,因为线程已经不存在了,所以会捕获异常,结束for循环 // 销毁构造器 grabber.close(); record.close(); System.err.println("设备中断推流成功..."); break; } catch (org.bytedeco.javacv.FrameGrabber.Exception e) { err_index++; } catch (org.bytedeco.javacv.FrameRecorder.Exception e) { err_index++; } } // 程序正常结束销毁构造器 grabber.close(); record.close(); System.err.println("设备推流完毕..."); return this; } }

定时任务Timer

定时任务用来执行两部分操作:
1.定时检查正在推流的通道使用人数,如果该通道当前使用人数为0,则中断线程,结束该路视频推流并清除缓存。
2.定时检查正在推流的通道最后打开请求时间,如果与当前时间超过配置的保活时间时,则结束推流,并清除缓存。
当前设置的定时任务执行间隔为1分钟,可自行修改。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72

package com.junction.util;

import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Set; import java.util.Timer; import java.util.TimerTask;

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.CommandLineRunner; import org.springframework.stereotype.Component;

import com.junction.cache.CacheUtil; import com.junction.controller.CameraController; import com.junction.pojo.Config;

/** * @Title TimerUtil.java * @description 定时任务 * @time 2019年12月16日 下午3:10:08 * @author wuguodong **/ @Component public class TimerUtil implements CommandLineRunner {

@Autowired private Config config;// 配置文件bean

public static Timer timer;

@Override public void run(String... args) throws Exception { // 超过5分钟,结束推流 timer = new Timer("timeTimer"); timer.schedule(new TimerTask() { @Override public void run() { System.err.println("开始执行定时任务..."); // 管理缓存 if (null != CacheUtil.STREAMMAP && 0 != CacheUtil.STREAMMAP.size()) { Set keys = CacheUtil.STREAMMAP.keySet(); for (String key : keys) { try { // 最后打开时间 long openTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss") .parse(CacheUtil.STREAMMAP.get(key).getOpenTime()).getTime(); // 当前系统时间 long newTime = new Date().getTime(); // 如果通道使用人数为0,则关闭推流 if (CacheUtil.STREAMMAP.get(key).getCount() == 0) { // 结束线程 CameraController.jobMap.get(key).setInterrupted(); // 清除缓存 CacheUtil.STREAMMAP.remove(key); CameraController.jobMap.remove(key); } else if ((newTime - openTime) / 1000 / 60 > Integer.valueOf(config.getKeepalive())) { CameraController.jobMap.get(key).setInterrupted(); CameraController.jobMap.remove(key); CacheUtil.STREAMMAP.remove(key); System.err.println("[定时任务] 关闭" + key + "摄像头..."); } } catch (ParseException e) { e.printStackTrace(); } } } System.err.println("定时任务执行完毕..."); } }, 1, 1000 * 60); } }

线程池管理

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58

package com.junction.thread;

import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors;

import com.junction.cache.CacheUtil; import com.junction.controller.CameraController; import com.junction.pojo.CameraPojo; import com.junction.util.CameraPush;

/** * @Title CameraThread.java * @description TODO * @time 2019年12月16日 上午9:32:43 * @author wuguodong **/ public class CameraThread { public static class MyRunnable implements Runnable { // 创建线程池 public static ExecutorService es = Executors.newCachedThreadPool();

private CameraPojo cameraPojo; private Thread nowThread;

public MyRunnable(CameraPojo cameraPojo) { this.cameraPojo = cameraPojo; }

// 中断线程 public void setInterrupted() { nowThread.interrupt(); }

@Override public void run() { // 直播流 try { // 获取当前线程存入缓存 nowThread = Thread.currentThread(); CacheUtil.STREAMMAP.put(cameraPojo.getToken(), cameraPojo); // 执行转流推流任务 CameraPush push = new CameraPush(cameraPojo).from(); if (push != null) { push.to().go(nowThread); } // 清除缓存 CacheUtil.STREAMMAP.remove(cameraPojo.getToken()); CameraController.jobMap.remove(cameraPojo.getToken()); } catch (Exception e) { System.err.println( "当前线程:" + Thread.currentThread().getName() + " 当前任务:" + cameraPojo.getRtsp() + "停止..."); CacheUtil.STREAMMAP.remove(cameraPojo.getToken()); CameraController.jobMap.remove(cameraPojo.getToken()); e.printStackTrace(); } } } }

编写controller

controller提供了五个接口,使用RESTful风格,故使用postman等软件测试时,选择相应的类型。
1.获取视频服务配置信息及服务运行时间
api: http://127.0.0.1:8082/camera/status (GET)
2.获取正在推送的所有视频流信息
api: http://127.0.0.1:8082/camera/cameras (GET)
3.开启视频流(直播or回放)
api: http://127.0.0.1:8082/camera/cameras (POST)
params: ip;username;password;channel;stream;starttime;endtime
4.关闭视频流
api: http://127.0.0.1:8082/camera/cameras/:tokens (DELETE)
5.视频流保活
api: http://127.0.0.1:8082/camera/cameras/:tokens (PUT)

开启视频流接口(POST)

先校验参数,然后判断缓存是否为空(如果为空说明目前没有推流任务,否则遍历缓存,通过参数判断当前通道是否在推流。如果找到,则该路视频的bean内人数count+1,反之调用openStream()方法进行推流)。

openStream()方法内先判断是否存在starttime参数,如果有则说明该流为历史流;在判断是否存在endtime,若无endtime则使用starttime前后各加一分钟作为历史流的开始时间和结束时间。若无starttime则视为该流为直播流。ffmpeg在拉取rtsp直播流和历史流时的命令不相同,所以需要上述判断!!

通过openStream()组装rtsp命令和rtmp命令以及UUID生成的token和其他参数,set进cameraPojo中。提交当前任务到线程池,并将当前任务线程存入jobMap(存放推流线程任务的缓存)中。

1 2 3 4

// 执行任务 CameraThread.MyRunnable job = new CameraThread.MyRunnable(cameraPojo); CameraThread.MyRunnable.es.execute(job); jobMap.put(token, job);

ffmpeg直播流与历史流命令格式:
1.ffmpeg -rtsp_transport tcp -i rtsp://admin:abc12345@192.168.1.8:554/h264/ch1/main/av_stream -vcodec h264 -f flv -an rtmp://localhost:1935/live/room
2.ffmpeg -rtsp_transport tcp -i rtsp://admin:abc12345@192.168.1.222:554/Streaming/tracks/101?starttime=20191227t084400z’&’endtime=20191227t084600z -vcodec copy -acodec copy -f flv rtmp://localhost:1935/history/room

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121

/** * @Title: openCamera * @Description: 开启视频流 * @param ip * @param username * @param password * @param channel 通道 * @param stream 码流 * @param starttime * @param endtime * @return Map<String,String> **/ @RequestMapping(value = "/cameras", method = RequestMethod.POST) public Map<String, String> openCamera(String ip, String username, String password, String channel, String stream, String starttime, String endtime) { // 返回结果 Map<String, String> map = new HashMap<String, String>(); // 校验参数 if (null != ip && "" != ip && null != username && "" != username && null != password && "" != password && null != channel && "" != channel) { CameraPojo cameraPojo = new CameraPojo(); // 获取当前时间 String openTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date().getTime()); Set keys = CacheUtil.STREAMMAP.keySet(); // 缓存是否为空 if (0 == keys.size()) { // 开始推流 cameraPojo = openStream(ip, username, password, channel, stream, starttime, endtime, openTime); map.put("token", cameraPojo.getToken()); map.put("url", cameraPojo.getRtmp()); } else { // 是否存在的标志;0:不存在;1:存在 int sign = 0; for (String key : keys) { // 是否已经在推流 if (ip.equals(CacheUtil.STREAMMAP.get(key).getIp()) && channel.equals(CacheUtil.STREAMMAP.get(key).getChannel())) { cameraPojo = CacheUtil.STREAMMAP.get(key); sign = 1; break; } } if (sign == 1) { cameraPojo.setCount(cameraPojo.getCount() + 1); cameraPojo.setOpenTime(openTime); } else { // 开始推流 cameraPojo = openStream(ip, username, password, channel, stream, starttime, endtime, openTime); } map.put("token", cameraPojo.getToken()); map.put("url", cameraPojo.getRtmp()); } }

return map; }

/** * @Title: openStream * @Description: 推流器 * @param ip * @param username * @param password * @param channel * @param stream * @param starttime * @param endtime * @param openTime * @return * @return CameraPojo **/ private CameraPojo openStream(String ip, String username, String password, String channel, String stream, String starttime, String endtime, String openTime) { CameraPojo cameraPojo = new CameraPojo(); // 生成token String token = UUID.randomUUID().toString(); String rtsp = ""; String rtmp = ""; // 历史流 if (null != starttime && "" != starttime) { if (null != endtime && "" != endtime) { rtsp = "rtsp://" + username + ":" + password + "@" + ip + ":554/Streaming/tracks/" + channel

  • "01?starttime=" + starttime.substring(0, 8) + "t" + starttime.substring(8) + "z'&'endtime="
  • endtime.substring(0, 8) + "t" + endtime.substring(8) + "z"; } else { try { SimpleDateFormat df = new SimpleDateFormat("yyyyMMddHHmmss"); String startTime = df.format(df.parse(starttime).getTime() - 60 * 1000); String endTime = df.format(df.parse(starttime).getTime() + 60 * 1000); rtsp = "rtsp://" + username + ":" + password + "@" + ip + ":554/Streaming/tracks/" + channel
  • "01?starttime=" + startTime.substring(0, 8) + "t" + startTime.substring(8)
  • "z'&'endtime=" + endTime.substring(0, 8) + "t" + endTime.substring(8) + "z"; } catch (ParseException e) { e.printStackTrace(); } } rtmp = "rtmp://" + config.getPush_ip() + ":" + config.getPush_port() + "/history/" + token; } else {// 直播流 rtsp = "rtsp://" + username + ":" + password + "@" + ip + ":554/h264/ch" + channel + "/" + stream
  • "/av_stream"; rtmp = "rtmp://" + config.getPush_ip() + ":" + config.getPush_port() + "/live/" + token; }

cameraPojo.setUsername(username); cameraPojo.setPassword(password); cameraPojo.setIp(ip); cameraPojo.setChannel(channel); cameraPojo.setStream(stream); cameraPojo.setRtsp(rtsp); cameraPojo.setRtmp(rtmp); cameraPojo.setOpenTime(openTime); cameraPojo.setCount(1); cameraPojo.setToken(token);

// 执行任务 CameraThread.MyRunnable job = new CameraThread.MyRunnable(cameraPojo); CameraThread.MyRunnable.es.execute(job); jobMap.put(token, job);

return cameraPojo; }

关闭视频流接口(DELETE)

传入参数为tokens,通过,分隔,可以同时关闭多路视频。通过token查找缓存判断是否存在,如果存在,则人数count-1。不直接调用结束线程的方法是为了满足如果多个客户端同时观看该路视频,一人关闭会影响其他人使用。故调用该接口只是使该路视频的使用人数-1,最终结束线程的操作交由定时任务处理,如果定时器查询到视频使用人数的count为0,则结束该路视频的推流操作,并清除缓存。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

/** * @Title: closeCamera * @Description:关闭视频流 * @param tokens * @return void **/ @RequestMapping(value = "/cameras/{tokens}", method = RequestMethod.DELETE) public void closeCamera(@PathVariable("tokens") String tokens) { if (null != tokens && "" != tokens) { String[] tokenArr = tokens.split(","); for (String token : tokenArr) { if (jobMap.containsKey(token) && CacheUtil.STREAMMAP.containsKey(token)) { if (0 < CacheUtil.STREAMMAP.get(token).getCount()) { // 人数-1 CacheUtil.STREAMMAP.get(token).setCount(CacheUtil.STREAMMAP.get(token).getCount() - 1); } } } } }

获取视频流(GET)

获取当前进行的推流任务。

1 2 3 4 5 6 7 8 9

/** * @Title: getCameras * @Description:获取视频流 * @return Map<String, CameraPojo> **/ @RequestMapping(value = "/cameras", method = RequestMethod.GET) public Map<String, CameraPojo> getCameras() { return CacheUtil.STREAMMAP; }

视频流保活(PUT)

视频流保活的作用是为了应付以下场景:
如果客户端比如浏览器直接关闭掉,并不会通知服务客户已经不再观看视频了,这是服务还在进行推流。所以添加保活机制,如果客户端没有触发保活机制,定时任务执行时,如果该路视频的最后打开时间距当前时间超过配置的保活时间时,关闭该路视频的推流任务。如果客户端触发保活机制时,更新该路视频的最后打开时间(opentime)为当前系统时间。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

/** * @Title: keepAlive * @Description:视频流保活 * @param tokens * @return void **/ @RequestMapping(value = "/cameras/{tokens}", method = RequestMethod.PUT) public void keepAlive(@PathVariable("tokens") String tokens) { // 校验参数 if (null != tokens && "" != tokens) { String[] tokenArr = tokens.split(","); for (String token : tokenArr) { CameraPojo cameraPojo = new CameraPojo(); // 直播流token if (null != CacheUtil.STREAMMAP.get(token)) { cameraPojo = CacheUtil.STREAMMAP.get(token); // 更新当前系统时间 cameraPojo.setOpenTime(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date().getTime())); } } } }

获取服务信息(GET)

通过该接口获取服务运行时间,以及配置文件的配置

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

/** * @Title: getConfig * @Description: 获取服务信息 * @return Map<String, Object> **/ @RequestMapping(value = "/status", method = RequestMethod.GET) public Map<String, Object> getConfig() { // 获取当前时间 long nowTime = new Date().getTime(); String upTime = (nowTime - CacheUtil.STARTTIME) / (1000 * 60 * 60) + "时"

  • (nowTime - CacheUtil.STARTTIME) % (1000 * 60 * 60) / (1000 * 60) + "分"; Map<String, Object> status = new HashMap<String, Object>(); status.put("config", config); status.put("uptime", upTime); return status; }

video.js

测试需要的video.js。video.js用来播放rtmp的视频。注意chrome需要先允许加载flash插件(百度一下很简单的)。使用以下代码,在src处添加推流成功的rtmp地址。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

Video.js | HTML5 Video Player

To view this video please enable JavaScript, and consider upgrading to a web browser that supports HTML5 video

-------------End-------------

点赞
收藏
评论区
推荐文章
blmius blmius
2年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
Jacquelyn38 Jacquelyn38
3年前
2020年前端实用代码段,为你的工作保驾护航
有空的时候,自己总结了几个代码段,在开发中也经常使用,谢谢。1、使用解构获取json数据let jsonData  id: 1,status: "OK",data: 'a', 'b';let  id, status, data: number   jsonData;console.log(id, status, number )
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
1星期前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
Wesley13 Wesley13
2年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
2年前
HIVE 时间操作函数
日期函数UNIX时间戳转日期函数: from\_unixtime语法:   from\_unixtime(bigint unixtime\, string format\)返回值: string说明: 转化UNIX时间戳(从19700101 00:00:00 UTC到指定时间的秒数)到当前时区的时间格式举例:hive   selec
Wesley13 Wesley13
2年前
00:Java简单了解
浅谈Java之概述Java是SUN(StanfordUniversityNetwork),斯坦福大学网络公司)1995年推出的一门高级编程语言。Java是一种面向Internet的编程语言。随着Java技术在web方面的不断成熟,已经成为Web应用程序的首选开发语言。Java是简单易学,完全面向对象,安全可靠,与平台无关的编程语言。
Stella981 Stella981
2年前
Django中Admin中的一些参数配置
设置在列表中显示的字段,id为django模型默认的主键list_display('id','name','sex','profession','email','qq','phone','status','create_time')设置在列表可编辑字段list_editable
Wesley13 Wesley13
2年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Python进阶者 Python进阶者
6个月前
Excel中这日期老是出来00:00:00,怎么用Pandas把这个去除
大家好,我是皮皮。一、前言前几天在Python白银交流群【上海新年人】问了一个Pandas数据筛选的问题。问题如下:这日期老是出来00:00:00,怎么把这个去除。二、实现过程后来【论草莓如何成为冻干莓】给了一个思路和代码如下:pd.toexcel之前把这