Commit 324ae744d757ad329e79770d1a0fbc4d3dfc4602

Authored by lawrencehj
1 parent bc37cdd5

增加历史媒体下载信令及API支持

src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java
@@ -103,7 +103,18 @@ public interface ISIPCommander { @@ -103,7 +103,18 @@ public interface ISIPCommander {
103 * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss 103 * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss
104 */ 104 */
105 void playbackStreamCmd(IMediaServerItem mediaServerItem,Device device, String channelId, String startTime, String endTime, ZLMHttpHookSubscribe.Event event, SipSubscribe.Event errorEvent); 105 void playbackStreamCmd(IMediaServerItem mediaServerItem,Device device, String channelId, String startTime, String endTime, ZLMHttpHookSubscribe.Event event, SipSubscribe.Event errorEvent);
106 - 106 +
  107 + /**
  108 + * 请求历史媒体下载
  109 + *
  110 + * @param device 视频设备
  111 + * @param channelId 预览通道
  112 + * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss
  113 + * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss
  114 + * @param downloadSpeed 下载倍速参数
  115 + */
  116 + void downloadStreamCmd(IMediaServerItem mediaServerItem,Device device, String channelId, String startTime, String endTime, String downloadSpeed, ZLMHttpHookSubscribe.Event event, SipSubscribe.Event errorEvent);
  117 +
107 /** 118 /**
108 * 视频流停止 119 * 视频流停止
109 * 120 *
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java
@@ -555,7 +555,121 @@ public class SIPCommander implements ISIPCommander { @@ -555,7 +555,121 @@ public class SIPCommander implements ISIPCommander {
555 } 555 }
556 } 556 }
557 557
  558 + /**
  559 + * 请求历史媒体下载
  560 + *
  561 + * @param device 视频设备
  562 + * @param channelId 预览通道
  563 + * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss
  564 + * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss
  565 + * @param downloadSpeed 下载倍速参数
  566 + */
  567 + @Override
  568 + public void downloadStreamCmd(IMediaServerItem mediaServerItem,Device device, String channelId, String startTime, String endTime, String downloadSpeed, ZLMHttpHookSubscribe.Event event
  569 + , SipSubscribe.Event errorEvent) {
  570 + try {
  571 + String ssrc = streamSession.createPlayBackSsrc();
  572 + String streamId = String.format("%08x", Integer.parseInt(ssrc)).toUpperCase();
  573 +
  574 + Integer mediaPort = null;
  575 + // 使用动态udp端口
  576 + if (mediaServerItem.isRtpEnable()) {
  577 + mediaPort = zlmrtpServerFactory.createRTPServer(mediaServerItem, streamId);
  578 + }else {
  579 + mediaPort = mediaServerItem.getRtpProxyPort();
  580 + }
  581 + logger.info("{} 分配的ZLM为: {} [{}:{}]", streamId, mediaServerItem.getId(), mediaServerItem.getIp(), mediaPort);
558 582
  583 + // 添加订阅
  584 + JSONObject subscribeKey = new JSONObject();
  585 + subscribeKey.put("app", "rtp");
  586 + subscribeKey.put("stream", streamId);
  587 + subscribeKey.put("regist", true);
  588 + subscribeKey.put("mediaServerId", mediaServerItem.getId());
  589 + logger.debug("录像回放添加订阅,订阅内容:" + subscribeKey.toString());
  590 + subscribe.addSubscribe(ZLMHttpHookSubscribe.HookType.on_stream_changed, subscribeKey,
  591 + (IMediaServerItem mediaServerItemInUse, JSONObject json)->{
  592 + if (userSetup.isWaitTrack() && json.getJSONArray("tracks") == null) return;
  593 + event.response(mediaServerItemInUse, json);
  594 + subscribe.removeSubscribe(ZLMHttpHookSubscribe.HookType.on_stream_changed, subscribeKey);
  595 + });
  596 +
  597 + StringBuffer content = new StringBuffer(200);
  598 + content.append("v=0\r\n");
  599 + content.append("o="+sipConfig.getSipId()+" 0 0 IN IP4 "+sipConfig.getSipIp()+"\r\n");
  600 + content.append("s=Download\r\n");
  601 + content.append("u="+channelId+":0\r\n");
  602 + content.append("c=IN IP4 "+mediaServerItem.getSdpIp()+"\r\n");
  603 + content.append("t="+DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(startTime)+" "
  604 + +DateUtil.yyyy_MM_dd_HH_mm_ssToTimestamp(endTime) +"\r\n");
  605 +
  606 +
  607 +
  608 + String streamMode = device.getStreamMode().toUpperCase();
  609 +
  610 + if (userSetup.isSeniorSdp()) {
  611 + if("TCP-PASSIVE".equals(streamMode)) {
  612 + content.append("m=video "+ mediaPort +" TCP/RTP/AVP 96 126 125 99 34 98 97\r\n");
  613 + }else if ("TCP-ACTIVE".equals(streamMode)) {
  614 + content.append("m=video "+ mediaPort +" TCP/RTP/AVP 96 126 125 99 34 98 97\r\n");
  615 + }else if("UDP".equals(streamMode)) {
  616 + content.append("m=video "+ mediaPort +" RTP/AVP 96 126 125 99 34 98 97\r\n");
  617 + }
  618 + content.append("a=recvonly\r\n");
  619 + content.append("a=rtpmap:96 PS/90000\r\n");
  620 + content.append("a=fmtp:126 profile-level-id=42e01e\r\n");
  621 + content.append("a=rtpmap:126 H264/90000\r\n");
  622 + content.append("a=rtpmap:125 H264S/90000\r\n");
  623 + content.append("a=fmtp:125 profile-level-id=42e01e\r\n");
  624 + content.append("a=rtpmap:99 MP4V-ES/90000\r\n");
  625 + content.append("a=fmtp:99 profile-level-id=3\r\n");
  626 + content.append("a=rtpmap:98 H264/90000\r\n");
  627 + content.append("a=rtpmap:97 MPEG4/90000\r\n");
  628 + if("TCP-PASSIVE".equals(streamMode)){ // tcp被动模式
  629 + content.append("a=setup:passive\r\n");
  630 + content.append("a=connection:new\r\n");
  631 + }else if ("TCP-ACTIVE".equals(streamMode)) { // tcp主动模式
  632 + content.append("a=setup:active\r\n");
  633 + content.append("a=connection:new\r\n");
  634 + }
  635 + }else {
  636 + if("TCP-PASSIVE".equals(streamMode)) {
  637 + content.append("m=video "+ mediaPort +" TCP/RTP/AVP 96 98 97\r\n");
  638 + }else if ("TCP-ACTIVE".equals(streamMode)) {
  639 + content.append("m=video "+ mediaPort +" TCP/RTP/AVP 96 98 97\r\n");
  640 + }else if("UDP".equals(streamMode)) {
  641 + content.append("m=video "+ mediaPort +" RTP/AVP 96 98 97\r\n");
  642 + }
  643 + content.append("a=recvonly\r\n");
  644 + content.append("a=rtpmap:96 PS/90000\r\n");
  645 + content.append("a=rtpmap:98 H264/90000\r\n");
  646 + content.append("a=rtpmap:97 MPEG4/90000\r\n");
  647 + if("TCP-PASSIVE".equals(streamMode)){ // tcp被动模式
  648 + content.append("a=setup:passive\r\n");
  649 + content.append("a=connection:new\r\n");
  650 + }else if ("TCP-ACTIVE".equals(streamMode)) { // tcp主动模式
  651 + content.append("a=setup:active\r\n");
  652 + content.append("a=connection:new\r\n");
  653 + }
  654 + }
  655 + content.append("a=downloadspeed:" + downloadSpeed + "\r\n");
  656 +
  657 + content.append("y="+ssrc+"\r\n");//ssrc
  658 +
  659 + String tm = Long.toString(System.currentTimeMillis());
  660 +
  661 + CallIdHeader callIdHeader = device.getTransport().equals("TCP") ? tcpSipProvider.getNewCallId()
  662 + : udpSipProvider.getNewCallId();
  663 +
  664 + Request request = headerProvider.createPlaybackInviteRequest(device, channelId, content.toString(), null, "fromplybck" + tm, null, callIdHeader);
  665 +
  666 + ClientTransaction transaction = transmitRequest(device, request, errorEvent);
  667 + streamSession.put(device.getDeviceId(), channelId, ssrc, streamId, transaction);
  668 +
  669 + } catch ( SipException | ParseException | InvalidArgumentException e) {
  670 + e.printStackTrace();
  671 + }
  672 + }
559 673
560 /** 674 /**
561 * 视频流停止, 不使用回调 675 * 视频流停止, 不使用回调
src/main/java/com/genersoft/iot/vmp/vmanager/gb28181/playback/DownloadController.java 0 → 100644
  1 +package com.genersoft.iot.vmp.vmanager.gb28181.playback;
  2 +
  3 +import com.genersoft.iot.vmp.common.StreamInfo;
  4 +import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
  5 +import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
  6 +//import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils;
  7 +import com.genersoft.iot.vmp.media.zlm.dto.IMediaServerItem;
  8 +import com.genersoft.iot.vmp.media.zlm.dto.MediaServerItem;
  9 +import com.genersoft.iot.vmp.storager.IRedisCatchStorage;
  10 +import com.genersoft.iot.vmp.service.IPlayService;
  11 +import io.swagger.annotations.Api;
  12 +import io.swagger.annotations.ApiImplicitParam;
  13 +import io.swagger.annotations.ApiImplicitParams;
  14 +import io.swagger.annotations.ApiOperation;
  15 +import org.slf4j.Logger;
  16 +import org.slf4j.LoggerFactory;
  17 +import org.springframework.beans.factory.annotation.Autowired;
  18 +import org.springframework.http.HttpStatus;
  19 +import org.springframework.http.ResponseEntity;
  20 +import org.springframework.web.bind.annotation.CrossOrigin;
  21 +import org.springframework.web.bind.annotation.GetMapping;
  22 +import org.springframework.web.bind.annotation.PathVariable;
  23 +import org.springframework.web.bind.annotation.RequestMapping;
  24 +import org.springframework.web.bind.annotation.RestController;
  25 +
  26 +import com.alibaba.fastjson.JSONObject;
  27 +import com.genersoft.iot.vmp.gb28181.bean.Device;
  28 +import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
  29 +import com.genersoft.iot.vmp.storager.IVideoManagerStorager;
  30 +import org.springframework.web.context.request.async.DeferredResult;
  31 +
  32 +import javax.sip.message.Response;
  33 +import java.util.UUID;
  34 +
  35 +@Api(tags = "历史媒体下载")
  36 +@CrossOrigin
  37 +@RestController
  38 +@RequestMapping("/api/download")
  39 +public class DownloadController {
  40 +
  41 + private final static Logger logger = LoggerFactory.getLogger(DownloadController.class);
  42 +
  43 + @Autowired
  44 + private SIPCommander cmder;
  45 +
  46 + @Autowired
  47 + private IVideoManagerStorager storager;
  48 +
  49 + @Autowired
  50 + private IRedisCatchStorage redisCatchStorage;
  51 +
  52 + // @Autowired
  53 + // private ZLMRESTfulUtils zlmresTfulUtils;
  54 +
  55 + @Autowired
  56 + private IPlayService playService;
  57 +
  58 + @Autowired
  59 + private DeferredResultHolder resultHolder;
  60 +
  61 + @ApiOperation("开始历史媒体下载")
  62 + @ApiImplicitParams({
  63 + @ApiImplicitParam(name = "deviceId", value = "设备ID", dataTypeClass = String.class),
  64 + @ApiImplicitParam(name = "channelId", value = "通道ID", dataTypeClass = String.class),
  65 + @ApiImplicitParam(name = "startTime", value = "开始时间", dataTypeClass = String.class),
  66 + @ApiImplicitParam(name = "endTime", value = "结束时间", dataTypeClass = String.class),
  67 + @ApiImplicitParam(name = "downloadSpeed", value = "下载倍速", dataTypeClass = String.class),
  68 + })
  69 + @GetMapping("/start/{deviceId}/{channelId}")
  70 + public DeferredResult<ResponseEntity<String>> play(@PathVariable String deviceId, @PathVariable String channelId,
  71 + String startTime, String endTime, String downloadSpeed) {
  72 +
  73 + if (logger.isDebugEnabled()) {
  74 + logger.debug(String.format("历史媒体下载 API调用,deviceId:%s,channelId:%s,downloadSpeed:%s", deviceId, channelId, downloadSpeed));
  75 + }
  76 + UUID uuid = UUID.randomUUID();
  77 + DeferredResult<ResponseEntity<String>> result = new DeferredResult<ResponseEntity<String>>(30000L);
  78 + // 超时处理
  79 + result.onTimeout(()->{
  80 + logger.warn(String.format("设备下载响应超时,deviceId:%s ,channelId:%s", deviceId, channelId));
  81 + RequestMessage msg = new RequestMessage();
  82 + msg.setId(DeferredResultHolder.CALLBACK_CMD_PlAY + uuid);
  83 + msg.setData("Timeout");
  84 + resultHolder.invokeResult(msg);
  85 + });
  86 + Device device = storager.queryVideoDevice(deviceId);
  87 + StreamInfo streamInfo = redisCatchStorage.queryPlaybackByDevice(deviceId, channelId);
  88 + if (streamInfo != null) {
  89 + // 停止之前的下载
  90 + cmder.streamByeCmd(deviceId, channelId);
  91 + }
  92 + resultHolder.put(DeferredResultHolder.CALLBACK_CMD_PlAY + uuid, result);
  93 + IMediaServerItem newMediaServerItem = playService.getNewMediaServerItem(device);
  94 + if (newMediaServerItem == null) {
  95 + logger.warn(String.format("设备下载响应超时,deviceId:%s ,channelId:%s", deviceId, channelId));
  96 + RequestMessage msg = new RequestMessage();
  97 + msg.setId(DeferredResultHolder.CALLBACK_CMD_PlAY + uuid);
  98 + msg.setData("Timeout");
  99 + resultHolder.invokeResult(msg);
  100 + return result;
  101 + }
  102 + cmder.downloadStreamCmd(newMediaServerItem, device, channelId, startTime, endTime, downloadSpeed, (IMediaServerItem mediaServerItem, JSONObject response) -> {
  103 + logger.info("收到订阅消息: " + response.toJSONString());
  104 + playService.onPublishHandlerForPlayBack(mediaServerItem, response, deviceId, channelId, uuid.toString());
  105 + }, event -> {
  106 + Response response = event.getResponse();
  107 + RequestMessage msg = new RequestMessage();
  108 + msg.setId(DeferredResultHolder.CALLBACK_CMD_PlAY + uuid);
  109 + msg.setData(String.format("回放失败, 错误码: %s, %s", response.getStatusCode(), response.getReasonPhrase()));
  110 + resultHolder.invokeResult(msg);
  111 + });
  112 +
  113 + return result;
  114 + }
  115 +
  116 + @ApiOperation("停止历史媒体下载")
  117 + @ApiImplicitParams({
  118 + @ApiImplicitParam(name = "deviceId", value = "设备ID", dataTypeClass = String.class),
  119 + @ApiImplicitParam(name = "channelId", value = "通道ID", dataTypeClass = String.class),
  120 + })
  121 + @GetMapping("/stop/{deviceId}/{channelId}")
  122 + public ResponseEntity<String> playStop(@PathVariable String deviceId, @PathVariable String channelId) {
  123 +
  124 + cmder.streamByeCmd(deviceId, channelId);
  125 +
  126 + if (logger.isDebugEnabled()) {
  127 + logger.debug(String.format("设备历史媒体下载停止 API调用,deviceId/channelId:%s/%s", deviceId, channelId));
  128 + }
  129 +
  130 + if (deviceId != null && channelId != null) {
  131 + JSONObject json = new JSONObject();
  132 + json.put("deviceId", deviceId);
  133 + json.put("channelId", channelId);
  134 + return new ResponseEntity<String>(json.toString(), HttpStatus.OK);
  135 + } else {
  136 + logger.warn("设备历史媒体下载停止API调用失败!");
  137 + return new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR);
  138 + }
  139 + }
  140 +}
web_src/src/components/dialog/devicePlayer.vue
@@ -48,7 +48,10 @@ @@ -48,7 +48,10 @@
48 48
49 <el-table-column label="操作"> 49 <el-table-column label="操作">
50 <template slot-scope="scope"> 50 <template slot-scope="scope">
51 - <el-button icon="el-icon-video-play" size="mini" @click="playRecord(scope.row)">播放</el-button> 51 + <el-button-group>
  52 + <el-button icon="el-icon-video-play" size="mini" @click="playRecord(scope.row)">播放</el-button>
  53 + <el-button icon="el-icon-download" size="mini" @click="downloadRecord(scope.row)">下载</el-button>
  54 + </el-button-group>
52 </template> 55 </template>
53 </el-table-column> 56 </el-table-column>
54 </el-table> 57 </el-table>
@@ -444,6 +447,38 @@ export default { @@ -444,6 +447,38 @@ export default {
444 if (callback) callback() 447 if (callback) callback()
445 }); 448 });
446 }, 449 },
  450 + downloadRecord: function (row) {
  451 + let that = this;
  452 + if (that.streamId != "") {
  453 + that.stopDownloadRecord(function () {
  454 + that.streamId = "",
  455 + that.downloadRecord(row);
  456 + })
  457 + } else {
  458 + this.$axios({
  459 + method: 'get',
  460 + url: '/api/download/start/' + this.deviceId + '/' + this.channelId + '?startTime=' + row.startTime + '&endTime=' +
  461 + row.endTime + '&downloadSpeed=4'
  462 + }).then(function (res) {
  463 + var streamInfo = res.data;
  464 + that.app = streamInfo.app;
  465 + that.streamId = streamInfo.streamId;
  466 + that.mediaServerId = streamInfo.mediaServerId;
  467 + that.videoUrl = that.getUrlByStreamInfo(streamInfo);
  468 + that.recordPlay = true;
  469 + });
  470 + }
  471 + },
  472 + stopDownloadRecord: function (callback) {
  473 + this.$refs.videoPlayer.pause();
  474 + this.videoUrl = '';
  475 + this.$axios({
  476 + method: 'get',
  477 + url: '/api/download/stop/' + this.deviceId + "/" + this.channelId
  478 + }).then(function (res) {
  479 + if (callback) callback()
  480 + });
  481 + },
447 ptzCamera: function (leftRight, upDown, zoom) { 482 ptzCamera: function (leftRight, upDown, zoom) {
448 console.log('云台控制:' + leftRight + ' : ' + upDown + " : " + zoom); 483 console.log('云台控制:' + leftRight + ' : ' + upDown + " : " + zoom);
449 let that = this; 484 let that = this;
web_src/src/components/dialog/easyPlayer.vue
@@ -60,8 +60,8 @@ export default { @@ -60,8 +60,8 @@ export default {
60 min-width: 70px; 60 min-width: 70px;
61 } 61 }
62 /* 隐藏logo */ 62 /* 隐藏logo */
63 - /* .iconqingxiLOGO { 63 + .iconqingxiLOGO {
64 display: none !important; 64 display: none !important;
65 - } */ 65 + }
66 66
67 </style> 67 </style>