Commit 324ae744d757ad329e79770d1a0fbc4d3dfc4602
1 parent
bc37cdd5
增加历史媒体下载信令及API支持
Showing
5 changed files
with
304 additions
and
4 deletions
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java
| ... | ... | @@ -103,7 +103,18 @@ public interface ISIPCommander { |
| 103 | 103 | * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss |
| 104 | 104 | */ |
| 105 | 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 | 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 | 48 | |
| 49 | 49 | <el-table-column label="操作"> |
| 50 | 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 | 55 | </template> |
| 53 | 56 | </el-table-column> |
| 54 | 57 | </el-table> |
| ... | ... | @@ -444,6 +447,38 @@ export default { |
| 444 | 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 | 482 | ptzCamera: function (leftRight, upDown, zoom) { |
| 448 | 483 | console.log('云台控制:' + leftRight + ' : ' + upDown + " : " + zoom); |
| 449 | 484 | let that = this; | ... | ... |