优化云端录像seek准确性

This commit is contained in:
lin
2025-07-28 06:56:44 +08:00
parent 33eb7cb931
commit a711407a3a
13 changed files with 176 additions and 46 deletions

View File

@@ -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) {

View File

@@ -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);

View File

@@ -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);

View File

@@ -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

View File

@@ -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 {

View File

@@ -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

View File

@@ -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);

View File

@@ -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"));

View File

@@ -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

View File

@@ -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() {

View File

@@ -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 = "加载录像文件形成播放地址")

View File

@@ -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
}
})
}

View File

@@ -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) {