| | |
| | | <!DOCTYPE html> |
| | | <html lang="en" xmlns:th="http://www.thymeleaf.org"> |
| | | <html> |
| | | <head> |
| | | <meta charset="UTF-8"> |
| | | <title>测试页</title> |
| | | <script th:src="@{/js/jquery-3.6.4.min.js}"></script> |
| | | <link rel="stylesheet" th:href="@{/css/bootstrap.css}"/> |
| | | <script th:src="@{/js/bootstrap.js}"></script> |
| | | <style> |
| | | .video-container { |
| | | display: inline-block; |
| | | vertical-align: top; |
| | | width: 33%; /* 3个视频平均分配一行的宽度 */ |
| | | /*padding: 2px; !* 可以根据需要调整内边距 *!*/ |
| | | box-sizing: border-box; |
| | | } |
| | | |
| | | .video-container video { |
| | | width: 100%; |
| | | height: 100%; |
| | | /*object-fit: cover; !* 视频填充容器,保持宽高比 *!*/ |
| | | } |
| | | </style> |
| | | <title>video.js播放rtmp流</title> |
| | | <!--引入播放器样式--> |
| | | <link href="http://vjs.zencdn.net/5.19/video-js.min.css" rel="stylesheet"> |
| | | <!--引入播放器js--> |
| | | <script src="http://vjs.zencdn.net/5.19/video.min.js"></script> |
| | | <script src="https://cdn.jsdelivr.net/npm/videojs-flash@2/dist/videojs-flash.min.js"></script> |
| | | </head> |
| | | <body> |
| | | <div> |
| | | <div class="row"> |
| | | <div class="video-container"> |
| | | <video id="video1" muted autoplay loop controls></video> |
| | | </div> |
| | | <div class="video-container"> |
| | | <video id="video2" muted autoplay loop controls></video> |
| | | </div> |
| | | <div class="video-container"> |
| | | <video id="video3" muted autoplay loop controls></video> |
| | | </div> |
| | | <div class="video-container"> |
| | | <video id="video4" muted autoplay loop controls></video> |
| | | </div> |
| | | <div class="video-container"> |
| | | <video id="video5" muted autoplay loop controls></video> |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <script th:inline="javascript"> |
| | | var chanMap = new Map(); |
| | | window.onload = function () { |
| | | |
| | | chanMap.set("video1", "http://127.0.0.1:8889/245/"); |
| | | chanMap.set("video2", "http://127.0.0.1:8889/164/"); |
| | | chanMap.set("video3", "http://127.0.0.1:8889/164/"); |
| | | chanMap.set("video4", "http://127.0.0.1:8889/165/"); |
| | | chanMap.set("video5", "http://127.0.0.1:8889/165/"); |
| | | console.log(chanMap); |
| | | } |
| | | const linkToIceServers = (links) => ( |
| | | (links !== null) ? links.split(', ').map((link) => { |
| | | const m = link.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i); |
| | | const ret = { |
| | | urls: [m[1]], |
| | | }; |
| | | |
| | | if (m[3] !== undefined) { |
| | | ret.username = unquoteCredential(m[3]); |
| | | ret.credential = unquoteCredential(m[4]); |
| | | ret.credentialType = "password"; |
| | | } |
| | | |
| | | return ret; |
| | | }) : [] |
| | | ); |
| | | const parseOffer = (offer) => { |
| | | const ret = { |
| | | iceUfrag: '', |
| | | icePwd: '', |
| | | medias: [], |
| | | }; |
| | | |
| | | for (const line of offer.split('\r\n')) { |
| | | if (line.startsWith('m=')) { |
| | | ret.medias.push(line.slice('m='.length)); |
| | | } else if (ret.iceUfrag === '' && line.startsWith('a=ice-ufrag:')) { |
| | | ret.iceUfrag = line.slice('a=ice-ufrag:'.length); |
| | | } else if (ret.icePwd === '' && line.startsWith('a=ice-pwd:')) { |
| | | ret.icePwd = line.slice('a=ice-pwd:'.length); |
| | | } |
| | | } |
| | | |
| | | return ret; |
| | | }; |
| | | const generateSdpFragment = (offerData, candidates) => { |
| | | const candidatesByMedia = {}; |
| | | for (const candidate of candidates) { |
| | | const mid = candidate.sdpMLineIndex; |
| | | if (candidatesByMedia[mid] === undefined) { |
| | | candidatesByMedia[mid] = []; |
| | | } |
| | | candidatesByMedia[mid].push(candidate); |
| | | } |
| | | |
| | | let frag = 'a=ice-ufrag:' + offerData.iceUfrag + '\r\n' |
| | | + 'a=ice-pwd:' + offerData.icePwd + '\r\n'; |
| | | |
| | | let mid = 0; |
| | | |
| | | for (const media of offerData.medias) { |
| | | if (candidatesByMedia[mid] !== undefined) { |
| | | frag += 'm=' + media + '\r\n' |
| | | + 'a=mid:' + mid + '\r\n'; |
| | | |
| | | for (const candidate of candidatesByMedia[mid]) { |
| | | frag += 'a=' + candidate.candidate + '\r\n'; |
| | | } |
| | | } |
| | | mid++; |
| | | } |
| | | |
| | | return frag; |
| | | } |
| | | |
| | | class WHEPClient { |
| | | constructor(wurl, videoId) { |
| | | this.video = videoId; |
| | | this.url = new URL('whep', wurl); |
| | | this.pc = null; |
| | | this.restartTimeout = null; |
| | | this.eTag = ''; |
| | | this.queuedCandidates = []; |
| | | this.start(); |
| | | } |
| | | |
| | | start() { |
| | | console.log("requesting ICE servers"); |
| | | fetch(this.url, { |
| | | method: 'OPTIONS', |
| | | }) |
| | | .then((res) => this.onIceServers(res)) |
| | | .catch((err) => { |
| | | console.log('error: ' + err); |
| | | this.scheduleRestart(); |
| | | }); |
| | | } |
| | | |
| | | onIceServers(res) { |
| | | this.pc = new RTCPeerConnection({ |
| | | iceServers: linkToIceServers(res.headers.get('Link')), |
| | | }); |
| | | |
| | | const direction = "sendrecv"; |
| | | this.pc.addTransceiver("video", {direction}); |
| | | this.pc.addTransceiver("audio", {direction}); |
| | | |
| | | this.pc.onicecandidate = (evt) => this.onLocalCandidate(evt); |
| | | this.pc.oniceconnectionstatechange = () => this.onConnectionState(); |
| | | |
| | | this.pc.ontrack = (evt) => { |
| | | console.log("new track:", evt.track.kind); |
| | | document.getElementById(this.video).srcObject = evt.streams[0]; |
| | | }; |
| | | |
| | | this.pc.createOffer() |
| | | .then((offer) => this.onLocalOffer(offer)); |
| | | } |
| | | |
| | | onLocalOffer(offer) { |
| | | this.offerData = parseOffer(offer.sdp); |
| | | this.pc.setLocalDescription(offer); |
| | | |
| | | console.log("sending offer"); |
| | | |
| | | fetch(this.url, { |
| | | method: 'POST', |
| | | headers: { |
| | | 'Content-Type': 'application/sdp', |
| | | }, |
| | | body: offer.sdp, |
| | | }) |
| | | .then((res) => { |
| | | if (res.status !== 201) { |
| | | throw new Error('bad status code'); |
| | | } |
| | | this.eTag = res.headers.get('E-Tag'); |
| | | return res.text(); |
| | | }) |
| | | .then((sdp) => this.onRemoteAnswer(new RTCSessionDescription({ |
| | | type: 'answer', |
| | | sdp, |
| | | }))) |
| | | .catch((err) => { |
| | | console.log('error: ' + err); |
| | | this.scheduleRestart(); |
| | | }); |
| | | } |
| | | |
| | | onConnectionState() { |
| | | if (this.restartTimeout !== null) { |
| | | return; |
| | | } |
| | | |
| | | console.log("peer connection state:", this.pc.iceConnectionState); |
| | | |
| | | switch (this.pc.iceConnectionState) { |
| | | case "disconnected": |
| | | this.scheduleRestart(); |
| | | } |
| | | } |
| | | |
| | | onRemoteAnswer(answer) { |
| | | if (this.restartTimeout !== null) { |
| | | return; |
| | | } |
| | | |
| | | this.pc.setRemoteDescription(new RTCSessionDescription(answer)); |
| | | |
| | | if (this.queuedCandidates.length !== 0) { |
| | | this.sendLocalCandidates(this.queuedCandidates); |
| | | this.queuedCandidates = []; |
| | | } |
| | | } |
| | | |
| | | onLocalCandidate(evt) { |
| | | if (this.restartTimeout !== null) { |
| | | return; |
| | | } |
| | | |
| | | if (evt.candidate !== null) { |
| | | if (this.eTag === '') { |
| | | this.queuedCandidates.push(evt.candidate); |
| | | } else { |
| | | this.sendLocalCandidates([evt.candidate]) |
| | | } |
| | | } |
| | | } |
| | | |
| | | sendLocalCandidates(candidates) { |
| | | fetch(this.url, { |
| | | method: 'PATCH', |
| | | headers: { |
| | | 'Content-Type': 'application/trickle-ice-sdpfrag', |
| | | 'If-Match': this.eTag, |
| | | }, |
| | | body: generateSdpFragment(this.offerData, candidates), |
| | | }) |
| | | .then((res) => { |
| | | if (res.status !== 204) { |
| | | throw new Error('bad status code'); |
| | | } |
| | | }) |
| | | .catch((err) => { |
| | | console.log('error: ' + err); |
| | | this.scheduleRestart(); |
| | | }); |
| | | } |
| | | |
| | | scheduleRestart() { |
| | | if (this.restartTimeout !== null) { |
| | | return; |
| | | } |
| | | |
| | | if (this.pc !== null) { |
| | | this.pc.close(); |
| | | this.pc = null; |
| | | } |
| | | |
| | | this.restartTimeout = window.setTimeout(() => { |
| | | this.restartTimeout = null; |
| | | this.start(); |
| | | }, restartPause); |
| | | |
| | | this.eTag = ''; |
| | | this.queuedCandidates = []; |
| | | } |
| | | |
| | | stop() { |
| | | if (this.pc) { |
| | | try { |
| | | this.pc.close(); |
| | | } catch (e) { |
| | | console.log("Failure close peer connection:" + e); |
| | | } |
| | | this.pc = null; |
| | | } |
| | | } |
| | | } |
| | | |
| | | let videoMap = new Map(); |
| | | $('video').click(function (e) { |
| | | let ID = e.target.id;//获取当前点击事件的元素 |
| | | console.log(ID); |
| | | console.log(videoMap); |
| | | if (videoMap.get(ID) != null) { |
| | | closeVideo(ID); |
| | | } else { |
| | | let stream = chanMap.get(ID); |
| | | let client = new WHEPClient(stream, ID); |
| | | videoMap.set(ID, client); |
| | | } |
| | | }); |
| | | |
| | | function closeVideo(id) { |
| | | console.log("关闭" + id) |
| | | let client = videoMap.get(id); |
| | | client.stop(id); |
| | | videoMap.delete(id); |
| | | } |
| | | <!--vjs-big-play-centered 播放按钮居中--> |
| | | <!--poster默认的显示界面,就是还没点播放,给你显示的界面--> |
| | | <!--controls 规定浏览器应该为视频提供播放控件--> |
| | | <!--preload="auto" 是否提前加载--> |
| | | <!--autoplay 自动播放--> |
| | | <!--loop=true 自动循环--> |
| | | <!--data-setup='{"example_option":true}' 可以把一些属性写到这个里面来,如data-setup={"autoplay":true}--> |
| | | <video id="my-player" |
| | | class="video-js vjs-default-skin vjs-big-play-centered" controls |
| | | preload="auto" autoplay="autoplay" |
| | | poster="images/logo.png" width="500" height="400" |
| | | data-setup='{}'> |
| | | <!--src: 规定媒体文件的 URL type:规定媒体资源的类型--> |
| | | <source src='rtmp://192.168.1.194:1935/164' type='rtmp/flv' /> |
| | | </video> |
| | | <script type="text/javascript"> |
| | | // 设置flash路径,用于在videojs发现浏览器不支持HTML5播放器的时候自动唤起flash播放器 |
| | | videojs.options.flash.swf = 'https://cdn.bootcss.com/videojs-swf/5.4.1/video-js.swf'; |
| | | //my-player为页面video元素的id |
| | | var player = videojs('my-player'); |
| | | //播放 |
| | | player.play(); |
| | | // 1. 播放 player.play() |
| | | // 2. 停止 player.pause() |
| | | // 3. 暂停 player.pause() |
| | | </script> |
| | | </body> |
| | | </html> |
| | | </html> |