服務支撐:FFmpeg? + srs(流媒體服務器)??
整個流程是 FFmpeg 收流轉碼 推 rtmp 到流媒體服務 流媒體服務再 分發流到公網
搭建流媒體服務:
1.??SRS (Simple Realtime Server) | SRS?(本例子使用的是SrS 安裝使用docker )
2.GitHub - ZLMediaKit/ZLMediaKit: WebRTC/RTSP/RTMP/HTTP/HLS/HTTP-FLV/WebSocket-FLV/HTTP-TS/HTTP-fMP4/WebSocket-TS/WebSocket-fMP4/GB28181/SRT server and client framework based on C++11
3.nginx實現 (自己百度)
4. 其他的還有收費的那種?
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();}}}@Overridepublic 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")@ResponseBodypublic void start(@RequestParam(defaultValue = "1") String channel){RtspUrlBuilder builder = new RtspUrlBuilder();RtmpUrlBuilder rtmpUrlBuilder = new RtmpUrlBuilder();//構建 rtsp 這個是客戶的nvr rtsp 地址 只有channel 是靈活的 他們是64路 現在接了50多攝像頭對應50 多路channelString 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")@ResponseBodypublic 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() {// 默認端口為554this.port = 554;// 默認子類型為0this.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.1this.ipAddress = "127.0.0.1";// 默認端口為1935this.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;}
}