Commit 09470bef96d48373219ac03315a4391c24accf98

Authored by 648540858
1 parent 4e9c6b2f

添加网页自动转码,以支持h265 g.711的播放

src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMHttpHookListener.java
... ... @@ -128,29 +128,29 @@ public class ZLMHttpHookListener {
128 128 }
129 129 String app = json.getString("app");
130 130 String streamId = json.getString("id");
  131 + if ("rtp".equals(app)) {
  132 + String ssrc = new DecimalFormat("0000000000").format(Integer.parseInt(streamId, 16));
  133 + StreamInfo streamInfoForPlay = storager.queryPlayBySSRC(ssrc);
  134 + if ("rtp".equals(app) && streamInfoForPlay != null ) {
  135 + MediaServerConfig mediaInfo = storager.getMediaInfo();
  136 + streamInfoForPlay.setFlv(String.format("http://%s:%s/rtp/%s.flv", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
  137 + streamInfoForPlay.setWs_flv(String.format("ws://%s:%s/rtp/%s.flv", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
  138 + streamInfoForPlay.setRtmp(String.format("rtmp://%s:%s/rtp/%s", mediaInfo.getWanIp(), mediaInfo.getRtmpPort(), streamId));
  139 + streamInfoForPlay.setHls(String.format("http://%s:%s/rtp/%s/hls.m3u8", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
  140 + streamInfoForPlay.setRtsp(String.format("rtsp://%s:%s/rtp/%s", mediaInfo.getWanIp(), mediaInfo.getRtspPort(), streamId));
  141 + storager.startPlay(streamInfoForPlay);
  142 + }
131 143  
132   -
133   - String ssrc = new DecimalFormat("0000000000").format(Integer.parseInt(streamId, 16));
134   - StreamInfo streamInfoForPlay = storager.queryPlayBySSRC(ssrc);
135   - if ("rtp".equals(app) && streamInfoForPlay != null ) {
136   - MediaServerConfig mediaInfo = storager.getMediaInfo();
137   - streamInfoForPlay.setFlv(String.format("http://%s:%s/rtp/%s.flv", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
138   - streamInfoForPlay.setWs_flv(String.format("ws://%s:%s/rtp/%s.flv", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
139   - streamInfoForPlay.setRtmp(String.format("rtmp://%s:%s/rtp/%s", mediaInfo.getWanIp(), mediaInfo.getRtmpPort(), streamId));
140   - streamInfoForPlay.setHls(String.format("http://%s:%s/rtp/%s/hls.m3u8", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
141   - streamInfoForPlay.setRtsp(String.format("rtsp://%s:%s/rtp/%s", mediaInfo.getWanIp(), mediaInfo.getRtspPort(), streamId));
142   - storager.startPlay(streamInfoForPlay);
143   - }
144   -
145   - StreamInfo streamInfoForPlayBack = storager.queryPlaybackBySSRC(ssrc);
146   - if ("rtp".equals(app) && streamInfoForPlayBack != null ) {
147   - MediaServerConfig mediaInfo = storager.getMediaInfo();
148   - streamInfoForPlayBack.setFlv(String.format("http://%s:%s/rtp/%s.flv", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
149   - streamInfoForPlayBack.setWs_flv(String.format("ws://%s:%s/rtp/%s.flv", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
150   - streamInfoForPlayBack.setRtmp(String.format("rtmp://%s:%s/rtp/%s", mediaInfo.getWanIp(), mediaInfo.getRtmpPort(), streamId));
151   - streamInfoForPlayBack.setHls(String.format("http://%s:%s/rtp/%s/hls.m3u8", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
152   - streamInfoForPlayBack.setRtsp(String.format("rtsp://%s:%s/rtp/%s", mediaInfo.getWanIp(), mediaInfo.getRtspPort(), streamId));
153   - storager.startPlayback(streamInfoForPlayBack);
  144 + StreamInfo streamInfoForPlayBack = storager.queryPlaybackBySSRC(ssrc);
  145 + if ("rtp".equals(app) && streamInfoForPlayBack != null ) {
  146 + MediaServerConfig mediaInfo = storager.getMediaInfo();
  147 + streamInfoForPlayBack.setFlv(String.format("http://%s:%s/rtp/%s.flv", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
  148 + streamInfoForPlayBack.setWs_flv(String.format("ws://%s:%s/rtp/%s.flv", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
  149 + streamInfoForPlayBack.setRtmp(String.format("rtmp://%s:%s/rtp/%s", mediaInfo.getWanIp(), mediaInfo.getRtmpPort(), streamId));
  150 + streamInfoForPlayBack.setHls(String.format("http://%s:%s/rtp/%s/hls.m3u8", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
  151 + streamInfoForPlayBack.setRtsp(String.format("rtsp://%s:%s/rtp/%s", mediaInfo.getWanIp(), mediaInfo.getRtspPort(), streamId));
  152 + storager.startPlayback(streamInfoForPlayBack);
  153 + }
154 154 }
155 155  
156 156 // TODO Auto-generated method stub
... ...
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRESTfulUtils.java
... ... @@ -89,6 +89,22 @@ public class ZLMRESTfulUtils {
89 89 return sendPost("getRtpInfo",param);
90 90 }
91 91  
  92 + public JSONObject addFFmpegSource(String src_url, String dst_url, String timeout_ms){
  93 + System.out.println(src_url);
  94 + System.out.println(dst_url);
  95 + Map<String, Object> param = new HashMap<>();
  96 + param.put("src_url", src_url);
  97 + param.put("dst_url", dst_url);
  98 + param.put("timeout_ms", timeout_ms);
  99 + return sendPost("addFFmpegSource",param);
  100 + }
  101 +
  102 + public JSONObject delFFmpegSource(String key){
  103 + Map<String, Object> param = new HashMap<>();
  104 + param.put("key", key);
  105 + return sendPost("delFFmpegSource",param);
  106 + }
  107 +
92 108 public JSONObject getMediaServerConfig(){
93 109 return sendPost("getServerConfig",null);
94 110 }
... ...
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMRunner.java
... ... @@ -101,7 +101,8 @@ public class ZLMRunner implements CommandLineRunner {
101 101  
102 102 String hookPrex = String.format("http://%s:%s/index/hook", hookIP, serverPort);
103 103 Map<String, Object> param = new HashMap<>();
104   - param.put("secret",mediaSecret);
  104 + param.put("api.secret",mediaSecret); // -profile:v Baseline
  105 + 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");
105 106 param.put("hook.enable","1");
106 107 param.put("hook.on_flow_report","");
107 108 param.put("hook.on_play","");
... ...
src/main/java/com/genersoft/iot/vmp/media/zlm/ZLMUtils.java
... ... @@ -29,7 +29,7 @@ public class ZLMUtils {
29 29 param.put("enable_tcp", 1);
30 30 param.put("stream_id", streamId);
31 31 JSONObject jsonObject = zlmresTfulUtils.openRtpServer(param);
32   - if (jsonObject.getInteger("code") == 0) {
  32 + if (jsonObject != null && jsonObject.getInteger("code") == 0) {
33 33 return newPort;
34 34 } else {
35 35 return getNewRTPPort(ssrc);
... ...
src/main/java/com/genersoft/iot/vmp/vmanager/play/PlayController.java
... ... @@ -3,6 +3,7 @@ package com.genersoft.iot.vmp.vmanager.play;
3 3 import com.alibaba.fastjson.JSON;
4 4 import com.alibaba.fastjson.JSONArray;
5 5 import com.genersoft.iot.vmp.common.StreamInfo;
  6 +import com.genersoft.iot.vmp.conf.MediaServerConfig;
6 7 import com.genersoft.iot.vmp.media.zlm.ZLMRESTfulUtils;
7 8 import org.slf4j.Logger;
8 9 import org.slf4j.LoggerFactory;
... ... @@ -46,7 +47,7 @@ public class PlayController {
46 47 Integer getEncoding) {
47 48  
48 49 if (getEncoding == null) getEncoding = 0;
49   - getEncoding = closeWaitRTPInfo ? 0: getEncoding;
  50 + getEncoding = closeWaitRTPInfo ? 0 : getEncoding;
50 51 Device device = storager.queryVideoDevice(deviceId);
51 52 StreamInfo streamInfo = storager.queryPlayByDevice(deviceId, channelId);
52 53  
... ... @@ -149,5 +150,73 @@ public class PlayController {
149 150 return new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR);
150 151 }
151 152 }
  153 +
  154 + /**
  155 + * 将不是h264的视频通过ffmpeg 转码为h264 + aac
  156 + * @param ssrc
  157 + * @return
  158 + */
  159 + @PostMapping("/play/{ssrc}/convert")
  160 + public ResponseEntity<String> playConvert(@PathVariable String ssrc) {
  161 + StreamInfo streamInfo = storager.queryPlayBySSRC(ssrc);
  162 + if (streamInfo == null) {
  163 + logger.warn("视频转码API调用失败!, 视频流已经停止!");
  164 + return new ResponseEntity<String>("未找到视频流信息, 视频流可能已经停止", HttpStatus.OK);
  165 + }
  166 + String streamId = String.format("%08x", Integer.parseInt(ssrc)).toUpperCase();
  167 + JSONObject rtpInfo = zlmresTfulUtils.getRtpInfo(streamId);
  168 + if (!rtpInfo.getBoolean("exist")) {
  169 + logger.warn("视频转码API调用失败!, 视频流已停止推流!");
  170 + return new ResponseEntity<String>("推流信息在流媒体中不存在, 视频流可能已停止推流", HttpStatus.OK);
  171 + } else {
  172 + MediaServerConfig mediaInfo = storager.getMediaInfo();
  173 + String dstUrl = String.format("rtmp://%s:%s/convert/%s", "127.0.0.1", mediaInfo.getRtmpPort(),
  174 + streamId );
  175 + JSONObject jsonObject = zlmresTfulUtils.addFFmpegSource(streamInfo.getRtsp(), dstUrl, "1000000");
  176 + System.out.println(jsonObject);
  177 + JSONObject result = new JSONObject();
  178 + if (jsonObject != null && jsonObject.getInteger("code") == 0) {
  179 + result.put("code", 0);
  180 + JSONObject data = jsonObject.getJSONObject("data");
  181 + if (data != null) {
  182 + result.put("key", data.getString("key"));
  183 + result.put("rtmp", dstUrl);
  184 + result.put("flv", String.format("http://%s:%s/convert/%s.flv", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
  185 + result.put("ws_flv", String.format("ws://%s:%s/convert/%s.flv", mediaInfo.getWanIp(), mediaInfo.getHttpPort(), streamId));
  186 + }
  187 + }else {
  188 + result.put("code", 1);
  189 + result.put("msg", "cover fail");
  190 + }
  191 + return new ResponseEntity<String>( result.toJSONString(), HttpStatus.OK);
  192 + }
  193 + }
  194 +
  195 + /**
  196 + * 结束转码
  197 + * @param key
  198 + * @return
  199 + */
  200 + @PostMapping("/play/convert/stop/{key}")
  201 + public ResponseEntity<String> playConvertStop(@PathVariable String key) {
  202 +
  203 + JSONObject jsonObject = zlmresTfulUtils.delFFmpegSource(key);
  204 + System.out.println(jsonObject);
  205 + JSONObject result = new JSONObject();
  206 + if (jsonObject != null && jsonObject.getInteger("code") == 0) {
  207 + result.put("code", 0);
  208 + JSONObject data = jsonObject.getJSONObject("data");
  209 + if (data != null && data.getBoolean("flag")) {
  210 + result.put("code", "0");
  211 + result.put("msg", "success");
  212 + }else {
  213 +
  214 + }
  215 + }else {
  216 + result.put("code", 1);
  217 + result.put("msg", "delFFmpegSource fail");
  218 + }
  219 + return new ResponseEntity<String>( result.toJSONString(), HttpStatus.OK);
  220 + }
152 221 }
153 222  
... ...
src/main/resources/application.yml
... ... @@ -44,8 +44,11 @@ media: #zlm服务器的ip与http端口, 重点: 这是http端口
44 44 wanIp:
45 45 port: 80
46 46 secret: 035c73f7-bb6b-4889-a715-d9eb2d1925cc
47   - streamNoneReaderDelayMS: 1800000 # 无人观看多久自动关闭流
48   - closeWaitRTPInfo: false # 强制关闭等待收到流编码信息后在返回, 设为true可以快速打开播放窗口, 设为false保证返回后流就可以播放
  47 + streamNoneReaderDelayMS: 600000 # 无人观看多久自动关闭流
  48 + # 关闭等待收到流编码信息后在返回,
  49 + # 设为false可以获得更好的兼容性,保证返回后流就可以播放,
  50 + # 设为true可以快速打开播放窗口,可以获得更好的体验
  51 + closeWaitRTPInfo: true
49 52 rtp: # 启用udp多端口模式
50 53 enable: true
51 54 udpPortRange: 30000,30500 # 端口范围
... ...
web_src/src/components/gb28181/devicePlayer.vue
1 1 <template>
2   -<div id="devicePlayer">
  2 +<div id="devicePlayer" v-loading="isLoging">
3 3 <el-dialog title="视频播放" top="0" :close-on-click-modal="false" :visible.sync="showVideoDialog" :destroy-on-close="true" @close="close()">
4   - <LivePlayer v-if="showVideoDialog" ref="videoPlayer" :videoUrl="videoUrl" :error="videoError" :hasaudio="hasaudio" fluent autoplay live></LivePlayer>
  4 + <LivePlayer v-if="showVideoDialog" ref="videoPlayer" :videoUrl="videoUrl" :error="videoError" :message="videoError" :hasaudio="hasaudio" fluent autoplay live></LivePlayer>
5 5 <div id="shared" style="text-align: right; margin-top: 1rem;">
6 6 <el-tabs v-model="tabActiveName">
7 7 <el-tab-pane label="实时视频" name="media">
... ... @@ -26,20 +26,6 @@
26 26 <!--{"code":0,"data":{"paths":["22-29-30.mp4"],"rootPath":"/home/kkkkk/Documents/ZLMediaKit/release/linux/Debug/www/record/hls/kkkkk/2020-05-11/"}}-->
27 27 <el-tab-pane label="录像查询" name="record">
28 28 <el-date-picker size="mini" v-model="videoHistory.date" type="date" value-format="yyyy-MM-dd" placeholder="日期" @change="queryRecords()"></el-date-picker>
29   - <!-- <el-slider style="margin: 0 1rem 1rem 1rem;"-->
30   - <!-- v-model="timeVal"-->
31   - <!-- :min="timeMin"-->
32   - <!-- :max="timeMax"-->
33   - <!-- :step="5"-->
34   - <!-- :marks="getTimeMakrs()"-->
35   - <!-- :format-tooltip="formatTooltip">-->
36   - <!-- </el-slider>-->
37   - <!-- <range-slider :min="timeMin"-->
38   - <!-- :max="timeMax"-->
39   - <!-- :step="5"></range-slider>-->
40   -
41   - <!-- <el-date-picker v-model="videoHistory.endTime" type="datetime" value-format="yyyy-MM-dd HH:mm:ss" placeholder="结束时间"-->
42   - <!-- @change="recordList()"></el-date-picker>-->
43 29 <el-table :data="videoHistory.searchHistoryResult" height="150" v-loading="recordsLoading">
44 30 <el-table-column label="名称" prop="name"></el-table-column>
45 31 <el-table-column label="文件" prop="filePath"></el-table-column>
... ... @@ -143,41 +129,16 @@ export default {
143 129 date: '',
144 130 searchHistoryResult: [] //媒体流历史记录搜索结果
145 131 },
146   - timeMakrs: {
147   - // 0 : "0:00",
148   - // // 60 : "1:00",
149   - // 120 : "2:00",
150   - // // 180 : "3:00",
151   - // 240 : "4:00",
152   - // // 300 : "5:00",
153   - // 360 : "6:00",
154   - // // 420 : "7:00",
155   - // 480 : "8:00",
156   - // 540 : "9:00",
157   - 600: "10:00",
158   - // 660 : "11:00",
159   - 720: "12:00",
160   - // 780 : "13:00",
161   - 840: "14:00",
162   - // 900 : "15:00",
163   - 960: "16:00",
164   - // 1020 : "17:00",
165   - 1080: "18:00",
166   - // 1140 : "19:00",
167   - // 1200 : "20:00",
168   - // // 1260 : "21:00",
169   - // 1320 : "22:00",
170   - // // 1380 : "23:00",
171   - // 1440 : "24:00"
172   - },
173 132 showVideoDialog: false,
174 133 ssrc: '',
  134 + convertKey: '',
175 135 deviceId: '',
176 136 channelId: '',
177 137 tabActiveName: 'media',
178 138 hasaudio: false,
179 139 loadingRecords: false,
180 140 recordsLoading: false,
  141 + isLoging: false,
181 142 timeVal: 0,
182 143 timeMin: 0,
183 144 timeMax: 1440,
... ... @@ -200,6 +161,7 @@ export default {
200 161 this.$refs.videoPlayer.pause();
201 162 }
202 163  
  164 +
203 165 switch (tab) {
204 166 case "media":
205 167 this.play(param.streamInfo, param.hasAudio)
... ... @@ -217,33 +179,97 @@ export default {
217 179 timeAxisSelTime: function (val) {
218 180 console.log(val)
219 181 },
220   - getTimeMakrs() {
221   - return this.timeMakrs;
222   - },
223 182 play: function (streamInfo, hasAudio) {
224 183 this.hasaudio = hasAudio;
225   - // 根据媒体流信息二次判断
226   - var realHasAudio = false;
227   - if (!!streamInfo.tracks && streamInfo.tracks.length > 0 && hasAudio) {
  184 + var that = this;
  185 + that.isLoging = false;
  186 + if (!!streamInfo.tracks && streamInfo.tracks.length > 0 ) {
228 187 for (let i = 0; i < streamInfo.tracks.length; i++) {
229   - if (streamInfo.tracks[i].codec_type == 1 && streamInfo.tracks[i].codec_id_name == "CodecAAC") { // 判断为AAC音频
230   - realHasAudio = true;
231   - }
  188 + if (streamInfo.tracks[i].codec_type == 0 && streamInfo.tracks[i].codec_id_name != "CodecH264") { // 判断为H265视频
  189 + that.coverPlay(streamInfo, streamInfo.tracks[i].codec_id_name, ()=>{
  190 + that.close();
  191 + return;
  192 + })
  193 + }else if (streamInfo.tracks[i].codec_type == 1 && streamInfo.tracks[i].codec_id_name != "CodecAAC") {
  194 + that.coverPlay(streamInfo, streamInfo.tracks[i].codec_id_name, ()=>{
  195 + that.playFromStreamInfo(false. streamInfo)
  196 + })
  197 + }else if (streamInfo.tracks[i].codec_type == 1 && streamInfo.tracks[i].codec_id_name == "CodecAAC") {
  198 + that.playFromStreamInfo(true, streamInfo)
  199 + }else {
  200 + that.playFromStreamInfo(false, streamInfo)
  201 + }
232 202 }
  203 + }else {
  204 + that.playFromStreamInfo(false, streamInfo)
233 205 }
234   - this.hasaudio = realHasAudio && this.hasaudio;
235   - this.ssrc = streamInfo.ssrc;
236   - // this.$refs.videoPlayer.hasaudio = hasAudio;
237   - // this.videoUrl = streamInfo.flv + "?" + new Date().getTime();
238   - this.videoUrl = streamInfo.ws_flv;
239   - this.showVideoDialog = true;
240   - console.log(this.ssrc);
  206 + },
  207 + coverPlay: function (streamInfo, codec_id_name, catchcallback) {
  208 + var that = this;
  209 +
  210 + that.$confirm(codec_id_name + ' 编码格式不支持播放, 是否转码播放?', '提示', {
  211 + confirmButtonText: '确定',
  212 + cancelButtonText: '取消',
  213 + type: 'warning'
  214 + }).then(() => {
  215 + that.isLoging = true;
  216 + that.$axios({
  217 + method: 'post',
  218 + url: '/api/play/' + streamInfo.ssrc + '/convert'
  219 + }).then(function (res) {
  220 + if (res.data.code == 0) {
  221 + streamInfo.ws_flv = res.data.ws_flv;
  222 + that.convertKey = res.data.key;
  223 + setTimeout(()=>{
  224 + that.isLoging = false;
  225 + that.playFromStreamInfo(false, streamInfo);
  226 + }, 2000)
  227 + } else {
  228 + that.isLoging = false;
  229 + that.$message({
  230 + showClose: true,
  231 + message: '转码失败',
  232 + type: 'error'
  233 + });
  234 + }
  235 + }).catch(function (e) {
  236 + that.$message({
  237 + showClose: true,
  238 + message: '播放错误',
  239 + type: 'error'
  240 + });
  241 + });
  242 + }).catch(function (e) {
  243 + if (catchcallback)catchcallback()
  244 + });
  245 + },
  246 + playFromStreamInfo: function (realHasAudio, streamInfo) {
  247 + this.videoUrl = streamInfo.ws_flv;
  248 + this.showVideoDialog = true;
  249 + this.hasaudio = realHasAudio && this.hasaudio;
  250 + this.ssrc = streamInfo.ssrc;
  251 + console.log(this.ssrc);
241 252 },
242 253 close: function () {
243 254 console.log('关闭视频');
244   - this.$refs.videoPlayer.pause();
  255 + if (!this.$refs.videoPlayer){
  256 + this.$refs.videoPlayer.pause();
  257 + }
245 258 this.videoUrl = '';
246 259 this.showVideoDialog = false;
  260 + if (this.convertKey != '') {
  261 + this.$axios({
  262 + method: 'post',
  263 + url: '/api/play/convert/stop/' + this.convertKey
  264 + }).then(function (res) {
  265 + if (res.data.code == 0) {
  266 + console.log(res.data.msg)
  267 + }else {
  268 + console.error(res.data.msg)
  269 + }
  270 + }).catch(function (e) {});
  271 + }
  272 + this.convertKey = ''
247 273 },
248 274 copySharedInfo: function (data) {
249 275 console.log('复制内容:' + data);
... ...