From 296bc1c12ed1cff4839a6387757845c98379c273 Mon Sep 17 00:00:00 2001
From: ‘liusuyi’ <1951119284@qq.com>
Date: 星期四, 31 八月 2023 16:52:02 +0800
Subject: [PATCH] 流媒体优化

---
 ard-work/src/main/java/com/ruoyi/device/hiksdk/controller/SdkController.java |    4 
 ard-work/src/main/resources/templates/preview.html                           |  397 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 2 files changed, 401 insertions(+), 0 deletions(-)

diff --git a/ard-work/src/main/java/com/ruoyi/device/hiksdk/controller/SdkController.java b/ard-work/src/main/java/com/ruoyi/device/hiksdk/controller/SdkController.java
index 2053d50..a0f602b 100644
--- a/ard-work/src/main/java/com/ruoyi/device/hiksdk/controller/SdkController.java
+++ b/ard-work/src/main/java/com/ruoyi/device/hiksdk/controller/SdkController.java
@@ -55,6 +55,10 @@
         sdk.loginAll();
     }
 
+    @RequestMapping("/preview")
+    private String preview() {
+        return "preview";
+    }
     @RequestMapping("/index")
     private String index() {
         return "test";
diff --git a/ard-work/src/main/resources/templates/preview.html b/ard-work/src/main/resources/templates/preview.html
new file mode 100644
index 0000000..9915e66
--- /dev/null
+++ b/ard-work/src/main/resources/templates/preview.html
@@ -0,0 +1,397 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+    <meta charset="UTF-8">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>鑷�傚簲瀹瑰櫒绀轰緥</title>
+    <script th:src="@{/js/jquery-3.6.4.min.js}"></script>
+    <style>
+        body, html {
+            margin: 0;
+            padding: 0;
+            height: 100%;
+        }
+
+        .container-wrapper {
+            display: flex;
+            flex-direction: column;
+            height: 100%;
+        }
+
+        .container {
+            background-color: #151414; /* 灏嗙綉鏍奸」鐩殑棰滆壊璁剧疆涓虹孩鑹茶儗鏅� */
+            flex: 9;
+            border: 1px solid #ccc;
+            box-sizing: border-box;
+            display: grid;
+            grid-template-columns: repeat(2, 1fr); /* 榛樿 2x2 缃戞牸 */
+            grid-gap: 10px;
+            overflow: hidden; /* 闃叉瑙嗛婧㈠嚭瀹瑰櫒 */
+        }
+
+        .grid-item {
+            background-color: #151414; /* 灏嗙綉鏍奸」鐩殑棰滆壊璁剧疆涓虹孩鑹茶儗鏅� */
+            text-align: center;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            border: 1px solid #ccc;
+            box-sizing: border-box;
+            padding: 10px; /* 鍐呰竟璺濅负 10px */
+            position: relative; /* 娣诲姞鐩稿瀹氫綅 */
+        }
+
+        .video-container {
+            width: 100%;
+            height: 100%;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            overflow: hidden; /* 闃叉瑙嗛婧㈠嚭瀹瑰櫒 */
+            position: absolute; /* 娣诲姞缁濆瀹氫綅 */
+            top: 0;
+            left: 0;
+            right: 0;
+            bottom: 0;
+        }
+
+        .container2 {
+            flex: 1;
+            border: 1px solid #ccc;
+            padding: 10px;
+            box-sizing: border-box;
+        }
+
+        /* 淇敼 video 鏍峰紡 */
+        video {
+            width: 100%;
+            height: 100%;
+            object-fit: fill;
+        }
+    </style>
+</head>
+<body>
+<div class="container-wrapper">
+    <div class="container" id="gridContainer">
+        <!-- 缃戞牸椤圭洰灏嗙敱 JavaScript 鍔ㄦ�佺敓鎴� -->
+    </div>
+    <div class="container2">
+        <div class="button-container">
+            <button class="toggle-button" onclick="changeGrid(1, 1)">1x1</button>
+            <button class="toggle-button" onclick="changeGrid(2, 2)">2x2</button>
+            <button class="toggle-button" onclick="changeGrid(3, 3)">3x3</button>
+            <button class="toggle-button" onclick="changeGrid(4, 4)">4x4</button>
+        </div>
+    </div>
+</div>
+
+<script>
+    function calculateAspectRatio(videoWidth, videoHeight) {
+        return videoWidth / videoHeight;
+    }
+
+    function adjustGridItemSize(gridItem, videoWidth, videoHeight) {
+        const aspectRatio = calculateAspectRatio(videoWidth, videoHeight);
+        gridItem.style.aspectRatio = aspectRatio; /* 璁剧疆瀹介珮姣� */
+    }
+
+    function changeGrid(rows, cols) {
+        closeAllVideo();
+        const gridContainer = document.getElementById('gridContainer');
+        gridContainer.innerHTML = '';
+
+        for (let i = 1; i <= rows * cols; i++) {
+            const gridItem = document.createElement('div');
+            gridItem.className = 'grid-item';
+            const videoContainer = document.createElement('div');
+            videoContainer.className = 'video-container';
+            const video = document.createElement('video');
+            video.id="video"+i;
+            video.controls = true;
+            video.autoplay = true;
+            video.muted = true;
+            videoContainer.appendChild(video);
+            gridItem.appendChild(videoContainer);
+            gridContainer.appendChild(gridItem);
+
+            video.addEventListener('loadedmetadata', function () {
+                adjustGridItemSize(gridItem, gridItem.videoWidth, gridItem.videoHeight);
+            });
+            console.log(video.id)
+        }
+
+        gridContainer.style.gridTemplateColumns = `repeat(${cols}, 1fr)`;
+    }
+
+
+    var chanMap = new Map();
+    window.onload = function () {
+        changeGrid(2, 2);
+        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();
+    $(document).on('click', 'video', function() {
+        let ID = this.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);
+    }
+    function closeAllVideo(){
+        videoMap.forEach((val,key) => {
+            console.log(val,key);
+            val.stop(key);
+            videoMap.delete(key);
+        })
+    }
+
+</script>
+</body>
+</html>

--
Gitblit v1.9.3