648540858
2022-10-17 7918f037342e95913fd6af62f32702930742456c
优化按需拉流配置,拉流代理支持按需拉流
14个文件已修改
173 ■■■■ 已修改文件
sql/mysql.sql 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/update.sql 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/conf/MediaConfig.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java 93 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/MediaServerItem.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamProxyItem.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/storager/dao/MediaServerMapper.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/genersoft/iot/vmp/storager/dao/StreamProxyMapper.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/all-application.yml 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/components/dialog/MediaServerEdit.vue 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/components/dialog/StreamProxyEdit.vue 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
web_src/src/components/setting/Media.vue 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql.sql
@@ -277,7 +277,6 @@
                                `rtspSSLPort` int NOT NULL,
                                `autoConfig` int NOT NULL,
                                `secret` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                `streamNoneReaderDelayMS` int NOT NULL,
                                `rtpEnable` int NOT NULL,
                                `rtpPortRange` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
                                `sendRtpPortRange` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
sql/update.sql
@@ -0,0 +1,5 @@
alter table wvp.media_server
    drop column streamNoneReaderDelayMS;
alter table stream_proxy
    add enable_disable_none_reader bit(1) default null;
src/main/java/com/genersoft/iot/vmp/conf/MediaConfig.java
@@ -69,9 +69,6 @@
    @Value("${media.secret}")
    private String secret;
    @Value("${media.stream-none-reader-delay-ms:15000}")
    private int streamNoneReaderDelayMS = 15000;
    @Value("${media.rtp.enable}")
    private boolean rtpEnable;
@@ -151,10 +148,6 @@
        return secret;
    }
    public int getStreamNoneReaderDelayMS() {
        return streamNoneReaderDelayMS;
    }
    public boolean isRtpEnable() {
        return rtpEnable;
    }
@@ -219,7 +212,6 @@
        mediaServerItem.setRtspSSLPort(rtspSSLPort);
        mediaServerItem.setAutoConfig(autoConfig);
        mediaServerItem.setSecret(secret);
        mediaServerItem.setStreamNoneReaderDelayMS(streamNoneReaderDelayMS);
        mediaServerItem.setRtpEnable(rtpEnable);
        mediaServerItem.setRtpPortRange(rtpPortRange);
        mediaServerItem.setSendRtpPortRange(sendRtpPortRange);
src/main/java/com/genersoft/iot/vmp/conf/UserSetting.java
@@ -33,6 +33,8 @@
    private Boolean usePushingAsStatus = Boolean.TRUE;
    private Boolean streamOnDemand = Boolean.TRUE;
    private String serverId = "000000";
    private String thirdPartyGBIdReg = "[\\s\\S]*";
@@ -146,4 +148,12 @@
    public void setUsePushingAsStatus(Boolean usePushingAsStatus) {
        this.usePushingAsStatus = usePushingAsStatus;
    }
    public Boolean getStreamOnDemand() {
        return streamOnDemand;
    }
    public void setStreamOnDemand(Boolean streamOnDemand) {
        this.streamOnDemand = streamOnDemand;
    }
}
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java
@@ -558,9 +558,12 @@
        String app = json.getString("app");
        JSONObject ret = new JSONObject();
        ret.put("code", 0);
        // 录像下载
        ret.put("close", userSetting.getStreamOnDemand());
        if ("rtp".equals(app)){
            ret.put("close", true);
            // 国标流, 点播/录像回放/录像下载
            StreamInfo streamInfoForPlayCatch = redisCatchStorage.queryPlayByStreamId(streamId);
            // 点播
            if (streamInfoForPlayCatch != null) {
                // 收到无人观看说明流也没有在往上级推送
                if (redisCatchStorage.isChannelSendingRTP(streamInfoForPlayCatch.getChannelId())) {
@@ -590,40 +593,39 @@
                redisCatchStorage.stopPlay(streamInfoForPlayCatch);
                storager.stopPlay(streamInfoForPlayCatch.getDeviceID(), streamInfoForPlayCatch.getChannelId());
            }else{
                StreamInfo streamInfoForPlayBackCatch = redisCatchStorage.queryPlayback(null, null, streamId, null);
                if (streamInfoForPlayBackCatch != null ) {
                    if (streamInfoForPlayBackCatch.isPause()) {
                        ret.put("close", false);
                    }else {
                        Device device = deviceService.queryDevice(streamInfoForPlayBackCatch.getDeviceID());
                        if (device != null) {
                            try {
                                cmder.streamByeCmd(device,streamInfoForPlayBackCatch.getChannelId(),
                                        streamInfoForPlayBackCatch.getStream(), null);
                            } catch (InvalidArgumentException | ParseException | SipException |
                                     SsrcTransactionNotFoundException e) {
                                logger.error("[无人观看]回放, 发送BYE失败 {}", e.getMessage());
                            }
                        }
                        redisCatchStorage.stopPlayback(streamInfoForPlayBackCatch.getDeviceID(),
                                streamInfoForPlayBackCatch.getChannelId(), streamInfoForPlayBackCatch.getStream(), null);
                    }
                return ret;
            }
            // 录像回放
            StreamInfo streamInfoForPlayBackCatch = redisCatchStorage.queryPlayback(null, null, streamId, null);
            if (streamInfoForPlayBackCatch != null ) {
                if (streamInfoForPlayBackCatch.isPause()) {
                    ret.put("close", false);
                }else {
                    StreamInfo streamInfoForDownload = redisCatchStorage.queryDownload(null, null, streamId, null);
                    // 进行录像下载时无人观看不断流
                    if (streamInfoForDownload != null) {
                        ret.put("close", false);
                    Device device = deviceService.queryDevice(streamInfoForPlayBackCatch.getDeviceID());
                    if (device != null) {
                        try {
                            cmder.streamByeCmd(device,streamInfoForPlayBackCatch.getChannelId(),
                                    streamInfoForPlayBackCatch.getStream(), null);
                        } catch (InvalidArgumentException | ParseException | SipException |
                                 SsrcTransactionNotFoundException e) {
                            logger.error("[无人观看]回放, 发送BYE失败 {}", e.getMessage());
                        }
                    }
                    redisCatchStorage.stopPlayback(streamInfoForPlayBackCatch.getDeviceID(),
                            streamInfoForPlayBackCatch.getChannelId(), streamInfoForPlayBackCatch.getStream(), null);
                }
                return ret;
            }
            MediaServerItem mediaServerItem = mediaServerService.getOne(mediaServerId);
            if (mediaServerItem != null && mediaServerItem.getStreamNoneReaderDelayMS() == -1) {
            // 录像下载
            StreamInfo streamInfoForDownload = redisCatchStorage.queryDownload(null, null, streamId, null);
            // 进行录像下载时无人观看不断流
            if (streamInfoForDownload != null) {
                ret.put("close", false);
                return ret;
            }
            return ret;
        }else {
            // 非国标流 推流/拉流代理
            // 拉流代理
            StreamProxyItem streamProxyItem = streamProxyService.getStreamProxyByAppAndStream(app, streamId);
            if (streamProxyItem != null ) {
                if (streamProxyItem.isEnable_remove_none_reader()) {
@@ -635,12 +637,21 @@
                }else if (streamProxyItem.isEnable_disable_none_reader()) {
                    // 无人观看停用
                    ret.put("close", true);
                    // 修改数据
                    streamProxyService.stop(app, streamId);
                }else {
                    ret.put("close", false);
                }
                return ret;
            }
            return ret;
            // 推流具有主动性,暂时不做处理
//            StreamPushItem streamPushItem = streamPushService.getPush(app, streamId);
//            if (streamPushItem != null) {
//                // TODO 发送停止
//
//            }
        }
        return ret;
    }
    
    /**
@@ -655,19 +666,27 @@
        }
        String mediaServerId = json.getString("mediaServerId");
        MediaServerItem mediaInfo = mediaServerService.getOne(mediaServerId);
        if (userSetting.isAutoApplyPlay() && mediaInfo != null && mediaInfo.isRtpEnable()) {
        if (userSetting.isAutoApplyPlay() && mediaInfo != null) {
            String app = json.getString("app");
            String streamId = json.getString("stream");
            if ("rtp".equals(app)) {
                String[] s = streamId.split("_");
                if (s.length == 2) {
                    String deviceId = s[0];
                    String channelId = s[1];
                    Device device = redisCatchStorage.getDevice(deviceId);
                    if (device != null) {
                        playService.play(mediaInfo,deviceId, channelId, null, null, null);
                if (mediaInfo.isRtpEnable()) {
                    String[] s = streamId.split("_");
                    if (s.length == 2) {
                        String deviceId = s[0];
                        String channelId = s[1];
                        Device device = redisCatchStorage.getDevice(deviceId);
                        if (device != null) {
                            playService.play(mediaInfo,deviceId, channelId, null, null, null);
                        }
                    }
                }
            }else {
                // 拉流代理
                StreamProxyItem streamProxyByAppAndStream = streamProxyService.getStreamProxyByAppAndStream(app, streamId);
                if (streamProxyByAppAndStream != null && streamProxyByAppAndStream.isEnable_disable_none_reader()) {
                    streamProxyService.start(app, streamId);
                }
            }
        }
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/MediaServerItem.java
@@ -54,9 +54,6 @@
    @Schema(description = "ZLM鉴权参数")
    private String secret;
    @Schema(description = "某个流无人观看时,触发hook.on_stream_none_reader事件的最大等待时间,单位毫秒")
    private int streamNoneReaderDelayMS;
    @Schema(description = "keepalive hook触发间隔,单位秒")
    private int hookAliveInterval;
@@ -119,7 +116,6 @@
        rtspSSLPort = zlmServerConfig.getRtspSSlport();
        autoConfig = true; // 默认值true;
        secret = zlmServerConfig.getApiSecret();
        streamNoneReaderDelayMS = zlmServerConfig.getGeneralStreamNoneReaderDelayMS();
        hookAliveInterval = zlmServerConfig.getHookAliveInterval();
        rtpEnable = false; // 默认使用单端口;直到用户自己设置开启多端口
        rtpPortRange = zlmServerConfig.getPortRange().replace("_",","); // 默认使用30000,30500作为级联时发送流的端口号
@@ -238,14 +234,6 @@
    public void setSecret(String secret) {
        this.secret = secret;
    }
    public int getStreamNoneReaderDelayMS() {
        return streamNoneReaderDelayMS;
    }
    public void setStreamNoneReaderDelayMS(int streamNoneReaderDelayMS) {
        this.streamNoneReaderDelayMS = streamNoneReaderDelayMS;
    }
    public boolean isRtpEnable() {
src/main/java/com/genersoft/iot/vmp/media/zlm/dto/StreamProxyItem.java
@@ -38,7 +38,7 @@
    @Schema(description = "是否 无人观看时删除")
    private boolean enable_remove_none_reader;
    @Schema(description = "是否 无人观看时不启用")
    @Schema(description = "是否 无人观看时自动停用")
    private boolean enable_disable_none_reader;
    @Schema(description = "上级平台国标ID")
    private String platformGbId;
src/main/java/com/genersoft/iot/vmp/service/impl/MediaServerServiceImpl.java
@@ -541,7 +541,6 @@
            param.put("hook.on_record_mp4","");
        }
        param.put("hook.timeoutSec","20");
        param.put("general.streamNoneReaderDelayMS",mediaServerItem.getStreamNoneReaderDelayMS()==-1?"3600000":mediaServerItem.getStreamNoneReaderDelayMS() );
        // 推流断开后可以在超时时间内重新连接上继续推流,这样播放器会接着播放。
        // 置0关闭此特性(推流断开会导致立即断开播放器)
        // 此参数不应大于播放器超时时间
@@ -606,7 +605,6 @@
        mediaServerItem.setStreamIp(ip);
        mediaServerItem.setHookIp(sipConfig.getIp());
        mediaServerItem.setSdpIp(ip);
        mediaServerItem.setStreamNoneReaderDelayMS(zlmServerConfig.getGeneralStreamNoneReaderDelayMS());
        return mediaServerItem;
    }
src/main/java/com/genersoft/iot/vmp/storager/dao/MediaServerMapper.java
@@ -26,7 +26,6 @@
            "rtspSSLPort, " +
            "autoConfig, " +
            "secret, " +
            "streamNoneReaderDelayMS, " +
            "rtpEnable, " +
            "rtpPortRange, " +
            "sendRtpPortRange, " +
@@ -51,7 +50,6 @@
            "${rtspSSLPort}, " +
            "${autoConfig}, " +
            "'${secret}', " +
            "${streamNoneReaderDelayMS}, " +
            "${rtpEnable}, " +
            "'${rtpPortRange}', " +
            "'${sendRtpPortRange}', " +
@@ -77,7 +75,6 @@
            "<if test=\"rtspPort != null\">, rtspPort=${rtspPort}</if>" +
            "<if test=\"rtspSSLPort != null\">, rtspSSLPort=${rtspSSLPort}</if>" +
            "<if test=\"autoConfig != null\">, autoConfig=${autoConfig}</if>" +
            "<if test=\"streamNoneReaderDelayMS != null\">, streamNoneReaderDelayMS=${streamNoneReaderDelayMS}</if>" +
            "<if test=\"rtpEnable != null\">, rtpEnable=${rtpEnable}</if>" +
            "<if test=\"rtpPortRange != null\">, rtpPortRange='${rtpPortRange}'</if>" +
            "<if test=\"sendRtpPortRange != null\">, sendRtpPortRange='${sendRtpPortRange}'</if>" +
@@ -102,7 +99,6 @@
            "<if test=\"rtspPort != null\">, rtspPort=${rtspPort}</if>" +
            "<if test=\"rtspSSLPort != null\">, rtspSSLPort=${rtspSSLPort}</if>" +
            "<if test=\"autoConfig != null\">, autoConfig=${autoConfig}</if>" +
            "<if test=\"streamNoneReaderDelayMS != null\">, streamNoneReaderDelayMS=${streamNoneReaderDelayMS}</if>" +
            "<if test=\"rtpEnable != null\">, rtpEnable=${rtpEnable}</if>" +
            "<if test=\"rtpPortRange != null\">, rtpPortRange='${rtpPortRange}'</if>" +
            "<if test=\"sendRtpPortRange != null\">, sendRtpPortRange='${sendRtpPortRange}'</if>" +
src/main/java/com/genersoft/iot/vmp/storager/dao/StreamProxyMapper.java
@@ -11,10 +11,10 @@
public interface StreamProxyMapper {
    @Insert("INSERT INTO stream_proxy (type, name, app, stream,mediaServerId, url, src_url, dst_url, " +
            "timeout_ms, ffmpeg_cmd_key, rtp_type, enable_hls, enable_mp4, enable, status, enable_remove_none_reader, createTime) VALUES" +
            "timeout_ms, ffmpeg_cmd_key, rtp_type, enable_hls, enable_mp4, enable, status, enable_remove_none_reader, enable_disable_none_reader, createTime) VALUES" +
            "('${type}','${name}', '${app}', '${stream}', '${mediaServerId}','${url}', '${src_url}', '${dst_url}', " +
            "'${timeout_ms}', '${ffmpeg_cmd_key}', '${rtp_type}', ${enable_hls}, ${enable_mp4}, ${enable}, ${status}, " +
            "${enable_remove_none_reader}, '${createTime}' )")
            "${enable_remove_none_reader}, ${enable_disable_none_reader}, '${createTime}' )")
    int add(StreamProxyItem streamProxyDto);
    @Update("UPDATE stream_proxy " +
@@ -33,6 +33,7 @@
            "enable=#{enable}, " +
            "status=#{status}, " +
            "enable_remove_none_reader=#{enable_remove_none_reader}, " +
            "enable_disable_none_reader=#{enable_disable_none_reader}, " +
            "enable_mp4=#{enable_mp4} " +
            "WHERE app=#{app} AND stream=#{stream}")
    int update(StreamProxyItem streamProxyDto);
src/main/resources/all-application.yml
@@ -146,8 +146,6 @@
    auto-config: true
    # [可选] zlm服务器的hook.admin_params=secret
    secret: 035c73f7-bb6b-4889-a715-d9eb2d1925cc
    # [可选] zlm服务器的general.streamNoneReaderDelayMS
    stream-none-reader-delay-ms:  18000  # 无人观看多久自动关闭流, -1表示永不自动关闭,即 关闭按需拉流
    # 启用多端口模式, 多端口模式使用端口区分每路流,兼容性更好。 单端口使用流的ssrc区分, 点播超时建议使用多端口测试
    rtp:
        # [可选] 是否启用多端口模式, 开启后会在portRange范围内选择端口用于媒体流传输
@@ -190,6 +188,8 @@
    logInDatebase: true
    # 使用推流状态作为推流通道状态
    use-pushing-as-status: true
    # 按需拉流, true:有人观看拉流,无人观看释放, false:拉起后不自动释放
    stream-on-demand: true
# 关闭在线文档(生产环境建议关闭)
springdoc:
web_src/src/components/dialog/MediaServerEdit.vue
@@ -41,10 +41,6 @@
                <el-input  v-if="currentStep === 2"  v-model="mediaServerForm.httpPort" disabled :disabled="mediaServerForm.defaultServer"></el-input>
                <el-input  v-if="currentStep === 3"  v-model="mediaServerForm.httpPort" :disabled="mediaServerForm.defaultServer"></el-input>
              </el-form-item>
              <el-form-item label="SECRET" prop="secret">
                <el-input v-if="currentStep === 2"  v-model="mediaServerForm.secret" disabled :disabled="mediaServerForm.defaultServer"></el-input>
                <el-input v-if="currentStep === 3"  v-model="mediaServerForm.secret" :disabled="mediaServerForm.defaultServer"></el-input>
              </el-form-item>
              <el-form-item label="HOOK IP" prop="ip">
                <el-input v-model="mediaServerForm.hookIp" placeholder="媒体服务HOOK_IP" clearable :disabled="mediaServerForm.defaultServer"></el-input>
              </el-form-item>
@@ -74,6 +70,10 @@
              <el-form-item label="RTMPS PORT" prop="rtmpSSlPort">
                <el-input v-model="mediaServerForm.rtmpSSlPort" placeholder="媒体服务RTMPS_PORT" clearable :disabled="mediaServerForm.defaultServer"></el-input>
              </el-form-item>
              <el-form-item label="SECRET" prop="secret">
                <el-input v-if="currentStep === 2"  v-model="mediaServerForm.secret" disabled :disabled="mediaServerForm.defaultServer"></el-input>
                <el-input v-if="currentStep === 3"  v-model="mediaServerForm.secret" :disabled="mediaServerForm.defaultServer"></el-input>
              </el-form-item>
              <el-form-item label="自动配置媒体服务" >
                <el-switch v-model="mediaServerForm.autoConfig" :disabled="mediaServerForm.defaultServer"></el-switch>
              </el-form-item>
@@ -93,9 +93,6 @@
                <el-input v-model="sendRtpPortRange1" placeholder="起始" @change="portRangeChange" clearable style="width: 100px" prop="sendRtpPortRange1" :disabled="mediaServerForm.defaultServer"></el-input>
                -
                <el-input v-model="sendRtpPortRange2" placeholder="终止" @change="portRangeChange" clearable style="width: 100px" prop="sendRtpPortRange2" :disabled="mediaServerForm.defaultServer"></el-input>
              </el-form-item>
              <el-form-item label="无人观看多久后停止拉流" >
                <el-input v-model.number="mediaServerForm.streamNoneReaderDelayMS" clearable :disabled="mediaServerForm.defaultServer"></el-input>
              </el-form-item>
              <el-form-item label="录像管理服务端口" prop="recordAssistPort">
                <el-input v-model.number="mediaServerForm.recordAssistPort" :disabled="mediaServerForm.defaultServer">
@@ -172,7 +169,6 @@
        hookIp: "",
        sdpIp: "",
        streamIp: "",
        streamNoneReaderDelayMS: "",
        secret: "035c73f7-bb6b-4889-a715-d9eb2d1925cc",
        httpPort: "",
        httpSSlPort: "",
@@ -332,7 +328,6 @@
        hookIp: "",
        sdpIp: "",
        streamIp: "",
        streamNoneReaderDelayMS: "",
        secret: "035c73f7-bb6b-4889-a715-d9eb2d1925cc",
        httpPort: "",
        httpSSlPort: "",
web_src/src/components/dialog/StreamProxyEdit.vue
@@ -105,7 +105,9 @@
                  <el-checkbox label="启用" v-model="proxyParam.enable" ></el-checkbox>
                  <el-checkbox label="转HLS" v-model="proxyParam.enable_hls" ></el-checkbox>
                  <el-checkbox label="MP4录制" v-model="proxyParam.enable_mp4" ></el-checkbox>
                  <el-checkbox label="无人观看自动删除" v-model="proxyParam.enable_remove_none_reader" ></el-checkbox>
                  <el-checkbox label="无人观看自动删除" v-model="proxyParam.enable_remove_none_reader" @change="removeNoneReader"></el-checkbox>
                  <el-checkbox label="无人观看停止拉流" v-model="proxyParam.enable_disable_none_reader" @change="disableNoneReaderHandType"></el-checkbox>
                </div>
              </el-form-item>
@@ -170,6 +172,7 @@
          enable_hls: true,
          enable_mp4: false,
          enable_remove_none_reader: false,
          enable_disable_none_reader: true,
          platformGbId: null,
          mediaServerId: null,
      },
@@ -276,6 +279,12 @@
      if (this.platform.enable && this.platform.expires == "0") {
        this.platform.expires = "300";
      }
    },
    removeNoneReader: function(checked) {
      this.proxyParam.enable_disable_none_reader = !checked;
    },
    disableNoneReaderHandType: function(checked) {
      this.proxyParam.enable_remove_none_reader = !checked;
    }
  },
};
web_src/src/components/setting/Media.vue
@@ -42,9 +42,6 @@
        <el-form-item label="接口密钥" prop="secret">
          <el-input v-model="form.secret" clearable></el-input>
        </el-form-item>
        <el-form-item label="无人观看触发时长">
          <el-input v-model.number="form.streamNoneReaderDelayMS" clearable></el-input>
        </el-form-item>
        <el-form-item label="自动配置">
          <el-switch v-model="form.autoConfig"></el-switch>
        </el-form-item>