服务支撑:FFmpeg + srs(流媒体服务器)
整个流程是 FFmpeg 收流转码 推 rtmp 到流媒体服务 流媒体服务再 分发流到公网
搭建流媒体服务:
- SRS (Simple Realtime Server) | SRS (本例子使用的是SrS 安装使用docker )
3.nginx实现 (自己百度)
- 其他的还有收费的那种
2.服务器安装FFmpeg yum 可以安装
java 服务实现调用ffmpeg
1.ProcessManager 用于执行指令以及 关闭这个流等操作
package io.renren.common.live;
import cn.hutool.core.thread.ThreadUtil;
import org.springframework.beans.factory.DisposableBean;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.ref.WeakReference;
import java.util.HashMap;
import java.util.Map;
/**
* @author chenkang
* @date 2023年8月3日09:43:21
*/
public class ProcessManager implements DisposableBean {
private Map<String, WeakReference<Process>> processMap=new HashMap<>();
/**
* 启动一个进程
* @param processName 进程名称key
* @param command 执行指令
*/
public void startProcess(String processName, String command) {
ThreadUtil.execAsync(() -> {
try {
ProcessBuilder processBuilder = new ProcessBuilder(command.split(" "));
Process process = processBuilder.start();
processMap.put(processName, new WeakReference<>(process));
BufferedReader reader = new BufferedReader(new InputStreamReader(process.getErrorStream()));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(processName + ": " + line);
}
int exitCode = process.waitFor();
System.out.println(processName + ": Process execution completed with exit code: " + exitCode);
} catch (IOException | InterruptedException e) {
e.printStackTrace();
}
});
}
/**
* 销毁
* @param processName key
*/
public void terminateProcess(String processName) {
WeakReference<Process> weakRef = processMap.get(processName);
if (weakRef != null) {
Process process = weakRef.get();
if (process != null) {
process.destroy();
}
processMap.remove(processName);
}
}
private void terminateAllProcesses() {
for (WeakReference<Process> weakRef : processMap.values()) {
Process process = weakRef.get();
if (process != null) {
process.destroy();
}
}
}
@Override
public void destroy() throws Exception {
this.terminateAllProcesses();
this.processMap.clear();
}
}
对接的是大华的摄像头
/**
* 开始推流
* 备注:现在客户的设备是NVR NVR 下大概有54个摄像头 公网映射rtsp 554 端口 根据channel 开区分是那个摄像头
* 必须条件:1.服务端要安装好 ffmpeg 2.要搭建一个流媒体服务器 这个使用的是 srs
* 流程:用户端想要查看某个摄像头->查询到设备信息获取到摄像头的channel 这个是提前维护好的
* ->拿到channel走如下方法 调用FFmpeg 执行转码推流指令(客户的摄像头是h265)-》rtsp流会被转码 重新设定分辨率 码率转h264 并把转码流推向流媒体服务器 srs
* ->客户想看的时候就 拉取 流媒体服务端的rtmp流 完成播放
*
* 其他:
* 1.用户再播放的时候 要先确定这个摄像头有没有别的人在观看 观看了就不在执行了 (1.可以调用srs 接口查询流是不是存在 这个比较稳妥 2.或者是 processManager 判断是否在推了)
* 2.有时候用户强制关闭客户端 无法感知用户不在观看了,这面还要 定时的去调用srs 接口查询闲置的流及时的给关闭 同时也要 把服务的process 给主动关闭不然一直推
* 这个要先去关闭process再调用接口关闭srs服务的流
*
*
* http://127.0.0.1:1985/api/v1/streams 查询服务端所有流
*{
* "code": 0,
* "server": "vid-f1gt8j3",
* "streams": [
* {
* "id": "vid-143p019",
* //streamName
* "name": "3",
* "vhost": "vid-5847096",
* "app": "live",
* "live_ms": 1691039306435,
* //客户端数量 这个要注意 默认就有1 个客户端是推流
* "clients": 1,
* "frames": 0,
* "send_bytes": 0,
* "recv_bytes": 1068,
* "kbps": {
* "recv_30s": 0,
* "send_30s": 0
* },
* "publish": {
* //是否正在推流 有时候服务端流停推了 但是还有客户端在看 这个流还能查到 但是 active 为false
* "active": false,
* "cid": ""
* },
* "video": null,
* "audio": null
* }
* ]
* }
*
*
*
* http://127.0.0.1:1985/api/v1/clients
*
*{
* "code": 0,
* "server": "vid-f1gt8j3",
* "clients": [
* {
* "id": "868249e9",
* "vhost": "vid-5847096",
* "stream": "vid-778ujy0",
* "ip": "172.17.0.1",
* "pageUrl": "",
* "swfUrl": "",
* "tcUrl": "rtmp://127.0.0.1:1935/live",
* "url": "/live/9",
* //类型是 fmle-publish 推流 删除掉这个推流就会停止
* //类型是 rtmp-play 拉流 删除掉这个拉流就会停止
* //剔除方法 Method DELETE api /api/v1/clients/{id} 停止推流/踢掉用户端
* "type": "fmle-publish",
* "publish": true,
* "alive": 16.18,
* "kbps": {
* "recv_30s": 0,
* "send_30s": 0
* }
* }
* ]
* }
*
*
* @param channel
*/
@GetMapping("/start")
@ResponseBody
public void start(@RequestParam(defaultValue = "1") String channel){
RtspUrlBuilder builder = new RtspUrlBuilder();
RtmpUrlBuilder rtmpUrlBuilder = new RtmpUrlBuilder();
//构建 rtsp 这个是客户的nvr rtsp 地址 只有channel 是灵活的 他们是64路 现在接了50多摄像头对应50 多路channel
String rstp = builder.setUsername("admin")
.setPassword("xx")
.setIpAddress("xx")
.setChannel(channel)
.build();
//这个是流媒体服务器的rtmp 推流地址
String rtmp = rtmpUrlBuilder.setApplication("live").setStreamName(channel).build();
final String vcodec="libx264";
String camera1=String.format(RTSP_RTMP, rstp,vcodec,rtmp);
//TODO 判断是否已经再推了 推就直接返回拉流地址
processManager.startProcess(channel,camera1);
//拉流地址和推流地址是一至的 除非 java 服务和srs 在一台服务器 那么 推流地址 rtmp ip为127.0.0.1 拉流 rtmp ip 为公网
// 就是java通过ffmpeg 收流转发到本地 rtmp srs分发流 到公网去
System.out.println("拉流地址:"+rtmp);
}
@GetMapping("/end")
@ResponseBody
public void end(String channel){
processManager.terminateProcess(channel);
}
两个辅助类:
package io.renren.common.live;
/**
* @author chenkang
* @date 2023-8-3 12:27
*/
public class RtspUrlBuilder {
private String username;
private String password;
private String ipAddress;
private int port;
private String channel;
private int subtype;
public RtspUrlBuilder() {
// 默认端口为554
this.port = 554;
// 默认子类型为0
this.subtype = 0;
}
public RtspUrlBuilder setUsername(String username) {
this.username = username;
return this;
}
public RtspUrlBuilder setPassword(String password) {
this.password = password;
return this;
}
public RtspUrlBuilder setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
return this;
}
public RtspUrlBuilder setPort(int port) {
this.port = port;
return this;
}
public RtspUrlBuilder setChannel(String channel) {
this.channel = channel;
return this;
}
public RtspUrlBuilder setSubtype(int subtype) {
this.subtype = subtype;
return this;
}
public String build() {
return "rtsp://" + username + ":" + password + "@" + ipAddress + ":" + port +
"/cam/realmonitor?channel=" + channel + "&subtype=" + subtype;
}
}
package io.renren.common.live;
/**
* @author chenkang
* @date 2023-8-3 12:30
*/
public class RtmpUrlBuilder {
private String ipAddress;
private int port;
private String application;
private String streamName;
public RtmpUrlBuilder() {
// 默认IP地址为127.0.0.1
this.ipAddress = "127.0.0.1";
// 默认端口为1935
this.port = 1935;
}
public RtmpUrlBuilder setIpAddress(String ipAddress) {
this.ipAddress = ipAddress;
return this;
}
public RtmpUrlBuilder setPort(int port) {
this.port = port;
return this;
}
public RtmpUrlBuilder setApplication(String application) {
this.application = application;
return this;
}
public RtmpUrlBuilder setStreamName(String streamName) {
this.streamName = streamName;
return this;
}
public String build() {
return "rtmp://" + ipAddress + ":" + port + "/" + application + "/" + streamName;
}
}