From a27d12fab08cc0ce2ca60b19b2809f30ee2bfb76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Olof=20K=C3=A4llander?= Date: Tue, 3 Dec 2024 08:21:13 +0100 Subject: [PATCH] added hangup --- examples/simpleclient/src/index.html | 1 + examples/simpleclient/src/main.ts | 154 +++++++++++++++--- .../symphony/simpleserver/Conferences.java | 26 +++ .../simpleserver/httpClient/HttpClient.java | 57 +++++-- .../simpleserver/smb/SymphonyMediaBridge.java | 34 ++-- 5 files changed, 217 insertions(+), 55 deletions(-) diff --git a/examples/simpleclient/src/index.html b/examples/simpleclient/src/index.html index 403330d8c..d299ce7bb 100644 --- a/examples/simpleclient/src/index.html +++ b/examples/simpleclient/src/index.html @@ -9,6 +9,7 @@ + diff --git a/examples/simpleclient/src/main.ts b/examples/simpleclient/src/main.ts index d09288ca2..764cd6012 100644 --- a/examples/simpleclient/src/main.ts +++ b/examples/simpleclient/src/main.ts @@ -8,18 +8,20 @@ let peerConnection: RTCPeerConnection|undefined = undefined let localMediaStream: MediaStream|undefined = undefined; let localDataChannel: RTCDataChannel|undefined = undefined; let endpointId: string|undefined = undefined; +let conferenceId: string|undefined = undefined; let remoteMediaStreams: Set = new Set(); const serverUrl = 'https://localhost:8081/conferences/'; interface UserVideoMapItem { - ssrc: number; + ssrc: Number; msid: String; element: HTMLVideoElement; } let receivers = new Map(); +let keepPolling = true; // Keeps a long-poll running for simpleserver to simpleclient communication async function startPoll() @@ -41,7 +43,10 @@ async function startPoll() return; } - startPoll(); + if (keepPolling) + { + startPoll(); + } } async function onPollMessage(resultJson: any) @@ -121,12 +126,71 @@ function onTrack(event: RTCTrackEvent) videoElement.height = 180; videoElement.muted = false; videoElementsDiv.appendChild(videoElement); - receivers.set(event.track.id, {ssrc : 0, msid : event.track.id as String, element : videoElement}); + var mapItem: UserVideoMapItem = { + ssrc : 0, // not available anyway + msid : event.track.id as String, + element : videoElement + }; + + receivers.set(event.track.id, mapItem); console.log('Added video element ' + stream.id); } } } +function getSsrcOfVideoMsid(trackId: String): Number +{ + var rtpReceivers = peerConnection.getReceivers(); + for (var rtpReceiver of rtpReceivers) + { + var ssrcs = rtpReceiver.getSynchronizationSources(); + if (ssrcs.length == 0 || rtpReceiver.track.kind != "video") + { + continue; + } + + if (rtpReceiver.track.label == trackId) + { + return ssrcs[0].source; + } + } + return null; +} + +function getVideoElementBySsrc(ssrc: Number): HTMLVideoElement +{ + var rtpReceivers = peerConnection.getReceivers(); + for (var rtpReceiver of rtpReceivers) + { + var ssrcs = rtpReceiver.getSynchronizationSources(); + if (ssrcs.length == 0 || rtpReceiver.track.kind != "video") + { + continue; + } + + if (ssrcs[0].source == ssrc) + { + var mapItem = receivers.get(rtpReceiver.track.label); + return mapItem.element; + } + } + return null; +} + +function getAllUserMapSsrcs(umap: any): Set +{ + var s = new Set(); + for (var endpoint of umap.endpoints) + { + for (var ssrc of endpoint.ssrcs) + { + s.add(ssrc); + } + } + + return s; +} + function onDataChannelMessage(event: MessageEvent) { console.log('onDataChannelMessage ' + event.data); @@ -147,31 +211,41 @@ function onDataChannelMessage(event: MessageEvent) } else if (message.type === 'UserMediaMap') { + /* var activeUsers = getAllUserMapSsrcs(message); + for (var v of videoElementsDiv.children) + { + const ssrc = v.getAttribute("custom_ssrc"); + if (ssrc != null && !(ssrc in activeUsers)) + { + var videoElem = v as HTMLVideoElement; + videoElem.currentTime = 0; + } + }*/ + for (const endpoint of message.endpoints) { for (var ssrc of endpoint.ssrcs) { - console.log("ssrc {} speaking", ssrc) + console.log("ssrc speaking", ssrc) - var rtpReceivers = peerConnection.getReceivers(); - for (var rtpReceiver of rtpReceivers) + var videoElement = getVideoElementBySsrc(ssrc); + /*if (videoElement.getAttribute("custom_ssrc") == null) { - var ssrcs = rtpReceiver.getSynchronizationSources(); - if (ssrcs.length == 0 || rtpReceiver.track.kind != "video") - { - continue; - } - - if (ssrcs[0].source == ssrc) - { - var mapItem = receivers.get(rtpReceiver.track.label); - if (videoElementsDiv.firstChild != mapItem.element) - { - videoElementsDiv.removeChild(mapItem.element); - videoElementsDiv.insertBefore(mapItem.element, videoElementsDiv.firstChild); - } - return; - } + videoElement.setAttribute("custom_ssrc", ssrc.toString()); + }*/ + + if (videoElement && videoElementsDiv.firstChild != videoElement) + { + videoElementsDiv.removeChild(videoElement); + var speaker = videoElementsDiv.firstChild as HTMLVideoElement + speaker.width = 320; + speaker.height = 180; + videoElementsDiv.insertBefore(videoElement, videoElementsDiv.firstChild); + var speaker = videoElementsDiv.firstChild as HTMLVideoElement + speaker.width = 640; + speaker.height = 360; + + return; } } @@ -224,7 +298,9 @@ async function joinClicked() console.log('Join result ' + JSON.stringify(resultJson)); endpointId = resultJson.endpointId; + conferenceId = resultJson endpointIdLabel.innerText = endpointId; + keepPolling = true; startPoll() } @@ -287,11 +363,45 @@ async function listVideoDevices() } } +async function hangupClicked() +{ + keepPolling = false; + const url = serverUrl + 'endpoints/' + endpointId + '/actions'; + const body = {type : 'hangup'}; + + const requestInit: RequestInit = {method : 'POST', mode : 'cors', cache : 'no-store', body : JSON.stringify(body)}; + const request = new Request(url, requestInit); + const result = await fetch(request); + + console.log('hangup result ' + result.status); + + localMediaStream.getTracks().forEach(function(track) { track.stop() }) + localMediaStream = null; + localDataChannel.close(); + localDataChannel.onmessage = null; + localDataChannel = null; + peerConnection.ontrack = null; + peerConnection.onicegatheringstatechange = null; + peerConnection.ondatachannel = null; + remoteMediaStreams.clear(); + peerConnection.close(); + peerConnection = null; + + const localVideo = document.getElementById("localVideo") as HTMLVideoElement; + localVideo.srcObject = null; + + audioElementsDiv.textContent = ''; + videoElementsDiv.textContent = ''; +} + async function main() { var joinButton = document.getElementById('join') as HTMLButtonElement; joinButton.onclick = joinClicked; + var hangupButton = document.getElementById('hangup') as HTMLButtonElement; + hangupButton.onclick = hangupClicked; + await listAudioDevices(); await listVideoDevices(); } diff --git a/examples/simpleserver/src/main/java/com/symphony/simpleserver/Conferences.java b/examples/simpleserver/src/main/java/com/symphony/simpleserver/Conferences.java index 744815274..4a4600059 100644 --- a/examples/simpleserver/src/main/java/com/symphony/simpleserver/Conferences.java +++ b/examples/simpleserver/src/main/java/com/symphony/simpleserver/Conferences.java @@ -100,6 +100,10 @@ public synchronized boolean message(String endpointId, String messageString) { return onAnswer(endpointId, message); } + else if (message.type.equals("hangup")) + { + return onHangup(endpointId); + } return false; } @@ -170,6 +174,28 @@ else if (mediaDescription.type == MediaDescription.Type.VIDEO) return true; } + private boolean onHangup(String endpointId) + throws ParserFailedException, IOException, InterruptedException, ParseException + { + final var endpoint = endpoints.get(endpointId); + if (endpoint == null) + { + return false; + } + endpoints.remove(endpointId); + messageQueues.remove(endpointId); + try + { + symphonyMediaBridge.deleteEndpoint(conferenceId, endpointId); + return true; + } + catch (IOException | ParseException e) + { + LOGGER.error("Error deleting endpoint ", e); + return false; + } + } + public String poll(String endpointId) throws InterruptedException { final var queue = messageQueues.get(endpointId); diff --git a/examples/simpleserver/src/main/java/com/symphony/simpleserver/httpClient/HttpClient.java b/examples/simpleserver/src/main/java/com/symphony/simpleserver/httpClient/HttpClient.java index 31bde0722..d628b60cc 100644 --- a/examples/simpleserver/src/main/java/com/symphony/simpleserver/httpClient/HttpClient.java +++ b/examples/simpleserver/src/main/java/com/symphony/simpleserver/httpClient/HttpClient.java @@ -1,21 +1,24 @@ package com.symphony.simpleserver.httpClient; import com.fasterxml.jackson.databind.JsonNode; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import org.apache.hc.client5.http.classic.methods.HttpDelete; import org.apache.hc.client5.http.classic.methods.HttpPost; import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; import org.apache.hc.core5.http.ParseException; import org.apache.hc.core5.http.io.entity.EntityUtils; import org.apache.hc.core5.http.io.entity.StringEntity; -import java.io.IOException; -import java.nio.charset.StandardCharsets; - -public class HttpClient { - public static class ResponsePair { +public class HttpClient +{ + public static class ResponsePair + { public final int statusCode; public final String body; - public ResponsePair(int statusCode, String body) { + public ResponsePair(int statusCode, String body) + { this.statusCode = statusCode; this.body = body; } @@ -23,21 +26,23 @@ public ResponsePair(int statusCode, String body) { private final CloseableHttpClient httpClient; - HttpClient(CloseableHttpClient httpClient) { - this.httpClient = httpClient; - } + HttpClient(CloseableHttpClient httpClient) { this.httpClient = httpClient; } - public ResponsePair post(String url, JsonNode data) throws IOException, ParseException { + public ResponsePair post(String url, JsonNode data) throws IOException, ParseException + { final var request = new HttpPost(url); request.addHeader("Content-Type", "application/json"); final var stringEntity = new StringEntity(data.toString(), StandardCharsets.UTF_8); request.setEntity(stringEntity); - try { - try (var httpResponse = httpClient.execute(request)) { + try + { + try (var httpResponse = httpClient.execute(request)) + { final var statusCode = httpResponse.getCode(); - if (statusCode == 204) { + if (statusCode == 204) + { return new ResponsePair(statusCode, null); } @@ -46,9 +51,31 @@ public ResponsePair post(String url, JsonNode data) throws IOException, ParseExc EntityUtils.consumeQuietly(httpEntity); return new ResponsePair(statusCode, responseBody); } - - } finally { + } + finally + { EntityUtils.consumeQuietly(stringEntity); } } + + public ResponsePair delete(String url)throws IOException, ParseException + { + final var request = new HttpDelete(url); + + try (var httpResponse = httpClient.execute(request)) + { + final var statusCode = httpResponse.getCode(); + if (statusCode == 204) + { + return new ResponsePair(statusCode, null); + } + + final var httpEntity = httpResponse.getEntity(); + EntityUtils.consumeQuietly(httpEntity); + return new ResponsePair(statusCode, null); + } + finally + { + } + } } diff --git a/examples/simpleserver/src/main/java/com/symphony/simpleserver/smb/SymphonyMediaBridge.java b/examples/simpleserver/src/main/java/com/symphony/simpleserver/smb/SymphonyMediaBridge.java index a6c8c19fa..02ccbb8d3 100644 --- a/examples/simpleserver/src/main/java/com/symphony/simpleserver/smb/SymphonyMediaBridge.java +++ b/examples/simpleserver/src/main/java/com/symphony/simpleserver/smb/SymphonyMediaBridge.java @@ -8,28 +8,28 @@ import com.symphony.simpleserver.httpClient.HttpClient; import com.symphony.simpleserver.httpClient.HttpClientFactory; import com.symphony.simpleserver.smb.api.SmbEndpointDescription; +import java.io.IOException; import org.apache.hc.core5.http.ParseException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; -import java.io.IOException; - -@Component -public class SymphonyMediaBridge { +@Component public class SymphonyMediaBridge +{ private static final Logger LOGGER = LoggerFactory.getLogger(SymphonyMediaBridge.class); private static final String BASE_URL = "http://127.0.0.1:8080/conferences/"; private final HttpClient httpClient; private final ObjectMapper objectMapper; - @Autowired - public SymphonyMediaBridge(HttpClientFactory httpClientFactory) { + @Autowired public SymphonyMediaBridge(HttpClientFactory httpClientFactory) + { this.httpClient = httpClientFactory.createClient(); this.objectMapper = new ObjectMapper().setSerializationInclusion(JsonInclude.Include.NON_NULL); } - public String allocateConference() throws IOException, ParseException { + public String allocateConference() throws IOException, ParseException + { final var requestBodyJson = JsonNodeFactory.instance.objectNode(); final var response = httpClient.post(BASE_URL, requestBodyJson); @@ -37,7 +37,8 @@ public String allocateConference() throws IOException, ParseException { return responseBodyJson.get("id").asText(); } - public JsonNode allocateEndpoint(String conferenceId, String endpointId) throws IOException, ParseException { + public JsonNode allocateEndpoint(String conferenceId, String endpointId) throws IOException, ParseException + { final var requestBodyJson = JsonNodeFactory.instance.objectNode(); requestBodyJson.put("action", "allocate"); @@ -63,10 +64,11 @@ public JsonNode allocateEndpoint(String conferenceId, String endpointId) throws return responseBodyJson; } - public void configureEndpoint( - String conferenceId, String endpointId, SmbEndpointDescription endpointDescription) throws IOException, ParseException { + public void configureEndpoint(String conferenceId, String endpointId, SmbEndpointDescription endpointDescription) + throws IOException, ParseException + { - final var requestBodyJson = (ObjectNode) objectMapper.valueToTree(endpointDescription); + final var requestBodyJson = (ObjectNode)objectMapper.valueToTree(endpointDescription); requestBodyJson.put("action", "configure"); LOGGER.info("Request\n{}", objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(requestBodyJson)); @@ -76,14 +78,10 @@ public void configureEndpoint( LOGGER.info("Response {}", response.statusCode); } - public void deleteEndpoint(String conferenceId, String endpointId) throws IOException, ParseException { - final var requestBodyJson = JsonNodeFactory.instance.objectNode(); - requestBodyJson.put("action", "expire"); - - LOGGER.info("Request\n{}", objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(requestBodyJson)); - + public void deleteEndpoint(String conferenceId, String endpointId) throws IOException, ParseException + { final var url = BASE_URL + conferenceId + "/" + endpointId; - final var response = httpClient.post(url, requestBodyJson); + final var response = httpClient.delete(url); LOGGER.info("Response {}", response.statusCode); } }
My endpoint id: