nginx-rtmp服务搭建
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>javacv</artifactId>
<version>1.5.1</version>
</dependency>
<dependency>
<groupId>org.bytedeco</groupId>
<artifactId>ffmpeg-platform</artifactId>
<version>4.1.3-1.5.1</version>
</dependency>
@SpringBootApplication
@EnableScheduling
public class VideoRealPlayApplication {
public static void main(String[] args) {
// 服务启动执行FFmpegFrameGrabber和FFmpegFrameRecorder的tryLoad(),以免导致第一次推流时耗时。
try {
FFmpegFrameGrabber.tryLoad();
FFmpegFrameRecorder.tryLoad();
} catch (org.bytedeco.javacv.FrameRecorder.Exception e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
SpringApplication.run(VideoRealPlayApplication.class, args);
}
@PreDestroy
public void destroy() {
ThreadPoolUtil.shutdownAndAwaitTermination();
}
}
@Slf4j
@RestController
@RequestMapping("/video")
public class IndexController {
@Reference
private IRoadsideDeviceService iRoadsideDeviceService;
@Reference
private IDeviceInfoService iDeviceInfoService;
/**
* 获取设备信息,并执行拉流、推流任务,并返回rtmp地址
* @return
*/
@GetMapping("/rtmp")
public BaseRespEntity rtmp(@RequestParam String deviceIp, @RequestParam String factory) {
/*if (StrUtil.isBlank(deviceno)) {
return BaseRespEntity.error("设备序列号不能为空!");
}
DeviceInfoBO device = iDeviceInfoService.getOne(DeviceInfoBO.builder().deviceno(deviceno).build());
if (ObjectUtil.isNull(device)) {
return BaseRespEntity.error("设备信息异常!");
}*/
// 如果设备已经存在拉流,直接返回rtmp
VideoDTO video = VideoDataCache.VIDEO_MAP.get(deviceIp);
if (ObjectUtil.isNotNull(video) && StrUtil.isNotBlank(video.getRtmp())) {
return BaseRespEntity.ok(video.getRtmp());
}
String rtsp;
if (factory.equals("DH")) {
rtsp = StrUtil.format(VideoConsts.DAHUA_RTSP_URL, VideoConsts.DAHUA_USERNAME,
VideoConsts.DAHUA_PASSWORD, deviceIp);
} else {
rtsp = StrUtil.format(VideoConsts.YUSHI_RTSP_URL, VideoConsts.YUSHI_USERNAME,
VideoConsts.YUSHI_PASSWORD, deviceIp);
}
String rtmp = StrUtil.format(VideoConsts.RTMP_URL, VideoConsts.RTMP_PUSH_IP,
VideoConsts.RTMP_PORT, deviceIp.hashCode());
video = new VideoDTO()
.setDeviceIp(deviceIp)
.setRtsp(rtsp)
.setRtmp(rtmp)
.setOpentime(LocalDateTime.now());
VideoStreamService videoStreamService = new VideoStreamService(video);
Future<?> task = ThreadPoolUtil.POOL.submit(() -> {
try {
videoStreamService.from().to().go(Thread.currentThread());
} catch (BaseException e) {
log.error("BaseException error {}", e.getMsg());
videoStreamService.close();
e.printStackTrace();
} catch (FrameGrabber.Exception e) {
log.error("FrameGrabber error {}", e.getMessage());
videoStreamService.close();
e.printStackTrace();
} catch (FrameRecorder.Exception e) {
log.error("FrameRecorder error {}", e.getMessage());
videoStreamService.close();
e.printStackTrace();
}
});
// 缓存信息
video.setRtmp(StrUtil.format(VideoConsts.RTMP_URL, VideoConsts.RTMP_ACCESS_IP,
VideoConsts.RTMP_PORT, deviceIp.hashCode()));
String key = String.valueOf(video.getDeviceIp().hashCode());
VideoDataCache.VIDEO_MAP.put(key, video);
VideoDataCache.RTMP_MAP.put(key, videoStreamService);
VideoDataCache.TASK_MAP.put(key, task);
return BaseRespEntity.ok(video.getRtmp());
}
}
public class VideoDataCache {
/**
* 保存已经开始推的流
*/
public static final ConcurrentHashMap<String, VideoStreamService> RTMP_MAP = new ConcurrentHashMap();
/**
* 保存正在推送的设备信息
*/
public static final ConcurrentHashMap<String, VideoDTO> VIDEO_MAP = new ConcurrentHashMap();
/**
* 保存正在推送的任务
*/
public static final ConcurrentHashMap<String, Future<?>> TASK_MAP = new ConcurrentHashMap<>();
public static void remove(String key) {
// 终止线程
ThreadPoolUtil.cancelTask(VideoDataCache.TASK_MAP.get(key));
// 清除缓存
VideoDataCache.TASK_MAP.remove(key);
VideoDataCache.VIDEO_MAP.remove(key);
VideoDataCache.RTMP_MAP.remove(key);
}
}
@Slf4j
public class ThreadPoolUtil {
private static final int CORE_POOL_SIZE = (int) (Runtime.getRuntime().availableProcessors() / (1 - 0.5f));
private static final int MAX_POOL_SIZE = 35;
private static final long KEEP_LIVE_TIME = 60L;
private static final BlockingQueue<Runnable> BLOCKING_QUEUE = new LinkedBlockingQueue<>();
public static final ThreadPoolExecutor POOL;
static{
POOL = new ThreadPoolExecutor(CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_LIVE_TIME,
TimeUnit.MILLISECONDS,
BLOCKING_QUEUE,
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy());
}
/**
* 取消当前正在执行的任务
*/
public static void cancelTask(Future<?> future) {
// 终止正在执行的任务
if (ObjectUtil.isNotNull(future) && !future.isDone() && !future.isCancelled()) {
future.cancel(true);
}
}
/**
* 释放线程池
*/
public static void shutdownAndAwaitTermination() {
if (POOL != null && !POOL.isShutdown()) {
POOL.shutdown();
try {
if (!POOL.awaitTermination(120, TimeUnit.SECONDS)) {
POOL.shutdownNow();
if (!POOL.awaitTermination(120, TimeUnit.SECONDS)) {
log.info("pool did not termination");
}
}
} catch (InterruptedException e) {
POOL.shutdownNow();
Thread.currentThread().interrupt();
}
}
}
}
/**
* 请求rtmp服务状态地址,获取nclients在线客户端数量,等于1时表示没有被预览
*
*/
@Slf4j
@Component
public class VideoTimer {
/** */
private static final String RTMP_STAT_URL = "http://***.***.***.***/stat";
private static final int TIME_OUT = 3000;
@Scheduled(cron = "0/5 * * * * ?")
public void configureTasks() {
List<String> rtmpStatList = getRtmpStat();
if (CollUtil.isNotEmpty(rtmpStatList)) {
for (String key : rtmpStatList) {
VideoDTO video = VideoDataCache.VIDEO_MAP.get(key);
if (ObjectUtil.isNotNull(video) && video.getOpentime().plusMinutes(1).isBefore(LocalDateTime.now())) {
log.info("Video Streaming Stop ={}", video);
VideoDataCache.remove(key);
}
}
}
}
private static List<String> getRtmpStat() {
try {
String body = HttpRequest.get(RTMP_STAT_URL)
.timeout(TIME_OUT)
.execute()
.body();
return xmlToObj(body);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
private static List<String> xmlToObj(String xmlStr) {
List<String> resList = new ArrayList<>();
if (StrUtil.isBlank(xmlStr) && !xmlStr.contains("" )) {
return resList;
}
String live = StrUtil.subBetween(xmlStr, "" , "");
if (!live.contains("" )) {
return resList;
}
String[] split = StrUtil.split(live, "");
for (int i = 0; i < split.length; i++) {
String s = split[i];
if (s.contains("" ) && s.contains("" )) {
Integer nclients = Integer.valueOf(StrUtil.subBetween(s, "" , ""));
if (nclients == 1) {
String name = StrUtil.subBetween(s, "" , "");
resList.add(name);
}
}
}
return resList;
}
}
@Slf4j
public class VideoStreamService {
private VideoDTO videoDTO;
/**
* 解码器
*/
private FFmpegFrameGrabber grabber = null;
/**
* 编码器
*/
private FFmpegFrameRecorder recorder = null;
/**
* 帧率
*/
private double FRAMERATE;
/**
* 比特率
*/
private int BITRATE;
/**
* 视频像素宽
*/
public int WIDTH;
/**
* 视频像素高
*/
public int HEIGHT;
public VideoStreamService(VideoDTO videoDTO) {
this.videoDTO = videoDTO;
}
/**
* 视频源
*/
public VideoStreamService from() throws FrameGrabber.Exception {
grabber = new FFmpegFrameGrabber(videoDTO.getRtsp());
// tcp用于解决丢包问题
grabber.setOption("rtsp_transport", "tcp");
// 设置采集器构造超时时间
grabber.setOption("stimeout", "3000");
// 开始之后ffmpeg会采集视频信息,之后就可以获取音视频信息
grabber.start();
WIDTH = grabber.getImageWidth();
HEIGHT = grabber.getImageHeight();
FRAMERATE = grabber.getVideoFrameRate();
BITRATE = grabber.getVideoBitrate();
// 若视频像素值为0,说明采集器构造超时,程序结束
if (WIDTH == 0 && HEIGHT == 0) {
log.error("Streaming Exception ...");
return null;
}
return this;
}
/**
* 输出
*
*/
public VideoStreamService to() throws FrameRecorder.Exception {
recorder = new FFmpegFrameRecorder(videoDTO.getRtmp(), WIDTH, HEIGHT);
// 画面质量参数,0~51;18~28是一个合理范围
recorder.setVideoOption("crf", "28");
// 该参数用于降低延迟
recorder.setVideoOption("tune", "zerolatency");
/**
** 权衡quality(视频质量)和encode speed(编码速度) values(值): *
* ultrafast(终极快),superfast(超级快), veryfast(非常快), faster(很快), fast(快), *
* medium(中等), slow(慢), slower(很慢), veryslow(非常慢) *
* ultrafast(终极快)提供最少的压缩(低编码器CPU)和最大的视频流大小;而veryslow(非常慢)提供最佳的压缩(高编码器CPU)的同时降低视频流的大小
*/
recorder.setVideoOption("preset", "ultrafast");
recorder.setGopSize(2);
recorder.setFrameRate(FRAMERATE);
recorder.setVideoBitrate(BITRATE);
AVFormatContext fc = null;
if (videoDTO.getRtmp().indexOf("rtmp") >= 0 || videoDTO.getRtmp().indexOf("flv") > 0) {
// 封装格式flv
recorder.setFormat("flv");
recorder.setAudioCodecName("aac");
fc = grabber.getFormatContext();
}
recorder.start(fc);
log.info("Push Stream Device Info:\ndeviceIp:{} \nrtsp:{} \nrtmp:{}",
videoDTO.getDeviceIp(), videoDTO.getRtsp(), videoDTO.getRtmp());
return this;
}
public VideoStreamService go(Thread nowThread) throws FrameGrabber.Exception, FrameRecorder.Exception {
// 采集或推流导致的错误次数
long errIndex = 0;
// 连续五次没有采集到帧则认为视频采集结束,程序错误次数超过5次即中断程序
// 将探测时留下的数据帧释放掉,以免因为dts,pts的问题对推流造成影响
grabber.flush();
for (int noFrameIndex = 0; noFrameIndex < 5 || errIndex < 5; ) {
try {
// 用于中断线程时,结束该循环
nowThread.sleep(1);
// 获取没有解码的音视频帧
AVPacket pkt = grabber.grabPacket();
if (pkt == null || pkt.size() <= 0 || pkt.data() == null) {
// 空包记录次数跳过
noFrameIndex++;
errIndex++;
continue;
}
// 不需要编码直接把音视频帧推出去
errIndex += (recorder.recordPacket(pkt) ? 0 : 1);
avcodec.av_packet_unref(pkt);
} catch (InterruptedException e) {
// 当需要结束推流时,调用线程中断方法,中断推流的线程。当前线程for循环执行到
// nowThread.sleep(1);这行代码时,因为线程已经不存在了,所以会捕获异常,结束for循环
log.info("Device interrupt push stream succeeded...");
break;
} catch (FrameGrabber.Exception e) {
errIndex++;
} catch (FrameRecorder.Exception e) {
errIndex++;
}
}
// 程序正常结束释放资源
this.close();
log.info("The device streaming is stop...");
return this;
}
public void close() {
try {
if (recorder != null) {
recorder.close();
log.info("Recorder close success!");
}
} catch (FrameRecorder.Exception e) {
e.printStackTrace();
}
try {
if (grabber != null) {
grabber.close();
log.info("Grabber close success!");
}
} catch (FrameGrabber.Exception e) {
e.printStackTrace();
}
}
}
@Data
@Accessors(chain = true)
public class VideoDTO {
private String rtsp;
private String rtmp;
private String deviceIp;
/**
* 打开时间
*/
private LocalDateTime opentime;
}