From 22ec333482eb155eef7c5f52ad63e881e9263f1e Mon Sep 17 00:00:00 2001 From: Sina Madani Date: Fri, 30 Aug 2024 12:46:23 +0100 Subject: [PATCH] feat: Add Video API (#10) * wip: Video API files * Bump surefire plugin * Add Video implementation * Test sessions * Test Experience Composer * Test generateToken * Factor out JWT duplication * Rename ExistingX IDs * Archive & Broadcast responses * Fix failing tests * Stream selection tests * listArchives / listBroadcasts tests * Test createArchive / createBroadcast --- CHANGELOG.md | 5 + README.md | 1 + .../com/vonage/client/kt/Subaccounts.kt | 8 +- src/main/kotlin/com/vonage/client/kt/Users.kt | 12 +- .../kotlin/com/vonage/client/kt/Verify.kt | 8 +- .../com/vonage/client/kt/VerifyLegacy.kt | 14 +- src/main/kotlin/com/vonage/client/kt/Video.kt | 196 ++++ src/main/kotlin/com/vonage/client/kt/Voice.kt | 28 +- .../kotlin/com/vonage/client/kt/Vonage.kt | 1 + .../com/vonage/client/kt/AbstractTest.kt | 15 +- .../com/vonage/client/kt/SubaccountsTest.kt | 2 +- .../kotlin/com/vonage/client/kt/UsersTest.kt | 2 +- .../kotlin/com/vonage/client/kt/VerifyTest.kt | 1 - .../kotlin/com/vonage/client/kt/VideoTest.kt | 1019 +++++++++++++++++ .../kotlin/com/vonage/client/kt/VoiceTest.kt | 2 - 15 files changed, 1271 insertions(+), 43 deletions(-) create mode 100644 src/main/kotlin/com/vonage/client/kt/Video.kt create mode 100644 src/test/kotlin/com/vonage/client/kt/VideoTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c3211b..e78f3e8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). +## [1.0.0-beta1] - 2024-09-?? + +### Added +- Video API + ## [0.9.0] - 2024-08-19 ### Added diff --git a/README.md b/README.md index dba7638..0de5fdc 100644 --- a/README.md +++ b/README.md @@ -31,6 +31,7 @@ You'll need to have [created a Vonage account](https://dashboard.nexmo.com/sign- - [SMS](https://developer.vonage.com/en/messaging/sms/overview) - [Subaccounts](https://developer.vonage.com/en/account/subaccounts/overview) - [Verify](https://developer.vonage.com/en/verify/overview) +- [Video](https://developer.vonage.com/en/video/overview) - [Voice](https://developer.vonage.com/en/voice/voice-api/overview) ## Other SDKs diff --git a/src/main/kotlin/com/vonage/client/kt/Subaccounts.kt b/src/main/kotlin/com/vonage/client/kt/Subaccounts.kt index dffc91b..66253ca 100644 --- a/src/main/kotlin/com/vonage/client/kt/Subaccounts.kt +++ b/src/main/kotlin/com/vonage/client/kt/Subaccounts.kt @@ -33,15 +33,15 @@ class Subaccounts internal constructor(private val client: SubaccountsClient) { fun subaccount(subaccountKey: String): ExistingSubaccount = ExistingSubaccount(subaccountKey) - inner class ExistingSubaccount internal constructor(val subaccountKey: String) { + inner class ExistingSubaccount internal constructor(val key: String) { - fun get(): Account = client.getSubaccount(subaccountKey) + fun get(): Account = client.getSubaccount(key) fun suspended(suspend: Boolean): Account = - client.updateSubaccount(UpdateSubaccountRequest.builder(subaccountKey).suspended(suspend).build()) + client.updateSubaccount(UpdateSubaccountRequest.builder(key).suspended(suspend).build()) fun update(name: String? = null, usePrimaryAccountBalance: Boolean? = null): Account { - val builder = UpdateSubaccountRequest.builder(subaccountKey).name(name) + val builder = UpdateSubaccountRequest.builder(key).name(name) if (usePrimaryAccountBalance != null) { builder.usePrimaryAccountBalance(usePrimaryAccountBalance) } diff --git a/src/main/kotlin/com/vonage/client/kt/Users.kt b/src/main/kotlin/com/vonage/client/kt/Users.kt index 038e33a..cbe5812 100644 --- a/src/main/kotlin/com/vonage/client/kt/Users.kt +++ b/src/main/kotlin/com/vonage/client/kt/Users.kt @@ -23,25 +23,25 @@ class Users internal constructor(private val client: UsersClient) { fun user(user: BaseUser): ExistingUser = ExistingUser(user.id) - inner class ExistingUser internal constructor(val userId: String) { - fun get(): User = client.getUser(userId) + inner class ExistingUser internal constructor(val id: String) { + fun get(): User = client.getUser(id) fun update(properties: User.Builder.() -> Unit): User = - client.updateUser(userId, User.builder().apply(properties).build()) + client.updateUser(id, User.builder().apply(properties).build()) - fun delete(): Unit = client.deleteUser(userId) + fun delete(): Unit = client.deleteUser(id) @Override override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false other as ExistingUser - return userId == other.userId + return id == other.id } @Override override fun hashCode(): Int { - return userId.hashCode() + return id.hashCode() } } diff --git a/src/main/kotlin/com/vonage/client/kt/Verify.kt b/src/main/kotlin/com/vonage/client/kt/Verify.kt index ca1edde..404c08c 100644 --- a/src/main/kotlin/com/vonage/client/kt/Verify.kt +++ b/src/main/kotlin/com/vonage/client/kt/Verify.kt @@ -27,14 +27,14 @@ class Verify(private val client: Verify2Client) { VerificationRequest.builder().brand(brand).apply(init).build() ) - inner class ExistingRequest internal constructor(val requestId: UUID) { + inner class ExistingRequest internal constructor(val id: UUID) { - fun cancel(): Unit = client.cancelVerification(requestId) + fun cancel(): Unit = client.cancelVerification(id) - fun nextWorkflow(): Unit = client.nextWorkflow(requestId) + fun nextWorkflow(): Unit = client.nextWorkflow(id) fun checkVerificationCode(code: String): VerifyCodeResponse = - client.checkVerificationCode(requestId, code) + client.checkVerificationCode(id, code) fun isValidVerificationCode(code: String): Boolean { try { diff --git a/src/main/kotlin/com/vonage/client/kt/VerifyLegacy.kt b/src/main/kotlin/com/vonage/client/kt/VerifyLegacy.kt index c56e001..de9ec74 100644 --- a/src/main/kotlin/com/vonage/client/kt/VerifyLegacy.kt +++ b/src/main/kotlin/com/vonage/client/kt/VerifyLegacy.kt @@ -32,27 +32,27 @@ class VerifyLegacy internal constructor(private val client: VerifyClient) { fun request(response: VerifyResponse): ExistingRequest = request(response.requestId) - inner class ExistingRequest internal constructor(private val requestId: String) { + inner class ExistingRequest internal constructor(val id: String) { - fun cancel(): ControlResponse = client.cancelVerification(requestId) + fun cancel(): ControlResponse = client.cancelVerification(id) - fun advance(): ControlResponse = client.advanceVerification(requestId) + fun advance(): ControlResponse = client.advanceVerification(id) - fun check(code: String): CheckResponse = client.check(requestId, code) + fun check(code: String): CheckResponse = client.check(id, code) - fun search(): SearchVerifyResponse = client.search(requestId) + fun search(): SearchVerifyResponse = client.search(id) @Override override fun equals(other: Any?): Boolean { if (this === other) return true if (javaClass != other?.javaClass) return false other as ExistingRequest - return requestId == other.requestId + return id == other.id } @Override override fun hashCode(): Int { - return requestId.hashCode() + return id.hashCode() } } diff --git a/src/main/kotlin/com/vonage/client/kt/Video.kt b/src/main/kotlin/com/vonage/client/kt/Video.kt new file mode 100644 index 0000000..fbf0e56 --- /dev/null +++ b/src/main/kotlin/com/vonage/client/kt/Video.kt @@ -0,0 +1,196 @@ +/* + * Copyright 2024 Vonage + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.vonage.client.kt + +import com.vonage.client.video.* +import java.util.* + +class Video(private val client: VideoClient) { + + fun createSession(properties: CreateSessionRequest.Builder.() -> Unit = {}): CreateSessionResponse = + client.createSession(CreateSessionRequest.builder().apply(properties).build()) + + fun session(sessionId: String): ExistingSession = ExistingSession(sessionId) + + inner class ExistingSession internal constructor(val id: String) { + + fun stream(streamId: String): ExistingStream = ExistingStream(streamId) + + inner class ExistingStream internal constructor(val streamId: String) { + + fun info(): GetStreamResponse = client.getStream(id, streamId) + + fun mute(): Unit = client.muteStream(id, streamId) + + fun setLayout(vararg layoutClasses: String): Unit = + client.setStreamLayout(id, + SessionStream.builder(streamId).layoutClassList(layoutClasses.toList()).build() + ) + } + + fun connection(connectionId: String): ExistingConnection = ExistingConnection(connectionId) + + inner class ExistingConnection internal constructor(val id: String) { + + fun disconnect(): Unit = client.forceDisconnect(this@ExistingSession.id, id) + + fun signal(type: String, data: String): Unit = + client.signal(this@ExistingSession.id, id, signalRequest(type, data)) + + fun sendDtmf(digits: String): Unit = client.sendDtmf(this@ExistingSession.id, id, digits) + } + + fun muteStreams(active: Boolean = true, vararg excludedStreamIds: String): ProjectDetails = + client.muteSession(id, active, + if (excludedStreamIds.isNotEmpty()) excludedStreamIds.toList() else null + ) + + fun listStreams(): List = client.listStreams(id) + + fun listArchives(count: Int = 1000, offset: Int = 0): List = + client.listArchives(listCompositionsFilter(count, offset, id)) + + fun listBroadcasts(count: Int = 1000, offset: Int = 0): List = + client.listBroadcasts(listCompositionsFilter(count, offset, id)) + + fun signalAll(type: String, data: String): Unit = + client.signalAll(id, signalRequest(type, data)) + + fun sendDtmf(digits: String): Unit = client.sendDtmf(id, digits) + + fun startCaptions(token: String, properties: CaptionsRequest.Builder.() -> Unit): UUID = + client.startCaptions(CaptionsRequest.builder() + .apply(properties).sessionId(id).token(token).build() + ).captionsId + + fun stopCaptions(captionsId: String): Unit = client.stopCaptions(captionsId) + + fun createArchive(properties: Archive.Builder.() -> Unit = {}): Archive = + client.createArchive(Archive.builder(id).apply(properties).build()) + + fun startBroadcast(properties: Broadcast.Builder.() -> Unit): Broadcast = + client.createBroadcast(Broadcast.builder(id).apply(properties).build()) + + fun generateToken(options: TokenOptions.Builder.() -> Unit = {}): String = + client.generateToken(id, TokenOptions.builder().apply(options).build()) + } + + fun sipDial(properties: SipDialRequest.Builder.() -> Unit): SipDialResponse = + client.sipDial(SipDialRequest.builder().apply(properties).build()) + + fun connectToWebsocket(properties: ConnectRequest.Builder.() -> Unit): ConnectResponse = + client.connectToWebsocket(ConnectRequest.builder().apply(properties).build()) + + fun listArchives(count: Int = 1000, offset: Int = 0): List = + client.listArchives(listCompositionsFilter(count, offset)) + + fun archive(archiveId: String): ExistingArchive = ExistingArchive(archiveId) + + inner class ExistingArchive internal constructor(val id: String) { + + fun info(): Archive = client.getArchive(id) + + fun stop(): Archive = client.stopArchive(id) + + fun delete(): Unit = client.deleteArchive(id) + + fun setLayout(initialLayout: ScreenLayoutType, + screenshareType: ScreenLayoutType? = null, + stylesheet: String? = null): Unit = + client.updateArchiveLayout(id, + StreamCompositionLayout.builder(initialLayout) + .screenshareType(screenshareType) + .stylesheet(stylesheet) + .build() + ) + + fun addStream(streamId: String, audio: Boolean = true, video: Boolean = true) = + client.addArchiveStream(id, streamId, audio, video) + + fun removeStream(streamId: String): Unit = client.removeArchiveStream(id, streamId) + } + + fun listBroadcasts(count: Int = 1000, offset: Int = 0): List = + client.listBroadcasts(listCompositionsFilter(count, offset)) + + fun broadcast(broadcastId: String): ExistingBroadcast = ExistingBroadcast(broadcastId) + + inner class ExistingBroadcast internal constructor(val id: String) { + + fun info(): Broadcast = client.getBroadcast(id) + + fun stop(): Broadcast = client.stopBroadcast(id) + + fun setLayout(initialLayout: ScreenLayoutType, + screenshareType: ScreenLayoutType? = null, + stylesheet: String? = null): Unit = + client.updateBroadcastLayout(id, + StreamCompositionLayout.builder(initialLayout) + .screenshareType(screenshareType) + .stylesheet(stylesheet) + .build() + ) + + fun addStream(streamId: String, audio: Boolean = true, video: Boolean = true) = + client.addBroadcastStream(id, streamId, audio, video) + + fun removeStream(streamId: String): Unit = client.removeBroadcastStream(id, streamId) + } + + fun startRender(properties: RenderRequest.Builder.() -> Unit): RenderResponse = + client.startRender(RenderRequest.builder().apply(properties).build()) + + fun listRenders(count: Int = 1000, offset: Int = 0): List = + client.listRenders(ListStreamCompositionsRequest.builder().count(count).offset(offset).build()) + + fun render(renderId: String): ExistingRender = ExistingRender(renderId) + + inner class ExistingRender internal constructor(val id: String) { + + fun info(): RenderResponse = client.getRender(id) + + fun stop(): Unit = client.stopRender(id) + } +} + +private fun listCompositionsFilter(count: Int, offset: Int, sessionId: String? = null): + ListStreamCompositionsRequest = ListStreamCompositionsRequest.builder() + .count(count).offset(offset).sessionId(sessionId).build() + +private fun signalRequest(type: String, data: String): SignalRequest = + SignalRequest.builder().type(type).data(data).build() + +private fun streamCompositionLayout(initialLayout: ScreenLayoutType, + screenshareType: ScreenLayoutType?, + stylesheet: String?): StreamCompositionLayout = + StreamCompositionLayout.builder(initialLayout) + .screenshareType(screenshareType).stylesheet(stylesheet).build() + +fun Broadcast.Builder.addRtmpStream(properties: Rtmp.Builder.() -> Unit): Broadcast.Builder = + addRtmpStream(Rtmp.builder().apply(properties).build()) + +fun Broadcast.Builder.hls(hls: Hls.Builder.() -> Unit = {}): Broadcast.Builder = + hls(Hls.builder().apply(hls).build()) + +fun Archive.Builder.layout(initialLayout: ScreenLayoutType, + screenshareType: ScreenLayoutType? = null, + stylesheet: String? = null): Archive.Builder = + layout(streamCompositionLayout(initialLayout, screenshareType, stylesheet)) + +fun Broadcast.Builder.layout(initialLayout: ScreenLayoutType, + screenshareType: ScreenLayoutType? = null, + stylesheet: String? = null): Broadcast.Builder = + layout(streamCompositionLayout(initialLayout, screenshareType, stylesheet)) diff --git a/src/main/kotlin/com/vonage/client/kt/Voice.kt b/src/main/kotlin/com/vonage/client/kt/Voice.kt index 2007f84..e90bfb7 100644 --- a/src/main/kotlin/com/vonage/client/kt/Voice.kt +++ b/src/main/kotlin/com/vonage/client/kt/Voice.kt @@ -25,37 +25,37 @@ class Voice internal constructor(private val client: VoiceClient) { fun call(callId: String): ExistingCall = ExistingCall(callId) - inner class ExistingCall internal constructor(val callId: String) { + inner class ExistingCall internal constructor(val id: String) { - fun info(): CallInfo = client.getCallDetails(callId) + fun info(): CallInfo = client.getCallDetails(id) - fun hangup(): Unit = client.terminateCall(callId) + fun hangup(): Unit = client.terminateCall(id) - fun mute(): Unit = client.muteCall(callId) + fun mute(): Unit = client.muteCall(id) - fun unmute(): Unit = client.unmuteCall(callId) + fun unmute(): Unit = client.unmuteCall(id) - fun earmuff(): Unit = client.earmuffCall(callId) + fun earmuff(): Unit = client.earmuffCall(id) - fun unearmuff(): Unit = client.unearmuffCall(callId) + fun unearmuff(): Unit = client.unearmuffCall(id) - fun transfer(vararg actions: Action): Unit = client.transferCall(callId, Ncco(actions.asList())) + fun transfer(vararg actions: Action): Unit = client.transferCall(id, Ncco(actions.asList())) - fun transfer(nccoUrl: String): Unit = client.transferCall(callId, nccoUrl) + fun transfer(nccoUrl: String): Unit = client.transferCall(id, nccoUrl) fun transfer(nccoUrl: URI): Unit = transfer(nccoUrl.toString()) - fun sendDtmf(digits: String): DtmfResponse = client.sendDtmf(callId, digits) + fun sendDtmf(digits: String): DtmfResponse = client.sendDtmf(id, digits) fun streamAudio(streamUrl: String, loop: Int = 1, level: Double = 0.0): StreamResponse = - client.startStream(callId, streamUrl, loop, level) + client.startStream(id, streamUrl, loop, level) - fun stopStream(): StreamResponse = client.stopStream(callId) + fun stopStream(): StreamResponse = client.stopStream(id) fun startTalk(text: String, properties: (TalkPayload.Builder.() -> Unit) = {}): TalkResponse = - client.startTalk(callId, TalkPayload.builder(text).apply(properties).build()) + client.startTalk(id, TalkPayload.builder(text).apply(properties).build()) - fun stopTalk(): TalkResponse = client.stopTalk(callId) + fun stopTalk(): TalkResponse = client.stopTalk(id) } fun listCalls(filter: (CallsFilter.Builder.() -> Unit)? = null): CallInfoPage = diff --git a/src/main/kotlin/com/vonage/client/kt/Vonage.kt b/src/main/kotlin/com/vonage/client/kt/Vonage.kt index 5738635..5a5080a 100644 --- a/src/main/kotlin/com/vonage/client/kt/Vonage.kt +++ b/src/main/kotlin/com/vonage/client/kt/Vonage.kt @@ -34,6 +34,7 @@ class Vonage(init: VonageClient.Builder.() -> Unit) { val users = Users(client.usersClient) val verify = Verify(client.verify2Client) val verifyLegacy = VerifyLegacy(client.verifyClient) + val video = Video(client.videoClient) val voice = Voice(client.voiceClient) } diff --git a/src/test/kotlin/com/vonage/client/kt/AbstractTest.kt b/src/test/kotlin/com/vonage/client/kt/AbstractTest.kt index 7d3618b..e9a7d9b 100644 --- a/src/test/kotlin/com/vonage/client/kt/AbstractTest.kt +++ b/src/test/kotlin/com/vonage/client/kt/AbstractTest.kt @@ -45,12 +45,15 @@ abstract class AbstractTest { private val signatureSecretName = "sig" private val apiSecretName = "api_secret" private val apiKeyName = "api_key" + private val contentTypeHeaderName = "Content-Type" private val authHeaderName = "Authorization" private val basicSecretEncodedHeader = "Basic $apiKeySecretEncoded" private val jwtBearerPattern = "Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9(\\..+){2}" private val accessTokenBearer = "Bearer $accessToken" protected val testUuidStr = "aaaaaaaa-bbbb-4ccc-8ddd-0123456789ab" protected val testUuid: UUID = UUID.fromString(testUuidStr) + protected val randomUuid: UUID = UUID.randomUUID() + protected val randomUuidStr = randomUuid.toString() protected val toNumber = "447712345689" protected val altNumber = "447700900001" protected val brand = "Nexmo KT" @@ -60,6 +63,7 @@ abstract class AbstractTest { protected val cursor = "7EjDNQrAcipmOnc0HCzpQRkhBULzY44ljGUX4lXKyUIVfiZay5pv9wg=" protected val vbcExt = "4321" protected val userName = "Sam_username" + protected val dtmf = "p*123#" protected val sipUri = "sip:rebekka@sip.example.com" protected val websocketUri = "wss://example.com/socket" protected val wsContentType = "audio/l16;rate=8000" @@ -154,13 +158,18 @@ abstract class AbstractTest { } } - private fun Map.toJson(): String = ObjectMapper().writeValueAsString(this) + private fun Any.toJson(): String = ObjectMapper().writeValueAsString(this) protected fun mockPostQueryParams(expectedUrl: String, expectedRequestParams: Map, authType: AuthType? = AuthType.API_KEY_SECRET_QUERY_PARAMS, - status: Int = 200, expectedResponseParams: Map? = null) { + contentType: Boolean = false, status: Int = 200, + expectedResponseParams: Any? = null) { val stub = post(urlPathEqualTo(expectedUrl)) + if (contentType) { + stub.withHeader(contentTypeHeaderName, equalTo(ContentType.FORM_URLENCODED.mime)) + } + when (authType) { AuthType.API_KEY_SECRET_QUERY_PARAMS -> { stub.withFormParam(apiKeyName, equalTo(apiKey)) @@ -194,7 +203,7 @@ abstract class AbstractTest { urlPath equalTo expectedUrl headers contains "User-Agent" like "vonage-java-sdk\\/.+ java\\/.+" if (contentType != null) { - headers contains "Content-Type" equalTo contentType.mime + headers contains contentTypeHeaderName equalTo contentType.mime } if (accept != null) { headers contains "Accept" equalTo accept.mime diff --git a/src/test/kotlin/com/vonage/client/kt/SubaccountsTest.kt b/src/test/kotlin/com/vonage/client/kt/SubaccountsTest.kt index 24d9f7d..0c83046 100644 --- a/src/test/kotlin/com/vonage/client/kt/SubaccountsTest.kt +++ b/src/test/kotlin/com/vonage/client/kt/SubaccountsTest.kt @@ -55,7 +55,7 @@ class SubaccountsTest : AbstractTest() { private fun assertEqualsSampleSubaccount(parsed: Account) { assertNotNull(parsed) assertEquals(secret, parsed.secret) - assertEquals(existingSubaccount.subaccountKey, parsed.apiKey) + assertEquals(existingSubaccount.key, parsed.apiKey) assertEquals(name, parsed.name) assertEquals(apiKey, parsed.primaryAccountApiKey) assertEquals(usePrimary, parsed.usePrimaryAccountBalance) diff --git a/src/test/kotlin/com/vonage/client/kt/UsersTest.kt b/src/test/kotlin/com/vonage/client/kt/UsersTest.kt index 4e4cb66..249d3be 100644 --- a/src/test/kotlin/com/vonage/client/kt/UsersTest.kt +++ b/src/test/kotlin/com/vonage/client/kt/UsersTest.kt @@ -194,7 +194,7 @@ class UsersTest : AbstractTest() { assertEquals(URI.create(navUrl), links.nextUrl) assertEquals(URI.create(navUrl), links.prevUrl) - assertEquals(existingUser.userId, client.user(idOnlyUser).userId) + assertEquals(existingUser.id, client.user(idOnlyUser).id) assert401ApiResponseException(baseUrl, HttpMethod.GET) { invocation.invoke(client) diff --git a/src/test/kotlin/com/vonage/client/kt/VerifyTest.kt b/src/test/kotlin/com/vonage/client/kt/VerifyTest.kt index 8db0ae8..04c4dd2 100644 --- a/src/test/kotlin/com/vonage/client/kt/VerifyTest.kt +++ b/src/test/kotlin/com/vonage/client/kt/VerifyTest.kt @@ -18,7 +18,6 @@ package com.vonage.client.kt import com.vonage.client.common.HttpMethod import com.vonage.client.verify2.Channel import com.vonage.client.verify2.VerifyResponseException -import org.junit.jupiter.api.Test import java.net.URI import java.util.UUID import kotlin.test.* diff --git a/src/test/kotlin/com/vonage/client/kt/VideoTest.kt b/src/test/kotlin/com/vonage/client/kt/VideoTest.kt new file mode 100644 index 0000000..f06c621 --- /dev/null +++ b/src/test/kotlin/com/vonage/client/kt/VideoTest.kt @@ -0,0 +1,1019 @@ +/* + * Copyright 2024 Vonage + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.vonage.client.kt + +import com.auth0.jwt.JWT +import com.vonage.client.video.* +import java.net.URI +import java.time.Duration +import java.time.Instant +import java.time.temporal.ChronoUnit +import java.util.* +import kotlin.test.* + +class VideoTest : AbstractTest() { + private val client = vonage.video + private val authType = AuthType.JWT + private val baseUrl = "/v2/project/$applicationId" + private val sessionId = "flR1ZSBPY3QgMjkgMTI6MTM6MjMgUERUIDIwMTN" + private val connectionId = "e9f8c166-6c67-440d-994a-04fb6dfed007" + private val streamId = "8b732909-0a06-46a2-8ea8-074e64d43422" + private val archiveId = "b40ef09b-3811-4726-b508-e41a0f96c68f" + private val broadcastId = "93e36bb9-b72c-45b6-a9ea-5c37dbc49906" + private val captionsId = "7c0680fc-6274-4de5-a66f-d0648e8d3ac2" + private val audioConnectorId = "b0a5a8c7-dc38-459f-a48d-a7f2008da853" + private val renderId = "1248e707-0b81-464c-9789-f46ad10e7764" + private val sipCallId = "b0a5a8c7-dc38-459f-a48d-a7f2008da853" + private val token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE2OTkwNDMxMTEsImV4cCI6MTY5OTA2NDcxMSwianRpIjoiMW1pODlqRk9meVpRIiwiYXBwbGljYXRpb25faWQiOiIxMjMxMjMxMi0zODExLTQ3MjYtYjUwOC1lNDFhMGY5NmM2OGYiLCJzdWIiOiJ2aWRlbyIsImFjbCI6IiJ9.o3U506EejsS8D5Tob90FG1NC1cR69fh3pFOpxnyTHVFfgqI6NWuuN8lEwrS3Zb8bGxE_A9LyyUZ2y4uqLpyXRw" + private val createdAtLong = 1414642898000L + private val updatedAtLong = 1437676551029L + private val createdAtInstant = Instant.ofEpochMilli(createdAtLong) + private val updatedAtInstant = Instant.ofEpochMilli(updatedAtLong) + private val sessionUrl = "$baseUrl/session/$sessionId" + private val connectionBaseUrl = "$sessionUrl/connection/$connectionId" + private val streamBaseUrl = "$sessionUrl/stream" + private val streamUrl = "$streamBaseUrl/$streamId" + private val archiveBaseUrl = "$baseUrl/archive/$archiveId" + private val broadcastBaseUrl = "$baseUrl/broadcast/$broadcastId" + private val broadcastLayoutUrl = "$broadcastBaseUrl/layout" + private val broadcastStreamsUrl = "$broadcastBaseUrl/streams" + private val archiveLayoutUrl = "$archiveBaseUrl/layout" + private val archiveStreamsUrl = "$archiveBaseUrl/streams" + private val renderBaseUrl = "$baseUrl/render" + private val renderUrl = "$renderBaseUrl/$renderId" + private val existingSession = client.session(sessionId) + private val existingConnection = existingSession.connection(connectionId) + private val existingStream = existingSession.stream(streamId) + private val existingArchive = client.archive(archiveId) + private val existingBroadcast = client.broadcast(broadcastId) + private val existingRender = client.render(renderId) + private val type = "chat" + private val data = "Text of the chat message" + private val signalRequestMap = mapOf("type" to type, "data" to data) + private val headers = mapOf("k1" to "Value 1", "Key 2" to "2 Val") + private val mediaUrl = "$exampleUrlBase/media" + private val maxDuration = 1800 + private val maxBitrate = 2000000 + private val archiveDuration = 5049L + private val archiveSize = 247748791L + private val videoType = VideoType.CAMERA + private val streamName = "My Stream" + private val archiveName = "Test Archive" + private val renderName = "Composed stream for Live event #1337" + private val multiBroadcastTag = "broadcast_tag_provided" + private val multiArchiveTag = "my-multi-archive" + private val offset = 16 + private val count = 450 + private val customOffsetCountMap = mapOf("offset" to offset, "count" to count) + private val defaultOffsetCountMap = mapOf("offset" to 0, "count" to 1000) + private val sessionIdMap = mapOf("sessionId" to sessionId) + private val customSessionOffsetCountMap = sessionIdMap + customOffsetCountMap + private val defaultSessionOffsetCountMap = sessionIdMap + defaultOffsetCountMap + private val layoutClasses = listOf("full", "no-border") + private val streamLayoutMap = mapOf( + "id" to streamId, + "videoType" to videoType.name.lowercase(), + "name" to streamName, + "layoutClassList" to layoutClasses + ) + private val renderResponseMap = sessionIdMap + mapOf( + "id" to renderId, + "applicationId" to applicationId, + "createdAt" to createdAtLong, + "callbackUrl" to statusCallbackUrl, + "updatedAt" to updatedAtLong, + "name" to renderName, + "url" to mediaUrl, + "resolution" to "480x640", + "status" to "starting", + "streamId" to streamId + ) + private val stylesheet = "stream.instructor {position: relative; width: 90%; height:45%;}" + private val rtmpId = randomUuidStr + private val rtmpServerUrl = "https://rtmp.example.org/server" + private val rtmpStatus = RtmpStatus.CONNECTING + private val archiveStatus = ArchiveStatus.UPLOADED + private val broadcastStatus = BroadcastStatus.STARTED + private val broadcastResolutionStr = "640x480" + private val broadcastResolution = Resolution.SD_LANDSCAPE + private val archiveResolutionStr = "1920x1080" + private val archiveResolution = Resolution.FHD_LANDSCAPE + private val dvr = false + private val lowLatency = true + private val hlsUrl = "https://hls.example.com/stream.m3u8" + private val broadcastAudio = true + private val broadcastVideo = false + private val archiveHasAudio = false + private val archiveHasVideo = true + private val broadcastStreamMode = StreamMode.MANUAL + private val archiveStreamMode = StreamMode.AUTO + private val archiveOutputMode = OutputMode.COMPOSED + private val streamIdOnly = mapOf("streamId" to streamId) + private val streamAudioAndVideo = streamIdOnly + mapOf("hasAudio" to true, "hasVideo" to true) + private val streamAudioNoVideo = mapOf("streamId" to randomUuidStr, "hasAudio" to true, "hasVideo" to false) + private val streamVideoNoAudio = mapOf("streamId" to randomUuidStr, "hasAudio" to false, "hasVideo" to true) + private val streamsList = listOf(streamIdOnly, streamAudioAndVideo, streamAudioNoVideo, streamVideoNoAudio) + private val removeStreamMap = mapOf("removeStream" to streamId) + private val pipLayoutMap = mapOf( + "type" to "bestFit", + "screenshareType" to "pip" + ) + private val customLayoutMap = mapOf( + "type" to "custom", + "stylesheet" to stylesheet + ) + private val hlsMap = "hls" to mapOf( + "dvr" to dvr, + "lowLatency" to lowLatency + ) + private val rtmpRequestMap = mapOf( + "id" to rtmpId, + "serverUrl" to rtmpServerUrl, + "streamName" to streamName + ) + private val rtmpResponseMap = rtmpRequestMap + mapOf("status" to rtmpStatus.name.lowercase()) + private val archiveVideoUrl = "https://tokbox.com.archive2.s3.amazonaws.com/123456/$archiveId/archive.mp4" + private val archiveBaseMap = sessionIdMap + mapOf( + "name" to archiveName, + "hasAudio" to archiveHasAudio, + "hasVideo" to archiveHasVideo, + "multiArchiveTag" to multiArchiveTag, + "resolution" to archiveResolutionStr, + "streamMode" to archiveStreamMode.name.lowercase() + ) + private val archiveRequestMap = archiveBaseMap + mapOf( + "outputMode" to archiveOutputMode.name.lowercase(), + "layout" to customLayoutMap + ) + private val archiveResponseMap = archiveBaseMap + mapOf( + "id" to archiveId, + "applicationId" to applicationId, + "createdAt" to createdAtLong, + "duration" to archiveDuration, + "size" to archiveSize, + "status" to archiveStatus.name.lowercase(), + "url" to archiveVideoUrl, + "streams" to streamsList + ) + private val broadcastBaseMap = sessionIdMap + mapOf( + "multiBroadcastTag" to multiBroadcastTag, + "maxDuration" to maxDuration, + "maxBitrate" to maxBitrate, + "resolution" to broadcastResolutionStr, + "streamMode" to broadcastStreamMode.name.lowercase() + ) + private val broadcastRequestMap = broadcastBaseMap + mapOf( + "layout" to pipLayoutMap, + "outputs" to mapOf( + hlsMap, "rtmp" to listOf(rtmpRequestMap) + ) + ) + private val broadcastResponseMap = broadcastBaseMap + mapOf( + "id" to broadcastId, + "applicationId" to applicationId, + "createdAt" to createdAtLong, + "updatedAt" to updatedAtLong, + "broadcastUrls" to mapOf( + "hls" to hlsUrl, + "rtmp" to listOf(rtmpResponseMap, emptyMap()) + ), + "settings" to mapOf(hlsMap), + "hasAudio" to broadcastAudio, + "hasVideo" to broadcastVideo, + "status" to broadcastStatus.name.lowercase(), + "streams" to streamsList + ) + + private fun addStreamMap(audio: Boolean = true, video: Boolean = true): Map = + mapOf("addStream" to streamId, "hasAudio" to audio, "hasVideo" to video) + + private fun assertEqualsSampleStream(response: GetStreamResponse) { + assertNotNull(response) + assertEquals(UUID.fromString(streamId), response.id) + assertEquals(videoType, response.videoType) + assertEquals(streamName, response.name) + assertEquals(layoutClasses, response.layoutClassList) + } + + private fun assertEqualsSampleRender(response: RenderResponse) { + assertNotNull(response) + assertEquals(UUID.fromString(renderId), response.id) + assertEquals(sessionId, response.sessionId) + assertEquals(UUID.fromString(applicationId), response.applicationId) + assertEquals(createdAtLong, response.createdAt) + assertEquals(URI.create(statusCallbackUrl), response.callbackUrl) + assertEquals(updatedAtLong, response.updatedAt) + assertEquals(URI.create(mediaUrl), response.url) + assertEquals(Resolution.SD_PORTRAIT, response.resolution) + assertEquals(RenderStatus.STARTING, response.status) + assertEquals(UUID.fromString(streamId), response.streamId) + } + + private fun assertEqualsVideoStreams(streams: List) { + assertNotNull(streams) + assertEquals(4, streams.size) + val idOnly = streams[0] + assertNotNull(idOnly) + assertEquals(UUID.fromString(streamId), idOnly.streamId) + assertNull(idOnly.hasAudio()) + assertNull(idOnly.hasVideo()) + val audioAndVideo = streams[1] + assertNotNull(audioAndVideo) + assertEquals(UUID.fromString(streamId), audioAndVideo.streamId) + assertTrue(audioAndVideo.hasAudio()) + assertTrue(audioAndVideo.hasVideo()) + val audioNoVideo = streams[2] + assertNotNull(audioNoVideo) + assertEquals(randomUuid, audioNoVideo.streamId) + assertTrue(audioNoVideo.hasAudio()) + assertFalse(audioNoVideo.hasVideo()) + val videoNoAudio = streams[3] + assertNotNull(videoNoAudio) + assertEquals(randomUuid, videoNoAudio.streamId) + assertFalse(videoNoAudio.hasAudio()) + assertTrue(videoNoAudio.hasVideo()) + } + + private fun assertEqualsSampleArchive(archive: Archive) { + assertNotNull(archive) + assertEquals(UUID.fromString(archiveId), archive.id) + assertEquals(sessionId, archive.sessionId) + assertEquals(UUID.fromString(applicationId), archive.applicationId) + assertEquals(multiArchiveTag, archive.multiArchiveTag) + assertEquals(archiveName, archive.name) + assertEquals(createdAtInstant, archive.createdAt) + assertEquals(Duration.ofSeconds(archiveDuration), archive.duration) + assertEquals(archiveSize, archive.size) + assertEquals(archiveStatus, archive.status) + assertEquals(archiveStreamMode, archive.streamMode) + assertEquals(archiveResolution, archive.resolution) + assertEquals(URI.create(archiveVideoUrl), archive.url) + assertTrue(archive.hasVideo()) + assertFalse(archive.hasAudio()) + assertEqualsVideoStreams(archive.streams) + } + + private fun assertEqualsSampleBroadcast(broadcast: Broadcast) { + assertNotNull(broadcast) + assertEquals(UUID.fromString(broadcastId), broadcast.id) + assertEquals(sessionId, broadcast.sessionId) + assertEquals(UUID.fromString(applicationId), broadcast.applicationId) + assertEquals(multiBroadcastTag, broadcast.multiBroadcastTag) + assertEquals(createdAtInstant, broadcast.createdAt) + assertEquals(updatedAtInstant, broadcast.updatedAt) + assertEquals(Duration.ofSeconds(maxDuration.toLong()), broadcast.maxDuration) + assertEquals(maxBitrate, broadcast.maxBitrate) + val broadcastUrls = broadcast.broadcastUrls + assertNotNull(broadcastUrls) + assertEquals(URI.create(hlsUrl), broadcastUrls.hls) + val rtmps = broadcastUrls.rtmps + assertNotNull(rtmps) + assertEquals(2, rtmps.size) + val mainRtmp = rtmps[0] + assertNotNull(mainRtmp) + assertEquals(rtmpId, mainRtmp.id) + assertEquals(URI.create(rtmpServerUrl), mainRtmp.serverUrl) + assertEquals(rtmpStatus, mainRtmp.status) + assertEquals(streamName, mainRtmp.streamName) + val emptyRtmp = rtmps[1] + assertNotNull(emptyRtmp) + assertNull(emptyRtmp.id) + assertNull(emptyRtmp.serverUrl) + assertNull(emptyRtmp.status) + assertNull(emptyRtmp.streamName) + val hls = broadcast.hlsSettings + assertNotNull(hls) + assertEquals(dvr, hls.dvr()) + assertEquals(lowLatency, hls.lowLatency()) + assertEquals(broadcastResolution, broadcast.resolution) + assertEquals(broadcastAudio, broadcast.hasAudio()) + assertEquals(broadcastVideo, broadcast.hasVideo()) + assertEquals(broadcastStreamMode, broadcast.streamMode) + assertEquals(broadcastStatus, broadcast.status) + assertEqualsVideoStreams(broadcast.streams) + } + + private fun assertEqualsEmptyArchive(archive: Archive) { + assertNotNull(archive) + assertNull(archive.id) + assertNull(archive.sessionId) + assertNull(archive.applicationId) + assertNull(archive.createdAt) + assertNull(archive.multiArchiveTag) + assertNull(archive.name) + assertNull(archive.duration) + assertNull(archive.size) + assertNull(archive.status) + assertNull(archive.streamMode) + assertNull(archive.resolution) + assertNull(archive.url) + assertNull(archive.hasAudio()) + assertNull(archive.hasVideo()) + assertNull(archive.streams) + } + + private fun assertEqualsEmptyBroadcast(broadcast: Broadcast) { + assertNotNull(broadcast) + assertNull(broadcast.id) + assertNull(broadcast.sessionId) + assertNull(broadcast.applicationId) + assertNull(broadcast.multiBroadcastTag) + assertNull(broadcast.createdAt) + assertNull(broadcast.updatedAt) + assertNull(broadcast.maxDuration) + assertNull(broadcast.maxBitrate) + assertNull(broadcast.broadcastUrls) + assertNull(broadcast.hlsSettings) + assertNull(broadcast.resolution) + assertNull(broadcast.hasAudio()) + assertNull(broadcast.hasVideo()) + assertNull(broadcast.streamMode) + assertNull(broadcast.status) + assertNull(broadcast.streams) + } + + private fun assertListArchives(params: Map, invocation: () -> List) { + mockGet(expectedUrl = "$baseUrl/archive", authType = authType, + expectedQueryParams = params, expectedResponseParams = mapOf( + "count" to count, + "items" to listOf(archiveResponseMap, mapOf()) + ) + ) + val response = invocation() + assertEquals(2, response.size) + assertEqualsSampleArchive(response[0]) + assertEqualsEmptyArchive(response[1]) + } + + private fun assertListBroadcasts(params: Map, invocation: () -> List) { + mockGet(expectedUrl = "$baseUrl/broadcast", authType = authType, + expectedQueryParams = params, expectedResponseParams = mapOf( + "count" to count, + "items" to listOf(broadcastResponseMap, mapOf()) + ) + ) + val response = invocation() + assertEquals(2, response.size) + assertEqualsSampleBroadcast(response[0]) + assertEqualsEmptyBroadcast(response[1]) + } + + private fun assertEqualsJwt(encoded: String, role: Role = Role.PUBLISHER, + ttl: Duration = Duration.ofHours(24), assertions: Map? = null) { + val decoded = JWT.decode(encoded) + assertNotNull(decoded) + assertNotNull(decoded.id) + assertNotNull(decoded.signature) + assertNotNull(decoded.issuedAt) + val iat = decoded.issuedAtAsInstant + assertTrue(Instant.now().isBefore(iat.plusSeconds(15))) + val expectedExpires = iat.plus(ttl).truncatedTo(ChronoUnit.SECONDS) + assertEquals(expectedExpires, decoded.expiresAtAsInstant?.truncatedTo(ChronoUnit.SECONDS)) + val claims = decoded.claims + assertNotNull(claims) + assertEquals(applicationId, claims["application_id"]?.asString()) + assertEquals("session.connect", claims["scope"]?.asString()) + assertEquals(sessionId, claims["session_id"]?.asString()) + assertEquals(role.name.lowercase(), claims["role"]?.asString()) + assertions?.forEach { (key, value) -> assertEquals(value, claims[key]?.asString()) } + } + + @Test + fun `generate token default parameters`() { + assertEqualsJwt(existingSession.generateToken()) + } + + @Test + fun `generate token all parameters`() { + val data = userName + val role = Role.SUBSCRIBER + val ttl = Duration.ofHours(8) + + val encoded = existingSession.generateToken { + data(data); role(role); expiryLength(ttl) + initialLayoutClassList(layoutClasses) + } + + assertEqualsJwt(encoded, role, ttl, mapOf( + "connection_data" to data, + "initial_layout_class_list" to layoutClasses.joinToString(separator = " ") + )) + } + + @Test + fun `start audio connector all fields`() { + mockPost(expectedUrl = "$baseUrl/connect", expectedRequestParams = mapOf( + "sessionId" to sessionId, + "token" to token, + "websocket" to mapOf( + "uri" to websocketUri, + "streams" to listOf(streamId, randomUuidStr), + "headers" to headers, + "audioRate" to 16000 + ) + ), + expectedResponseParams = mapOf( + "id" to audioConnectorId, + "connectionId" to connectionId + ) + ) + + val response = client.connectToWebsocket { + uri(websocketUri); headers(headers) + sessionId(sessionId); token(token) + streams(streamId, randomUuidStr) + audioRate(Websocket.AudioRate.L16_16K) + } + assertNotNull(response) + assertEquals(UUID.fromString(audioConnectorId), response.id) + assertEquals(UUID.fromString(connectionId), response.connectionId) + } + + @Test + fun `start audio connector required fields`() { + mockPost(expectedUrl = "$baseUrl/connect", expectedRequestParams = mapOf( + "sessionId" to sessionId, "token" to token, + "websocket" to mapOf("uri" to websocketUri) + ), + expectedResponseParams = mapOf("id" to audioConnectorId)) + + val response = client.connectToWebsocket { + uri(websocketUri); sessionId(sessionId); token(token) + } + assertNotNull(response) + assertEquals(UUID.fromString(audioConnectorId), response.id) + assertNull(response.connectionId) + } + + @Test + fun `start live captions all parameters`() { + val partialCaptions = true + mockPost(expectedUrl = "$baseUrl/captions", status = 202, + expectedRequestParams = mapOf( + "sessionId" to sessionId, + "token" to token, + "languageCode" to "en-US", + "maxDuration" to maxDuration, + "partialCaptions" to partialCaptions, + "statusCallbackUrl" to statusCallbackUrl + ), + expectedResponseParams = mapOf("captionsId" to captionsId) + ) + assertEquals(UUID.fromString(captionsId), existingSession.startCaptions(token) { + languageCode(Language.EN_US); maxDuration(maxDuration) + partialCaptions(partialCaptions); statusCallbackUrl(statusCallbackUrl) + }) + } + + @Test + fun `stop live captions`() { + mockPost(expectedUrl = "$baseUrl/captions/$captionsId/stop", status = 202) + existingSession.stopCaptions(captionsId) + } + + @Test + fun `play DTMF into SIP call`() { + mockPost(expectedUrl = "$sessionUrl/play-dtmf", expectedRequestParams = mapOf("digits" to dtmf)) + existingSession.sendDtmf(dtmf) + } + + @Test + fun `send DTMF to specific participant`() { + mockPost(expectedUrl = "$connectionBaseUrl/play-dtmf", expectedRequestParams = mapOf("digits" to dtmf)) + existingConnection.sendDtmf(dtmf) + } + + @Test + fun `initiate outbound SIP call all parameters`() { + val from = "from@example.com" + val secure = true + val video = false + val observeForceMute = true + val password = "P@s5w0rd123" + + mockPost(expectedUrl = "$baseUrl/dial", expectedRequestParams = mapOf( + "sessionId" to sessionId, "token" to token, + "sip" to mapOf( + "uri" to "$sipUri;transport=tls", + "from" to from, + "headers" to headers, + "auth" to mapOf( + "username" to userName, + "password" to password + ), + "secure" to secure, + "video" to video, + "observeForceMute" to observeForceMute + ) + ), + expectedResponseParams = mapOf( + "id" to sipCallId, + "connectionId" to connectionId, + "streamId" to streamId + ) + ) + val response = client.sipDial { + sessionId(sessionId); token(token) + uri(URI.create(sipUri), true) + addHeaders(headers); secure(secure) + from(from); video(video) + observeForceMute(observeForceMute) + username(userName); password(password) + } + assertNotNull(response) + assertEquals(sipCallId, response.id) + assertEquals(connectionId, response.connectionId) + assertEquals(streamId, response.streamId) + } + + @Test + fun `signal all participants`() { + mockPost(expectedUrl = "$sessionUrl/signal", expectedRequestParams = signalRequestMap, status = 204) + existingSession.signalAll(type, data) + } + + @Test + fun `signal single participant`() { + mockPost(expectedUrl = "$connectionBaseUrl/signal", expectedRequestParams = signalRequestMap, status = 204) + existingConnection.signal(type, data) + } + + @Test + fun `force disconnect`() { + mockDelete(expectedUrl = connectionBaseUrl) + existingConnection.disconnect() + } + + @Test + fun `mute participant stream`() { + mockPost(expectedUrl = "$streamUrl/mute") + existingStream.mute() + } + + @Test + fun `mute all streams empty response`() { + mockPost(expectedUrl = "$sessionUrl/mute", + expectedRequestParams = mapOf("active" to true), + expectedResponseParams = mapOf() + ) + val response = existingSession.muteStreams() + assertNotNull(response) + assertNull(response.applicationId) + assertNull(response.status) + assertNull(response.name) + assertNull(response.environment) + assertNull(response.createdAt) + } + + @Test + fun `mute selected streams full response`() { + val active = false + val status = ProjectStatus.ACTIVE + val name = "Project Name" + val environment = ProjectEnvironment.STANDARD + + mockPost(expectedUrl = "$sessionUrl/mute", + expectedRequestParams = mapOf( + "active" to active, + "excludedStreamIds" to listOf(streamId, randomUuidStr) + ), + expectedResponseParams = mapOf( + "applicationId" to applicationId, + "status" to status.name, + "name" to name, + "environment" to environment.name.lowercase(), + "createdAt" to createdAtLong + ) + ) + val response = existingSession.muteStreams(active, streamId, randomUuidStr) + assertNotNull(response) + assertEquals(applicationId, response.applicationId) + assertEquals(status, response.status) + assertEquals(name, response.name) + assertEquals(environment, response.environment) + assertEquals(createdAtLong, response.createdAt) + } + + @Test + fun `get single stream layout`() { + mockGet(expectedUrl = streamUrl, expectedResponseParams = streamLayoutMap) + assertEqualsSampleStream(existingStream.info()) + } + + @Test + fun `get all stream layouts`() { + mockGet(expectedUrl = streamBaseUrl, expectedResponseParams = mapOf( + "count" to 4, + "items" to listOf( + mapOf(), + streamLayoutMap, + mapOf("id" to randomUuidStr), + mapOf("layoutClassList" to listOf()) + ) + )) + val response = existingSession.listStreams() + assertEquals(4, response.size) + val empty = response[0] + assertNotNull(empty) + assertNull(empty.id) + assertNull(empty.videoType) + assertNull(empty.name) + assertNull(empty.layoutClassList) + assertEqualsSampleStream(response[1]) + val idOnly = response[2] + assertNotNull(idOnly) + assertEquals(UUID.fromString(randomUuidStr), idOnly.id) + assertNull(idOnly.videoType) + assertNull(idOnly.name) + assertNull(idOnly.layoutClassList) + val emptyLayout = response[3] + assertNotNull(emptyLayout) + assertEquals(0, emptyLayout.layoutClassList.size) + } + + @Test + fun `change stream layout`() { + mockPut(expectedUrl = streamBaseUrl, expectedRequestParams = mapOf( + "items" to listOf(mapOf( + "id" to streamId, + "layoutClassList" to layoutClasses + )) + )) + existingStream.setLayout(*layoutClasses.toTypedArray()) + } + + @Test + fun `create session no parameters`() { + mockPostQueryParams( + expectedUrl = "/session/create", + authType = authType, + expectedRequestParams = mapOf(), + expectedResponseParams = listOf(mapOf()) + ) + val response = client.createSession() + assertNotNull(response) + assertNull(response.sessionId) + assertNull(response.applicationId) + assertNull(response.createDt) + assertNull(response.mediaServerUrl) + } + + @Test + fun `create session all parameters`() { + val location = "127.0.0.1" + val archiveMode = ArchiveMode.ALWAYS + + mockPostQueryParams( + expectedUrl = "/session/create", + authType = authType, + expectedRequestParams = mapOf( + "archiveMode" to archiveMode.name.lowercase(), + "location" to location, + "p2p.preference" to "disabled" + ), + expectedResponseParams = listOf(mapOf( + "session_id" to sessionId, + "application_id" to applicationId, + "create_dt" to createdAtLong, + "media_server_url" to mediaUrl + )) + ) + val response = client.createSession { + location(location) + mediaMode(MediaMode.ROUTED) + archiveMode(archiveMode) + } + assertNotNull(response) + assertEquals(sessionId, response.sessionId) + assertEquals(UUID.fromString(applicationId), response.applicationId) + assertEquals(createdAtLong.toString(), response.createDt) + assertEquals(URI.create(mediaUrl), response.mediaServerUrl) + } + + @Test + fun `list archives for session no parameters`() { + assertListArchives(defaultSessionOffsetCountMap, existingSession::listArchives) + } + + @Test + fun `list archives for session both parameters`() { + assertListArchives(customSessionOffsetCountMap) { + existingSession.listArchives(count, offset) + } + } + + @Test + fun `list archives both parameters`() { + assertListArchives(customOffsetCountMap) { + client.listArchives(count, offset) + } + } + + @Test + fun `list archives default parameters`() { + assertListArchives(defaultOffsetCountMap, client::listArchives) + } + + @Test + fun `list broadcasts for session no parameters`() { + assertListBroadcasts(defaultSessionOffsetCountMap, existingSession::listBroadcasts) + } + + @Test + fun `list broadcasts for session both parameters`() { + assertListBroadcasts(customSessionOffsetCountMap) { + existingSession.listBroadcasts(count, offset) + } + } + + @Test + fun `list broadcasts both parameters`() { + assertListBroadcasts(customOffsetCountMap) { + client.listBroadcasts(count, offset) + } + } + + @Test + fun `list broadcasts default parameters`() { + assertListBroadcasts(defaultOffsetCountMap, client::listBroadcasts) + } + + @Test + fun `create archive required parameters`() { + mockPost(expectedUrl = "$baseUrl/archive", authType = authType, + expectedRequestParams = sessionIdMap, + expectedResponseParams = sessionIdMap + ) + val response = existingSession.createArchive() + assertNotNull(response) + assertEquals(sessionId, response.sessionId) + } + + @Test + fun `create archive all parameters`() { + mockPost(expectedUrl = "$baseUrl/archive", authType = authType, + expectedRequestParams = archiveRequestMap, + expectedResponseParams = archiveResponseMap + ) + assertEqualsSampleArchive(existingSession.createArchive { + name(archiveName); resolution(archiveResolution) + multiArchiveTag(multiArchiveTag) + hasVideo(archiveHasVideo); hasAudio(archiveHasAudio) + streamMode(archiveStreamMode); outputMode(archiveOutputMode) + layout(ScreenLayoutType.HORIZONTAL) // This is to get 100% coverage; override below + layout(ScreenLayoutType.CUSTOM, stylesheet = stylesheet) + }) + } + + @Test + fun `create broadcast required parameters`() { + mockPost(expectedUrl = "$baseUrl/broadcast", authType = authType, + expectedRequestParams = sessionIdMap + mapOf( + "outputs" to mapOf("hls" to emptyMap()) + ), + expectedResponseParams = sessionIdMap + mapOf("broadcastUrls" to mapOf("hls" to hlsUrl)) + ) + val response = existingSession.startBroadcast { + hls() + } + assertNotNull(response) + assertEquals(sessionId, response.sessionId) + assertNull(response.hlsSettings) + assertNotNull(response.broadcastUrls) + assertEquals(URI.create(hlsUrl), response.broadcastUrls.hls) + } + + @Test + fun `create broadcast all parameters`() { + mockPost(expectedUrl = "$baseUrl/broadcast", authType = authType, + expectedRequestParams = broadcastRequestMap, + expectedResponseParams = broadcastResponseMap + ) + assertEqualsSampleBroadcast(existingSession.startBroadcast { + multiBroadcastTag(multiBroadcastTag) + maxDuration(maxDuration); maxBitrate(maxBitrate) + resolution(broadcastResolution); streamMode(broadcastStreamMode) + layout(ScreenLayoutType.VERTICAL) // This is to get 100% coverage; override below + layout(ScreenLayoutType.BEST_FIT, ScreenLayoutType.PIP) + hls { + dvr(dvr); lowLatency(lowLatency) + } + addRtmpStream { + id(rtmpId); serverUrl(rtmpServerUrl); streamName(streamName) + } + }) + } + + @Test + fun `get archive`() { + mockGet(expectedUrl = archiveBaseUrl, expectedResponseParams = archiveResponseMap) + assertEqualsSampleArchive(existingArchive.info()) + } + + @Test + fun `stop archive`() { + mockPost(expectedUrl = "$archiveBaseUrl/stop", expectedResponseParams = archiveResponseMap) + assertEqualsSampleArchive(existingArchive.stop()) + } + + @Test + fun `delete archive`() { + mockDelete(expectedUrl = archiveBaseUrl, authType = authType) + existingArchive.delete() + } + + @Test + fun `add archive stream id only`() { + mockPatch( + expectedUrl = archiveStreamsUrl, + expectedRequestParams = addStreamMap(), + authType = authType, status = 204 + ) + existingArchive.addStream(streamId) + } + + @Test + fun `add archive stream audio and video`() { + val audio = true; val video = false + mockPatch( + expectedUrl = archiveStreamsUrl, + expectedRequestParams = addStreamMap(audio = audio, video = video), + authType = authType, status = 204 + ) + existingArchive.addStream(streamId, audio = audio, video = video) + } + + @Test + fun `remove archive stream`() { + mockPatch( + expectedUrl = archiveStreamsUrl, + expectedRequestParams = removeStreamMap, + authType = authType, status = 204 + ) + existingArchive.removeStream(streamId) + } + + @Test + fun `change archive layout vertical`() { + mockPut(expectedUrl = archiveLayoutUrl, expectedRequestParams = mapOf("type" to "verticalPresentation")) + existingArchive.setLayout(ScreenLayoutType.VERTICAL) + } + + @Test + fun `change archive layout pip`() { + mockPut(expectedUrl = archiveLayoutUrl, expectedRequestParams = pipLayoutMap) + existingArchive.setLayout(ScreenLayoutType.BEST_FIT, ScreenLayoutType.PIP) + } + + @Test + fun `change archive layout stylesheet`() { + mockPut(expectedUrl = archiveLayoutUrl, expectedRequestParams = customLayoutMap) + existingArchive.setLayout(ScreenLayoutType.CUSTOM, stylesheet = stylesheet) + } + + @Test + fun `get broadcast`() { + mockGet(expectedUrl = broadcastBaseUrl, expectedResponseParams = broadcastResponseMap) + assertEqualsSampleBroadcast(existingBroadcast.info()) + } + + @Test + fun `stop broadcast`() { + mockPost(expectedUrl = "$broadcastBaseUrl/stop", expectedResponseParams = broadcastResponseMap) + assertEqualsSampleBroadcast(existingBroadcast.stop()) + } + + @Test + fun `add broadcast stream id only`() { + mockPatch( + expectedUrl = broadcastStreamsUrl, + expectedRequestParams = addStreamMap(), + authType = authType, status = 204 + ) + existingBroadcast.addStream(streamId) + } + + @Test + fun `add broadcast stream audio and video`() { + val audio = false; val video = true + mockPatch( + expectedUrl = broadcastStreamsUrl, + expectedRequestParams = addStreamMap(audio = audio, video = video), + authType = authType, status = 204 + ) + existingBroadcast.addStream(streamId, audio = audio, video = video) + } + + @Test + fun `remove broadcast stream`() { + mockPatch( + expectedUrl = broadcastStreamsUrl, + expectedRequestParams = removeStreamMap, + authType = authType, status = 204 + ) + existingBroadcast.removeStream(streamId) + } + + @Test + fun `change broadcast layout horizontal`() { + mockPut(expectedUrl = broadcastLayoutUrl, expectedRequestParams = mapOf("type" to "horizontalPresentation")) + existingBroadcast.setLayout(ScreenLayoutType.HORIZONTAL) + } + + @Test + fun `change broadcast layout pip`() { + mockPut(expectedUrl = broadcastLayoutUrl, expectedRequestParams = pipLayoutMap) + existingBroadcast.setLayout(ScreenLayoutType.BEST_FIT, ScreenLayoutType.PIP) + } + + @Test + fun `change broadcast layout stylesheet`() { + mockPut(expectedUrl = broadcastLayoutUrl, expectedRequestParams = customLayoutMap) + existingBroadcast.setLayout(ScreenLayoutType.CUSTOM, stylesheet = stylesheet) + } + + @Test + fun `stop experience composer`() { + mockDelete(expectedUrl = renderUrl, authType = authType) + existingRender.stop() + } + + @Test + fun `get experience composer`() { + mockGet(expectedUrl = renderUrl, expectedResponseParams = renderResponseMap) + assertEqualsSampleRender(existingRender.info()) + } + + @Test + fun `list experience composers no parameters`() { + mockGet(expectedUrl = renderBaseUrl, + expectedQueryParams = defaultOffsetCountMap, + expectedResponseParams = mapOf( + "count" to count, + "items" to listOf>() + ) + ) + val response = client.listRenders() + assertNotNull(response) + assertEquals(0, response.size) + } + + @Test + fun `list experience composers both parameters`() { + mockGet(expectedUrl = renderBaseUrl, + expectedQueryParams = customOffsetCountMap, + expectedResponseParams = mapOf( + "count" to 2, + "items" to listOf(renderResponseMap, mapOf()) + ) + ) + val response = client.listRenders(count, offset) + assertNotNull(response) + assertEquals(2, response.size) + assertEqualsSampleRender(response[0]) + val empty = response[1] + assertNotNull(empty) + assertNull(empty.id) + assertNull(empty.sessionId) + assertNull(empty.applicationId) + assertNull(empty.createdAt) + assertNull(empty.callbackUrl) + assertNull(empty.updatedAt) + assertNull(empty.url) + assertNull(empty.resolution) + assertNull(empty.status) + assertNull(empty.streamId) + } + + @Test + fun `start experience composer all parameters`() { + mockPost(expectedUrl = renderBaseUrl, expectedRequestParams = mapOf( + "sessionId" to sessionId, + "token" to token, + "url" to mediaUrl, + "maxDuration" to maxDuration, + "resolution" to "1280x720", + "properties" to mapOf("name" to renderName) + ), + expectedResponseParams = renderResponseMap + ) + assertEqualsSampleRender(client.startRender { + url(mediaUrl); maxDuration(maxDuration) + resolution(Resolution.HD_LANDSCAPE) + sessionId(sessionId); token(token); name(renderName) + }) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/vonage/client/kt/VoiceTest.kt b/src/test/kotlin/com/vonage/client/kt/VoiceTest.kt index 9d112d6..9c6be80 100644 --- a/src/test/kotlin/com/vonage/client/kt/VoiceTest.kt +++ b/src/test/kotlin/com/vonage/client/kt/VoiceTest.kt @@ -20,7 +20,6 @@ import com.vonage.client.voice.* import com.vonage.client.voice.ncco.* import java.net.URI import java.util.* -import kotlin.test.Test import kotlin.test.* class VoiceTest : AbstractTest() { @@ -36,7 +35,6 @@ class VoiceTest : AbstractTest() { private val count = 89 private val pageSize = 25 private val recordIndex = 14 - private val dtmf = "p*123#" private val fromPstn = "14155550100" private val streamUrl = "$exampleUrlBase/waiting.mp3" private val onAnswerUrl = "$exampleUrlBase/ncco.json"