Lawrence
2020-12-16 44d216100b45c3337c593ee82ee68e7e0f35d24b
与master分支同步
25个文件已修改
5个文件已添加
1279 ■■■■■ 已修改文件
src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/event/online/OnlineEventListener.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/session/VideoStreamSessionManager.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java 51 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/request/impl/AckRequestProcessor.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/request/impl/ByeRequestProcessor.java 34 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/request/impl/MessageRequestProcessor.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookSubscribe.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRunner.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/storager/redis/VideoManagerRedisStoragerImpl.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/vmanager/play/PlayController.java 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/vmanager/playback/PlaybackController.java 117 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/vmanager/service/IPlayService.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/vmanager/service/impl/PlayServiceImpl.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/web/ApiStreamController.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-dev.yml 94 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application.yml 65 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/build/webpack.dev.conf.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/build/webpack.prod.conf.js 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/index.html 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/package-lock.json 8 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/package.json 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/components/gb28181/devicePlayer.vue 222 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/components/gb28181/player.vue 57 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/components/videoList.vue 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/router/index.js 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/common/StreamInfo.java
@@ -5,12 +5,18 @@
public class StreamInfo {
    private String ssrc;
    private String streamId;
    private String deviceID;
    private String cahnnelId;
    private String flv;
    private String ws_flv;
    private String rtmp;
    private String fmp4;
    private String ws_fmp4;
    private String hls;
    private String ws_hls;
    private String ts;
    private String ws_ts;
    private String rtmp;
    private String rtsp;
    private JSONArray tracks;
@@ -85,4 +91,52 @@
    public void setTracks(JSONArray tracks) {
        this.tracks = tracks;
    }
    public String getFmp4() {
        return fmp4;
    }
    public void setFmp4(String fmp4) {
        this.fmp4 = fmp4;
    }
    public String getWs_fmp4() {
        return ws_fmp4;
    }
    public void setWs_fmp4(String ws_fmp4) {
        this.ws_fmp4 = ws_fmp4;
    }
    public String getWs_hls() {
        return ws_hls;
    }
    public void setWs_hls(String ws_hls) {
        this.ws_hls = ws_hls;
    }
    public String getTs() {
        return ts;
    }
    public void setTs(String ts) {
        this.ts = ts;
    }
    public String getWs_ts() {
        return ws_ts;
    }
    public void setWs_ts(String ws_ts) {
        this.ws_ts = ws_ts;
    }
    public String getStreamId() {
        return streamId;
    }
    public void setStreamId(String streamId) {
        this.streamId = streamId;
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/SipLayer.java
@@ -113,6 +113,7 @@
     */
    @Override
    public void processRequest(RequestEvent evt) {
        logger.debug(evt.getRequest().toString());
        // 由于jainsip是单线程程序,为提高性能并发处理
        processThreadPool.execute(() -> {
            processorFactory.createRequestProcessor(evt).process();
@@ -122,6 +123,7 @@
    @Override
    public void processResponse(ResponseEvent evt) {
        Response response = evt.getResponse();
        logger.debug(evt.getResponse().toString());
        int status = response.getStatusCode();
        if (((status >= 200) && (status < 300)) || status == 401) { // Success!
            ISIPResponseProcessor processor = processorFactory.createResponseProcessor(evt);
src/main/java/com/genersoft/iot/vmp/gb28181/event/online/OnlineEventListener.java
@@ -32,7 +32,7 @@
    public void onApplicationEvent(OnlineEvent event) {
        
        if (logger.isDebugEnabled()) {
            logger.debug("设备离线事件触发,deviceId:" + event.getDeviceId() + ",from:" + event.getFrom());
            logger.debug("设备上线事件触发,deviceId:" + event.getDeviceId() + ",from:" + event.getFrom());
        }
        
        String key = VideoManagerConstants.KEEPLIVEKEY_PREFIX + event.getDeviceId();
src/main/java/com/genersoft/iot/vmp/gb28181/session/VideoStreamSessionManager.java
@@ -17,13 +17,11 @@
    private ConcurrentHashMap<String, ClientTransaction> sessionMap = new ConcurrentHashMap<>();
    public String createPlaySsrc(){
        String ssrc = SsrcUtil.getPlaySsrc();
        return ssrc;
        return SsrcUtil.getPlaySsrc();
    }
    
    public String createPlayBackSsrc(){
        String ssrc = SsrcUtil.getPlayBackSsrc();
        return ssrc;
        return SsrcUtil.getPlayBackSsrc();
    }
    
    public void put(String ssrc,ClientTransaction transaction){
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/callback/DeferredResultHolder.java
@@ -22,6 +22,8 @@
    
    public static final String CALLBACK_CMD_RECORDINFO = "CALLBACK_RECORDINFO";
    public static final String CALLBACK_CMD_PlAY = "CALLBACK_PLAY";
    private Map<String, DeferredResult> map = new HashMap<String, DeferredResult>();
    
    public void put(String key, DeferredResult result) {
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/ISIPCommander.java
@@ -2,6 +2,7 @@
import com.genersoft.iot.vmp.common.StreamInfo;
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.media.zlm.ZLMHttpHookSubscribe;
import com.genersoft.iot.vmp.gb28181.bean.ParentPlatform;
/**    
@@ -20,7 +21,7 @@
     * @param upDown     镜头上移下移 0:停止 1:上移 2:下移
     * @param moveSpeed  镜头移动速度
     */
    public boolean ptzdirectCmd(Device device,String channelId,int leftRight, int upDown);
    boolean ptzdirectCmd(Device device,String channelId,int leftRight, int upDown);
    
    /**
     * 云台方向放控制
@@ -31,7 +32,7 @@
     * @param upDown     镜头上移下移 0:停止 1:上移 2:下移
     * @param moveSpeed  镜头移动速度
     */
    public boolean ptzdirectCmd(Device device,String channelId,int leftRight, int upDown, int moveSpeed);
    boolean ptzdirectCmd(Device device,String channelId,int leftRight, int upDown, int moveSpeed);
    
    /**
     * 云台缩放控制,使用配置文件中的默认镜头缩放速度
@@ -40,7 +41,7 @@
     * @param channelId  预览通道
     * @param inOut      镜头放大缩小 0:停止 1:缩小 2:放大
     */
    public boolean ptzZoomCmd(Device device,String channelId,int inOut);
    boolean ptzZoomCmd(Device device,String channelId,int inOut);
    
    /**
     * 云台缩放控制
@@ -50,7 +51,7 @@
     * @param inOut      镜头放大缩小 0:停止 1:缩小 2:放大
     * @param zoomSpeed  镜头缩放速度
     */
    public boolean ptzZoomCmd(Device device,String channelId,int inOut, int moveSpeed);
    boolean ptzZoomCmd(Device device,String channelId,int inOut, int moveSpeed);
    
    /**
     * 云台控制,支持方向与缩放控制
@@ -63,7 +64,7 @@
     * @param moveSpeed  镜头移动速度
     * @param zoomSpeed  镜头缩放速度
     */
    public boolean ptzCmd(Device device,String channelId,int leftRight, int upDown, int inOut, int moveSpeed, int zoomSpeed);
    boolean ptzCmd(Device device,String channelId,int leftRight, int upDown, int inOut, int moveSpeed, int zoomSpeed);
    
    /**
     * 前端控制,包括PTZ指令、FI指令、预置位指令、巡航指令、扫描指令和辅助开关指令
@@ -75,7 +76,7 @@
     * @param parameter2    数据2
     * @param combineCode2    组合码2
     */
    public boolean frontEndCmd(Device device, String channelId, int cmdCode, int parameter1, int parameter2, int combineCode2);
    boolean frontEndCmd(Device device, String channelId, int cmdCode, int parameter1, int parameter2, int combineCode2);
    
    /**
     * 请求预览视频流
@@ -83,7 +84,7 @@
     * @param device  视频设备
     * @param channelId  预览通道
     */
    public StreamInfo playStreamCmd(Device device, String channelId);
    void playStreamCmd(Device device, String channelId, ZLMHttpHookSubscribe.Event event);
    
    /**
     * 请求回放视频流
@@ -93,14 +94,14 @@
     * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss
     * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss
     */
    public StreamInfo playbackStreamCmd(Device device,String channelId, String startTime, String endTime);
    void playbackStreamCmd(Device device, String channelId, String startTime, String endTime, ZLMHttpHookSubscribe.Event event);
    
    /**
     * 视频流停止
     * 
     * @param ssrc  ssrc
     */
    public void streamByeCmd(String ssrc);
    void streamByeCmd(String ssrc);
    
    /**
     * 语音广播
@@ -108,7 +109,7 @@
     * @param device  视频设备
     * @param channelId  预览通道
     */
    public boolean audioBroadcastCmd(Device device,String channelId);
    boolean audioBroadcastCmd(Device device,String channelId);
    
    /**
     * 音视频录像控制
@@ -116,21 +117,21 @@
     * @param device  视频设备
     * @param channelId  预览通道
     */
    public boolean recordCmd(Device device,String channelId);
    boolean recordCmd(Device device,String channelId);
    
    /**
     * 报警布防/撤防命令
     * 
     * @param device  视频设备
     */
    public boolean guardCmd(Device device);
    boolean guardCmd(Device device);
    
    /**
     * 报警复位命令
     * 
     * @param device  视频设备
     */
    public boolean alarmCmd(Device device);
    boolean alarmCmd(Device device);
    
    /**
     * 强制关键帧命令,设备收到此命令应立刻发送一个IDR帧
@@ -138,21 +139,21 @@
     * @param device  视频设备
     * @param channelId  预览通道
     */
    public boolean iFameCmd(Device device,String channelId);
    boolean iFameCmd(Device device,String channelId);
    
    /**
     * 看守位控制命令
     * 
     * @param device  视频设备
     */
    public boolean homePositionCmd(Device device);
    boolean homePositionCmd(Device device);
    
    /**
     * 设备配置命令
     * 
     * @param device  视频设备
     */
    public boolean deviceConfigCmd(Device device);
    boolean deviceConfigCmd(Device device);
    
    
    /**
@@ -160,7 +161,7 @@
     * 
     * @param device 视频设备
     */
    public boolean deviceStatusQuery(Device device);
    boolean deviceStatusQuery(Device device);
    
    /**
     * 查询设备信息
@@ -168,14 +169,14 @@
     * @param device 视频设备
     * @return 
     */
    public boolean deviceInfoQuery(Device device);
    boolean deviceInfoQuery(Device device);
    
    /**
     * 查询目录列表
     * 
     * @param device 视频设备
     */
    public boolean catalogQuery(Device device);
    boolean catalogQuery(Device device);
    
    /**
     * 查询录像信息
@@ -184,35 +185,33 @@
     * @param startTime 开始时间,格式要求:yyyy-MM-dd HH:mm:ss
     * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss
     */
    public boolean recordInfoQuery(Device device, String channelId, String startTime, String endTime);
    boolean recordInfoQuery(Device device, String channelId, String startTime, String endTime);
    
    /**
     * 查询报警信息
     * 
     * @param device 视频设备
     */
    public boolean alarmInfoQuery(Device device);
    boolean alarmInfoQuery(Device device);
    
    /**
     * 查询设备配置
     * 
     * @param device 视频设备
     */
    public boolean configQuery(Device device);
    boolean configQuery(Device device);
    
    /**
     * 查询设备预置位置
     * 
     * @param device 视频设备
     */
    public boolean presetQuery(Device device);
    boolean presetQuery(Device device);
    
    /**
     * 查询移动设备位置数据
     * 
     * @param device 视频设备
     */
    public boolean mobilePostitionQuery(Device device);
    boolean mobilePostitionQuery(Device device);
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/cmd/impl/SIPCommander.java
@@ -19,6 +19,7 @@
import com.genersoft.iot.vmp.common.StreamInfo;
import com.genersoft.iot.vmp.conf.MediaServerConfig;
import com.genersoft.iot.vmp.gb28181.bean.DeviceChannel;
import com.genersoft.iot.vmp.media.zlm.ZLMHttpHookSubscribe;
import com.genersoft.iot.vmp.media.zlm.ZLMUtils;
import com.genersoft.iot.vmp.storager.IVideoManagerStorager;
import org.springframework.beans.factory.annotation.Autowired;
@@ -66,6 +67,9 @@
    @Value("${media.rtp.enable}")
    private boolean rtpEnable;
    @Autowired
    private ZLMHttpHookSubscribe subscribe;
@@ -264,12 +268,12 @@
    }
    /**
     * 请求预览视频流
     *
     *
     * @param device  视频设备
     * @param channelId  预览通道
     */
     */
    @Override
    public StreamInfo playStreamCmd(Device device, String channelId) {
    public void playStreamCmd(Device device, String channelId, ZLMHttpHookSubscribe.Event event) {
        try {
            String ssrc = streamSession.createPlaySsrc();
@@ -282,53 +286,63 @@
            }else {
                mediaPort = mediaInfo.getRtpProxyPort();
            }
            String streamId = String.format("%08x", Integer.parseInt(ssrc)).toUpperCase();
            // 添加订阅
            JSONObject subscribeKey = new JSONObject();
            subscribeKey.put("app", "rtp");
            subscribeKey.put("id", streamId);
            subscribe.addSubscribe(ZLMHttpHookSubscribe.HookType.on_publish, subscribeKey, event);
            //
            StringBuffer content = new StringBuffer(200);
            content.append("v=0\r\n");
            content.append("o="+channelId+" 0 0 IN IP4 "+mediaInfo.getWanIp()+"\r\n");
            content.append("s=Play\r\n");
            content.append("c=IN IP4 "+mediaInfo.getWanIp()+"\r\n");
            content.append("t=0 0\r\n");
            if("TCP-PASSIVE".equals(streamMode)) {
                content.append("m=video "+ mediaPort +" TCP/RTP/AVP 96 98 97\r\n");
            content.append("v=0\r\n");
            content.append("o="+channelId+" 0 0 IN IP4 "+mediaInfo.getWanIp()+"\r\n");
            content.append("s=Play\r\n");
            content.append("c=IN IP4 "+mediaInfo.getWanIp()+"\r\n");
            content.append("t=0 0\r\n");
            if("TCP-PASSIVE".equals(streamMode)) {
                content.append("m=video "+ mediaPort +" TCP/RTP/AVP 126 125 99 34 98 97 96\r\n");
            }else if ("TCP-ACTIVE".equals(streamMode)) {
                content.append("m=video "+ mediaPort +" TCP/RTP/AVP 96 98 97\r\n");
                content.append("m=video "+ mediaPort +" TCP/RTP/AVP 126 125 99 34 98 97 96\r\n");
            }else if("UDP".equals(streamMode)) {
                content.append("m=video "+ mediaPort +" RTP/AVP 96 98 97\r\n");
                content.append("m=video "+ mediaPort +" RTP/AVP 126 125 99 34 98 97 96\r\n");
            }
            content.append("a=recvonly\r\n");
            content.append("a=rtpmap:96 PS/90000\r\n");
            content.append("a=rtpmap:98 H264/90000\r\n");
            content.append("a=rtpmap:97 MPEG4/90000\r\n");
            if("TCP-PASSIVE".equals(streamMode)){ // tcp被动模式
                content.append("a=setup:passive\r\n");
            content.append("a=recvonly\r\n");
            content.append("a=fmtp:126 profile-level-id=42e01e\r\n");
            content.append("a=rtpmap:126 H264/90000\r\n");
            content.append("a=rtpmap:125 H264S/90000\r\n");
            content.append("a=fmtp:125 profile-level-id=42e01e\r\n");
            content.append("a=rtpmap:99 MP4V-ES/90000\r\n");
            content.append("a=fmtp:99 profile-level-id=3\r\n");
            content.append("a=rtpmap:98 H264/90000\r\n");
            content.append("a=rtpmap:97 MPEG4/90000\r\n");
            content.append("a=rtpmap:96 PS/90000\r\n");
            if("TCP-PASSIVE".equals(streamMode)){ // tcp被动模式
                content.append("a=setup:passive\r\n");
                content.append("a=connection:new\r\n");
            }else if ("TCP-ACTIVE".equals(streamMode)) { // tcp主动模式
            }else if ("TCP-ACTIVE".equals(streamMode)) { // tcp主动模式
                content.append("a=setup:active\r\n");
                content.append("a=connection:new\r\n");
            }
            content.append("y="+ssrc+"\r\n");//ssrc
            Request request = headerProvider.createInviteRequest(device, channelId, content.toString(), null, "live", null, ssrc);
            ClientTransaction transaction = transmitRequest(device, request);
            streamSession.put(ssrc, transaction);
            content.append("y="+ssrc+"\r\n");//ssrc
            Request request = headerProvider.createInviteRequest(device, channelId, content.toString(), null, "live", null, ssrc);
            ClientTransaction transaction = transmitRequest(device, request);
            streamSession.put(ssrc, transaction);
            DeviceChannel deviceChannel = storager.queryChannel(device.getDeviceId(), channelId);
            if (deviceChannel != null) {
                deviceChannel.setSsrc(ssrc);
                storager.updateChannel(device.getDeviceId(), deviceChannel);
            }
            StreamInfo streamInfo = new StreamInfo();
            streamInfo.setSsrc(ssrc);
            streamInfo.setCahnnelId(channelId);
            streamInfo.setDeviceID(device.getDeviceId());
            storager.startPlay(streamInfo);
            return streamInfo;
            // TODO 订阅SIP response,处理对方的错误返回
        } catch ( SipException | ParseException | InvalidArgumentException e) {
            e.printStackTrace();
            return null;
        }
        }
    }
    
    /**
@@ -340,10 +354,18 @@
     * @param endTime 结束时间,格式要求:yyyy-MM-dd HH:mm:ss
     */ 
    @Override
    public StreamInfo playbackStreamCmd(Device device, String channelId, String startTime, String endTime) {
    public void playbackStreamCmd(Device device, String channelId, String startTime, String endTime, ZLMHttpHookSubscribe.Event event) {
        try {
            MediaServerConfig mediaInfo = storager.getMediaInfo();
            String ssrc = streamSession.createPlayBackSsrc();
            String streamId = String.format("%08x", Integer.parseInt(ssrc)).toUpperCase();
            // 添加订阅
            JSONObject subscribeKey = new JSONObject();
            subscribeKey.put("app", "rtp");
            subscribeKey.put("id", streamId);
            subscribe.addSubscribe(ZLMHttpHookSubscribe.HookType.on_publish, subscribeKey, event);
            //
            StringBuffer content = new StringBuffer(200);
            content.append("v=0\r\n");
@@ -362,16 +384,22 @@
            }
            String streamMode = device.getStreamMode().toUpperCase();
            if("TCP-PASSIVE".equals(streamMode)) {
                content.append("m=video "+ mediaPort +" TCP/RTP/AVP 96 98 97\r\n");
                content.append("m=video "+ mediaPort +" TCP/RTP/AVP 126 125 99 34 98 97 96\r\n");
            }else if ("TCP-ACTIVE".equals(streamMode)) {
                content.append("m=video "+ mediaPort +" TCP/RTP/AVP 96 98 97\r\n");
                content.append("m=video "+ mediaPort +" TCP/RTP/AVP 126 125 99 34 98 97 96\r\n");
            }else if("UDP".equals(streamMode)) {
                content.append("m=video "+ mediaPort +" RTP/AVP 96 98 97\r\n");
                content.append("m=video "+ mediaPort +" RTP/AVP 126 125 99 34 98 97 96\r\n");
            }
            content.append("a=recvonly\r\n");
            content.append("a=rtpmap:96 PS/90000\r\n");
            content.append("a=rtpmap:98 H264/90000\r\n");
            content.append("a=rtpmap:97 MPEG4/90000\r\n");
            content.append("a=recvonly\r\n");
            content.append("a=fmtp:126 profile-level-id=42e01e\r\n");
            content.append("a=rtpmap:126 H264/90000\r\n");
            content.append("a=rtpmap:125 H264S/90000\r\n");
            content.append("a=fmtp:125 profile-level-id=42e01e\r\n");
            content.append("a=rtpmap:99 MP4V-ES/90000\r\n");
            content.append("a=fmtp:99 profile-level-id=3\r\n");
            content.append("a=rtpmap:98 H264/90000\r\n");
            content.append("a=rtpmap:97 MPEG4/90000\r\n");
            content.append("a=rtpmap:96 PS/90000\r\n");
            if("TCP-PASSIVE".equals(streamMode)){ // tcp被动模式
                content.append("a=setup:passive\r\n");
                content.append("a=connection:new\r\n");
@@ -386,16 +414,8 @@
            ClientTransaction transaction = transmitRequest(device, request);
            streamSession.put(ssrc, transaction);
            StreamInfo streamInfo = new StreamInfo();
            streamInfo.setSsrc(ssrc);
            streamInfo.setCahnnelId(channelId);
            streamInfo.setDeviceID(device.getDeviceId());
            boolean b = storager.startPlayback(streamInfo);
            return streamInfo;
        } catch ( SipException | ParseException | InvalidArgumentException e) {
            e.printStackTrace();
            return null;
        }
    }
    
@@ -433,6 +453,7 @@
                clientTransaction = udpSipProvider.getNewClientTransaction(byeRequest);
            }
            dialog.sendRequest(clientTransaction);
            streamSession.remove(ssrc);
        } catch (TransactionDoesNotExistException e) {
            e.printStackTrace();
        } catch (SipException e) {
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/request/impl/AckRequestProcessor.java
@@ -21,14 +21,12 @@
     * 处理  ACK请求
     * 
     * @param evt
     * @param layer
     * @param transaction
     * @param config
     */
     */
    @Override
    public void process(RequestEvent evt) {
        Request request = evt.getRequest();
        Dialog dialog = evt.getDialog();
        if (dialog == null) return;
        try {
            Request ackRequest = null;
            CSeq csReq = (CSeq) request.getHeader(CSeq.NAME);
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/request/impl/ByeRequestProcessor.java
@@ -1,8 +1,13 @@
package com.genersoft.iot.vmp.gb28181.transmit.request.impl;
import javax.sip.InvalidArgumentException;
import javax.sip.RequestEvent;
import javax.sip.SipException;
import javax.sip.message.Response;
import com.genersoft.iot.vmp.gb28181.transmit.request.SIPRequestAbstractProcessor;
import java.text.ParseException;
/**    
 * @Description: BYE请求处理器
@@ -11,18 +16,35 @@
 */
public class ByeRequestProcessor extends SIPRequestAbstractProcessor {
    /**
    /**
     * 处理BYE请求
     *
     * @param evt
     * @param layer
     * @param transaction
     * @param config
     */
     */
    @Override
    public void process(RequestEvent evt) {
        try {
            responseAck(evt);
        } catch (SipException e) {
            e.printStackTrace();
        } catch (InvalidArgumentException e) {
            e.printStackTrace();
        } catch (ParseException e) {
            e.printStackTrace();
        }
        // TODO 优先级99 Bye Request消息实现,此消息一般为级联消息,上级给下级发送视频停止指令
        
    }
    /***
     * 回复200 OK
     * @param evt
     * @throws SipException
     * @throws InvalidArgumentException
     * @throws ParseException
     */
    private void responseAck(RequestEvent evt) throws SipException, InvalidArgumentException, ParseException {
        Response response = getMessageFactory().createResponse(Response.OK, evt.getRequest());
        getServerTransaction(evt).sendResponse(response);
    }
}
src/main/java/com/genersoft/iot/vmp/gb28181/transmit/request/impl/MessageRequestProcessor.java
@@ -184,10 +184,11 @@
                    DeviceChannel deviceChannel = new DeviceChannel();
                    deviceChannel.setName(channelName);
                    deviceChannel.setChannelId(channelDeviceId);
                    if (status.equals("ON") || status.equals("On")) {
                    // ONLINE OFFLINE  HIKVISION DS-7716N-E4 NVR的兼容性处理
                    if (status.equals("ON") || status.equals("On") || status.equals("ONLINE")) {
                        deviceChannel.setStatus(1);
                    }
                    if (status.equals("OFF") || status.equals("Off")) {
                    if (status.equals("OFF") || status.equals("Off") || status.equals("OFFLINE")) {
                        deviceChannel.setStatus(0);
                    }
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java
@@ -49,6 +49,9 @@
    @Autowired
    private ZLMRESTfulUtils zlmresTfulUtils;
    @Autowired
    private ZLMHttpHookSubscribe subscribe;
    @Value("${media.ip}")
    private String mediaIp;
@@ -128,30 +131,38 @@
        }
        String app = json.getString("app");
        String streamId = json.getString("id");
        if ("rtp".equals(app)) {
            String ssrc = new DecimalFormat("0000000000").format(Integer.parseInt(streamId, 16));
            StreamInfo streamInfoForPlay = storager.queryPlayBySSRC(ssrc);
            if ("rtp".equals(app) && streamInfoForPlay != null ) {
                MediaServerConfig mediaInfo = storager.getMediaInfo();
                streamInfoForPlay.setFlv(String.format("http://%s:%s/rtp/%s.flv", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
                streamInfoForPlay.setWs_flv(String.format("ws://%s:%s/rtp/%s.flv", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
                streamInfoForPlay.setRtmp(String.format("rtmp://%s:%s/rtp/%s", mediaInfo.getWanIp(), mediaInfo.getRtmpPort(), streamId));
                streamInfoForPlay.setHls(String.format("http://%s:%s/rtp/%s/hls.m3u8", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
                streamInfoForPlay.setRtsp(String.format("rtsp://%s:%s/rtp/%s", mediaInfo.getWanIp(), mediaInfo.getRtspPort(), streamId));
                storager.startPlay(streamInfoForPlay);
            }
            StreamInfo streamInfoForPlayBack = storager.queryPlaybackBySSRC(ssrc);
            if ("rtp".equals(app) && streamInfoForPlayBack != null ) {
                MediaServerConfig mediaInfo = storager.getMediaInfo();
                streamInfoForPlayBack.setFlv(String.format("http://%s:%s/rtp/%s.flv", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
                streamInfoForPlayBack.setWs_flv(String.format("ws://%s:%s/rtp/%s.flv", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
                streamInfoForPlayBack.setRtmp(String.format("rtmp://%s:%s/rtp/%s", mediaInfo.getWanIp(), mediaInfo.getRtmpPort(), streamId));
                streamInfoForPlayBack.setHls(String.format("http://%s:%s/rtp/%s/hls.m3u8", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
                streamInfoForPlayBack.setRtsp(String.format("rtsp://%s:%s/rtp/%s", mediaInfo.getWanIp(), mediaInfo.getRtspPort(), streamId));
                storager.startPlayback(streamInfoForPlayBack);
            }
        }
        ZLMHttpHookSubscribe.Event subscribe = this.subscribe.getSubscribe(ZLMHttpHookSubscribe.HookType.on_publish, json);
        if (subscribe != null) subscribe.response(json);
//        if ("rtp".equals(app)) {
//            String ssrc = new DecimalFormat("0000000000").format(Integer.parseInt(streamId, 16));
//            StreamInfo streamInfoForPlay = storager.queryPlayBySSRC(ssrc);
//            if ("rtp".equals(app) && streamInfoForPlay != null ) {
//                MediaServerConfig mediaInfo = storager.getMediaInfo();
//                streamInfoForPlay.setFlv(String.format("http://%s:%s/rtp/%s.flv", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
//                streamInfoForPlay.setWs_flv(String.format("ws://%s:%s/rtp/%s.flv", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
//                streamInfoForPlay.setFmp4(String.format("http://%s:%s/rtp/%s.live.mp4", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
//                streamInfoForPlay.setWs_fmp4(String.format("ws://%s:%s/rtp/%s.live.mp4", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
//                streamInfoForPlay.setRtmp(String.format("rtmp://%s:%s/rtp/%s", mediaInfo.getWanIp(), mediaInfo.getRtmpPort(), streamId));
//                streamInfoForPlay.setHls(String.format("http://%s:%s/rtp/%s/hls.m3u8", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
//                streamInfoForPlay.setRtsp(String.format("rtsp://%s:%s/rtp/%s", mediaInfo.getWanIp(), mediaInfo.getRtspPort(), streamId));
//                storager.startPlay(streamInfoForPlay);
//            }
//
//            StreamInfo streamInfoForPlayBack = storager.queryPlaybackBySSRC(ssrc);
//            if ("rtp".equals(app) && streamInfoForPlayBack != null ) {
//                MediaServerConfig mediaInfo = storager.getMediaInfo();
//                streamInfoForPlayBack.setFlv(String.format("http://%s:%s/rtp/%s.flv", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
//                streamInfoForPlayBack.setWs_flv(String.format("ws://%s:%s/rtp/%s.flv", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
//                streamInfoForPlayBack.setFmp4(String.format("http://%s:%s/rtp/%s.live.mp4", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
//                streamInfoForPlayBack.setWs_fmp4(String.format("ws://%s:%s/rtp/%s.live.mp4", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
//                streamInfoForPlayBack.setRtmp(String.format("rtmp://%s:%s/rtp/%s", mediaInfo.getWanIp(), mediaInfo.getRtmpPort(), streamId));
//                streamInfoForPlayBack.setHls(String.format("http://%s:%s/rtp/%s/hls.m3u8", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
//                streamInfoForPlayBack.setRtsp(String.format("rtsp://%s:%s/rtp/%s", mediaInfo.getWanIp(), mediaInfo.getRtspPort(), streamId));
//                storager.startPlayback(streamInfoForPlayBack);
//            }
//        }
        // TODO Auto-generated method stub
        
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookSubscribe.java
New file
@@ -0,0 +1,90 @@
package com.genersoft.iot.vmp.media.zlm;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.genersoft.iot.vmp.common.StreamInfo;
import com.genersoft.iot.vmp.conf.MediaServerConfig;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
import com.genersoft.iot.vmp.storager.IVideoManagerStorager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Component;
import org.springframework.util.ConcurrentReferenceHashMap;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.math.BigInteger;
import java.text.DecimalFormat;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
 * @Description:针对 ZLMediaServer的hook事件订阅
 * @author: pan
 * @date:   2020年12月2日 21:17:32
 */
@Component
public class ZLMHttpHookSubscribe {
    private final static Logger logger = LoggerFactory.getLogger(ZLMHttpHookSubscribe.class);
    public enum HookType{
        on_flow_report,
        on_http_access,
        on_play,
        on_publish,
        on_record_mp4,
        on_rtsp_auth,
        on_rtsp_realm,
        on_shell_login,
        on_stream_changed,
        on_stream_none_reader,
        on_stream_not_found,
        on_server_started
    }
    public interface Event{
        void response(JSONObject response);
    }
    private Map<HookType, Map<JSONObject, ZLMHttpHookSubscribe.Event>> allSubscribes = new ConcurrentHashMap<>();
    public void addSubscribe(HookType type, JSONObject hookResponse, ZLMHttpHookSubscribe.Event event) {
        Map<JSONObject, Event> eventMap = allSubscribes.get(type);
        if (eventMap == null) {
            eventMap = new HashMap<JSONObject, Event>();
            allSubscribes.put(type,eventMap);
        }
        eventMap.put(hookResponse, event);
    }
    public ZLMHttpHookSubscribe.Event getSubscribe(HookType type, JSONObject hookResponse) {
        ZLMHttpHookSubscribe.Event event= null;
        Map<JSONObject, Event> eventMap = allSubscribes.get(type);
        if (eventMap == null) {
            return null;
        }
        for (JSONObject key : eventMap.keySet()) {
            Boolean result = null;
            for (String s : key.keySet()) {
                String string = hookResponse.getString(s);
                String string1 = key.getString(s);
                if (result == null) {
                    result = key.getString(s).equals(hookResponse.getString(s));
                }else {
                    result = result && key.getString(s).equals(hookResponse.getString(s));
                }
            }
            if (result) {
                event = eventMap.get(key);
            }
        }
        return event;
    }
}
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRunner.java
@@ -36,6 +36,9 @@
    @Value("${media.wanIp}")
    private String mediaWanIp;
    @Value("${media.hookIp}")
    private String mediaHookIp;
    @Value("${media.port}")
    private int mediaPort;
@@ -51,6 +54,9 @@
    @Value("${server.port}")
    private String serverPort;
    @Value("${media.autoConfig}")
    private boolean autoConfig;
    @Autowired
    private ZLMRESTfulUtils zlmresTfulUtils;
@@ -61,8 +67,7 @@
        MediaServerConfig mediaServerConfig = getMediaServerConfig();
        if (mediaServerConfig != null) {
            logger.info("zlm接入成功...");
            logger.info("设置zlm...");
            saveZLMConfig();
            if (autoConfig) saveZLMConfig();
            mediaServerConfig = getMediaServerConfig();
            storager.updateMediaInfo(mediaServerConfig);
        }
@@ -91,12 +96,12 @@
    }
    private void saveZLMConfig() {
        String hookIP = sipIP;
        if (mediaIp.equals(sipIP)) {
            hookIP = "127.0.0.1";
        logger.info("设置zlm...");
        if (StringUtils.isEmpty(mediaHookIp)) {
            mediaHookIp = sipIP;
        }
        String hookPrex = String.format("http://%s:%s/index/hook", hookIP, serverPort);
        String hookPrex = String.format("http://%s:%s/index/hook", mediaHookIp, serverPort);
        Map<String, Object> param = new HashMap<>();
        param.put("api.secret",mediaSecret); // -profile:v Baseline
        param.put("ffmpeg.cmd","%s -fflags nobuffer -rtsp_transport tcp -i %s -c:a aac -strict -2 -ar 44100 -ab 48k -c:v libx264  -f flv %s");
src/main/java/com/genersoft/iot/vmp/storager/redis/VideoManagerRedisStoragerImpl.java
@@ -555,6 +555,10 @@
        List<Object> playLeys = redis.scan(String.format("%S_*_%s_%s", VideoManagerConstants.PLAY_BLACK_PREFIX,
                deviceId,
                code));
        if (playLeys == null || playLeys.size() == 0) {
            playLeys = redis.scan(String.format("%S_*_*_%s", VideoManagerConstants.PLAY_BLACK_PREFIX,
                deviceId));
        }
        if (playLeys == null || playLeys.size() == 0) return null;
        return (StreamInfo)redis.get(playLeys.get(0).toString());
    }
src/main/java/com/genersoft/iot/vmp/vmanager/play/PlayController.java
@@ -4,7 +4,10 @@
import com.alibaba.fastjson.JSONArray;
import com.genersoft.iot.vmp.common.StreamInfo;
import com.genersoft.iot.vmp.conf.MediaServerConfig;
import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils;
import com.genersoft.iot.vmp.vmanager.service.IPlayService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -22,6 +25,10 @@
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
import com.genersoft.iot.vmp.storager.IVideoManagerStorager;
import org.springframework.web.context.request.async.DeferredResult;
import java.text.DecimalFormat;
import java.util.UUID;
@CrossOrigin
@RestController
@@ -39,94 +46,57 @@
    @Autowired
    private ZLMRESTfulUtils zlmresTfulUtils;
    @Value("${media.closeWaitRTPInfo}")
    private boolean closeWaitRTPInfo;
    @Autowired
    private DeferredResultHolder resultHolder;
    @Autowired
    private IPlayService playService;
    @GetMapping("/play/{deviceId}/{channelId}")
    public ResponseEntity<String> play(@PathVariable String deviceId, @PathVariable String channelId,
    Integer getEncoding) {
    public DeferredResult<ResponseEntity<String>> play(@PathVariable String deviceId,
                                                       @PathVariable String channelId) {
        if (getEncoding == null) getEncoding = 0;
        getEncoding = closeWaitRTPInfo ?  0 : getEncoding;
        Device device = storager.queryVideoDevice(deviceId);
        StreamInfo streamInfo = storager.queryPlayByDevice(deviceId, channelId);
        UUID uuid = UUID.randomUUID();
        DeferredResult<ResponseEntity<String>> result = new DeferredResult<ResponseEntity<String>>();
        // 超时处理
        result.onTimeout(()->{
            logger.warn(String.format("设备点播超时,deviceId:%s ,channelId:%s", deviceId, channelId));
            RequestMessage msg = new RequestMessage();
            msg.setId(DeferredResultHolder.CALLBACK_CMD_PlAY + uuid);
            msg.setData("Timeout");
            resultHolder.invokeResult(msg);
        });
        // 录像查询以channelId作为deviceId查询
        resultHolder.put(DeferredResultHolder.CALLBACK_CMD_PlAY + uuid, result);
        if (streamInfo == null) {
            streamInfo = cmder.playStreamCmd(device, channelId);
            // 发送点播消息
            cmder.playStreamCmd(device, channelId, (JSONObject response) -> {
                logger.info("收到订阅消息: " + response.toJSONString());
                playService.onPublishHandlerForPlay(response, deviceId, channelId, uuid.toString());
            });
        } else {
            String streamId = String.format("%08x", Integer.parseInt(streamInfo.getSsrc())).toUpperCase();
            JSONObject rtpInfo = zlmresTfulUtils.getRtpInfo(streamId);
            if (rtpInfo.getBoolean("exist")) {
                return new ResponseEntity<String>(JSON.toJSONString(streamInfo), HttpStatus.OK);
                RequestMessage msg = new RequestMessage();
                msg.setId(DeferredResultHolder.CALLBACK_CMD_PlAY + uuid);
                msg.setData(JSON.toJSONString(streamInfo));
                resultHolder.invokeResult(msg);
            } else {
                storager.stopPlay(streamInfo);
                streamInfo = cmder.playStreamCmd(device, channelId);
                // TODO playStreamCmd 超时处理
                cmder.playStreamCmd(device, channelId, (JSONObject response) -> {
                    logger.info("收到订阅消息: " + response.toJSONString());
                    playService.onPublishHandlerForPlay(response, deviceId, channelId, uuid.toString());
                });
            }
        }
        String streamId = String.format("%08x", Integer.parseInt(streamInfo.getSsrc())).toUpperCase();
        // 等待推流, TODO 默认超时30s
        boolean lockFlag = true;
        boolean rtpPushed = false;
        long startTime = System.currentTimeMillis();
        JSONObject rtpInfo = null;
        if (getEncoding == 1) {
            while (lockFlag) {
                try {
                    if (System.currentTimeMillis() - startTime > 60 * 1000) {
                        storager.stopPlay(streamInfo);
                        logger.info("播放等待超时");
                        return new ResponseEntity<String>("timeout", HttpStatus.OK);
                    } else {
                        streamInfo = storager.queryPlayByDevice(deviceId, channelId);
                        if (!rtpPushed) {
                            logger.info("查询RTP推流信息...");
                            rtpInfo = zlmresTfulUtils.getRtpInfo(streamId);
                        }
                        if (rtpInfo != null && rtpInfo.getBoolean("exist") && streamInfo != null
                                && streamInfo.getFlv() != null) {
                            logger.info("查询流编码信息:" + streamInfo.getFlv());
                            rtpPushed = true;
                            Thread.sleep(2000);
                            JSONObject mediaInfo = zlmresTfulUtils.getMediaInfo("rtp", "rtmp", streamId);
                            if (mediaInfo.getInteger("code") == 0 && mediaInfo.getBoolean("online")) {
                                lockFlag = false;
                                logger.info("流编码信息已获取");
                                JSONArray tracks = mediaInfo.getJSONArray("tracks");
                                streamInfo.setTracks(tracks);
                                storager.startPlay(streamInfo);
                            } else {
                                logger.info("流编码信息未获取,2秒后重试...");
                            }
                        } else {
                            Thread.sleep(2000);
                            continue;
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        } else {
            String flv = storager.getMediaInfo().getWanIp() + ":" + storager.getMediaInfo().getHttpPort() + "/rtp/"
                    + streamId + ".flv";
            streamInfo.setFlv("http://" + flv);
            streamInfo.setWs_flv("ws://" + flv);
            storager.startPlay(streamInfo);
        }
        if (logger.isDebugEnabled()) {
            logger.debug(String.format("设备预览 API调用,deviceId:%s ,channelId:%s", deviceId, channelId));
            logger.debug("设备预览 API调用,ssrc:" + streamInfo.getSsrc() + ",ZLMedia streamId:"
                    + Integer.toHexString(Integer.parseInt(streamInfo.getSsrc())));
        }
        if (streamInfo != null) {
            return new ResponseEntity<String>(JSON.toJSONString(streamInfo), HttpStatus.OK);
        } else {
            logger.warn("设备预览API调用失败!");
            return new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
        return result;
    }
    @PostMapping("/play/{ssrc}/stop")
@@ -172,17 +142,28 @@
            MediaServerConfig mediaInfo = storager.getMediaInfo();
            String dstUrl = String.format("rtmp://%s:%s/convert/%s", "127.0.0.1", mediaInfo.getRtmpPort(),
                    streamId );
            JSONObject jsonObject = zlmresTfulUtils.addFFmpegSource(streamInfo.getRtsp(), dstUrl, "1000000");
            String srcUrl = String.format("rtsp://%s:%s/rtp/%s", "127.0.0.1", mediaInfo.getRtspPort(), streamId);
            JSONObject jsonObject = zlmresTfulUtils.addFFmpegSource(srcUrl, dstUrl, "1000000");
            System.out.println(jsonObject);
            JSONObject result = new JSONObject();
            if (jsonObject != null && jsonObject.getInteger("code") == 0) {
                   result.put("code", 0);
                JSONObject data = jsonObject.getJSONObject("data");
                if (data != null) {
                   result.put("key", data.getString("key"));
                    result.put("rtmp", dstUrl);
                    result.put("flv", String.format("http://%s:%s/convert/%s.flv", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
                    result.put("ws_flv", String.format("ws://%s:%s/convert/%s.flv", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
                       result.put("key", data.getString("key"));
                    StreamInfo streamInfoResult = new StreamInfo();
                    streamInfoResult.setRtmp(dstUrl);
                    streamInfoResult.setRtsp(String.format("rtsp://%s:%s/convert/%s", mediaInfo.getWanIp(), mediaInfo.getRtspPort(), streamId));
                    streamInfoResult.setStreamId(streamId);
                    streamInfoResult.setFlv(String.format("http://%s:%s/convert/%s.flv", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
                    streamInfoResult.setWs_flv(String.format("ws://%s:%s/convert/%s.flv", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
                    streamInfoResult.setHls(String.format("http://%s:%s/convert/%s/hls.m3u8", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
                    streamInfoResult.setWs_hls(String.format("ws://%s:%s/convert/%s/hls.m3u8", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
                    streamInfoResult.setFmp4(String.format("http://%s:%s/convert/%s.live.mp4", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
                    streamInfoResult.setWs_fmp4(String.format("ws://%s:%s/convert/%s.live.mp4", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
                    streamInfoResult.setTs(String.format("http://%s:%s/convert/%s.live.ts", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
                    streamInfoResult.setWs_ts(String.format("ws://%s:%s/convert/%s.live.ts", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
                    result.put("data", streamInfoResult);
                }
            }else {
                result.put("code", 1);
src/main/java/com/genersoft/iot/vmp/vmanager/playback/PlaybackController.java
@@ -3,7 +3,10 @@
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONArray;
import com.genersoft.iot.vmp.common.StreamInfo;
import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils;
import com.genersoft.iot.vmp.vmanager.service.IPlayService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
@@ -22,6 +25,9 @@
import com.genersoft.iot.vmp.gb28181.bean.Device;
import com.genersoft.iot.vmp.gb28181.transmit.cmd.impl.SIPCommander;
import com.genersoft.iot.vmp.storager.IVideoManagerStorager;
import org.springframework.web.context.request.async.DeferredResult;
import java.util.UUID;
@CrossOrigin
@RestController
@@ -39,105 +45,42 @@
    @Autowired
    private ZLMRESTfulUtils zlmresTfulUtils;
    @Value("${media.closeWaitRTPInfo}")
    private boolean closeWaitRTPInfo;
    @Autowired
    private IPlayService playService;
    @Autowired
    private DeferredResultHolder resultHolder;
    @GetMapping("/playback/{deviceId}/{channelId}")
    public ResponseEntity<String> play(@PathVariable String deviceId, @PathVariable String channelId, String startTime,
            String endTime) {
    public DeferredResult<ResponseEntity<String>> play(@PathVariable String deviceId, @PathVariable String channelId, String startTime,
                                                       String endTime) {
        if (logger.isDebugEnabled()) {
            logger.debug(String.format("设备回放 API调用,deviceId:%s ,channelId:%s", deviceId, channelId));
        }
        if (StringUtils.isEmpty(deviceId) || StringUtils.isEmpty(channelId)) {
            String log = String.format("设备回放 API调用失败,deviceId:%s ,channelId:%s", deviceId, channelId);
            logger.warn(log);
            return new ResponseEntity<String>(log, HttpStatus.BAD_REQUEST);
        }
        UUID uuid = UUID.randomUUID();
        DeferredResult<ResponseEntity<String>> result = new DeferredResult<ResponseEntity<String>>();
        // 超时处理
        result.onTimeout(()->{
            logger.warn(String.format("设备回放超时,deviceId:%s ,channelId:%s", deviceId, channelId));
            RequestMessage msg = new RequestMessage();
            msg.setId(DeferredResultHolder.CALLBACK_CMD_PlAY + uuid);
            msg.setData("Timeout");
            resultHolder.invokeResult(msg);
        });
        Device device = storager.queryVideoDevice(deviceId);
        StreamInfo streamInfo = storager.queryPlaybackByDevice(deviceId, channelId);
        if (streamInfo != null) {
            // 停止之前的回放
            cmder.streamByeCmd(streamInfo.getSsrc());
        }
        resultHolder.put(DeferredResultHolder.CALLBACK_CMD_PlAY + uuid, result);
        cmder.playbackStreamCmd(device, channelId, startTime, endTime, (JSONObject response) -> {
            logger.info("收到订阅消息: " + response.toJSONString());
            playService.onPublishHandlerForPlayBack(response, deviceId, channelId, uuid.toString());
        });
        // }else {
        // String streamId = String.format("%08x",
        // Integer.parseInt(streamInfo.getSsrc())).toUpperCase();
        // JSONObject rtpInfo = zlmresTfulUtils.getRtpInfo(streamId);
        // if (rtpInfo.getBoolean("exist")) {
        // return new
        // ResponseEntity<String>(JSON.toJSONString(streamInfo),HttpStatus.OK);
        // }else {
        // storager.stopPlayback(streamInfo);
        // streamInfo = cmder.playbackStreamCmd(device, channelId, startTime, endTime);
        // }
        // }
        streamInfo = cmder.playbackStreamCmd(device, channelId, startTime, endTime);
        String streamId = String.format("%08x", Integer.parseInt(streamInfo.getSsrc())).toUpperCase();
        if (logger.isDebugEnabled()) {
            logger.debug("设备回放 API调用,ssrc:" + streamInfo.getSsrc() + ",ZLMedia streamId:" + streamId);
        }
        // 等待推流, TODO 默认超时15s
        boolean lockFlag = true;
        boolean rtpPushed = false;
        long lockStartTime = System.currentTimeMillis();
        JSONObject rtpInfo = null;
        if (closeWaitRTPInfo) {
            String flv = storager.getMediaInfo().getWanIp() + ":" + storager.getMediaInfo().getHttpPort() + "/rtp/"
                    + streamId + ".flv";
            streamInfo.setFlv("http://" + flv);
            streamInfo.setWs_flv("ws://" + flv);
            storager.startPlayback(streamInfo);
        } else {
            while (lockFlag) {
                try {
                    if (System.currentTimeMillis() - lockStartTime > 75 * 1000) {
                        storager.stopPlayback(streamInfo);
                        logger.info("播放等待超时");
                        return new ResponseEntity<String>("timeout", HttpStatus.OK);
                    } else {
                        streamInfo = storager.queryPlaybackByDevice(deviceId, channelId);
                        if (!rtpPushed) {
                            logger.info("查询RTP推流信息...");
                            rtpInfo = zlmresTfulUtils.getRtpInfo(streamId);
                        }
                        if (rtpInfo != null && rtpInfo.getBoolean("exist") && streamInfo != null
                                && streamInfo.getFlv() != null) {
                            logger.info("查询流编码信息:" + streamInfo.getFlv());
                            rtpPushed = true;
                            Thread.sleep(2000);
                            JSONObject mediaInfo = zlmresTfulUtils.getMediaInfo("rtp", "rtmp", streamId);
                            if (mediaInfo.getInteger("code") == 0 && mediaInfo.getBoolean("online")) {
                                lockFlag = false;
                                logger.info("流编码信息已获取");
                                JSONArray tracks = mediaInfo.getJSONArray("tracks");
                                streamInfo.setTracks(tracks);
                                storager.startPlayback(streamInfo);
                            } else {
                                logger.info("流编码信息未获取,2秒后重试...");
                            }
                        } else {
                            Thread.sleep(2000);
                            continue;
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
        if (streamInfo != null) {
            return new ResponseEntity<String>(JSON.toJSONString(streamInfo), HttpStatus.OK);
        } else {
            logger.warn("设备回放API调用失败!");
            return new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR);
        }
        return result;
    }
    @RequestMapping("/playback/{ssrc}/stop")
src/main/java/com/genersoft/iot/vmp/vmanager/service/IPlayService.java
New file
@@ -0,0 +1,13 @@
package com.genersoft.iot.vmp.vmanager.service;
import com.alibaba.fastjson.JSONObject;
import com.genersoft.iot.vmp.common.StreamInfo;
/**
 * 点播处理
 */
public interface IPlayService {
    void onPublishHandlerForPlayBack(JSONObject resonse, String deviceId, String channelId, String uuid);
    void onPublishHandlerForPlay(JSONObject resonse, String deviceId, String channelId, String uuid);
}
src/main/java/com/genersoft/iot/vmp/vmanager/service/impl/PlayServiceImpl.java
New file
@@ -0,0 +1,90 @@
package com.genersoft.iot.vmp.vmanager.service.impl;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.genersoft.iot.vmp.common.StreamInfo;
import com.genersoft.iot.vmp.conf.MediaServerConfig;
import com.genersoft.iot.vmp.gb28181.transmit.callback.DeferredResultHolder;
import com.genersoft.iot.vmp.gb28181.transmit.callback.RequestMessage;
import com.genersoft.iot.vmp.storager.IVideoManagerStorager;
import com.genersoft.iot.vmp.vmanager.play.PlayController;
import com.genersoft.iot.vmp.vmanager.service.IPlayService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.text.DecimalFormat;
@Service
public class PlayServiceImpl implements IPlayService {
    private final static Logger logger = LoggerFactory.getLogger(PlayServiceImpl.class);
    @Autowired
    private IVideoManagerStorager storager;
    @Autowired
    private DeferredResultHolder resultHolder;
    @Override
    public void onPublishHandlerForPlay(JSONObject resonse, String deviceId, String channelId, String uuid) {
        RequestMessage msg = new RequestMessage();
        msg.setId(DeferredResultHolder.CALLBACK_CMD_PlAY + uuid);
        StreamInfo streamInfo = onPublishHandler(resonse, deviceId, channelId, uuid);
        if (streamInfo != null) {
            storager.startPlay(streamInfo);
            msg.setData(JSON.toJSONString(streamInfo));
            resultHolder.invokeResult(msg);
        } else {
            logger.warn("设备预览API调用失败!");
            msg.setData("设备预览API调用失败!");
            resultHolder.invokeResult(msg);
        }
    }
    @Override
    public void onPublishHandlerForPlayBack(JSONObject resonse, String deviceId, String channelId, String uuid) {
        RequestMessage msg = new RequestMessage();
        msg.setId(DeferredResultHolder.CALLBACK_CMD_PlAY + uuid);
        StreamInfo streamInfo = onPublishHandler(resonse, deviceId, channelId, uuid);
        if (streamInfo != null) {
            storager.startPlayback(streamInfo);
            msg.setData(JSON.toJSONString(streamInfo));
            resultHolder.invokeResult(msg);
        } else {
            logger.warn("设备预览API调用失败!");
            msg.setData("设备预览API调用失败!");
            resultHolder.invokeResult(msg);
        }
    }
    public StreamInfo onPublishHandler(JSONObject resonse, String deviceId, String channelId, String uuid) {
        String streamId = resonse.getString("id");
        String ssrc = new DecimalFormat("0000000000").format(Integer.parseInt(streamId, 16));
        StreamInfo streamInfo = new StreamInfo();
        streamInfo.setSsrc(ssrc);
        streamInfo.setStreamId(streamId);
        streamInfo.setDeviceID(deviceId);
        streamInfo.setCahnnelId(channelId);
        MediaServerConfig mediaServerConfig = storager.getMediaInfo();
        streamInfo.setFlv(String.format("http://%s:%s/rtp/%s.flv", mediaServerConfig.getWanIp(), mediaServerConfig.getHttpPort(), streamId));
        streamInfo.setWs_flv(String.format("ws://%s:%s/rtp/%s.flv", mediaServerConfig.getWanIp(), mediaServerConfig.getHttpPort(), streamId));
        streamInfo.setFmp4(String.format("http://%s:%s/rtp/%s.live.mp4", mediaServerConfig.getWanIp(), mediaServerConfig.getHttpPort(), streamId));
        streamInfo.setWs_fmp4(String.format("ws://%s:%s/rtp/%s.live.mp4", mediaServerConfig.getWanIp(), mediaServerConfig.getHttpPort(), streamId));
        streamInfo.setHls(String.format("http://%s:%s/rtp/%s/hls.m3u8", mediaServerConfig.getWanIp(), mediaServerConfig.getHttpPort(), streamId));
        streamInfo.setWs_hls(String.format("ws://%s:%s/rtp/%s/hls.m3u8", mediaServerConfig.getWanIp(), mediaServerConfig.getHttpPort(), streamId));
        streamInfo.setTs(String.format("http://%s:%s/rtp/%s.live.ts", mediaServerConfig.getWanIp(), mediaServerConfig.getHttpPort(), streamId));
        streamInfo.setWs_ts(String.format("ws://%s:%s/rtp/%s.live.ts", mediaServerConfig.getWanIp(), mediaServerConfig.getHttpPort(), streamId));
        streamInfo.setRtmp(String.format("rtmp://%s:%s/rtp/%s", mediaServerConfig.getWanIp(), mediaServerConfig.getRtmpPort(), streamId));
        streamInfo.setRtsp(String.format("rtsp://%s:%s/rtp/%s", mediaServerConfig.getWanIp(), mediaServerConfig.getRtspPort(), streamId));
        return streamInfo;
    }
}
src/main/java/com/genersoft/iot/vmp/web/ApiStreamController.java
@@ -34,8 +34,7 @@
    @Autowired
    private IVideoManagerStorager storager;
    @Value("${media.closeWaitRTPInfo}")
    private boolean closeWaitRTPInfo;
    private boolean closeWaitRTPInfo = false;
    @Autowired
@@ -94,7 +93,7 @@
        StreamInfo streamInfo = storager.queryPlayByDevice(device.getDeviceId(), code);
        if (streamInfo == null) {
            logger.debug("streamInfo 等于null, 重新点播");
            streamInfo = cmder.playStreamCmd(device, code);
//            streamInfo = cmder.playStreamCmd(device, code);
        }else {
            logger.debug("streamInfo 不等于null, 向流媒体查询是否正在推流");
            String streamId = String.format("%08x", Integer.parseInt(streamInfo.getSsrc())).toUpperCase();
@@ -136,7 +135,7 @@
            } else {
                logger.debug("向流媒体查询没有推流, 重新点播");
                storager.stopPlay(streamInfo);
                streamInfo = cmder.playStreamCmd(device, code);
//                streamInfo = cmder.playStreamCmd(device, code);
            }
        }
src/main/resources/application-dev.yml
New file
@@ -0,0 +1,94 @@
spring:
    # [不需要改]
    application:
        name: iot-vmp-vmanager
        # [不需要改] 影子数据存储方式,支持redis、jdbc,暂不支持mysql,
        database: redis
        # [不需要改] 通信方式,支持kafka、http
        communicate: http
    # REDIS数据库配置
    redis:
        # [必须修改] Redis服务器IP, REDIS安装在本机的,使用127.0.0.1
        host: 127.0.0.1
        # [必须修改] 端口号
        port: 6379
        # [可选] 数据库 DB
        database: 6
        # [可选] 访问密码,若你的redis服务器没有设置密码,就不需要用密码去连接
        password:
        # [可选] 超时时间
        timeout: 10000
    # [不可用] jdbc数据库配置, 暂不支持
    datasource:
        name: eiot
        url: jdbc:mysql://127.0.0.1:3306/eiot?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true
        username:
        password:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.jdbc.Driver
# [可选] WVP监听的HTTP端口, 网页和接口调用都是这个端口
server:
    port: 18080
# 作为28181服务器的配置
sip:
    # [必须修改] 本机的IP, 必须是网卡上的IP
    ip: 192.168.0.100
    # [可选] 28181服务监听的端口
    port: 5060
    # 根据国标6.1.2中规定,domain宜采用ID统一编码的前十位编码。国标附录D中定义前8位为中心编码(由省级、市级、区级、基层编号组成,参照GB/T 2260-2007)
    # 后两位为行业编码,定义参照附录D.3
    # 3701020049标识山东济南历下区 信息行业接入
    # [可选]
    domain: 4401020049
    # [可选]
    id: 44010200492000000001
    # [可选] 默认设备认证密码,后续扩展使用设备单独密码
    password: admin123
# 登陆的用户名密码
auth:
    # [可选] 用户名
    username: admin
    # [可选] 密码, 默认为admin
    password: 21232f297a57a5a743894a0e4a801fc3
#zlm服务器配置
media:
    # [必须修改] zlm服务器的内网IP
    ip: 192.168.0.100
    # [可选] zlm服务器的公网IP, 内网部署置空即可
    wanIp:
    # [可选] zlm服务器的hook所使用的IP, 默认使用sip.ip
    hookIp:
    # [必须修改] zlm服务器的http.port
    port: 80
    # [可选] 是否自动配置ZLM, 如果希望手动配置ZLM, 可以设为false, 不建议新接触的用户修改
    autoConfig: true
    # [可选] zlm服务器的hook.admin_params=secret
    secret: 035c73f7-bb6b-4889-a715-d9eb2d1925cc
    # [可选] zlm服务器的general.streamNoneReaderDelayMS
    streamNoneReaderDelayMS:  18000  # 无人观看多久自动关闭流
    # [可选] 关闭等待收到流编码信息后在返回,
    # 设为false可以获得更好的兼容性,保证返回后流就可以播放,
    # 设为true可以快速打开播放窗口,可以获得更好的体验
    closeWaitRTPInfo: false
    # 启用udp多端口模式
    rtp:
        # [可选] 是否启用udp多端口模式, 开启后会在udpPortRange范围内选择端口用于媒体流传输
        enable: true
        # [可选] 在此范围内选择端口用于媒体流传输, 不只是udp, 使用TCP被动传输模式时,也是从这个范围内选择端口
        udpPortRange: 30000,30500 # 端口范围
# [可选] 日志配置, 一般不需要改
logging:
    file:
        name: logs/wvp.log
        max-history: 30
        max-size: 10MB
        total-size-cap: 300MB
    level:
        com:
            genersoft:
                iot: debug
src/main/resources/application.yml
@@ -1,64 +1,3 @@
spring:
    application:
        name: iot-vmp-vmanager
        # 影子数据存储方式,支持redis、jdbc,暂不支持mysql
        database: redis
        # 通信方式,支持kafka、http
        communicate: http
    redis:
        # Redis服务器IP
        host: 192.168.1.141
        #端口号
        port: 6379
        database: 6
        #访问密码,若你的redis服务器没有设置密码,就不需要用密码去连接
        password: 4767cb971b40a1300fa09b7f87b09d1c
        #超时时间
        timeout: 10000
    datasource:
        name: eiot
        url: jdbc:mysql://127.0.0.1:3306/eiot?useUnicode=true&characterEncoding=UTF8&rewriteBatchedStatements=true
        username:
        password:
        type: com.alibaba.druid.pool.DruidDataSource
        driver-class-name: com.mysql.jdbc.Driver
server:
    port: 18080
sip:
    ip: 192.168.1.20
    port: 5060
    # 根据国标6.1.2中规定,domain宜采用ID统一编码的前十位编码。国标附录D中定义前8位为中心编码(由省级、市级、区级、基层编号组成,参照GB/T 2260-2007)
    # 后两位为行业编码,定义参照附录D.3
    # 3701020049标识山东济南历下区 信息行业接入
    domain: 3402000000
    id: 34020000002000000001
    # 默认设备认证密码,后续扩展使用设备单独密码
    password: 12345678
auth: #32位小写md5加密(默认密码为admin)
    username: admin
    password: 21232f297a57a5a743894a0e4a801fc3
media: #zlm服务器的ip与http端口, 重点: 这是http端口
    ip: 127.0.0.1
    wanIp: 192.168.1.20
    port: 6080
    secret: 035c73f7-bb6b-4889-a715-d9eb2d1925cc
    streamNoneReaderDelayMS:  600000  # 无人观看多久自动关闭流
    # 关闭等待收到流编码信息后在返回,
    # 设为false可以获得更好的兼容性,保证返回后流就可以播放,
    # 设为true可以快速打开播放窗口,可以获得更好的体验
    closeWaitRTPInfo: true
    rtp: # 启用udp多端口模式
        enable: true
        udpPortRange: 30000,30500 # 端口范围
logging:
    file:
        name: logs/wvp.log
        max-history: 30
        max-size: 10MB
        total-size-cap: 300MB
    level:
        com:
            genersoft:
                iot: debug
  profiles:
    active: dev
web_src/build/webpack.dev.conf.js
@@ -64,9 +64,8 @@
        to: config.dev.assetsSubDirectory,
        ignore: ['.*']
      },
      { from: 'node_modules/@liveqing/liveplayer/dist/component/crossdomain.xml'},
      { from: 'node_modules/@liveqing/liveplayer/dist/component/liveplayer.swf'},
      { from: 'node_modules/@liveqing/liveplayer/dist/component/liveplayer-lib.min.js', to: 'js/'}
      { from: 'node_modules/@easydarwin/easywasmplayer/libDecoder.wasm'},
      { from: 'node_modules/@easydarwin/easywasmplayer/EasyWasmPlayer.js', to: 'js/'}
    ])
  ]
})
web_src/build/webpack.prod.conf.js
@@ -115,9 +115,8 @@
        to: config.build.assetsSubDirectory,
        ignore: ['.*']
      },
      { from: 'node_modules/@liveqing/liveplayer/dist/component/crossdomain.xml'},
      { from: 'node_modules/@liveqing/liveplayer/dist/component/liveplayer.swf'},
      { from: 'node_modules/@liveqing/liveplayer/dist/component/liveplayer-lib.min.js', to: 'js/'}
      { from: 'node_modules/@easydarwin/easywasmplayer/libDecoder.wasm'},
      { from: 'node_modules/@easydarwin/easywasmplayer/EasyWasmPlayer.js', to: 'js/'}
    ])
  ]
})
web_src/index.html
@@ -6,7 +6,7 @@
    <title>国标28181</title>
  </head>
  <body>
    <script type="text/javascript" src="./js/liveplayer-lib.min.js"></script>
    <script type="text/javascript" src="./js/EasyWasmPlayer.js"></script>
    <div id="app"></div>
    <!-- built files will be auto injected -->
  </body>
web_src/package-lock.json
@@ -4,10 +4,10 @@
  "lockfileVersion": 1,
  "requires": true,
  "dependencies": {
    "@liveqing/liveplayer": {
      "version": "1.9.9",
      "resolved": "https://registry.npm.taobao.org/@liveqing/liveplayer/download/@liveqing/liveplayer-1.9.9.tgz",
      "integrity": "sha1-K7wiab+BiY5qe1/nTpKVyeGdIGo="
    "@easydarwin/easywasmplayer": {
      "version": "4.0.7",
      "resolved": "https://registry.npm.taobao.org/@easydarwin/easywasmplayer/download/@easydarwin/easywasmplayer-4.0.7.tgz",
      "integrity": "sha1-FNtIUXbdwIWdalvIMEaH0+zUGx4="
    },
    "@types/q": {
      "version": "1.5.4",
web_src/package.json
@@ -10,7 +10,7 @@
    "build": "node build/build.js"
  },
  "dependencies": {
    "@liveqing/liveplayer": "^1.9.6",
    "@easydarwin/easywasmplayer": "^4.0.7",
    "axios": "^0.19.2",
    "core-js": "^2.6.5",
    "echarts": "^4.7.0",
web_src/src/components/gb28181/devicePlayer.vue
@@ -1,9 +1,11 @@
<template>
<div id="devicePlayer" v-loading="isLoging">
    <el-dialog title="视频播放" top="0" :close-on-click-modal="false" :visible.sync="showVideoDialog" :destroy-on-close="true" @close="close()">
        <LivePlayer v-if="showVideoDialog" ref="videoPlayer" :videoUrl="videoUrl" :error="videoError" :message="videoError" :hasaudio="hasaudio" fluent autoplay live></LivePlayer>
        <!-- <LivePlayer v-if="showVideoDialog" ref="videoPlayer" :videoUrl="videoUrl" :error="videoError" :message="videoError" :hasaudio="hasaudio" fluent autoplay live></LivePlayer> -->
        <player ref="videoPlayer" :visible.sync="showVideoDialog" :videoUrl="videoUrl" :error="videoError" :message="videoError" :hasaudio="hasaudio" fluent autoplay live></player>
        <div id="shared" style="text-align: right; margin-top: 1rem;">
            <el-tabs v-model="tabActiveName">
            <el-tabs v-model="tabActiveName" @tab-click="tabHandleClick">
                <el-tab-pane label="实时视频" name="media">
                    <div style="margin-bottom: 0.5rem;">
                        <!--        <el-button type="primary" size="small" @click="playRecord(true, '')">播放</el-button>-->
@@ -97,6 +99,32 @@
                        </div>
                    </div>
                </el-tab-pane>
                <el-tab-pane label="编码信息" name="codec" v-loading="tracksLoading">
                    <p>
                        无法播放或者没有声音?&nbsp&nbsp&nbsp试一试
                        <el-button size="mini" type="primary" v-if="!coverPlaying" @click="coverPlay">转码播放</el-button>
                        <el-button size="mini" type="danger" v-if="coverPlaying" @click="convertStopClick">停止转码</el-button>
                    </p>
                    <div class="trank" >
                        <div v-for="(item, index) in tracks">
                            <span >流 {{index}}</span>
                            <div class="trankInfo" v-if="item.codec_type == 0">
                                <p>格式: {{item.codec_id_name}}</p>
                                <p>类型: 视频</p>
                                <p>分辨率: {{item.width}} x {{item.height}}</p>
                                <p>帧率: {{item.fps}}</p>
                            </div>
                            <div class="trankInfo" v-if="item.codec_type == 1">
                                <p>格式: {{item.codec_id_name}}</p>
                                <p>类型: 音频</p>
                                <p>采样位数: {{item.sample_bit}}</p>
                                <p>采样率: {{item.sample_rate}}</p>
                            </div>
                        </div>
                    </div>
                </el-tab-pane>
            </el-tabs>
        </div>
    </el-dialog>
@@ -104,12 +132,12 @@
</template>
<script>
import LivePlayer from '@liveqing/liveplayer'
import player from './player.vue'
export default {
    name: 'devicePlayer',
    props: {},
    components: {
        LivePlayer
        player,
    },
    computed: {
        getPlayerShared: function () {
@@ -131,6 +159,7 @@
            },
            showVideoDialog: false,
            ssrc: '',
            streamId: '',
            convertKey: '',
            deviceId: '',
            channelId: '',
@@ -148,20 +177,45 @@
            cruisingGroup: 0,
            scanSpeed: 100,
            scanGroup: 0,
            tracks: [],
            coverPlaying:false,
            tracksLoading: false
        };
    },
    methods: {
        tabHandleClick: function(tab, event) {
            console.log(tab)
            var that = this;
            that.tracks = [];
            that.tracksLoading = true;
            if (tab.name == "codec") {
                this.$axios({
                    method: 'get',
                    url: '/zlm/index/api/getMediaInfo?vhost=__defaultVhost__&schema=rtmp&app=rtp&stream='+ this.streamId
                }).then(function (res) {
                    that.tracksLoading = false;
                    if (res.data.code == 0 && res.data.online) {
                        that.tracks = res.data.tracks;
                    }else{
                        that.$message({
                            showClose: true,
                            message: '获取编码信息失败,',
                            type: 'warning'
                        });
                    }
                }).catch(function (e) {});
            }
        },
        openDialog: function (tab, deviceId, channelId, param) {
            this.tabActiveName = tab;
            this.channelId = channelId;
            this.deviceId = deviceId;
            this.ssrc = "";
            this.streamId = "";
            this.videoUrl = ""
            if (!!this.$refs.videoPlayer) {
                this.$refs.videoPlayer.pause();
            }
            switch (tab) {
                case "media":
                    this.play(param.streamInfo, param.hasAudio)
@@ -180,85 +234,56 @@
            console.log(val)
        },
        play: function (streamInfo, hasAudio) {
            this.hasaudio = hasAudio;
            var that = this;
            that.isLoging = false;
            if (!!streamInfo.tracks && streamInfo.tracks.length > 0 ) {
                for (let i = 0; i < streamInfo.tracks.length; i++) {
                  if (streamInfo.tracks[i].codec_type == 0 && streamInfo.tracks[i].codec_id_name != "CodecH264") { // 判断为H265视频
                    that.coverPlay(streamInfo, streamInfo.tracks[i].codec_id_name, ()=>{
                      that.close();
                      return;
                    })
                  }else if (streamInfo.tracks[i].codec_type == 1 && streamInfo.tracks[i].codec_id_name != "CodecAAC") {
                    that.coverPlay(streamInfo, streamInfo.tracks[i].codec_id_name, ()=>{
                      that.playFromStreamInfo(false. streamInfo)
                    })
                  }else if (streamInfo.tracks[i].codec_type == 1 && streamInfo.tracks[i].codec_id_name == "CodecAAC") {
                    that.playFromStreamInfo(true, streamInfo)
                  }else {
                    that.playFromStreamInfo(false, streamInfo)
                  }
                }
            }else {
              that.playFromStreamInfo(false, streamInfo)
            }
            this.isLoging = false;
            this.videoUrl = streamInfo.ws_flv;
            this.ssrc = streamInfo.ssrc;
            this.streamId = streamInfo.streamId;
            this.playFromStreamInfo(false, streamInfo)
        },
        coverPlay: function (streamInfo, codec_id_name, catchcallback) {
          var that = this;
          that.$confirm(codec_id_name + ' 编码格式不支持播放, 是否转码播放?', '提示', {
              confirmButtonText: '确定',
              cancelButtonText: '取消',
              type: 'warning'
            }).then(() => {
              that.isLoging = true;
              that.$axios({
        coverPlay: function () {
            var that = this;
            this.coverPlaying = true;
            this.$refs.videoPlayer.pause()
            that.$axios({
                method: 'post',
                url: '/api/play/' + streamInfo.ssrc + '/convert'
              }).then(function (res) {
                if (res.data.code == 0) {
                  streamInfo.ws_flv = res.data.ws_flv;
                  that.convertKey = res.data.key;
                  setTimeout(()=>{
                    that.isLoging = false;
                    that.playFromStreamInfo(false, streamInfo);
                  }, 2000)
                } else {
                  that.isLoging = false;
                  that.$message({
                    showClose: true,
                    message: '转码失败',
                    type: 'error'
                  });
                }
              }).catch(function (e) {
                that.$message({
                  showClose: true,
                  message: '播放错误',
                  type: 'error'
                url: '/api/play/' + that.ssrc + '/convert'
                }).then(function (res) {
                    if (res.data.code == 0) {
                        that.convertKey = res.data.key;
                        setTimeout(()=>{
                            that.isLoging = false;
                            that.playFromStreamInfo(false, res.data.data);
                        }, 2000)
                    } else {
                        that.isLoging = false;
                        that.coverPlaying = false;
                        that.$message({
                            showClose: true,
                            message: '转码失败',
                            type: 'error'
                        });
                    }
                }).catch(function (e) {
                    console.log(e)
                    that.coverPlaying = false;
                    that.$message({
                        showClose: true,
                        message: '播放错误',
                        type: 'error'
                    });
                });
              });
            }).catch(function (e) {
                if (catchcallback)catchcallback()
        },
        convertStopClick: function() {
            this.convertStop(()=>{
                this.$refs.videoPlayer.play(this.videoUrl)
            });
        },
        playFromStreamInfo: function (realHasAudio, streamInfo) {
          this.videoUrl = streamInfo.ws_flv;
          this.showVideoDialog = true;
          this.hasaudio = realHasAudio && this.hasaudio;
          this.ssrc = streamInfo.ssrc;
          console.log(this.ssrc);
        },
        close: function () {
            console.log('关闭视频');
            if (!this.$refs.videoPlayer){
              this.$refs.videoPlayer.pause();
            }
            this.videoUrl = '';
            this.showVideoDialog = false;
            if (this.convertKey != '') {
              this.$axios({
        convertStop: function(callback) {
            var that = this;
            that.$refs.videoPlayer.pause()
            this.$axios({
                method: 'post',
                url: '/api/play/convert/stop/' + this.convertKey
              }).then(function (res) {
@@ -267,12 +292,36 @@
                }else {
                  console.error(res.data.msg)
                }
                 if (callback )callback();
              }).catch(function (e) {});
            }
          this.convertKey = ''
            that.coverPlaying = false;
            that.convertKey = "";
            // if (callback )callback();
        },
        playFromStreamInfo: function (realHasAudio, streamInfo) {
          this.showVideoDialog = true;
          this.hasaudio = realHasAudio && this.hasaudio;
          this.$refs.videoPlayer.play(streamInfo.ws_flv)
        },
        close: function () {
            console.log('关闭视频');
            if (!!this.$refs.videoPlayer){
              this.$refs.videoPlayer.pause();
            }
            this.videoUrl = '';
            this.coverPlaying = false;
            this.showVideoDialog = false;
            if (this.convertKey != '') {
              this.convertStop();
            }
            this.convertKey = ''
        },
        copySharedInfo: function (data) {
            console.log('复制内容:' + data);
            this.coverPlaying = false;
            this.tracks = []
            let _this = this;
            this.$copyText(data).then(
                function (e) {
@@ -602,4 +651,15 @@
.control-bottom .fa {
    transform: rotate(-45deg) translateY(7px);
}
.trank {
    width: 80%;
    height: 180px;
    text-align: left;
    padding: 0 10%;
    overflow: auto;
}
.trankInfo {
    width: 80%;
    padding: 0 10%;
}
</style>
web_src/src/components/gb28181/player.vue
New file
@@ -0,0 +1,57 @@
<template>
    <div id="player">
        <div id="easyplayer"></div>
    </div>
</template>
<script>
export default {
    name: 'player',
    data() {
        return {
            easyPlayer: null
        };
    },
    props: ['videoUrl', 'error', 'hasaudio'],
    mounted () {
       this.$nextTick(() =>{
           console.log("初始化时的地址为: " + this.videoUrl)
            this.easyPlayer = new WasmPlayer(null, 'easyplayer', this.eventcallbacK)
            this.easyPlayer.play(this.videoUrl, 1)
        })
    },
    watch:{
        videoUrl(newData, oldData){
            this.easyPlayer.destroy()
            this.easyPlayer = new WasmPlayer(null, 'easyplayer', this.eventcallbacK)
            this.easyPlayer.play(newData, 1)
        },
        immediate:true
    },
    methods: {
        play: function (url) {
            this.easyPlayer = new WasmPlayer(null, 'easyplayer', this.eventcallbacK)
            this.easyPlayer.play(url, 1)
        },
        pause: function () {
            this.easyPlayer.destroy();
        },
        eventcallbacK: function(type, message) {
            console.log("player 事件回调")
            console.log(type)
            console.log(message)
        }
    },
}
</script>
<style>
    .LodingTitle {
        min-width: 70px;
    }
    /* 隐藏logo */
    /* .iconqingxiLOGO {
        display: none !important;
    } */
</style>
web_src/src/components/videoList.vue
@@ -73,12 +73,10 @@
</template>
<script>
     import devicePlayer from './gb28181/devicePlayer.vue'
     import uiHeader from './UiHeader.vue'
    export default {
        name: 'app',
        components: {
            devicePlayer,
            uiHeader
        },
        data() {
web_src/src/router/index.js
@@ -35,7 +35,7 @@
      path: '/channelList/:deviceId/:parentChannelId/:count/:page',
      name: 'channelList',
      component: channelList,
    },,
    },
    {
      path: '/parentPlatformList/:count/:page',
      name: 'parentPlatformList',