From 74abd63c7bec4926ce0ea87da83f822d83d6eb50 Mon Sep 17 00:00:00 2001 From: Sina Madani Date: Thu, 9 Mar 2023 10:13:59 +0000 Subject: [PATCH] v4.9.0 Release (#235) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Minor fixes in build.gradle * Added support for MultiBroadcastTag and MultiArchiveTag (#226) * Added multiArchiveTag * Added multiBroadcastTag * Updated docs * Removed getMulti*Tag from responses * Revert "Removed getMulti*Tag from responses" This reverts commit 6e7f123c6d1ad402754dda09ca1b7a9c81436de6. * Docs edits re multiArchive/BroadcastTag. Co-authored-by: Jeff Swartz * Add Experience Composer API (#227) * Added startRender * Added listRenders * Deserialize listRenders response to native List * Added stopRender & getRender * Use enum for Render status * Improved RenderProperties * Experience composer docs edits ... And other docs corrections Co-authored-by: Jeff Swartz * Bump dependency versions * Improved release process. bump2version is now manual, but release to Nexus is fully automated. * Ensure version is passed in bumpversion.sh * Bumped dependencies (including WireMock) * Bump version: v4.8.0 → v4.8.1 * Update copyright year * Use wiremock-jre8 * Boost coverage * Boost coverage * Disable codecov temporarily * Revert "Disable codecov temporarily" This reverts commit 613bff0794ff299d1165952a57da903a700136ec. * Add Audio Streamer (lite) API endpoint (#215) * Added Audio Streamer (lite) endpoint * Don't include headers or streams if empty in connect request * Renamed Connect for clarity * Addressed PR comments * Fixed failing test * URI in AudioStreamerConnectionProperties constructor * Docs edits * Bumped dependencies (notably WireMock to 2.x) * Bumped dependencies * Bump dependency versions (#233) Improved release process, bumped dependency versions (including WireMock), slightly improved test coverage, updated copyright year, merged main, ready for release v4.8.1. * Renamed to Audio Connector * Bumped dependencies * Added audio connector to README * Minor docs edits --------- Co-authored-by: Jeff Swartz * Bump version: v4.8.1 → v4.9.0 --------- Co-authored-by: Jeff Swartz --- .bumpversion.cfg | 2 +- .github/workflows/publish.yml | 2 +- README.md | 25 ++- build.gradle | 7 +- sample/Archiving/build.gradle | 4 +- sample/HelloWorld/build.gradle | 4 +- settings.gradle | 2 - src/main/java/com/opentok/AudioConnector.java | 46 +++++ .../com/opentok/AudioConnectorProperties.java | 183 ++++++++++++++++++ src/main/java/com/opentok/OpenTok.java | 29 ++- .../java/com/opentok/constants/Version.java | 2 +- .../java/com/opentok/util/HttpClient.java | 78 ++++++-- .../java/com/opentok/test/OpenTokTest.java | 142 +++++++++++++- 13 files changed, 490 insertions(+), 36 deletions(-) create mode 100644 src/main/java/com/opentok/AudioConnector.java create mode 100644 src/main/java/com/opentok/AudioConnectorProperties.java diff --git a/.bumpversion.cfg b/.bumpversion.cfg index 8fe10a2d..f663577e 100644 --- a/.bumpversion.cfg +++ b/.bumpversion.cfg @@ -1,7 +1,7 @@ [bumpversion] commit = True tag = False -current_version = v4.8.1 +current_version = v4.9.0 parse = (?P\d+)\.(?P\d+)\.(?P\d+)(\-(?P[a-z]+)(?P\d+))? serialize = {major}.{minor}.{patch}-{release}{build} diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml index 7e2ba3a1..05909e3b 100644 --- a/.github/workflows/publish.yml +++ b/.github/workflows/publish.yml @@ -23,7 +23,7 @@ jobs: path: ~/.gradle/caches key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle') }} restore-keys: ${{ runner.os }}-gradle - - name: publish with gradle + - name: Publish with Gradle env: signingKey: ${{secrets.SIGNING_KEY}} signingPassword: ${{secrets.SIGNING_PASSWORD}} diff --git a/README.md b/README.md index d1dbe586..84cf0e02 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,8 @@ The OpenTok Java SDK provides methods for: * [Sending signals to clients connected to a session](https://tokbox.com/developer/guides/signaling/) * [Disconnecting clients from sessions](https://tokbox.com/developer/guides/moderation/rest/) * [Forcing clients in a session to disconnect or mute published audio](https://tokbox.com/developer/guides/moderation/) -* Working with OpenTok [Experience Composers](https://tokbox.com/developer/guides/experience-composer) +* Working with [Experience Composers](https://tokbox.com/developer/guides/experience-composer) +* Working with [Audio Connectors](/https://tokbox.com/developer/guides/audio-connector) ## Installation @@ -33,7 +34,7 @@ When you use Maven as your build tool, you can manage dependencies in the `pom.x com.tokbox opentok-server-sdk - 4.8.1 + 4.9.0 ``` @@ -43,7 +44,7 @@ When you use Gradle as your build tool, you can manage dependencies in the `buil ```groovy dependencies { - compile group: 'com.tokbox', name: 'opentok-server-sdk', version: '4.8.1' + compile group: 'com.tokbox', name: 'opentok-server-sdk', version: '4.9.0' } ``` @@ -579,7 +580,7 @@ method: ```java RenderProperties properties = new RenderProperties.Builder() - .url(("http://example.com/path-to-page/") + .url("http://example.com/path-to-page/") .build(); Render render = opentok.startRender(sessionId, token, properties); @@ -590,6 +591,22 @@ You can stop an Experience Composer by calling the `OpenTok.stopRender(String re You can get information about Experience Composers by calling the `OpenTok.getRender(String renderId)`, `OpenTok.listRenders()` or `OpenTok.listRenders(Integer offset, Integer count)` methods. +### Working with Audio Connector + +You can start an [Audio Connector stream](https://tokbox.com/developer/guides/audio-connector) +by calling the `OpenTok.connectAudioStream(String sessionId, String token, AudioConnectorProperties properties)` +method: + +```java +AudioConnectorProperties properties = new AudioConnectorProperties.Builder("wss://service.com/ws-endpoint") + .addStreams("streamId-1", "streamId-2") + .addHeader("X-CustomHeader-Key", "headerValue") + .build(); + +AudioConnector ac = opentok.connectAudioStream(sessionId, token, properties); +``` + + ## Samples There are two sample applications included with the SDK. To get going as fast as possible, clone the whole diff --git a/build.gradle b/build.gradle index 6c71dc68..ebe8f69d 100644 --- a/build.gradle +++ b/build.gradle @@ -11,7 +11,7 @@ plugins { group = 'com.tokbox' archivesBaseName = 'opentok-server-sdk' -version = '4.8.1' +version = '4.9.0' sourceCompatibility = "1.8" targetCompatibility = "1.8" @@ -27,9 +27,10 @@ dependencies { implementation 'commons-lang:commons-lang:2.6' implementation 'commons-validator:commons-validator:1.7' implementation 'org.asynchttpclient:async-http-client:2.12.3' - implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.1' + implementation 'com.fasterxml.jackson.core:jackson-databind:2.14.2' implementation 'commons-codec:commons-codec:1.15' - implementation 'org.bitbucket.b_c:jose4j:0.9.2' + implementation 'org.bitbucket.b_c:jose4j:0.9.3' + implementation 'io.netty:netty-handler:4.1.89.Final' } task sourcesJar(type: Jar) { diff --git a/sample/Archiving/build.gradle b/sample/Archiving/build.gradle index bdc1f933..87ba7588 100644 --- a/sample/Archiving/build.gradle +++ b/sample/Archiving/build.gradle @@ -8,8 +8,8 @@ repositories { dependencies { implementation project(':') implementation 'com.github.codingricky:spark-core-16:1.1' - implementation 'org.freemarker:freemarker:2.3.19' - implementation 'org.slf4j:slf4j-simple:1.7.6' + implementation 'org.freemarker:freemarker:2.3.32' + implementation 'org.slf4j:slf4j-simple:1.7.36' } run.systemProperty 'API_KEY', '' diff --git a/sample/HelloWorld/build.gradle b/sample/HelloWorld/build.gradle index ec32666e..e90c94e8 100644 --- a/sample/HelloWorld/build.gradle +++ b/sample/HelloWorld/build.gradle @@ -8,8 +8,8 @@ repositories { dependencies { implementation project(':') implementation 'com.github.codingricky:spark-core-16:1.1' - implementation 'org.freemarker:freemarker:2.3.19' - implementation 'org.slf4j:slf4j-simple:1.7.6' + implementation 'org.freemarker:freemarker:2.3.32' + implementation 'org.slf4j:slf4j-simple:1.7.36' } run.systemProperty 'API_KEY', '' diff --git a/settings.gradle b/settings.gradle index a1b85796..e69de29b 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +0,0 @@ -include 'sample:HelloWorld' -include 'sample:Archiving' \ No newline at end of file diff --git a/src/main/java/com/opentok/AudioConnector.java b/src/main/java/com/opentok/AudioConnector.java new file mode 100644 index 00000000..3785b8b2 --- /dev/null +++ b/src/main/java/com/opentok/AudioConnector.java @@ -0,0 +1,46 @@ +/** + * OpenTok Java SDK + * Copyright (C) 2023 Vonage. + * http://www.tokbox.com + * + * Licensed under The MIT License (MIT). See LICENSE file for more information. + */ +package com.opentok; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents an Audio Connector instance. + */ +@JsonIgnoreProperties(ignoreUnknown=true) +public class AudioConnector { + @JsonProperty private String id; + @JsonProperty private String connectionId; + + protected AudioConnector() {} + + /** + * Do not call this method. To initiate an audio stream connection to a session, call the + * {@link OpenTok#connectAudioStream(String, String, AudioConnectorProperties)} method. + */ + @JsonCreator + public static AudioConnector makeConnection() { + return new AudioConnector(); + } + + /** + * @return A unique ID identifying the Audio Connector WebSocket connection. + */ + public String getId() { + return id; + } + + /** + * @return The OpenTok connection ID for the Audio Connector WebSocket connection in the OpenTok session. + */ + public String getConnectionId() { + return connectionId; + } +} diff --git a/src/main/java/com/opentok/AudioConnectorProperties.java b/src/main/java/com/opentok/AudioConnectorProperties.java new file mode 100644 index 00000000..bf4b3807 --- /dev/null +++ b/src/main/java/com/opentok/AudioConnectorProperties.java @@ -0,0 +1,183 @@ +/** + * OpenTok Java SDK + * Copyright (C) 2023 Vonage. + * http://www.tokbox.com + * + * Licensed under The MIT License (MIT). See LICENSE file for more information. + */ +package com.opentok; + +import org.apache.commons.lang.StringUtils; + +import java.net.URI; +import java.util.*; + +/** + * Defines values for the properties parameter of the + * {@link OpenTok#connectAudioStream(String, String, AudioConnectorProperties)} method. + */ +public class AudioConnectorProperties { + private final URI uri; + private final Collection streams; + private final Map headers; + + /** + * The WebSocket URI to be used for the destination of the audio stream. + * + * @return A valid, non-null URI. + */ + public URI uri() { + return uri; + } + + /** + * (OPTIONAL) + * A collection of stream IDs for the OpenTok streams included in the WebSocket audio. + * If this collection is empty, all streams in the session will be included. + * + * @return An immutable collection of stream IDs, if present. + */ + public Collection streams() { + return streams; + } + + /** + * Additional headers to include in the request body. + * NOTE: The maximum length is 512 bytes. + * + * @return An immutable map of additional properties, if present. + */ + public Map headers() { + return headers; + } + + /** + * Returns the name of the JSON object for the connection request body. + * + * @return "websocket". + */ + public String type() { + return "websocket"; + } + + protected AudioConnectorProperties(Builder builder) { + this.uri = Objects.requireNonNull(builder.uri); + this.streams = builder.streams.isEmpty() ? null : Collections.unmodifiableCollection(builder.streams); + this.headers = builder.headers.isEmpty() ? null : Collections.unmodifiableMap(builder.headers); + } + + /** + * Intermediary stateful object used to construct {@link AudioConnectorProperties}. + */ + public static class Builder { + private final URI uri; + private final Collection streams = new ArrayList<>(); + private final Map headers = new HashMap<>(); + + /** + * Constructor for the AudioConnectorProperties.Builder, using a URI to + * define the WebSocket URI. + * + * @param uri The publicly reachable WebSocket URI to be used for the destination + * of the audio stream. + */ + public Builder(URI uri) { + this.uri = uri; + } + + /** + * Constructor for the AudioConnectorProperties.Builder, using a string to + * define the WebSocket URI. + * + * @param uri The publicly reachable WebSocket URI to be used for the destination of + * the audio stream, as a string (such as "wss://example.com/ws-endpoint"). + */ + public Builder(String uri) { + this(URI.create(uri)); + } + + /** + * Adds an OpenTok stream (with the corresponding stream ID) to include in the WebSocket audio. + * If the AudioConnectorProperties includes no streams, all streams in the session + * will be included. + * + * @param stream The stream ID. + * + * @return This builder with the additional stream ID. + */ + public Builder addStream(String stream) { + if (StringUtils.isBlank(stream)) { + throw new IllegalArgumentException("Stream ID cannot be blank"); + } + streams.add(stream); + return this; + } + + /** + * Adds OpenTok streams (with the corresponding stream IDs) to include in the WebSocket audio. + * If the AudioConnectorProperties includes no streams, all streams in the session + * will be included. + * + * @param streams The stream IDs to add. + * + * @return This builder with the additional stream IDs. + */ + public Builder addStreams(String... streams) { + return addStreams(Arrays.asList(streams)); + } + + /** + * Adds OpenTok streams (with the corresponding stream IDs) to include in the WebSocket audio. + * If the AudioConnectorProperties includes no streams, all streams in the session + * will be included. + * + * @param streams The collection of stream IDs to add. + * + * @return This builder with the additional stream IDs. + */ + public Builder addStreams(Collection streams) { + this.streams.addAll(Objects.requireNonNull(streams)); + return this; + } + + /** + * Puts all entries of the map into the headers parameter. The headers will + * be sent to your WebSocket server with each message. + * + * @param headers The map of header key-value pairs to append. + * + * @return This builder with the specified headers included. + */ + public Builder addHeaders(Map headers) { + this.headers.putAll(Objects.requireNonNull(headers)); + return this; + } + + /** + * Adds a header entry to this object's headers property. The header will + * be sent to your WebSocket server with each message. + * + * @param key Header key. + * @param value Header value. + * + * @return This builder with the additional header property. + */ + public Builder addHeader(String key, String value) { + if (StringUtils.isBlank(key)) { + throw new IllegalArgumentException("Property key cannot be blank"); + } + headers.put(key, value); + return this; + } + + /** + * Builds the AudioConnectorProperties object. + * + * @return The constructed {@link AudioConnectorProperties} object. + */ + public AudioConnectorProperties build() { + return new AudioConnectorProperties(this); + } + } + +} diff --git a/src/main/java/com/opentok/OpenTok.java b/src/main/java/com/opentok/OpenTok.java index ddfddb73..95a1b286 100644 --- a/src/main/java/com/opentok/OpenTok.java +++ b/src/main/java/com/opentok/OpenTok.java @@ -51,7 +51,8 @@ public class OpenTok { sipReader = new ObjectMapper().readerFor(Sip.class), broadcastReader = new ObjectMapper().readerFor(Broadcast.class), renderReader = new ObjectMapper().readerFor(Render.class), - renderListReader = new ObjectMapper().readerForListOf(Render.class); + renderListReader = new ObjectMapper().readerForListOf(Render.class), + connectReader = new ObjectMapper().readerFor(AudioConnector.class); static final String defaultApiUrl = "https://api.opentok.com"; @@ -535,7 +536,7 @@ public void removeArchiveStream(String archiveId, String streamId) throws OpenTo * * @param archiveId {String} The archive ID. * - * @param properties This ArchiveProperties object defining the arachive layout. + * @param properties The ArchiveProperties object defining the archive layout. */ public void setArchiveLayout(String archiveId, ArchiveProperties properties) throws OpenTokException { if (StringUtils.isEmpty(archiveId) || properties == null) { @@ -562,7 +563,7 @@ public void setArchiveLayout(String archiveId, ArchiveProperties properties) thr * * @param sessionId The session ID of the OpenTok session to broadcast. * - * @param properties This BroadcastProperties object defines options for the broadcast. + * @param properties The BroadcastProperties object defines options for the broadcast. * * @return The Broadcast object. This object includes properties defining the broadcast, * including the broadcast ID. @@ -863,6 +864,28 @@ public void playDTMF(String sessionId, String connectionId, String dtmfDigits) t client.playDtmfSingle(sessionId, connectionId, dtmfDigits); } + /** + * Send audio from a Vonage Video API session to a WebSocket. For more information, see the + * Audio Connector developer guide. + * + * @param sessionId The session ID. + * @param token The OpenTok token to be used for the Audio Connector connection to the + * OpenTok session. You can add token data to identify that the connection + * is the Audio Connector endpoint or for other identifying data. + * @param properties The ConnectProperties object defines options used in the request + * to the Audio Connector API endpoint. + * + * @return The Audio Connect response object from the server. + * + */ + public AudioConnector connectAudioStream(String sessionId, String token, AudioConnectorProperties properties) throws OpenTokException { + try { + return connectReader.readValue(client.connectAudioStream(sessionId, token, properties)); + } catch (JsonProcessingException ex) { + throw new RequestException("Exception mapping json: " + ex.getMessage(), ex); + } + } + /** * Starts an Experience Composer render for an OpenTok session. For more information, see the * Experience Composer developer guide. diff --git a/src/main/java/com/opentok/constants/Version.java b/src/main/java/com/opentok/constants/Version.java index 45b0c750..f89f5457 100644 --- a/src/main/java/com/opentok/constants/Version.java +++ b/src/main/java/com/opentok/constants/Version.java @@ -8,5 +8,5 @@ package com.opentok.constants; public class Version { - public static final String VERSION = "4.8.1"; + public static final String VERSION = "4.9.0"; } diff --git a/src/main/java/com/opentok/util/HttpClient.java b/src/main/java/com/opentok/util/HttpClient.java index 12695b4b..d726b3fe 100644 --- a/src/main/java/com/opentok/util/HttpClient.java +++ b/src/main/java/com/opentok/util/HttpClient.java @@ -1096,6 +1096,61 @@ public String listStreams(String sessionId) throws RequestException { } } + public String connectAudioStream(String sessionId, String token, AudioConnectorProperties properties) throws OpenTokException { + String url = this.apiUrl + "/v2/project/" + this.apiKey + "/connect"; + + ObjectNode requestJson = JsonNodeFactory.instance.objectNode() + .put("sessionId", sessionId) + .put("token", token); + + ObjectNode mainBody = requestJson.putObject(properties.type()); + mainBody.put("uri", properties.uri().toString()); + Collection streamsProperty = properties.streams(); + if (streamsProperty != null && !streamsProperty.isEmpty()) { + ArrayNode streams = mainBody.putArray("streams"); + streamsProperty.forEach(streams::add); + } + Map headersProperty = properties.headers(); + if (headersProperty != null && !headersProperty.isEmpty()) { + ObjectNode headers = mainBody.putObject("headers"); + headersProperty.forEach(headers::put); + } + + String requestBody; + try { + requestBody = new ObjectMapper().writeValueAsString(requestJson); + } catch (JsonProcessingException ex) { + throw new OpenTokException("Could not connect audio stream(s). The JSON body encoding failed", ex); + } + + Future request = preparePost(url) + .setBody(requestBody) + .setHeader("Content-Type", "application/json") + .execute(); + + try { + Response response = request.get(); + switch (response.getStatusCode()) { + case 200: + return response.getResponseBody(); + case 400: + throw new RequestException("Invalid request invalid session ID or invalid stream ID. " + + "sessionId: " + sessionId); + case 403: + throw new RequestException("Invalid OpenTok API key or JWT token."); + case 409: + throw new RequestException("Conflict. Only routed sessions are allowed to initiate Connect Calls."); + case 500: + throw new RequestException("OpenTok server error."); + default: + throw new RequestException("Could not connect audio stream. The server response was invalid." + + " Response code: " + response.getStatusCode()); + } + } catch (InterruptedException | ExecutionException ex) { + throw new RequestException("Could not get stream information", ex); + } + } + public String startRender(String sessionId, String token, RenderProperties properties) throws OpenTokException { String url = this.apiUrl + "/v2/project/" + this.apiKey + "/render"; @@ -1125,10 +1180,10 @@ public String startRender(String sessionId, String token, RenderProperties prope } Future request = this.preparePost(url) - .setHeader("Content-Type", "application/json") - .setHeader("Accept", "application/json") - .setBody(outputStream.toString()) - .execute(); + .setHeader("Content-Type", "application/json") + .setHeader("Accept", "application/json") + .setBody(outputStream.toString()) + .execute(); try { Response response = request.get(); @@ -1143,7 +1198,7 @@ public String startRender(String sessionId, String token, RenderProperties prope throw new RequestException("Could not start render. A server error occurred."); default: throw new RequestException("Could not start render. The server response was invalid." + - " response code: " + response.getStatusCode()); + " response code: " + response.getStatusCode()); } } catch (InterruptedException | ExecutionException e) { throw new RequestException("Could not start render", e); @@ -1154,9 +1209,9 @@ public String getRender(String renderId) throws OpenTokException { String url = this.apiUrl + "/v2/project/" + this.apiKey + "/render/" + renderId; Future request = this.prepareGet(url) - .setHeader("Content-Type", "application/json") - .setHeader("Accept", "application/json") - .execute(); + .setHeader("Content-Type", "application/json") + .setHeader("Accept", "application/json") + .execute(); try { Response response = request.get(); @@ -1173,7 +1228,7 @@ public String getRender(String renderId) throws OpenTokException { throw new RequestException("Could not get render. A server error occurred."); default: throw new RequestException("Could not get render. The server response was invalid." + - " response code: " + response.getStatusCode()); + " response code: " + response.getStatusCode()); } } catch (InterruptedException | ExecutionException e) { throw new RequestException("Could not get render '"+renderId+"'", e); @@ -1197,7 +1252,7 @@ public void stopRender(String renderId) throws OpenTokException { throw new RequestException("Could not stop render. A server error occurred."); default: throw new RequestException("Could not stop render. The server response was invalid." + - " response code: " + response.getStatusCode()); + " response code: " + response.getStatusCode()); } } catch (InterruptedException | ExecutionException e) { throw new RequestException("Could not start render", e); @@ -1226,13 +1281,12 @@ public String listRenders(Integer offset, Integer count) throws OpenTokException throw new RequestException("Could not list renders. A server error occurred."); default: throw new RequestException("Could not list renders. The server response was invalid." + - " response code: " + response.getStatusCode()); + " response code: " + response.getStatusCode()); } } catch (InterruptedException | ExecutionException e) { throw new RequestException("Could not start render", e); } } - public enum ProxyAuthScheme { BASIC, DIGEST, diff --git a/src/test/java/com/opentok/test/OpenTokTest.java b/src/test/java/com/opentok/test/OpenTokTest.java index f7e9b540..9ee804e4 100644 --- a/src/test/java/com/opentok/test/OpenTokTest.java +++ b/src/test/java/com/opentok/test/OpenTokTest.java @@ -1815,11 +1815,10 @@ public void testBroadcastWithScreenShareType() throws OpenTokException { BroadcastLayout layout = new BroadcastLayout(ScreenShareLayoutType.PIP); BroadcastProperties properties = new BroadcastProperties.Builder() - .hasHls(true) - .maxDuration(5400) - .layout(layout) - .build(); - + .hasHls(true) + .maxDuration(5400) + .layout(layout) + .build(); String expectedJson = String.format("{\"sessionId\":\"%s\",\"streamMode\":\"auto\",\"layout\":{\"type\":\"bestFit\",\"screenshareType\":\"pip\"},\"maxDuration\":5400,\"resolution\":\"640x480\",\"outputs\":{\"hls\":{},\"rtmp\":[]}}",sessionId); Broadcast broadcast = sdk.startBroadcast(sessionId, properties); assertNotNull(broadcast); @@ -2493,4 +2492,137 @@ public void testListRenders() throws Exception { findAll(getRequestedFor(urlMatching(endpoint))))); Helpers.verifyUserAgent(); } + + @Test + public void testConnectAudioStream200() throws OpenTokException { + String url = "/v2/project/" + apiKey + "/connect"; + String callId = UUID.randomUUID().toString(); + String connectionId = UUID.randomUUID().toString(); + String sessionId = UUID.randomUUID().toString(); + String uri = "ws://service.com/wsendpoint"; + + stubFor(post(urlEqualTo(url)) + .withRequestBody(equalToJson("{\"sessionId\":\""+sessionId+"\",\"token\":\""+apiSecret+"\",\"websocket\":{\"uri\":\"ws://service.com/wsendpoint\",\"streams\":[\"STREAMID1\",\"STREAMID2\"],\"headers\":{\"key1\":\"header1\",\"content-type\":\"audio/l16;rate=16000\"}}}")) + .willReturn(aResponse() + .withBody("{\"id\": \""+callId+"\", \"connectionId\": \""+connectionId+"\"}") + .withStatus(200) + )); + + AudioConnectorProperties connectProperties = new AudioConnectorProperties.Builder(uri) + .addHeader("content-type", "audio/l16;rate=16000") + .addHeader("key1", "header1") + .addStreams("STREAMID1", "STREAMID2") + .build(); + + AudioConnector connectResponse = sdk.connectAudioStream(sessionId, apiSecret, connectProperties); + + assertNotNull(connectResponse); + assertEquals(connectionId, connectResponse.getConnectionId()); + assertEquals(callId, connectResponse.getId()); + } + + @Test + public void testConnectAudioStreamNoHeadersOrStreams() throws OpenTokException { + String url = "/v2/project/" + apiKey + "/connect"; + String sessionId = UUID.randomUUID().toString(); + String callId = UUID.randomUUID().toString(); + String connectionId = UUID.randomUUID().toString(); + String endpoint = "ws://service.com/wsendpoint"; + String requestJson = "{\"sessionId\":\""+sessionId+"\",\"token\":\""+apiSecret+"\",\"websocket\":{\"uri\":\""+endpoint+"\"}}"; + + stubFor(post(urlEqualTo(url)) + .withRequestBody(equalToJson(requestJson)) + .willReturn(aResponse() + .withBody("{\"id\": \""+callId+"\", \"connectionId\": \""+connectionId+"\"}") + .withStatus(200) + ) + ); + + AudioConnectorProperties connectProperties = new AudioConnectorProperties.Builder(endpoint).build(); + + assertTrue(connectProperties.streams() == null || connectProperties.streams().isEmpty()); + assertTrue(connectProperties.headers() == null || connectProperties.headers().isEmpty()); + + AudioConnector connectResponse = sdk.connectAudioStream(sessionId, apiSecret, connectProperties); + + assertNotNull(connectResponse); + assertEquals(connectionId, connectResponse.getConnectionId()); + assertEquals(callId, connectResponse.getId()); + } + + @Test + public void testConnectAudioStreamUnknownResponseCode() throws OpenTokException { + String url = "/v2/project/" + apiKey + "/connect"; + String sessionId = UUID.randomUUID().toString(); + String uri = "ws://service.com/wsendpoint"; + String json = "{\"sessionId\":\""+sessionId+"\",\"token\":\""+apiSecret+"\",\"websocket\":{\"uri\":\""+uri+"\"}}"; + + stubFor(post(urlEqualTo(url)) + .withRequestBody(equalToJson(json)) + .willReturn(aResponse().withStatus(503)) + ); + + AudioConnectorProperties connectProperties = new AudioConnectorProperties.Builder(uri).build(); + assertThrows(RequestException.class, () -> sdk.connectAudioStream(sessionId, apiSecret, connectProperties)); + } + + @Test + public void testConnectAudioStreamErrors() throws OpenTokException { + String url = "/v2/project/" + apiKey + "/connect"; + String sessionId = UUID.randomUUID().toString(); + String uri = "ws://service.com/wsendpoint"; + AudioConnectorProperties connectProperties = new AudioConnectorProperties.Builder(uri).build(); + + stubFor(post(urlEqualTo(url)).willReturn(aResponse().withStatus(400))); + assertThrows(RequestException.class, () -> sdk.connectAudioStream(sessionId, apiSecret, connectProperties)); + + stubFor(post(urlEqualTo(url)).willReturn(aResponse().withStatus(403))); + assertThrows(RequestException.class, () -> sdk.connectAudioStream(sessionId, apiSecret, connectProperties)); + + stubFor(post(urlEqualTo(url)).willReturn(aResponse().withStatus(404))); + assertThrows(RequestException.class, () -> sdk.connectAudioStream(sessionId, apiSecret, connectProperties)); + + stubFor(post(urlEqualTo(url)).willReturn(aResponse().withStatus(500))); + assertThrows(RequestException.class, () -> sdk.connectAudioStream(sessionId, apiSecret, connectProperties)); + } + + @Test + public void testConnectProperties() throws Exception { + assertThrows( + "Should not be possible to construct audio stream without URI", + Exception.class, + () -> new AudioConnectorProperties.Builder((java.net.URI) null).build() + ); + assertThrows( + "Should not be possible to construct audio stream without URI", + Exception.class, + () -> new AudioConnectorProperties.Builder((String) null).build() + ); + String uriStr = "ws://service.com/wsendpoint"; + AudioConnectorProperties cp1 = new AudioConnectorProperties.Builder(new URI(uriStr)).build(); + assertTrue(cp1.headers() == null || cp1.headers().isEmpty()); + assertTrue(cp1.streams() == null || cp1.streams().isEmpty()); + assertNotNull(cp1.uri()); + assertNotNull(cp1.type()); + + AudioConnectorProperties cp2 = new AudioConnectorProperties.Builder(uriStr) + .addStreams(new HashSet<>()) + .addHeaders(new HashMap<>()) + .addStream("STREAMID") + .addStreams(new LinkedList<>()) + .addHeader("k1", "v1") + .build(); + + assertThrows(UnsupportedOperationException.class, () -> cp2.headers().put("k2", "v2")); + assertThrows(UnsupportedOperationException.class, () -> cp2.headers().clear()); + assertThrows(UnsupportedOperationException.class, () -> cp2.streams().add("streamID_2")); + assertThrows(UnsupportedOperationException.class, () -> cp2.streams().clear()); + + assertThrows(IllegalArgumentException.class, () -> + new AudioConnectorProperties.Builder(uriStr).addHeader(" ", "value").build() + ); + assertThrows(IllegalArgumentException.class, () -> + new AudioConnectorProperties.Builder(uriStr).addStream(" ").build() + ); + } } \ No newline at end of file