mirror of
https://github.com/648540858/wvp-GB28181-pro.git
synced 2026-05-06 15:22:55 +08:00
优化云端录像seek准确性
This commit is contained in:
@@ -101,6 +101,9 @@ public class StreamInfo implements Serializable, Cloneable{
|
||||
@Schema(description = "使用的WVP ID")
|
||||
private String serverId;
|
||||
|
||||
@Schema(description = "加载录像文件的时长")
|
||||
private Long duration;
|
||||
|
||||
public void setRtmp(String host, int port, int sslPort, String app, String stream, String callIdParam) {
|
||||
String file = String.format("%s/%s%s", app, stream, callIdParam);
|
||||
if (port > 0) {
|
||||
|
||||
@@ -70,7 +70,7 @@ public interface IMediaNodeServerService {
|
||||
|
||||
List<String> listRtpServer(MediaServer mediaServer);
|
||||
|
||||
void loadMP4File(MediaServer mediaServer, String app, String stream, String datePath);
|
||||
long loadMP4File(MediaServer mediaServer, String app, String stream, String datePath);
|
||||
|
||||
void seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema);
|
||||
|
||||
|
||||
@@ -160,7 +160,7 @@ public interface IMediaServerService {
|
||||
|
||||
List<String> listRtpServer(MediaServer mediaServer);
|
||||
|
||||
StreamInfo loadMP4File(MediaServer mediaServer, String app, String stream, String datePath);
|
||||
long loadMP4File(MediaServer mediaServer, String app, String stream, String datePath);
|
||||
|
||||
void seekRecordStamp(MediaServer mediaServer, String app, String stream, Double stamp, String schema);
|
||||
|
||||
|
||||
@@ -972,14 +972,13 @@ public class MediaServerServiceImpl implements IMediaServerService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public StreamInfo loadMP4File(MediaServer mediaServer, String app, String stream, String datePath) {
|
||||
public long loadMP4File(MediaServer mediaServer, String app, String stream, String datePath) {
|
||||
IMediaNodeServerService mediaNodeServerService = nodeServerServiceMap.get(mediaServer.getType());
|
||||
if (mediaNodeServerService == null) {
|
||||
log.info("[loadMP4File] 失败, mediaServer的类型: {},未找到对应的实现类", mediaServer.getType());
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "未找到mediaServer对应的实现类");
|
||||
}
|
||||
mediaNodeServerService.loadMP4File(mediaServer, app, stream, datePath);
|
||||
return getStreamInfoByAppAndStream(mediaServer, app, stream, null, null);
|
||||
return mediaNodeServerService.loadMP4File(mediaServer, app, stream, datePath);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -137,7 +137,7 @@ public class ZLMHttpHookListener {
|
||||
}
|
||||
if (param.getSchema().equalsIgnoreCase("rtsp")) {
|
||||
if (param.isRegist()) {
|
||||
log.info("[ZLM HOOK] 流注册, {}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream());
|
||||
log.info("[ZLM HOOK]流注册, {}->{}->{}/{}", param.getMediaServerId(), param.getSchema(), param.getApp(), param.getStream());
|
||||
String queryParams = param.getParams();
|
||||
if (queryParams == null) {
|
||||
try {
|
||||
|
||||
@@ -538,7 +538,7 @@ public class ZLMMediaNodeServerService implements IMediaNodeServerService {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadMP4File(MediaServer mediaServer, String app, String stream, String datePath) {
|
||||
public long loadMP4File(MediaServer mediaServer, String app, String stream, String datePath) {
|
||||
JSONObject jsonObject = zlmresTfulUtils.loadMP4File(mediaServer, app, stream, datePath);
|
||||
if (jsonObject == null) {
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "请求失败");
|
||||
@@ -546,6 +546,11 @@ public class ZLMMediaNodeServerService implements IMediaNodeServerService {
|
||||
if (jsonObject.getInteger("code") != 0) {
|
||||
throw new ControllerException(jsonObject.getInteger("code"), jsonObject.getString("msg"));
|
||||
}
|
||||
JSONObject data = jsonObject.getJSONObject("data");
|
||||
if (data == null || data.isEmpty()) {
|
||||
return 0;
|
||||
}
|
||||
return data.getLong("duration_ms");
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -61,7 +61,9 @@ public interface ICloudRecordService {
|
||||
*/
|
||||
void loadRecord(String app, String stream, CloudRecordItem cloudRecordItem, ErrorCallback<StreamInfo> callback);
|
||||
|
||||
void seekRecord(String mediaServerId,String app, String stream, Double seek, String schema);
|
||||
void loadRecordDay(String app, String stream, String day, ErrorCallback<StreamInfo> callback);
|
||||
|
||||
void seekRecord(String mediaServerId, String app, String stream, Double seek, String schema);
|
||||
|
||||
void setRecordSpeed(String mediaServerId, String app, String stream, Integer speed, String schema);
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ public class CloudRecordItem {
|
||||
cloudRecordItem.setFilePath(param.getRecordInfo().getFilePath());
|
||||
cloudRecordItem.setMediaServerId(param.getMediaServer().getId());
|
||||
cloudRecordItem.setTimeLen(param.getRecordInfo().getTimeLen() * 1000);
|
||||
cloudRecordItem.setEndTime((param.getRecordInfo().getStartTime() + (long)param.getRecordInfo().getTimeLen()) * 1000);
|
||||
cloudRecordItem.setEndTime(System.currentTimeMillis());
|
||||
Map<String, String> paramsMap = MediaServerUtils.urlParamToMap(param.getRecordInfo().getParams());
|
||||
if (paramsMap.get("callId") != null) {
|
||||
cloudRecordItem.setCallId(paramsMap.get("callId"));
|
||||
|
||||
@@ -301,15 +301,54 @@ public class CloudRecordServiceImpl implements ICloudRecordService {
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
long duration = mediaServerService.loadMP4File(mediaServer, buildApp, buildStream, cloudRecordItem.getFilePath());
|
||||
Hook hook = Hook.getInstance(HookType.on_media_arrival, buildApp, buildStream, mediaServerId);
|
||||
subscribe.addSubscribe(hook, (hookData) -> {
|
||||
StreamInfo streamInfo = mediaServerService.getStreamInfoByAppAndStream(mediaServer, buildApp, buildStream, hookData.getMediaInfo(), null);
|
||||
streamInfo.setDuration(duration);
|
||||
if (callback != null) {
|
||||
callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadRecordDay(String app, String stream, String day, ErrorCallback<StreamInfo> callback) {
|
||||
|
||||
long startTimestamp = DateUtil.yyyy_MM_dd_HH_mm_ssToTimestampMs(day + " 00:00:00");
|
||||
long endTimestamp = startTimestamp + 24 * 60 * 60 * 1000;
|
||||
|
||||
List<CloudRecordItem> recordItemList = cloudRecordServiceMapper.getList(null, app, stream, startTimestamp, endTimestamp, null, null, null, false);
|
||||
if (recordItemList.isEmpty()) {
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "此时间无录像");
|
||||
}
|
||||
String mediaServerId = recordItemList.get(0).getMediaServerId();
|
||||
MediaServer mediaServer = mediaServerService.getOne(mediaServerId);
|
||||
if (mediaServer == null) {
|
||||
throw new ControllerException(ErrorCode.ERROR100.getCode(), "媒体节点不存在: " + mediaServerId);
|
||||
}
|
||||
String buildApp = "mp4_record";
|
||||
String buildStream = app + "_" + stream + "_" + day;
|
||||
MediaInfo mediaInfo = mediaServerService.getMediaInfo(mediaServer, buildApp, buildStream);
|
||||
if (mediaInfo != null) {
|
||||
if (callback != null) {
|
||||
StreamInfo streamInfo = mediaServerService.getStreamInfoByAppAndStream(mediaServer, buildApp, buildStream, mediaInfo, null);
|
||||
callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
String dateDir = recordItemList.get(0).getFilePath().substring(0, recordItemList.get(0).getFilePath().lastIndexOf("/"));
|
||||
long duration = mediaServerService.loadMP4File(mediaServer, buildApp, buildStream, dateDir);
|
||||
Hook hook = Hook.getInstance(HookType.on_media_arrival, buildApp, buildStream, mediaServerId);
|
||||
subscribe.addSubscribe(hook, (hookData) -> {
|
||||
StreamInfo streamInfo = mediaServerService.getStreamInfoByAppAndStream(mediaServer, buildApp, buildStream, hookData.getMediaInfo(), null);
|
||||
streamInfo.setDuration(duration);
|
||||
if (callback != null) {
|
||||
callback.run(ErrorCode.SUCCESS.getCode(), ErrorCode.SUCCESS.getMsg(), streamInfo);
|
||||
}
|
||||
});
|
||||
mediaServerService.loadMP4File(mediaServer, buildApp, buildStream, cloudRecordItem.getFilePath());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
@@ -103,6 +103,9 @@ public class StreamContent {
|
||||
|
||||
private double progress;
|
||||
|
||||
private Long duration;
|
||||
|
||||
|
||||
public StreamContent(StreamInfo streamInfo) {
|
||||
if (streamInfo == null) {
|
||||
return;
|
||||
@@ -187,6 +190,9 @@ public class StreamContent {
|
||||
if (streamInfo.getTranscodeStream() != null) {
|
||||
this.transcodeStream = new StreamContent(streamInfo.getTranscodeStream());
|
||||
}
|
||||
if (streamInfo.getDuration() != null) {
|
||||
this.duration = streamInfo.getDuration();
|
||||
}
|
||||
}
|
||||
|
||||
public StreamContent getTranscodeStream() {
|
||||
|
||||
@@ -249,6 +249,66 @@ public class CloudRecordController {
|
||||
return cloudRecordService.getPlayUrlPath(recordId);
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@GetMapping("/loadRecordDay")
|
||||
@Operation(summary = "加载录像日期形成播放地址")
|
||||
@Parameter(name = "app", description = "应用名", required = true)
|
||||
@Parameter(name = "stream", description = "流ID", required = true)
|
||||
@Parameter(name = "day", description = "日期,例如2025-10-03", required = true)
|
||||
public DeferredResult<WVPResult<StreamContent>> loadRecordDay(
|
||||
HttpServletRequest request,
|
||||
@RequestParam(required = true) String app,
|
||||
@RequestParam(required = true) String stream,
|
||||
@RequestParam(required = true) String day
|
||||
) {
|
||||
DeferredResult<WVPResult<StreamContent>> result = new DeferredResult<>();
|
||||
|
||||
result.onTimeout(()->{
|
||||
log.info("[加载录像日期超时] app={}, stream={}, fileId={}", app, stream, day);
|
||||
WVPResult<StreamContent> wvpResult = new WVPResult<>();
|
||||
wvpResult.setCode(ErrorCode.ERROR100.getCode());
|
||||
wvpResult.setMsg("加载录像日期超时");
|
||||
result.setResult(wvpResult);
|
||||
});
|
||||
|
||||
ErrorCallback<StreamInfo> callback = (code, msg, streamInfo) -> {
|
||||
|
||||
WVPResult<StreamContent> wvpResult = new WVPResult<>();
|
||||
if (code == InviteErrorCode.SUCCESS.getCode()) {
|
||||
wvpResult.setCode(ErrorCode.SUCCESS.getCode());
|
||||
wvpResult.setMsg(ErrorCode.SUCCESS.getMsg());
|
||||
|
||||
if (streamInfo != null) {
|
||||
if (userSetting.getUseSourceIpAsStreamIp()) {
|
||||
streamInfo=streamInfo.clone();//深拷贝
|
||||
String host;
|
||||
try {
|
||||
URL url=new URL(request.getRequestURL().toString());
|
||||
host=url.getHost();
|
||||
} catch (MalformedURLException e) {
|
||||
host=request.getLocalAddr();
|
||||
}
|
||||
streamInfo.changeStreamIp(host);
|
||||
}
|
||||
if (!org.springframework.util.ObjectUtils.isEmpty(streamInfo.getMediaServer().getTranscodeSuffix()) && !"null".equalsIgnoreCase(streamInfo.getMediaServer().getTranscodeSuffix())) {
|
||||
streamInfo.setStream(streamInfo.getStream() + "_" + streamInfo.getMediaServer().getTranscodeSuffix());
|
||||
}
|
||||
wvpResult.setData(new StreamContent(streamInfo));
|
||||
}else {
|
||||
wvpResult.setCode(code);
|
||||
wvpResult.setMsg(msg);
|
||||
}
|
||||
}else {
|
||||
wvpResult.setCode(code);
|
||||
wvpResult.setMsg(msg);
|
||||
}
|
||||
result.setResult(wvpResult);
|
||||
};
|
||||
|
||||
cloudRecordService.loadRecordDay(app, stream, day, callback);
|
||||
return result;
|
||||
}
|
||||
|
||||
@ResponseBody
|
||||
@GetMapping("/loadRecord")
|
||||
@Operation(summary = "加载录像文件形成播放地址")
|
||||
|
||||
@@ -27,14 +27,14 @@ export function queryListByData(params) {
|
||||
})
|
||||
}
|
||||
|
||||
export function loadRecord({ app, stream, fileId }) {
|
||||
export function loadRecord({ app, stream, day }) {
|
||||
return request({
|
||||
method: 'get',
|
||||
url: `/api/cloud/record/loadRecord`,
|
||||
url: `/api/cloud/record/loadRecordDay`,
|
||||
params: {
|
||||
app: app,
|
||||
stream: stream,
|
||||
fileId: fileId
|
||||
day: day
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -141,6 +141,8 @@ export default {
|
||||
currentPage: 1,
|
||||
count: 1000000, // TODO 分页导致滑轨视频有效值无法获取完全
|
||||
total: 0,
|
||||
totalDuration: 0, // 实际总时长
|
||||
totalFileDuration: 0, // 实际文件时长
|
||||
playLoading: false,
|
||||
showTime: true,
|
||||
isFullScreen: false,
|
||||
@@ -289,6 +291,7 @@ export default {
|
||||
},
|
||||
queryRecordDetails: function(callback) {
|
||||
this.timeSegments = []
|
||||
this.totalDuration = 0
|
||||
this.$store.dispatch('cloudRecord/queryList', {
|
||||
app: this.app,
|
||||
stream: this.stream,
|
||||
@@ -314,6 +317,7 @@ export default {
|
||||
endRatio: 0.85,
|
||||
index: i
|
||||
})
|
||||
this.totalDuration += (this.detailFiles[i].endTime - this.detailFiles[i].startTime);
|
||||
}
|
||||
this.mediaServerList = Array.from(temp)
|
||||
if (this.mediaServerList.length === 1) {
|
||||
@@ -330,39 +334,53 @@ export default {
|
||||
},
|
||||
chooseFile(index) {
|
||||
this.chooseFileIndex = index
|
||||
this.playSeekValue = 0
|
||||
let timeLength = 0
|
||||
for (let i = 0; i < this.detailFiles.length; i++) {
|
||||
if (i < index) {
|
||||
timeLength += (this.detailFiles[i].endTime - this.detailFiles[i].startTime)
|
||||
}
|
||||
}
|
||||
this.playSeekValue = timeLength
|
||||
this.playRecord()
|
||||
},
|
||||
playRecord() {
|
||||
if (!this.$refs.recordVideoPlayer.playing) {
|
||||
this.$refs.recordVideoPlayer.destroy()
|
||||
}
|
||||
this.$store.dispatch('cloudRecord/loadRecord', {
|
||||
app: this.app,
|
||||
stream: this.stream,
|
||||
fileId: this.detailFiles[this.chooseFileIndex].id
|
||||
})
|
||||
.then(data => {
|
||||
this.streamInfo = data
|
||||
if (location.protocol === 'https:') {
|
||||
this.videoUrl = data['https_fmp4'] + '&time=' + new Date().getTime()
|
||||
} else {
|
||||
this.videoUrl = data['fmp4'] + '&time=' + new Date().getTime()
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
})
|
||||
.finally(() => {
|
||||
this.playLoading = false
|
||||
if (this.streamInfo === null) {
|
||||
this.totalFileDuration = 0
|
||||
this.$store.dispatch('cloudRecord/loadRecord', {
|
||||
app: this.app,
|
||||
stream: this.stream,
|
||||
day: this.chooseDate
|
||||
})
|
||||
.then(data => {
|
||||
this.streamInfo = data
|
||||
this.totalFileDuration = data.duration
|
||||
if (location.protocol === 'https:') {
|
||||
this.videoUrl = data['https_fmp4'] + '&time=' + new Date().getTime()
|
||||
} else {
|
||||
this.videoUrl = data['fmp4'] + '&time=' + new Date().getTime()
|
||||
}
|
||||
this.seekRecord()
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(error)
|
||||
})
|
||||
.finally(() => {
|
||||
this.playLoading = false
|
||||
})
|
||||
}else {
|
||||
this.seekRecord()
|
||||
}
|
||||
|
||||
},
|
||||
seekRecord() {
|
||||
this.$store.dispatch('cloudRecord/seek', {
|
||||
mediaServerId: this.streamInfo.mediaServerId,
|
||||
app: this.streamInfo.app,
|
||||
stream: this.streamInfo.stream,
|
||||
seek: this.playSeekValue,
|
||||
seek: this.playSeekValue * (this.totalFileDuration / this.totalDuration),
|
||||
schema: 'fmp4'
|
||||
})
|
||||
.catch((error) => {
|
||||
@@ -404,8 +422,6 @@ export default {
|
||||
return
|
||||
}
|
||||
this.playTime = val
|
||||
let chooseFile = this.detailFiles[this.chooseFileIndex]
|
||||
console.log(chooseFile)
|
||||
},
|
||||
timelineMouseDown() {
|
||||
this.timelineControl = true
|
||||
@@ -415,23 +431,23 @@ export default {
|
||||
this.timelineControl = false
|
||||
return
|
||||
}
|
||||
this.chooseFileIndex = null
|
||||
this.timelineControl = false
|
||||
let timeLength = 0
|
||||
for (let i = 0; i < this.detailFiles.length; i++) {
|
||||
const item = this.detailFiles[i]
|
||||
if (this.playTime > item.startTime && this.playTime < item.endTime) {
|
||||
if (this.playTime > item.endTime) {
|
||||
timeLength += (item.endTime - item.startTime)
|
||||
} else if (this.playTime === item.endTime) {
|
||||
timeLength += (item.endTime - item.startTime)
|
||||
this.chooseFileIndex = i
|
||||
break
|
||||
} else if (this.playTime > item.startTime && this.playTime < item.endTime) {
|
||||
timeLength += (this.playTime - item.startTime)
|
||||
this.chooseFileIndex = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if (this.chooseFileIndex === null) {
|
||||
this.$message({
|
||||
showClose: true,
|
||||
message: '此时段无录像',
|
||||
type: 'error'
|
||||
})
|
||||
return;
|
||||
}
|
||||
this.playSeekValue = timeLength
|
||||
this.playRecord()
|
||||
},
|
||||
getTimeForFile(file) {
|
||||
|
||||
Reference in New Issue
Block a user