一、功能介绍
该部分 为 自主搭建一个流媒体中间件作为视频模块的生产者,主要功能如下所示:
1、根据设备标识和码流地址进行推流
2、将摄像头rtsp、hls流转为rtmp
3、限制 同时转流数量,缓解服务器压力
4、所在的服务器使用nginx-rtmp模块作为视频流服务器,使得web端能够访问http-flv格式的视频
5、同时推流数达到限制后对最先播放的视频 停止推流(因服务器性能原因,不得已为之)
二、资源下载
1、nginx-rtmp 目前百度有很多,请自行搜索下载,如需相关资源,可以➕wei liu30723 索要相关压缩包
2、ffmpeg下载,同时配置全局变量
Nginx相关配置如下:
worker_processes 1;
error_log logs/error.log info;
events {
worker_connections 1024;
}
#流媒体端口服务
rtmp {
server {
listen 6004; # 监听端口
chunk_size 4000;
application live {
live on;
}
}
}
http {
include mime.types;
default_type application/octet-stream;
#access_log logs/access.log main;
server {
listen 8080; # 监听端口
location /stat.xsl {
root html;
}
#统计相关路由配置
location /stat {
rtmp_stat all;
rtmp_stat_stylesheet stat.xsl;
}
location / {
root html;
}
location /live {
#开启flv直播
flv_live on;
chunked_transfer_encoding on; #open 'Transfer-Encoding: chunked' response
add_header 'Access-Control-Allow-Credentials' 'true'; #add additional HTTP header
add_header 'Access-Control-Allow-Origin' '*'; #add additional HTTP header
add_header Access-Control-Allow-Headers X-Requested-With;
add_header Access-Control-Allow-Methods GET,POST,OPTIONS;
add_header 'Cache-Control' 'no-cache';
}
}
}
以上前期准备工作完毕;
三 、服务实现
1、pom内容.
cloud-alibaba-pom内容
<?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">
<parent>
<artifactId>DeviceManagerCluster</artifactId>
<groupId>com.haiot</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<groupId>com.haiot.dm</groupId>
<artifactId>cloud-provider-videotranscode</artifactId>
<version>1.0</version>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!--cloud部分基础工具-->
<dependency>
<groupId>com.haiot</groupId>
<artifactId>single-common-basic</artifactId>
<version>${sc.basic.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--nacos-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
<!--nacos-config-->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
</dependencies>
<build>
<finalName>videoTranscode</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
<!--防止打包后启动找不到主函数-->
<mainClass>com.haiot.dm.ProviderVideoTranscodeApplication</mainClass>
</configuration>
<executions>
<execution>
<goals>
<goal>repackage</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
2、相关配置
!! 注意:nacos配置中心的配置文件不要加 注释,在服务器部署可能出现错误. 文件命名要符合nacos命名法则,否则找不到。具体命名规范请自行去nacos官网查看
3、最终代码实现
package com.haiot.dm.controller;
import com.haiot.dm.service.FlowConvertService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.annotation.Resource;
/**
* @Description: 视频转码
* @Param:
* @return:
* @Author: LiuAnmin
* @Date: 2021/7/28
*/
@RestController
@RequestMapping("/run")
@Slf4j
public class VideoTranscodeController {
@Resource
private FlowConvertService flowConvertService;
@PostMapping("/create.rtmp")
public String createRtmpVideo(String deviceUid,
String rtspH264,
String rtspH265,
String hls) {
if (null == deviceUid || "".equals(deviceUid)) {
return "设备标识不能为空!";
}
//h264 rtsp
if (null != rtspH264 && !"".equals(rtspH264)) {
flowConvertService.executeH264RtmpCodecs(rtspH264, deviceUid);
}
//h265 rtsp
if (null != rtspH265 && !"".equals(rtspH265)) {
flowConvertService.executeH265RtmpCodecs(rtspH265, deviceUid);
}
//hls
if (null != hls && !"".equals(hls)) {
flowConvertService.executeHlsCodecs(hls, deviceUid);
}
return null;
}
}
具体命令由convert拼接而成,以应对各个平台视频转播。故对此做出整合
内含:
1、H264的rtsp转 rtmp
2、h265的rtsp转 rtmp
3、hls 转 rtmp
package com.haiot.dm.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.*;
@Slf4j
@Service
public class FlowConvertService{
private static boolean isWhileTrue = true;
@Value("${video.rtmpAddress}")
String rtmpAddress;
public void executeH264RtmpCodecs(String rtspUrl, String deviceCode) {
//生成命令
List<String> convert = getH264RtspRtmpCommand(rtspUrl, deviceCode);
//开启进程
this.startProcess(deviceCode,convert);
}
public void executeH265RtmpCodecs(String rtspUrl, String deviceUid) {
//生成命令
List<String> convert = this.getH265RtspRtmpCommand(rtspUrl, deviceUid);
//开启进程
this.startProcess(deviceUid,convert);
}
public void executeHlsCodecs(String hlsUrl, String deviceUid) {
//生成命令
List<String> convert = this.getHlsRtmpCommand(hlsUrl, deviceUid);
//开启进程
this.startProcess(deviceUid,convert);
}
private void startProcess(String c,
List<String> l) {
//设备标识
final String deviceCode = c;
//命令
final List<String> command = l;
//whileTrue方法只执行一次
if(isWhileTrue){
new Thread(()->{
ProcessLinkMap.whileTrue();
}).start();
isWhileTrue = false;
}
ProcessBuilder builder = new ProcessBuilder();
builder.command(command);
builder.redirectErrorStream(true);
try {
Process process = ProcessLinkMap.get(deviceCode);
if(null == process){
process = builder.start();
}else{
log.info("重复编号,设备编号【{}】",deviceCode );
return;
}
ProcessLinkMap.add(deviceCode,process);
BufferedReader br = new BufferedReader(new InputStreamReader(process.getInputStream()));
String output;
log.info("任务开启,设备编号【{}】",deviceCode);
while (null != (output = br.readLine())) {
log.info(output);
if (output.contains("error") || output.contains("This may result") || output.contains("Server returned 5XX Server Error reply")) {
log.info("任务中断,设备编号【{}】,原因【{}】",deviceCode,output);
}
}
} catch (IOException e) {
e.printStackTrace();
ProcessLinkMap.remove(deviceCode);
}
}
private List<String> getH264RtspRtmpCommand(String rtspUrl, String deviceCode) {
List<String> convert = new ArrayList<String>();
convert.add("ffmpeg");
convert.add("-i");
convert.add("\"" + rtspUrl + "\"");
convert.add("-vcodec");
convert.add("copy");
convert.add("-acodec");
convert.add("copy");
convert.add("-f");
convert.add("flv");
convert.add(rtmpAddress + deviceCode);
return convert;
}
private List<String> getH265RtspRtmpCommand(String rtspUrl, String deviceUid) {
List<String> convert = new ArrayList<String>();
convert.add("ffmpeg");
convert.add("-rtsp_transport");
convert.add("tcp");
convert.add("-i");
convert.add("\"" + rtspUrl + "\"");
convert.add("-vcodec");
convert.add("libx264");
convert.add("-f");
convert.add("flv");
convert.add("-r");
convert.add("25");
convert.add("-s");
convert.add("768*650");
convert.add("-an");
convert.add(rtmpAddress + deviceUid);
return convert;
}
private List<String> getHlsRtmpCommand(String hlsUrl, String deviceUid) {
List<String> convert = new ArrayList<String>();
convert.add("ffmpeg");
convert.add("-i");
convert.add("\"" + hlsUrl + "\"");
convert.add("-vcodec");
convert.add("libx264");
convert.add("-f");
convert.add("flv");
convert.add("-r");
convert.add("25");
convert.add("-s");
convert.add("768*650");
convert.add("-an");
convert.add(rtmpAddress + deviceUid);
return convert;
}
/**
* @Description: 进程存储控制
* @Param:
* @return:
* @Author: LiuAnmin
* @Date: 2021/7/28
*/
public static class ProcessLinkMap {
private static int i = 2;
@Value("${video.max-online-flow}")
public static void setI(int i) {
ProcessLinkMap.i = i;
}
private static LinkedHashMap<String, Process> processMap = new LinkedHashMap<>();
public synchronized static void add(String deviceCode, Process process) {
processMap.put(deviceCode, process);
log.info("有DEVICE_CODE【{}】新任务\n现Map内容为【{}】",deviceCode,processMap);
}
public static Process get(String deviceCode) {
return processMap.get(deviceCode);
}
public synchronized static void remove(String deviceCode) {
processMap.remove(deviceCode);
}
public synchronized static void removeOnce() {
for (String key : processMap.keySet()) {
Process process = processMap.get(key);
process.destroy();
if(process.isAlive()){
process.destroy();
}
if(!process.isAlive()){
processMap.remove(key);
}
log.info("停止转码,设备编号标识【{}】", key);
break;
}
}
public static void whileTrue() {
while (true) {
if (processMap.size() > i) {
removeOnce();
}
try {
Thread.sleep(1500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}