ard-work/src/main/java/com/ruoyi/device/camera/controller/CameraSdkController.java
@@ -23,13 +23,11 @@ import org.springframework.web.bind.annotation.*; import javax.annotation.Resource; import javax.servlet.ServletOutputStream; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.io.OutputStream; import java.util.ArrayList; import java.util.List; import java.util.Map; /** * @Description: ç¸æºéç¨SDKæ¥å£ ard-work/src/main/java/com/ruoyi/media/controller/MediaController.java
@@ -2,16 +2,14 @@ import com.github.xiaoymin.knife4j.annotations.ApiOperationSupport; import com.ruoyi.common.annotation.Anonymous; import com.ruoyi.common.constant.HttpStatus; import com.ruoyi.common.core.controller.BaseController; import com.ruoyi.common.core.domain.AjaxResult; import com.ruoyi.common.core.domain.BaseEntity; import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.common.utils.uuid.IdUtils; import com.ruoyi.media.domain.Config; import com.ruoyi.media.domain.Paths; import com.ruoyi.media.domain.StreamInfo; import com.ruoyi.media.domain.Vtdu; import com.ruoyi.media.domain.*; import com.ruoyi.media.service.IMediaService; import com.ruoyi.media.service.IVtduService; import io.swagger.annotations.Api; @@ -107,33 +105,21 @@ } /** * ç§»é¤ææµ * ç§»é¤webrtcææµ */ @ApiOperation("ç§»é¤ææµ") @ApiOperationSupport(order =4 ) @ApiOperation("ç§»é¤webrtcææµ") @ApiOperationSupport(order = 4) @PreAuthorize("@ss.hasPermi('media:stream:remove')") @DeleteMapping("/{sessionId}") public AjaxResult removePullStreamSession(@PathVariable String sessionId) { List<StreamInfo> pullStreamList = mediaService.getPullStreamList(1,1000); StreamInfo streamInfo = pullStreamList.stream() .filter(object -> object.getId().equals(sessionId)) .collect(Collectors.toList()).get(0); switch (streamInfo.getSessionType()) { case "rtsp": return AjaxResult.success(mediaService.kickRtspSession(sessionId)); case "webrtc": return AjaxResult.success(mediaService.kickWebrtcSession(sessionId)); case "rtmp": return AjaxResult.success(mediaService.kickRtmpSession(sessionId)); } return AjaxResult.error(); public AjaxResult removeWebrtcPullStreamSession(@PathVariable String sessionId) { return AjaxResult.success(mediaService.kickWebrtcSession(sessionId)); } /** * è·åéé详ç»ä¿¡æ¯ */ @ApiOperation("è·åéé详ç»ä¿¡æ¯") @ApiOperationSupport(order =4 ) @ApiOperationSupport(order = 4) @GetMapping(value = "/{name}") public AjaxResult getInfo(@PathVariable("name") String name) { return success(mediaService.getPathInfo(name)); @@ -145,30 +131,18 @@ @GetMapping("/path/list") @ApiOperation("è·åå½åééå表") @ApiOperationSupport(order = 5) public TableDataInfo getPaths(Integer pageNum,Integer pageSize) { public TableDataInfo getPaths(Integer pageNum, Integer pageSize) { startPage(); return getDataTable(mediaService.paths(pageNum,pageSize)); return getDataTable(mediaService.paths(pageNum, pageSize)); } /** * æIDæ¥è¯¢ææµè¯¦æ */ @GetMapping("/getRtspSessionById") @ApiOperation("æIDæ¥è¯¢ææµè¯¦æ ") public AjaxResult getRtspSessionById(String sessionId) { List<StreamInfo> pullStreamList = mediaService.getPullStreamList(1,1000); StreamInfo streamInfo = pullStreamList.stream() .filter(object -> object.getId().equals(sessionId)) .collect(Collectors.toList()).get(0); switch (streamInfo.getSessionType()) { case "rtsp": return AjaxResult.success(mediaService.getRtspSessionById(sessionId)); case "rtmp": return AjaxResult.success(mediaService.getRtmpSessionById(sessionId)); case "webrtc": return AjaxResult.success(mediaService.getWebrtcSessionById(sessionId)); } return AjaxResult.error(); @GetMapping("/getWebrtcSessionById") @ApiOperation("æIDæ¥è¯¢webrtcææµè¯¦æ ") public AjaxResult getWebrtcSessionById(String sessionId) { return AjaxResult.success(mediaService.getWebrtcSessionById(sessionId)); } /** @@ -178,21 +152,42 @@ @GetMapping("/pushList") @ApiOperation("è·åæ¨æµå表") @ApiOperationSupport(order = 6) public TableDataInfo getPushStreamList(Integer pageNum,Integer pageSize) { startPage(); return getDataTable(mediaService.getPushStreamList(pageNum,pageSize)); public TableDataInfo getPushStreamList(Integer pageNum, Integer pageSize) { TableDataInfo tableDataInfo = mediaService.getPushStreamList(pageNum, pageSize); return tableDataInfo; } /** * è·åææµå表 * è·åWebRtcææµå表 */ @PreAuthorize("@ss.hasPermi('media:stream:list')") @GetMapping("/pullList") @ApiOperation("è·åææµå表") @GetMapping("/getPullWebrtcStreamList") @ApiOperation("è·åWebRtcææµå表") @ApiOperationSupport(order = 7) public TableDataInfo getPullStreamList(Integer pageNum,Integer pageSize) { startPage(); return getDataTable(mediaService.getPullStreamList(pageNum,pageSize)); public TableDataInfo getPullWebrtcStreamList(Integer pageNum, Integer pageSize) { return mediaService.getPullWebrtcStreamList(pageNum, pageSize); } /** * è·årtspææµå表 */ @PreAuthorize("@ss.hasPermi('media:stream:list')") @GetMapping("/getPullRtspStreamList") @ApiOperation("è·årtspææµå表") @ApiOperationSupport(order = 8) public TableDataInfo getPullRtspStreamList(Integer pageNum, Integer pageSize) { return mediaService.getPullRtspStreamList(pageNum, pageSize); } /** * è·årtmpææµå表 */ @PreAuthorize("@ss.hasPermi('media:stream:list')") @GetMapping("/getPullRtmpStreamList") @ApiOperation("è·årtmpææµå表") @ApiOperationSupport(order = 9) public TableDataInfo getPullRtmpStreamList(Integer pageNum, Integer pageSize) { return mediaService.getPullRtmpStreamList(pageNum, pageSize); } } ard-work/src/main/java/com/ruoyi/media/domain/RtmpSession.java
@@ -20,4 +20,8 @@ private String state; private long bytesReceived; private long bytesSent; private String upStream; private String downStream; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date beginTime; } ard-work/src/main/java/com/ruoyi/media/domain/RtmpSessions.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,21 @@ package com.ruoyi.media.domain; import lombok.Data; import java.util.List; /** * @ClassName:Sessions * @Description: * @Author:ard * @Date:2024å¹´07æ12æ¥13:34 * @Version:1.0 **/ @Data public class RtmpSessions { private int itemCount; private int pageCount; private List<RtmpSession> items; } ard-work/src/main/java/com/ruoyi/media/domain/RtspSession.java
@@ -14,11 +14,16 @@ */ @Data public class RtspSession { private String name; private String id; private String path; private Date created; private String remoteAddr; private String state; private String transport; private long bytesReceived; private long bytesSent; private String upStream; private String downStream; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date beginTime; } ard-work/src/main/java/com/ruoyi/media/domain/RtspSessions.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,21 @@ package com.ruoyi.media.domain; import lombok.Data; import java.util.List; /** * @ClassName:Sessions * @Description: * @Author:ard * @Date:2024å¹´07æ12æ¥13:34 * @Version:1.0 **/ @Data public class RtspSessions { private int itemCount; private int pageCount; private List<RtspSession> items; } ard-work/src/main/java/com/ruoyi/media/domain/WebrtcSession.java
@@ -13,11 +13,18 @@ **/ @Data public class WebrtcSession { private String name; private String id; private String path; private Date created; private String remoteAddr; private String peerConnectionEstablished; private String localCandidate; private String remoteCandidate; private String state; private long bytesReceived; private long bytesSent; private String upStream; private String downStream; @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss") private Date beginTime; } ard-work/src/main/java/com/ruoyi/media/domain/WebrtcSessions.java
¶Ô±ÈÐÂÎļþ @@ -0,0 +1,20 @@ package com.ruoyi.media.domain; import lombok.Data; import java.util.List; /** * @ClassName:Sessions * @Description: * @Author:ard * @Date:2024å¹´07æ12æ¥13:34 * @Version:1.0 **/ @Data public class WebrtcSessions { private int itemCount; private int pageCount; private List<WebrtcSession> items; } ard-work/src/main/java/com/ruoyi/media/service/IMediaService.java
@@ -1,5 +1,6 @@ package com.ruoyi.media.service; import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.media.domain.*; import java.util.List; import java.util.Map; @@ -45,9 +46,11 @@ RtmpSession getRtmpSessionById(String sessionId); List<StreamInfo> getPushStreamList(Integer pageNum,Integer pageSize); TableDataInfo getPushStreamList(Integer pageNum,Integer pageSize); List<StreamInfo> getPullStreamList(Integer pageNum,Integer pageSize); TableDataInfo getPullWebrtcStreamList(Integer pageNum, Integer pageSize); TableDataInfo getPullRtspStreamList(Integer pageNum, Integer pageSize); TableDataInfo getPullRtmpStreamList(Integer pageNum, Integer pageSize); Boolean kickRtspSession(String sessionId); ard-work/src/main/java/com/ruoyi/media/service/impl/MediaServiceImpl.java
@@ -3,6 +3,8 @@ import com.alibaba.fastjson2.JSONObject; import com.dtflys.forest.exceptions.ForestNetworkException; import com.dtflys.forest.exceptions.ForestRuntimeException; import com.ruoyi.common.constant.HttpStatus; import com.ruoyi.common.core.page.TableDataInfo; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.media.domain.*; import com.ruoyi.media.service.IMediaService; @@ -10,6 +12,7 @@ import com.ruoyi.utils.tools.ArdTool; import io.swagger.models.auth.In; import lombok.extern.slf4j.Slf4j; import org.apache.poi.ss.formula.functions.T; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.ApplicationArguments; import org.springframework.boot.ApplicationRunner; @@ -243,9 +246,12 @@ * 2023/8/29 9:37:05 */ @Override public List<StreamInfo> getPushStreamList(Integer pageNum, Integer pageSize) { public TableDataInfo getPushStreamList(Integer pageNum, Integer pageSize) { TableDataInfo tableDataInfo = new TableDataInfo(); List<StreamInfo> PushStreamInfoList = new ArrayList<>(); Paths paths = mediaClient.paths(pageNum - 1, pageSize); int itemCount = paths.getItemCount(); tableDataInfo.setTotal(itemCount); List<Items> items = paths.getItems(); for (Items item : items) { StreamInfo info = new StreamInfo(); @@ -309,105 +315,82 @@ PushStreamInfoList.add(info); } return PushStreamInfoList; tableDataInfo.setRows(PushStreamInfoList); tableDataInfo.setCode(HttpStatus.SUCCESS); tableDataInfo.setMsg("æ¥è¯¢æå"); return tableDataInfo; } /** * è·åææµå表 * è·åwebrtcææµå表 * åèä¹ * 2023/8/29 9:37:05 */ @Override public List<StreamInfo> getPullStreamList(Integer pageNum, Integer pageSize) { List<StreamInfo> PullStreamInfoList = new ArrayList<>(); Paths paths = mediaClient.paths(pageNum - 1, pageSize); List<Items> items = paths.getItems(); for (Items item : items) { List<Readers> readers = item.getReaders(); for (Readers reader : readers) { StreamInfo info = new StreamInfo(); //ID String name = item.getName(); info.setName(name); Conf conf = mediaClient.getPathInfo(name); //ä¼ è¾åè®® info.setProtocol(conf.getSourceProtocol()); String type = reader.getType(); switch (type) { case "rtmpConn": info.setSessionType("rtmp"); //webrtcææ¾å°å String url = "rtmp://" + mediamtxHost + ":1935/" + name; info.setRtspUrl(url); RtmpSession rtmpSession = getRtmpSessionById(reader.getId()); //ä¼è¯ID info.setId(rtmpSession.getId()); //å¼å§ææµæ¶é´ info.setBeginTime(rtmpSession.getCreated()); //ä¸è¡æµé long bytesReceived = rtmpSession.getBytesReceived(); String formatReceivedSize = ArdTool.formatFileSize(bytesReceived); info.setUpTraffic(formatReceivedSize); //ä¸è¡æµé long bytesSent = rtmpSession.getBytesSent(); String formatSentSize = ArdTool.formatFileSize(bytesSent); info.setDownTraffic(formatSentSize); //ææµæå¡å¨ info.setRemoteAddr(rtmpSession.getRemoteAddr()); PullStreamInfoList.add(info); break; case "webRTCSession": case "webrtcSession": info.setSessionType("webrtc"); //webrtcææ¾å°å url = "http://" + mediamtxHost + ":8889/" + name; info.setRtspUrl(url); WebrtcSession webrtcSession = getWebrtcSessionById(reader.getId()); //ä¼è¯ID info.setId(webrtcSession.getId()); //å¼å§ææµæ¶é´ info.setBeginTime(webrtcSession.getCreated()); //ä¸è¡æµé bytesReceived = webrtcSession.getBytesReceived(); formatReceivedSize = ArdTool.formatFileSize(bytesReceived); info.setUpTraffic(formatReceivedSize); //ä¸è¡æµé bytesSent = webrtcSession.getBytesSent(); formatSentSize = ArdTool.formatFileSize(bytesSent); info.setDownTraffic(formatSentSize); //ææµæå¡å¨ info.setRemoteAddr(webrtcSession.getRemoteAddr()); PullStreamInfoList.add(info); break; case "rtspSession": info.setSessionType("rtsp"); //RTSPææ¾å°å String rtspUrl = "rtsp://" + mediamtxHost + ":8554/" + name; info.setRtspUrl(rtspUrl); RtspSession rtspSession = getRtspSessionById(reader.getId()); //ä¼è¯ID info.setId(rtspSession.getId()); //å¼å§ææµæ¶é´ info.setBeginTime(rtspSession.getCreated()); //ä¸è¡æµé bytesReceived = rtspSession.getBytesReceived(); formatReceivedSize = ArdTool.formatFileSize(bytesReceived); info.setUpTraffic(formatReceivedSize); //ä¸è¡æµé bytesSent = rtspSession.getBytesSent(); formatSentSize = ArdTool.formatFileSize(bytesSent); info.setDownTraffic(formatSentSize); //ææµæå¡å¨ info.setRemoteAddr(rtspSession.getRemoteAddr()); PullStreamInfoList.add(info); break; public TableDataInfo getPullWebrtcStreamList(Integer pageNum, Integer pageSize) { TableDataInfo tableDataInfo = new TableDataInfo(); WebrtcSessions WebrtcSessions = mediaClient.webrtcsessions(pageNum-1, pageSize); List<WebrtcSession> webrtcsessions = WebrtcSessions.getItems(); webrtcsessions.stream().forEach( webrtcSession -> { webrtcSession.setUpStream(ArdTool.formatFileSize(webrtcSession.getBytesReceived())); webrtcSession.setDownStream(ArdTool.formatFileSize(webrtcSession.getBytesSent())); webrtcSession.setBeginTime(webrtcSession.getCreated()); } } } Comparator<StreamInfo> comparator = Comparator.comparing(streamInfo -> streamInfo.getBeginTime()); // 使ç¨Collections.sortæ¹æ³è¿è¡æåº Collections.sort(personList, comparator); Collections.sort(PullStreamInfoList, comparator.reversed()); return PullStreamInfoList; ); tableDataInfo.setTotal(WebrtcSessions.getItemCount()); tableDataInfo.setRows(webrtcsessions); tableDataInfo.setCode(HttpStatus.SUCCESS); tableDataInfo.setMsg("æ¥è¯¢æå"); return tableDataInfo; } /** * è·årtmpææµå表 * åèä¹ * 2023/8/29 9:37:05 */ @Override public TableDataInfo getPullRtmpStreamList(Integer pageNum, Integer pageSize) { TableDataInfo tableDataInfo = new TableDataInfo(); RtmpSessions rtmpSessions = mediaClient.rtmpsessions(pageNum-1, pageSize); List<RtmpSession> webrtcsessions = rtmpSessions.getItems(); webrtcsessions.stream().forEach( webrtcSession -> { webrtcSession.setUpStream(ArdTool.formatFileSize(webrtcSession.getBytesReceived())); webrtcSession.setDownStream(ArdTool.formatFileSize(webrtcSession.getBytesSent())); webrtcSession.setBeginTime(webrtcSession.getCreated()); } ); tableDataInfo.setTotal(rtmpSessions.getItemCount()); tableDataInfo.setRows(webrtcsessions); tableDataInfo.setCode(HttpStatus.SUCCESS); tableDataInfo.setMsg("æ¥è¯¢æå"); return tableDataInfo; } /** * è·årtspææµå表 * åèä¹ * 2023/8/29 9:37:05 */ @Override public TableDataInfo getPullRtspStreamList(Integer pageNum, Integer pageSize) { TableDataInfo tableDataInfo = new TableDataInfo(); RtspSessions rtspSessions = mediaClient.rtspsessions(pageNum-1, pageSize); List<RtspSession> webrtcsessions = rtspSessions.getItems(); webrtcsessions.stream().forEach( webrtcSession -> { webrtcSession.setUpStream(ArdTool.formatFileSize(webrtcSession.getBytesReceived())); webrtcSession.setDownStream(ArdTool.formatFileSize(webrtcSession.getBytesSent())); webrtcSession.setBeginTime(webrtcSession.getCreated()); } ); tableDataInfo.setTotal(rtspSessions.getItemCount()); tableDataInfo.setRows(webrtcsessions); tableDataInfo.setCode(HttpStatus.SUCCESS); tableDataInfo.setMsg("æ¥è¯¢æå"); return tableDataInfo; } /** ard-work/src/main/java/com/ruoyi/utils/forest/MediaClient.java
@@ -4,6 +4,7 @@ import com.ruoyi.media.domain.*; import java.nio.file.Path; import java.util.List; /** * @Description: mediamtxæµåªä½å®¢æ·ç«¯ @@ -47,8 +48,20 @@ /** * æ¥è¯¢æærtspä¼è¯ */ @Get("/rtspsessions/list") public String rtspsessions(); @Get("/rtspsessions/list?page={pageNum}&itemsPerPage={pageSize}") public RtspSessions rtspsessions(@Var("pageNum") Integer pageNum, @Var("pageSize") Integer pageSize); /** * æ¥è¯¢ææwebrtä¼è¯ */ @Get("/webrtcsessions/list?page={pageNum}&itemsPerPage={pageSize}") public WebrtcSessions webrtcsessions(@Var("pageNum") Integer pageNum, @Var("pageSize") Integer pageSize); /** * æ¥è¯¢æærtmpä¼è¯ */ @Get("/rtmpsessions/list?page={pageNum}&itemsPerPage={pageSize}") public RtmpSessions rtmpsessions(@Var("pageNum") Integer pageNum, @Var("pageSize") Integer pageSize); /** * æsessionIdæ¥è¯¢rtspä¼è¯ ard-work/src/main/java/com/ruoyi/utils/sdk/hiksdk/controller/HikSdkController.java
@@ -18,7 +18,6 @@ import org.springframework.web.bind.annotation.*; import springfox.documentation.annotations.ApiIgnore; import javax.annotation.Resource; import javax.servlet.http.HttpServletResponse; import java.util.List; import java.util.Map; ard-work/src/main/resources/templates/preview.html
@@ -101,7 +101,7 @@ <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.227:8889/164/" style="width: 250px"/> <input id="videoUrl" type="text" value="http://192.168.1.227:8889/0d1c9f80a7b4480c8b401ba6b140b581_1/" style="width: 250px"/> </div> </div> </div> @@ -242,6 +242,7 @@ } // this.eTag = res.headers.get('ETag'); this.eTag = res.headers.get("ETag") || res.headers.get('E-Tag'); this.wurl = new URL(res.headers.get('location'), this.wurl.origin).toString(); return res.text(); }) .then((sdp) => this.onRemoteAnswer(new RTCSessionDescription({ ard-work/src/main/resources/templates/test.html
@@ -10,7 +10,6 @@ .top-buffer { margin-top: 10px; } .container { border: 2px solid #1b6d85; padding: 15px; @@ -143,7 +142,7 @@ </div> </div> <script th:inline="javascript"> var cameraId, chanNo, opt, optOpen, optClose, token; var cameraId, chanNo,opt, optOpen, optClose, token; window.onload = function () { console.log(RTCRtpReceiver.getCapabilities('video').codecs) opt = {"username": "admin", "password": "admin123"}; @@ -879,7 +878,7 @@ function commondMethod(url, code, enable) { cameraId = $('#selectDev option:selected').val(); chanNo = $('#selectChn option:selected').val(); opt = {"cameraId": cameraId, "chanNo": chanNo, "speed": 8, "enable": enable, "code": code}; opt = {"cameraId": cameraId, "chanNo": chanNo, "speed": 4, "enable": enable, "code": code}; $.ajax({ headers: { 'Accept': 'application/json', @@ -910,30 +909,17 @@ type: "get", dataType: "json", success: function (data) { realView(data.data.webrtcUrl + "/"); realView(data.data.webrtcUrl + "/", e.target.id); } }) }); let webrtcClient; //whepæä½æ¹æ³ const retryPause = 2000; const video = document.getElementById('video'); let nonAdvertisedCodecs = []; let pc = null; let restartTimeout = null; let sessionUrl = ''; let offerData = ''; let queuedCandidates = []; let defaultControls = false; let whepUrl = ''; const restartPause = 2000; const unquoteCredential = (v) => ( JSON.parse(`"${v}"`) ); const linkToIceServers = (links) => ( (links !== null) ? links.split(', ').map((link) => { const m = link.match(/^<(.+?)>; rel="ice-server"(; username="(.*?)"; credential="(.*?)"; credential-type="password")?/i); @@ -944,21 +930,20 @@ if (m[3] !== undefined) { ret.username = unquoteCredential(m[3]); ret.credential = unquoteCredential(m[4]); ret.credentialType = 'password'; ret.credentialType = "password"; } return ret; }) : [] ); const parseOffer = (sdp) => { const parseOffer = (offer) => { const ret = { iceUfrag: '', icePwd: '', medias: [], }; for (const line of sdp.split('\r\n')) { 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:')) { @@ -970,131 +955,7 @@ return ret; }; const enableStereoPcmau = (section) => { let lines = section.split('\r\n'); lines[0] += ' 118'; lines.splice(lines.length - 1, 0, 'a=rtpmap:118 PCMU/8000/2'); lines.splice(lines.length - 1, 0, 'a=rtcp-fb:118 transport-cc'); lines[0] += ' 119'; lines.splice(lines.length - 1, 0, 'a=rtpmap:119 PCMA/8000/2'); lines.splice(lines.length - 1, 0, 'a=rtcp-fb:119 transport-cc'); return lines.join('\r\n'); }; const enableMultichannelOpus = (section) => { let lines = section.split('\r\n'); lines[0] += " 112"; lines.splice(lines.length - 1, 0, "a=rtpmap:112 multiopus/48000/3"); lines.splice(lines.length - 1, 0, "a=fmtp:112 channel_mapping=0,2,1;num_streams=2;coupled_streams=1"); lines.splice(lines.length - 1, 0, "a=rtcp-fb:112 transport-cc"); lines[0] += " 113"; lines.splice(lines.length - 1, 0, "a=rtpmap:113 multiopus/48000/4"); lines.splice(lines.length - 1, 0, "a=fmtp:113 channel_mapping=0,1,2,3;num_streams=2;coupled_streams=2"); lines.splice(lines.length - 1, 0, "a=rtcp-fb:113 transport-cc"); lines[0] += " 114"; lines.splice(lines.length - 1, 0, "a=rtpmap:114 multiopus/48000/5"); lines.splice(lines.length - 1, 0, "a=fmtp:114 channel_mapping=0,4,1,2,3;num_streams=3;coupled_streams=2"); lines.splice(lines.length - 1, 0, "a=rtcp-fb:114 transport-cc"); lines[0] += " 115"; lines.splice(lines.length - 1, 0, "a=rtpmap:115 multiopus/48000/6"); lines.splice(lines.length - 1, 0, "a=fmtp:115 channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2"); lines.splice(lines.length - 1, 0, "a=rtcp-fb:115 transport-cc"); lines[0] += " 116"; lines.splice(lines.length - 1, 0, "a=rtpmap:116 multiopus/48000/7"); lines.splice(lines.length - 1, 0, "a=fmtp:116 channel_mapping=0,4,1,2,3,5,6;num_streams=4;coupled_streams=4"); lines.splice(lines.length - 1, 0, "a=rtcp-fb:116 transport-cc"); lines[0] += " 117"; lines.splice(lines.length - 1, 0, "a=rtpmap:117 multiopus/48000/8"); lines.splice(lines.length - 1, 0, "a=fmtp:117 channel_mapping=0,6,1,4,5,2,3,7;num_streams=5;coupled_streams=4"); lines.splice(lines.length - 1, 0, "a=rtcp-fb:117 transport-cc"); return lines.join('\r\n'); }; const enableL16 = (section) => { let lines = section.split('\r\n'); lines[0] += " 120"; lines.splice(lines.length - 1, 0, "a=rtpmap:120 L16/8000/2"); lines.splice(lines.length - 1, 0, "a=rtcp-fb:120 transport-cc"); lines[0] += " 121"; lines.splice(lines.length - 1, 0, "a=rtpmap:121 L16/16000/2"); lines.splice(lines.length - 1, 0, "a=rtcp-fb:121 transport-cc"); lines[0] += " 122"; lines.splice(lines.length - 1, 0, "a=rtpmap:122 L16/48000/2"); lines.splice(lines.length - 1, 0, "a=rtcp-fb:122 transport-cc"); return lines.join('\r\n'); }; const enableStereoOpus = (section) => { let opusPayloadFormat = ''; let lines = section.split('\r\n'); for (let i = 0; i < lines.length; i++) { if (lines[i].startsWith('a=rtpmap:') && lines[i].toLowerCase().includes('opus/')) { opusPayloadFormat = lines[i].slice('a=rtpmap:'.length).split(' ')[0]; break; } } if (opusPayloadFormat === '') { return section; } for (let i = 0; i < lines.length; i++) { if (lines[i].startsWith('a=fmtp:' + opusPayloadFormat + ' ')) { if (!lines[i].includes('stereo')) { lines[i] += ';stereo=1'; } if (!lines[i].includes('sprop-stereo')) { lines[i] += ';sprop-stereo=1'; } } } return lines.join('\r\n'); }; const editOffer = (sdp) => { const sections = sdp.split('m='); for (let i = 0; i < sections.length; i++) { if (sections[i].startsWith('audio')) { sections[i] = enableStereoOpus(sections[i]); if (nonAdvertisedCodecs.includes('pcma/8000/2')) { sections[i] = enableStereoPcmau(sections[i]); } if (nonAdvertisedCodecs.includes('multiopus/48000/6')) { sections[i] = enableMultichannelOpus(sections[i]); } if (nonAdvertisedCodecs.includes('L16/48000/2')) { sections[i] = enableL16(sections[i]); } break; } } return sections.join('m='); }; const generateSdpFragment = (od, candidates) => { const generateSdpFragment = (offerData, candidates) => { const candidatesByMedia = {}; for (const candidate of candidates) { const mid = candidate.sdpMLineIndex; @@ -1104,12 +965,12 @@ candidatesByMedia[mid].push(candidate); } let frag = 'a=ice-ufrag:' + od.iceUfrag + '\r\n' + 'a=ice-pwd:' + od.icePwd + '\r\n'; let frag = 'a=ice-ufrag:' + offerData.iceUfrag + '\r\n' + 'a=ice-pwd:' + offerData.icePwd + '\r\n'; let mid = 0; for (const media of od.medias) { for (const media of offerData.medias) { if (candidatesByMedia[mid] !== undefined) { frag += 'm=' + media + '\r\n' + 'a=mid:' + mid + '\r\n'; @@ -1122,256 +983,181 @@ } return frag; }; } const loadStream = () => { console.log('loadStream'); requestICEServers(); }; class WHEPClient { constructor(whepUrl, videoId) { this.video = videoId; this.wurl = new URL('whep', whepUrl); this.pc = null; this.restartTimeout = null; this.eTag = ''; this.queuedCandidates = []; this.start(); } const supportsNonAdvertisedCodec = (codec, fmtp) => ( new Promise((resolve, reject) => { const pc = new RTCPeerConnection({iceServers: []}); pc.addTransceiver('audio', {direction: 'recvonly'}); pc.createOffer() .then((offer) => { if (offer.sdp.includes(' ' + codec)) { // codec is advertised, there's no need to add it manually resolve(false); return; start() { console.log("requesting ICE servers"); fetch(this.wurl, { 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"); console.log(this.wurl); fetch(this.wurl, { method: 'POST', headers: { 'Content-Type': 'application/sdp', }, body: offer.sdp, }) .then((res) => { if (res.status !== 201) { throw new Error('bad status code'); } const sections = offer.sdp.split('m=audio'); const lines = sections[1].split('\r\n'); lines[0] += ' 118'; lines.splice(lines.length - 1, 0, 'a=rtpmap:118 ' + codec); if (fmtp !== undefined) { lines.splice(lines.length - 1, 0, 'a=fmtp:118 ' + fmtp); // this.eTag = res.headers.get('ETag'); this.eTag = res.headers.get("ETag") || res.headers.get('E-Tag'); this.wurl = new URL(res.headers.get('location'), this.wurl.origin).toString(); 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 === '') { console.log("222222222222222222222") this.queuedCandidates.push(evt.candidate); } else { console.log("333333333333333333333") this.sendLocalCandidates([evt.candidate]) } } } sendLocalCandidates(candidates) { fetch(this.wurl, { 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'); } sections[1] = lines.join('\r\n'); offer.sdp = sections.join('m=audio'); return pc.setLocalDescription(offer); }) .then(() => { return pc.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp: 'v=0\r\n' + 'o=- 6539324223450680508 0 IN IP4 0.0.0.0\r\n' + 's=-\r\n' + 't=0 0\r\n' + 'a=fingerprint:sha-256 0D:9F:78:15:42:B5:4B:E6:E2:94:3E:5B:37:78:E1:4B:54:59:A3:36:3A:E5:05:EB:27:EE:8F:D2:2D:41:29:25\r\n' + 'm=audio 9 UDP/TLS/RTP/SAVPF 118\r\n' + 'c=IN IP4 0.0.0.0\r\n' + 'a=ice-pwd:7c3bf4770007e7432ee4ea4d697db675\r\n' + 'a=ice-ufrag:29e036dc\r\n' + 'a=sendonly\r\n' + 'a=rtcp-mux\r\n' + 'a=rtpmap:118 ' + codec + '\r\n' + ((fmtp !== undefined) ? 'a=fmtp:118 ' + fmtp + '\r\n' : ''), })); }) .then(() => { resolve(true); }) .catch((err) => { resolve(false); }) .finally(() => { pc.close(); console.log('error: ' + err); this.scheduleRestart(); }); }) ); } const getNonAdvertisedCodecs = () => { Promise.all([ ['pcma/8000/2'], ['multiopus/48000/6', 'channel_mapping=0,4,1,2,3,5;num_streams=4;coupled_streams=2'], ['L16/48000/2'] ].map((c) => supportsNonAdvertisedCodec(c[0], c[1]).then((r) => (r) ? c[0] : false))) .then((c) => c.filter((e) => e !== false)) .then((codecs) => { nonAdvertisedCodecs = codecs; loadStream(); }); }; const onError = (err) => { if (restartTimeout === null) { if (pc !== null) { pc.close(); pc = null; scheduleRestart() { if (this.restartTimeout !== null) { return; } restartTimeout = window.setTimeout(() => { restartTimeout = null; loadStream(); }, retryPause); if (this.sessionUrl) { fetch(this.sessionUrl, { method: 'DELETE', }); if (this.pc !== null) { this.pc.close(); this.pc = null; } this.sessionUrl = ''; queuedCandidates = []; this.restartTimeout = window.setTimeout(() => { this.restartTimeout = null; this.start(); }, restartPause); this.eTag = ''; this.queuedCandidates = []; } }; const sendLocalCandidates = (candidates) => { fetch(new URL('whep', this.whepUrl), { method: 'PATCH', headers: { 'Content-Type': 'application/trickle-ice-sdpfrag', 'If-Match': '*', }, body: generateSdpFragment(offerData, candidates), }) .then((res) => { switch (res.status) { case 204: break; case 404: throw new Error('stream not found'); default: throw new Error(`bad status code ${res.status}`); stop() { if (this.pc) { try { this.pc.close(); } catch (e) { console.log("Failure close peer connection:" + e); } }) .catch((err) => { onError(err.toString()); }); }; const onLocalCandidate = (evt) => { if (restartTimeout !== null) { return; } if (evt.candidate !== null) { if (this.sessionUrl === '') { queuedCandidates.push(evt.candidate); } else { //sendLocalCandidates([evt.candidate]) this.pc = null; } } }; } const onRemoteAnswer = (sdp) => { if (restartTimeout !== null) { return; } pc.setRemoteDescription(new RTCSessionDescription({ type: 'answer', sdp, })) .then(() => { if (queuedCandidates.length !== 0) { sendLocalCandidates(queuedCandidates); queuedCandidates = []; } }) .catch((err) => { onError(err.toString()); }); }; const sendOffer = (offer) => { fetch(new URL('whep', this.whepUrl), { method: 'POST', headers: { 'Content-Type': 'application/sdp', }, body: offer.sdp, }) .then((res) => { switch (res.status) { case 201: break; case 404: throw new Error('stream not found'); case 400: return res.json().then((e) => { throw new Error(e.error); }); default: throw new Error(`bad status code ${res.status}`); } this.sessionUrl = new URL(res.headers.get('location'), new URL(this.whepUrl).origin).toString(); return res.text() .then((sdp) => onRemoteAnswer(sdp)); }) .catch((err) => { onError(err.toString()); }); }; const createOffer = () => { pc.createOffer() .then((offer) => { offer.sdp = editOffer(offer.sdp); offerData = parseOffer(offer.sdp); pc.setLocalDescription(offer) .then(() => { sendOffer(offer); }) .catch((err) => { onError(err.toString()); }); }) .catch((err) => { onError(err.toString()); }); }; const onConnectionState = () => { if (restartTimeout !== null) { return; } if (pc.iceConnectionState === 'disconnected') { onError('peer connection closed'); } }; const onTrack = (evt) => { video.srcObject = evt.streams[0]; }; const requestICEServers = () => { fetch(new URL('whep', this.whepUrl), { method: 'OPTIONS', }) .then((res) => { pc = new RTCPeerConnection({ iceServers: linkToIceServers(res.headers.get('Link')), // https://webrtc.org/getting-started/unified-plan-transition-guide sdpSemantics: 'unified-plan', }); const direction = 'sendrecv'; pc.addTransceiver('video', {direction}); pc.addTransceiver('audio', {direction}); pc.onicecandidate = (evt) => onLocalCandidate(evt); pc.oniceconnectionstatechange = () => onConnectionState(); pc.ontrack = (evt) => onTrack(evt); createOffer(); }) .catch((err) => { onError(err.toString()); }); }; const init = () => { getNonAdvertisedCodecs(); }; function realView(whepUrl) { console.log(whepUrl) this.whepUrl = whepUrl init(); function realView(whepUrl, videoId) { webrtcClient = new WHEPClient(whepUrl, videoId); } </script> </body> ard-work/src/main/resources/templates/test1.html
ÎļþÒÑɾ³ý server/mediamtx/mediamtx.yml
@@ -222,7 +222,7 @@ password: '' # List of interfaces that will be used to gather IPs to send # to the counterpart to establish a connection. webrtcICEInterfaces: [] webrtcICEInterfaces: [192.168.1.1] # List of public IP addresses that are to be used as a host. # This is used typically for servers that are behind 1:1 D-NAT. webrtcICEHostNAT1To1IPs: [192.168.1.227]