ard-work/src/main/java/com/ruoyi/media/controller/MediaController.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
ard-work/src/main/java/com/ruoyi/media/service/IMediaService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
ard-work/src/main/java/com/ruoyi/media/service/impl/MediaServiceImpl.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
ard-work/src/main/java/com/ruoyi/utils/forest/MediaClient.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
ard-work/src/main/resources/templates/mediaMTX.html | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
ard-work/src/main/resources/templates/preview.html | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
lib/mediamtx/mediamtx.yml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
ard-work/src/main/java/com/ruoyi/media/controller/MediaController.java
@@ -22,7 +22,7 @@ import java.util.stream.Collectors; /** * @Description: * @Description: 流媒体业务 * @ClassName: controller * @Author: 刘苏义 * @Date: 2023年07月13日9:26 @@ -75,9 +75,7 @@ @PreAuthorize("@ss.hasPermi('media:stream:edit')") @PutMapping public AjaxResult edit(@RequestBody StreamInfo streamInfo) { mediaService.removePath(new String[]{streamInfo.getName()}); vtduService.deleteVtduByName(streamInfo.getName()); Map<String, String> map = mediaService.addPath(streamInfo.getName(), streamInfo.getRtspSource(), streamInfo.getMode(), streamInfo.getIsCode()); Map<String, String> map = mediaService.editPath(streamInfo.getName(), streamInfo.getRtspSource(), streamInfo.getMode(), streamInfo.getIsCode()); Vtdu vtdu = new Vtdu(); vtdu.setName(streamInfo.getName()); vtdu.setSourceUrl(streamInfo.getRtspSource()); @@ -86,6 +84,7 @@ vtdu.setRtspUrl(map.get("rtspUrl")); vtdu.setRtmpUrl(map.get("rtmpUrl")); vtdu.setWebrtcUrl(map.get("webrtcUrl")); vtduService.updateVtdu(vtdu); return AjaxResult.success(map); } ard-work/src/main/java/com/ruoyi/media/service/IMediaService.java
@@ -16,7 +16,17 @@ * 刘苏义 * 2023/8/12 13:56:52 */ Map<String,String> addPath(String name, String rtspPath, String mode, String isCode); Map<String,String> addPath(String name, String sourceUrl, String mode, String isCode); /** * 修改路径 * name 名称 * rtspPath rtsp地址 * mode 模式:实时/按需 * isCode 是否转码 * 刘苏义 * 2023/8/12 13:56:52 */ Map<String,String> editPath(String name, String sourceUrl, String mode, String isCode); StreamInfo getPathInfo(String name); ard-work/src/main/java/com/ruoyi/media/service/impl/MediaServiceImpl.java
@@ -131,18 +131,17 @@ //GPU硬解码编码 -hwaccel cuvid -c:v h264_cuvid 使用cuda解码 -c:v h264_nvenc 使用cuda编码 //String cmd = rootPath + "/lib/mediamtx/" + "ffmpeg -hwaccel cuvid -c:v h264_cuvid -rtsp_transport udp -i " + rtspPath + " -c:v h264_nvenc -r 25 -threads 4 -b:v 2048k -bf 0 -f rtsp rtsp://localhost:$RTSP_PORT/$MTX_PATH"; if (isCode.equals("1")) { String cmd = rootPath + "ffmpeg -rtsp_transport tcp -i " + sourceUrl + " -vcodec libx264 -preset:v ultrafast -r 25 -keyint_min 25 -g 25 -sc_threshold 0 -threads 6 -b:v 4096k -f rtsp rtsp://localhost:$RTSP_PORT/$MTX_PATH"; String cmd = "ffmpeg -rtsp_transport tcp -i " + sourceUrl + " -vcodec libx264 -preset:v ultrafast -r 25 -keyint_min 25 -g 25 -sc_threshold 0 -threads 6 -b:v 2048k -f rtsp rtsp://localhost:$RTSP_PORT/$MTX_PATH"; if (!softwareDecoding) { cmd = rootPath + "ffmpeg -hwaccel cuvid -c:v h264_cuvid -rtsp_transport tcp -i " + sourceUrl + " -c:v h264_nvenc -r 25 -g 60 -threads 6 -b:v 4096k -bf 0 -f rtsp rtsp://localhost:$RTSP_PORT/$MTX_PATH"; cmd = "ffmpeg -hwaccel cuvid -c:v h264_cuvid -rtsp_transport tcp -i " + sourceUrl + " -c:v h264_nvenc -r 25 -g 60 -threads 6 -b:v 2048k -bf 0 -f rtsp rtsp://localhost:$RTSP_PORT/$MTX_PATH"; } if (mode.equals("0")) { mediaInfo.setRunondemand(cmd); mediaInfo.setRunondemandrestart(true); mediaInfo.setRunondemandcloseafter("5s"); } else { mediaInfo.setRunoninit(cmd); mediaInfo.setRunoninitrestart(true); //mediaInfo.setRunonready(cmd); //mediaInfo.setRunonreadyrestart(true); } } else { mediaInfo.setSource(sourceUrl); @@ -158,6 +157,63 @@ } @Override public Map<String, String> editPath(String name, String sourceUrl, String mode, String isCode) { String rtspUrl = "rtsp://" + mediamtxHost + ":7554/" + name; String rtmpUrl = "rtmp://" + mediamtxHost + ":1935/" + name; String webrtcUrl = "http://" + mediamtxHost + ":8889/" + name; Conf mediaInfo = new Conf(); String rootPath = System.getProperty("user.dir").replaceAll("\\\\", "/") + "/lib/mediamtx/"; //-vcodec libx264 //指定视频编码器为 libx264,使用 H.264 编码格式进行视频压缩 //-preset ultrafast //--preset的参数主要调节编码速度和质量的平衡,有ultrafast(转码速度最快,视频往往也最模糊)、superfast、veryfast、faster、fast、medium、slow、slower、veryslow、placebo这10个选项,从快到慢 //-r 25 //设置输出视频的帧率为 25 帧/秒 //-g 20 //关键帧间隔20 //-sc_threshold 0 //将其设置为0(-sc_threshold 0)禁用场景变化检测 //-rtsp_transport tcp //这个选项告诉 FFmpeg 使用 TCP 作为 RTSP 的传输协议 //-threads 4: 指定要使用的线程数为 4。//这允许 FFmpeg 在多核处理器上使用多个线程来进行视频编码,以加快速度。 // -i //用于指定输入媒体文件或输入流的地址 // -bf 0 禁用B帧,因为webrtc在网页调用时控制台一直输出 WebRTC doesn’t support H264 streams with B-frames //-f rtsp //这个选项告诉 FFmpeg 输出为 RTSP 格式。 //CPU软解码编码 //String cmd = rootPath + "/lib/mediamtx/" +"ffmpeg -rtsp_transport tcp -i " + rtspPath + " -vcodec libx264 -preset:v ultrafast -r 25 -threads 4 -b:v 2048k -f rtsp rtsp://localhost:$RTSP_PORT/$MTX_PATH"; //GPU硬解码编码 -hwaccel cuvid -c:v h264_cuvid 使用cuda解码 -c:v h264_nvenc 使用cuda编码 //String cmd = rootPath + "/lib/mediamtx/" + "ffmpeg -hwaccel cuvid -c:v h264_cuvid -rtsp_transport udp -i " + rtspPath + " -c:v h264_nvenc -r 25 -threads 4 -b:v 2048k -bf 0 -f rtsp rtsp://localhost:$RTSP_PORT/$MTX_PATH"; if (isCode.equals("1")) { mediaInfo.setSource("publisher"); String cmd = "ffmpeg -rtsp_transport tcp -i " + sourceUrl + " -vcodec libx264 -preset:v ultrafast -r 25 -keyint_min 25 -g 25 -sc_threshold 0 -threads 6 -b:v 2048k -f rtsp rtsp://localhost:$RTSP_PORT/$MTX_PATH"; if (!softwareDecoding) { cmd = "ffmpeg -hwaccel cuvid -c:v h264_cuvid -rtsp_transport tcp -i " + sourceUrl + " -c:v h264_nvenc -r 25 -g 60 -threads 6 -b:v 2048k -bf 0 -f rtsp rtsp://localhost:$RTSP_PORT/$MTX_PATH"; } if (mode.equals("0")) { mediaInfo.setRunondemand(cmd); mediaInfo.setRunondemandcloseafter("5s"); mediaInfo.setRunondemandrestart(true); mediaInfo.setRunoninit(""); mediaInfo.setRunoninitrestart(false); } else { mediaInfo.setRunoninit(cmd); mediaInfo.setRunoninitrestart(true); mediaInfo.setRunondemand(""); mediaInfo.setRunondemandrestart(false); } } else { mediaInfo.setSource(sourceUrl); mediaInfo.setRunondemand(""); mediaInfo.setRunondemandrestart(false); mediaInfo.setRunoninit(""); mediaInfo.setRunoninitrestart(false); } mediaInfo.setMaxReaders(100); mediaInfo.setSourceprotocol("tcp"); mediaClient.editPath(name, mediaInfo); Map<String,String> map=new HashMap<>(); map.put("rtspUrl",rtspUrl); map.put("rtmpUrl",rtmpUrl); map.put("webrtcUrl",webrtcUrl); return map; } @Override public StreamInfo getPathInfo(String name) { Items item = mediaClient.getPathInfo(name); StreamInfo info = new StreamInfo(); ard-work/src/main/java/com/ruoyi/utils/forest/MediaClient.java
@@ -18,7 +18,11 @@ */ @Post("/config/paths/add/{name}") String addPath( @Var("name") String name, @JSONBody Conf body); /** * 修改路径 */ @Post("/config/paths/edit/{name}") String editPath( @Var("name") String name, @JSONBody Conf body); /** * 移除路径 */ ard-work/src/main/resources/templates/mediaMTX.html
@@ -1,307 +1,40 @@ <!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> ard-work/src/main/resources/templates/preview.html
@@ -20,7 +20,7 @@ .container { background-color: #151414; /* 将网格项目的颜色设置为红色背景 */ flex: 9; flex: 30; border: 10px solid; box-sizing: border-box; display: grid; @@ -68,6 +68,7 @@ height: 100%; object-fit: fill; } #loadingMessage { position: absolute; top: 50%; @@ -90,87 +91,23 @@ <div id="loadingMessage">正在取流</div> <div class="container2"> <div class="button-container"> <button class="toggle-button" onclick="closeAllVideo()">关闭</button> <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> <button class="toggle-button" onclick="changeGrid(5, 5)">5x5</button> <button class="toggle-button" onclick="changeGrid(6, 6)">6x6</button> <button class="toggle-button" onclick="changeGrid(7, 7)">7x7</button> <button class="toggle-button" onclick="changeGrid(8, 8)">8x8</button> <button class="toggle-button" onclick="changeGrid(9, 9)">9x9</button> <input id="videoUrl" type="text" value="http://192.168.1.11:8889/164/" style="width: 250px"/> </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; video.loop= true; videoContainer.appendChild(video); gridItem.appendChild(videoContainer); gridContainer.appendChild(gridItem); video.addEventListener('loadedmetadata', function () { adjustGridItemSize(gridItem, gridItem.videoWidth, gridItem.videoHeight); }); video.addEventListener("click", function () { loadingMessage.style.display = "block"; video.play().then(function () { loadingMessage.style.display = "none"; }).catch(function (error) { console.error("Error playing the video:", error); loadingMessage.style.display = "none"; }); }); console.log(video.id) } gridContainer.style.gridTemplateColumns = `repeat(${cols}, 1fr)`; } let mediamtxHost = "192.168.1.227" var chanMap = new Map(); window.onload = function () { changeGrid(2, 2); chanMap.set("video1", "http://" + mediamtxHost + ":8889/164/"); chanMap.set("video2", "http://" + mediamtxHost + ":8889/164/"); chanMap.set("video3", "http://" + mediamtxHost + ":8889/164/"); chanMap.set("video4", "http://" + mediamtxHost + ":8889/164/"); chanMap.set("video5", "http://" + mediamtxHost + ":8889/164/"); chanMap.set("video6", "http://" + mediamtxHost + ":8889/164/"); chanMap.set("video7", "http://" + mediamtxHost + ":8889/164/"); chanMap.set("video8", "http://" + mediamtxHost + ":8889/164/"); chanMap.set("video9", "http://" + mediamtxHost + ":8889/164/"); chanMap.set("video10", "http://" + mediamtxHost + ":8889/164/"); chanMap.set("video11", "http://" + mediamtxHost + ":8889/164/"); chanMap.set("video12", "http://" + mediamtxHost + ":8889/164/"); chanMap.set("video13", "http://" + mediamtxHost + ":8889/164/"); chanMap.set("video14", "http://" + mediamtxHost + ":8889/164/"); chanMap.set("video15", "http://" + mediamtxHost + ":8889/164/"); chanMap.set("video16", "http://" + mediamtxHost + ":8889/164/"); console.log(chanMap); } //whep操作方法 const linkToIceServers = (links) => ( (links !== null) ? links.split(', ').map((link) => { const m = link.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i); @@ -401,36 +338,104 @@ } } let videoMap = new Map(); //初始化加载 var chanMap = new Map(); window.onload = function () { let videoUrl = $("#videoUrl").val(); for (let i = 1; i < 82; i++) { chanMap.set("video" + i, videoUrl); } console.log(chanMap); changeGrid(2, 2); } //绑定点击事件 let playMap = new Map(); $(document).on('click', 'video', function () { let ID = this.id;//获取当前点击事件的元素 console.log(ID); console.log(videoMap); if (videoMap.get(ID) != null) { console.log(playMap); if (playMap.get(ID) != null) { closeVideo(ID); } else { let stream = chanMap.get(ID); let client = new WHEPClient(stream, ID); videoMap.set(ID, client); playMap.set(ID, client); } }); //关闭一个video function closeVideo(id) { console.log("关闭" + id) let client = videoMap.get(id); let client = playMap.get(id); client.stop(id); videoMap.delete(id); playMap.delete(id); } //关闭所有video function closeAllVideo() { videoMap.forEach((val, key) => { console.log(val, key); playMap.forEach((val, key) => { val.stop(key); videoMap.delete(key); closeVideo(key); playMap.delete(key); }) } //动态改变grid 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(); let num = rows * cols; let videoUrl = $("#videoUrl").val(); for (let i = 1; i < num + 1; i++) { chanMap.set("video" + i, videoUrl); } 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; video.loop = true; videoContainer.appendChild(video); gridItem.appendChild(videoContainer); gridContainer.appendChild(gridItem); video.addEventListener('loadedmetadata', function () { adjustGridItemSize(gridItem, gridItem.videoWidth, gridItem.videoHeight); }); video.addEventListener("click", function () { loadingMessage.style.display = "block"; video.play().then(function () { loadingMessage.style.display = "none"; }).catch(function (error) { console.error("Error playing the video:", error); loadingMessage.style.display = "none"; }); }); console.log(video.id) let stream = chanMap.get(video.id); let client = new WHEPClient(stream, video.id); playMap.set(video.id, client); } gridContainer.style.gridTemplateColumns = `repeat(${cols}, 1fr)`; } </script> </body> </html> lib/mediamtx/mediamtx.yml
@@ -15,7 +15,7 @@ writeTimeout: 10s # Size of the queue of outgoing packets. # A higher value allows to increase throughput, a lower value allows to save RAM. writeQueueSize: 8192 writeQueueSize: 512 # Maximum size of outgoing UDP packets. # This can be decreased to avoid fragmentation on networks with a low UDP MTU. udpMaxPayloadSize: 1472