|
|
|
@ -1,5 +1,8 @@ |
|
|
|
package com.inspect.ivs.service; |
|
|
|
|
|
|
|
import cn.hutool.http.HttpUtil; |
|
|
|
import com.alibaba.fastjson.JSONArray; |
|
|
|
import com.alibaba.fastjson.JSONObject; |
|
|
|
import com.inspect.base.core.constant.Color; |
|
|
|
import com.inspect.ivs.config.ZlMediaConfig; |
|
|
|
import com.inspect.ivs.constant.ZlMediaConst; |
|
|
|
@ -11,9 +14,15 @@ import org.slf4j.LoggerFactory; |
|
|
|
import org.springframework.http.HttpMethod; |
|
|
|
import org.springframework.stereotype.Component; |
|
|
|
|
|
|
|
import java.net.URLEncoder; |
|
|
|
import java.nio.charset.StandardCharsets; |
|
|
|
import java.util.HashSet; |
|
|
|
import java.util.Set; |
|
|
|
|
|
|
|
@Component |
|
|
|
public class ZlMediaComponent extends IvsCommonService { |
|
|
|
private static final Logger log = LoggerFactory.getLogger(ZlMediaComponent.class); |
|
|
|
private static final int API_TIMEOUT_MS = 5000; |
|
|
|
private final ZlMediaConfig zlMediaConfig; |
|
|
|
|
|
|
|
public ZlMediaComponent(ZlMediaConfig zlMediaConfig) { |
|
|
|
@ -24,24 +33,31 @@ public class ZlMediaComponent extends IvsCommonService { |
|
|
|
return this.zlMediaConfig.getUrl(); |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 确保该 streamId 的拉流代理存在并回填播放地址。 |
|
|
|
* 代理已存在时直接复用,避免同一台摄像头被重复拉流。 |
|
|
|
*/ |
|
|
|
public void convert(String rtspUrl, String streamId, IvsVideoRtspView ivsVideoRtspView) { |
|
|
|
if(zlMediaConfig.isEnable()) { |
|
|
|
if (zlMediaConfig.isEnable()) { |
|
|
|
log.info(Color.YELLOW + "zlMediaConfig: {}" + Color.END, zlMediaConfig); |
|
|
|
ZlMediaRtspVo rtspVo = new ZlMediaRtspVo(zlMediaConfig.getSecret(), |
|
|
|
streamId, |
|
|
|
rtspUrl); |
|
|
|
log.debug("[GAB] rtspVo: {}", rtspVo); |
|
|
|
String block = this.http(HttpMethod.POST, ZlMediaConst.CONVERT_RTSP_URL) |
|
|
|
.header("Accept", new String[]{"*/*"}) |
|
|
|
.header("Accept-Encoding", new String[]{"gzip, deflate, br"}) |
|
|
|
.header("Connection", new String[]{"keep-alive"}) |
|
|
|
.header("Postman-Token", "93a405a2-2d6f-4c58-b085-d73badbe7331") |
|
|
|
.bodyValue(rtspVo) |
|
|
|
.retrieve() |
|
|
|
.bodyToMono(String.class) |
|
|
|
.block(); |
|
|
|
log.debug("[GAB] ZL-MEDIA Convert:"); |
|
|
|
log.debug("{}", block); |
|
|
|
if (proxyExists(streamId)) { |
|
|
|
log.info("[ZLM] 拉流代理已存在,复用: stream={}", streamId); |
|
|
|
} else { |
|
|
|
ZlMediaRtspVo rtspVo = new ZlMediaRtspVo(zlMediaConfig.getSecret(), |
|
|
|
streamId, |
|
|
|
rtspUrl); |
|
|
|
log.info("[GAB] rtspVo: {}", rtspVo); |
|
|
|
String block = this.http(HttpMethod.POST, ZlMediaConst.CONVERT_RTSP_URL) |
|
|
|
.header("Accept", new String[]{"*/*"}) |
|
|
|
.header("Accept-Encoding", new String[]{"gzip, deflate, br"}) |
|
|
|
.header("Connection", new String[]{"keep-alive"}) |
|
|
|
.header("Postman-Token", "93a405a2-2d6f-4c58-b085-d73badbe7331") |
|
|
|
.bodyValue(rtspVo) |
|
|
|
.retrieve() |
|
|
|
.bodyToMono(String.class) |
|
|
|
.block(); |
|
|
|
log.info("[ZLM] addStreamProxy stream={} resp={}", streamId, block); |
|
|
|
} |
|
|
|
ZlmConvertUrl zlmConvertUrl = new ZlmConvertUrl(zlMediaConfig.getUrl(), zlMediaConfig.getWebrtcPrefix(), streamId); |
|
|
|
ivsVideoRtspView.setWebrtcURL(zlmConvertUrl.getWebrtc()); |
|
|
|
ivsVideoRtspView.setHlsURL(zlmConvertUrl.getM3u8()); |
|
|
|
@ -49,4 +65,138 @@ public class ZlMediaComponent extends IvsCommonService { |
|
|
|
ivsVideoRtspView.setMp4URL(zlmConvertUrl.getMp4()); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** 查询拉流代理是否已存在。 */ |
|
|
|
public boolean proxyExists(String streamId) { |
|
|
|
if (!zlMediaConfig.isEnable() || isEmpty(streamId)) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
try { |
|
|
|
String body = HttpUtil.get(apiUrl(ZlMediaConst.GET_PROXY_INFO, "key=" + proxyKey(streamId)), API_TIMEOUT_MS); |
|
|
|
JSONObject json = JSONObject.parseObject(body); |
|
|
|
return json != null && json.getIntValue("code") == 0; |
|
|
|
} catch (Exception e) { |
|
|
|
log.warn("[ZLM] getProxyInfo failed, stream={}, err={}", streamId, e.getMessage()); |
|
|
|
return false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 删除拉流代理并强制关闭对应媒体源。 |
|
|
|
* ZLM 配置 auto_close=0 时仅删除代理不会停止拉流,必须再调 close_streams。 |
|
|
|
*/ |
|
|
|
public boolean stop(String streamId) { |
|
|
|
if (!zlMediaConfig.isEnable() || isEmpty(streamId)) { |
|
|
|
return false; |
|
|
|
} |
|
|
|
boolean deleted = false; |
|
|
|
try { |
|
|
|
String body = HttpUtil.get(apiUrl(ZlMediaConst.DEL_STREAM_PROXY, "key=" + proxyKey(streamId)), API_TIMEOUT_MS); |
|
|
|
JSONObject json = JSONObject.parseObject(body); |
|
|
|
// code=0 但 flag=false 表示代理已不存在,同样视为已关闭 |
|
|
|
deleted = json != null && json.getIntValue("code") == 0; |
|
|
|
log.info("[ZLM] delStreamProxy stream={} deleted={} resp={}", streamId, deleted, body); |
|
|
|
} catch (Exception e) { |
|
|
|
log.warn("[ZLM] delStreamProxy failed, stream={}, err={}", streamId, e.getMessage()); |
|
|
|
} |
|
|
|
try { |
|
|
|
String query = "vhost=" + ZlMediaConst.DEFAULT_VHOST |
|
|
|
+ "&app=" + ZlMediaConst.DEFAULT_APP |
|
|
|
+ "&stream=" + encode(streamId) |
|
|
|
+ "&force=1"; |
|
|
|
String body = HttpUtil.get(apiUrl(ZlMediaConst.CLOSE_STREAMS, query), API_TIMEOUT_MS); |
|
|
|
log.info("[ZLM] close_streams stream={} resp={}", streamId, body); |
|
|
|
} catch (Exception e) { |
|
|
|
log.warn("[ZLM] close_streams failed, stream={}, err={}", streamId, e.getMessage()); |
|
|
|
} |
|
|
|
return deleted; |
|
|
|
} |
|
|
|
|
|
|
|
/** |
|
|
|
* 兜底回收:清理本服务创建、长时间无观看者的拉流代理。 |
|
|
|
* 用于前端未调用 stop(关页面、断网等场景)时避免 NVR 会话和带宽被占满。 |
|
|
|
* |
|
|
|
* @param minIdleSeconds 至少空闲多久才回收 |
|
|
|
* @return 本次回收的流数量 |
|
|
|
*/ |
|
|
|
public int closeIdleStreams(long minIdleSeconds) { |
|
|
|
if (!zlMediaConfig.isEnable()) { |
|
|
|
return 0; |
|
|
|
} |
|
|
|
try { |
|
|
|
String body = HttpUtil.get(apiUrl(ZlMediaConst.GET_MEDIA_LIST, null), API_TIMEOUT_MS); |
|
|
|
JSONObject root = JSONObject.parseObject(body); |
|
|
|
if (root == null || root.getIntValue("code") != 0) { |
|
|
|
log.warn("[ZLM] getMediaList failed: {}", body); |
|
|
|
return 0; |
|
|
|
} |
|
|
|
JSONArray data = root.getJSONArray("data"); |
|
|
|
if (data == null || data.isEmpty()) { |
|
|
|
return 0; |
|
|
|
} |
|
|
|
// getMediaList 会按协议返回多份记录,这里按 stream 去重 |
|
|
|
Set<String> idle = new HashSet<>(); |
|
|
|
for (int i = 0; i < data.size(); i++) { |
|
|
|
JSONObject item = data.getJSONObject(i); |
|
|
|
String streamId = item.getString("stream"); |
|
|
|
if (isEmpty(streamId) || !streamId.startsWith(ZlMediaConst.STREAM_ID_PREFIX)) { |
|
|
|
continue; |
|
|
|
} |
|
|
|
if (item.getIntValue("originType") != 4) { |
|
|
|
continue; |
|
|
|
} |
|
|
|
if (item.getIntValue("readerCount") > 0 || item.getIntValue("totalReaderCount") > 0) { |
|
|
|
continue; |
|
|
|
} |
|
|
|
if (item.getLongValue("aliveSecond") < minIdleSeconds) { |
|
|
|
continue; |
|
|
|
} |
|
|
|
idle.add(streamId); |
|
|
|
} |
|
|
|
for (String streamId : idle) { |
|
|
|
stop(streamId); |
|
|
|
} |
|
|
|
if (!idle.isEmpty()) { |
|
|
|
log.info("[ZLM] 回收空闲拉流代理 {} 路: {}", idle.size(), idle); |
|
|
|
} |
|
|
|
return idle.size(); |
|
|
|
} catch (Exception e) { |
|
|
|
log.warn("[ZLM] closeIdleStreams failed: {}", e.getMessage()); |
|
|
|
return 0; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/** ZLM 的代理 key 格式为 vhost/app/stream,与 addStreamProxy 返回的 key 一致。 */ |
|
|
|
private static String proxyKey(String streamId) { |
|
|
|
return ZlMediaConst.DEFAULT_VHOST + "/" + ZlMediaConst.DEFAULT_APP + "/" + streamId; |
|
|
|
} |
|
|
|
|
|
|
|
private String apiUrl(String path, String query) { |
|
|
|
String base = zlMediaConfig.getUrl() == null ? "" : zlMediaConfig.getUrl(); |
|
|
|
while (base.endsWith("/")) { |
|
|
|
base = base.substring(0, base.length() - 1); |
|
|
|
} |
|
|
|
StringBuilder url = new StringBuilder(base).append(path) |
|
|
|
.append(path.contains("?") ? "&" : "?") |
|
|
|
.append("secret=").append(encode(zlMediaConfig.getSecret())); |
|
|
|
if (!isEmpty(query)) { |
|
|
|
url.append("&").append(query); |
|
|
|
} |
|
|
|
return url.toString(); |
|
|
|
} |
|
|
|
|
|
|
|
private static String encode(String value) { |
|
|
|
if (value == null) { |
|
|
|
return ""; |
|
|
|
} |
|
|
|
try { |
|
|
|
return URLEncoder.encode(value, StandardCharsets.UTF_8.name()); |
|
|
|
} catch (Exception e) { |
|
|
|
return value; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private static boolean isEmpty(String value) { |
|
|
|
return value == null || value.isEmpty(); |
|
|
|
} |
|
|
|
} |