From c23b0db52f84b65b4d77fd230605da5983d232f4 Mon Sep 17 00:00:00 2001 From: Sina Madani Date: Thu, 5 Sep 2024 14:32:08 +0100 Subject: [PATCH] Video minor refactor & KDocs --- CHANGELOG.md | 3 + src/main/kotlin/com/vonage/client/kt/Video.kt | 288 +++++++++++++++++- .../kotlin/com/vonage/client/kt/VideoTest.kt | 23 +- 3 files changed, 291 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0a7bd21..b533b6a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ GA release! ### Added - Documentation (KDocs) for all classes and methods. +### Changed +- Moved Video API's `connectToWebSocket`, `startRender` and `sipDial` methods to `ExistingSession`. + ## [1.0.0-beta1] - 2024-09-02 Feature-complete beta release. diff --git a/src/main/kotlin/com/vonage/client/kt/Video.kt b/src/main/kotlin/com/vonage/client/kt/Video.kt index e228408..29df65f 100644 --- a/src/main/kotlin/com/vonage/client/kt/Video.kt +++ b/src/main/kotlin/com/vonage/client/kt/Video.kt @@ -25,86 +25,304 @@ import java.util.* */ class Video(private val client: VideoClient) { + /** + * Create a new session. + * + * @param properties (OPTIONAL) A lambda function to set the parameters of the session. + * + * @return The created session metadata. + */ fun createSession(properties: CreateSessionRequest.Builder.() -> Unit = {}): CreateSessionResponse = client.createSession(CreateSessionRequest.builder().apply(properties).build()) + /** + * Call this method to work with an existing session. + * + * @param sessionId The UUID of the session to work with. + * + * @return An [ExistingSession] object with methods to interact with the session. + */ fun session(sessionId: String): ExistingSession = ExistingSession(sessionId) - + /** + * Class for working with an existing session. + * + * @property id The session ID. + */ inner class ExistingSession internal constructor(id: String): ExistingResource(id) { + /** + * Call this method to work with an existing stream. + * + * @param streamId UUID of the stream to work with. + */ fun stream(streamId: String): ExistingStream = ExistingStream(streamId) + /** + * Class for working with an existing stream. + * + * @property id The stream ID. + */ inner class ExistingStream internal constructor(id: String): ExistingResource(id) { + /** + * Retrieves the stream details. + * + * @return The stream metadata. + */ fun info(): GetStreamResponse = client.getStream(this@ExistingSession.id, id) + /** + * Mute the stream. + */ fun mute(): Unit = client.muteStream(this@ExistingSession.id, id) + /** + * Update the stream's video layout. + * + * @param layoutClasses The layout class names to apply to the stream. + */ fun setLayout(vararg layoutClasses: String): Unit = client.setStreamLayout(this@ExistingSession.id, SessionStream.builder(id).layoutClassList(layoutClasses.toList()).build() ) } + /** + * Call this method to work with an existing client (participant). + * + * @param connectionId UUID of the connection to work with. + * + * @return An [ExistingConnection] object with methods to interact with the connection. + */ fun connection(connectionId: String): ExistingConnection = ExistingConnection(connectionId) + /** + * Class for working with an existing connection. + * + * @property id The connection ID. + */ inner class ExistingConnection internal constructor(id: String): ExistingResource(id) { + /** + * Force the client to disconnect from the session. + */ fun disconnect(): Unit = client.forceDisconnect(this@ExistingSession.id, id) + /** + * Send a signal to the client. + * + * @param type Type of data that is being sent to the client. This cannot exceed 128 bytes. + * @param data Payload that is being sent to the client. This cannot exceed 8kb. + */ fun signal(type: String, data: String): Unit = client.signal(this@ExistingSession.id, id, signalRequest(type, data)) + /** + * Send DTMF tones to the client. Telephony events are negotiated over SDP and transmitted as + * RFC4733/RFC2833 digits to the remote endpoint. + * + * @param digits The string of DTMF digits to send. This can include 0-9, '*', '#', and 'p'. + * A 'p' indicates a pause of 500ms (if you need to add a delay in sending the digits). + */ fun sendDtmf(digits: String): Unit = client.sendDtmf(this@ExistingSession.id, id, digits) } + /** + * Mute or unmute all streams in the session, except for those specified. + * + * @param active Whether to mute streams in the session (`true`) and enable the mute state of the session, + * or to disable the mute state of the session (`false`). With the mute state enabled (true), all current + * and future streams published to the session (except streams in the `excludedStreamIds` array) are muted. + * When you call this method with the active property set to `false`, future streams published to the + * session are not muted (but any existing muted streams remain muted). + * + * @param excludedStreamIds (OPTIONAL) The stream IDs for streams that should not be muted. If you omit + * this property, all streams in the session will be muted. This property only applies when the active + * property is set to true. When the active property is set to false, it is ignored. + * + * @return The updated project details. + */ fun muteStreams(active: Boolean = true, vararg excludedStreamIds: String): ProjectDetails = client.muteSession(id, active, if (excludedStreamIds.isNotEmpty()) excludedStreamIds.toList() else null ) + /** + * List all streams in the session. This can be used to get information about layout classes used by a Vonage + * Video stream. The layout classes define how the stream is displayed in the layout of a broadcast stream. + * + * @return A list of streams and their layouts for this session. + */ fun listStreams(): List = client.listStreams(id) + /** + * List all Archives of this session. + * + * @param count (OPTIONAL) The number of Archives to return (maximum 1000). + * @param offset (OPTIONAL) Index of the first Archive to return (used for pagination). + * + * @return A list of Archives. + */ fun listArchives(count: Int = 1000, offset: Int = 0): List = client.listArchives(listCompositionsFilter(count, offset, id)) + /** + * List all Broadcasts in the application. + * + * @param count (OPTIONAL) The number of Broadcasts to return (maximum 1000). + * @param offset (OPTIONAL) Index of the first Broadcast to return (used for pagination). + * + * @return A list of Broadcasts. + */ fun listBroadcasts(count: Int = 1000, offset: Int = 0): List = client.listBroadcasts(listCompositionsFilter(count, offset, id)) + /** + * Send a signal to all participants (clients) in the session. + * + * @param type Type of data that is being sent to the clients. This cannot exceed 128 bytes. + * @param data Payload that is being sent to the clients. This cannot exceed 8kb. + */ fun signalAll(type: String, data: String): Unit = client.signalAll(id, signalRequest(type, data)) + /** + * Send DTMF tones to all participants in the session. Telephony events are negotiated over SDP and transmitted + * as RFC4733/RFC2833 digits to the remote endpoint. + * + * @param digits The string of DTMF digits to send. This can include 0-9, '*', '#', and 'p'. + * A 'p' indicates a pause of 500ms (if you need to add a delay in sending the digits). + */ fun sendDtmf(digits: String): Unit = client.sendDtmf(id, digits) + /** + * Start real-time Live Captions for the session. + * + * The maximum allowed duration is 4 hours, after which the audio captioning will stop without any effect + * on the ongoing Vonage Video Session. An event will be posted to your callback URL if provided when + * starting the captions. Each Vonage Video Session supports only one audio captioning session. + * + * @param token A valid Vonage JWT with role set to Moderator. + * @param properties (OPTIONAL) A lambda function to set the parameters of the audio captioning session. + * + * @return Unique ID of the audio captioning session + */ fun startCaptions(token: String, properties: CaptionsRequest.Builder.() -> Unit = {}): UUID = client.startCaptions(CaptionsRequest.builder() .apply(properties).sessionId(id).token(token).build() ).captionsId + /** + * Stop Live Captions for the session. + * + * @param captionsId The unique ID of the audio captioning session. + */ fun stopCaptions(captionsId: String): Unit = client.stopCaptions(captionsId) + /** + * Establish a SIP connection to this session. + * + * The audio from your end of the SIP call is added to the sessionnas an audio-only stream. The Vonage Video + * Media Router mixes audio from other streams in the session and sends the mixed audio to your SIP endpoint. + * The call ends when your SIP server sends a `BYE` message to terminate the call. You can also end a call + * using the [ExistingSession.ExistingConnection.disconnect] method. The Vonage Video SIP gateway automatically + * ends a call after 5 minutes of inactivity (5 minutes without media received). Also, as a security measure, + * the Vonage Video SIP gateway closes any SIP call that lasts longer than 6 hours. The SIP interconnect + * feature requires the session's media mode to be set to [MediaMode.ROUTED]. For more information, including + * technical details and security considerations, see the Vonage Video SIP interconnect developer guide. + * + * @param properties A lambda function to set the parameters of the SIP dial request. + * You need to provide a token and a SIP URI to establish the connection. + * + * @return Details of the SIP connection. + */ + fun sipDial(properties: SipDialRequest.Builder.() -> Unit): SipDialResponse = + client.sipDial(SipDialRequest.builder().sessionId(id).apply(properties).build()) + + /** + * Send audio from the session to a WebSocket. This feature is only supported in `routed` sessions. + * + * @param properties A lambda function to set the parameters of the WebSocket connection. + * + * @return Details of the WebSocket connection. + */ + fun connectToWebsocket(properties: ConnectRequest.Builder.() -> Unit): ConnectResponse = + client.connectToWebsocket(ConnectRequest.builder().sessionId(id).apply(properties).build()) + + /** + * Create a new Experience Composer for this session. + * + * @param properties A lambda function to set the parameters of the Experience Composer. + * + * @return Details of the created Experience Composer. + */ + fun startRender(properties: RenderRequest.Builder.() -> Unit): RenderResponse = + client.startRender(RenderRequest.builder().sessionId(id).apply(properties).build()) + + /** + * Create a new Archive from this session. + * + * @param properties (OPTIONAL) A lambda function to set the parameters of the Archive. + * + * @return Details of the created Archive. + */ fun createArchive(properties: Archive.Builder.() -> Unit = {}): Archive = client.createArchive(Archive.builder(id).apply(properties).build()) + /** + * Start a live broadcast for the session to HLS (HTTP Live-Streaming) or RTMP streams. + * + * To successfully start broadcasting a session, at least one client must be connected to the session. + * The live-streaming broadcast can target one HLS endpoint and up to five RTMP servers simultaneously + * for a session. You can only start live-streaming for sessions that use the Vonage Video Media Router + * (with the media mode set to [MediaMode.ROUTED]); you cannot use live-streaming with sessions that have + * the media mode set to [MediaMode.RELAYED]. + * + * @param properties A lambda function to set the parameters of the broadcast. See [Broadcast.Builder] + * for details of required and optional parameters. + * + * @return Details of the created broadcast. + */ fun startBroadcast(properties: Broadcast.Builder.() -> Unit): Broadcast = client.createBroadcast(Broadcast.builder(id).apply(properties).build()) + /** + * Generate a JWT for the session, which can be used in various other methods as required. + * + * @param options (OPTIONAL) A lambda function to set the parameters (claims) of the token. + * + * @return A new JWT with the specified claims. + */ 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()) - + /** + * List all Archives in the application. + * + * @param count (OPTIONAL) The number of Archives to return (maximum 1000). + * @param offset (OPTIONAL) Index of the first Archive to return (used for pagination). + * + * @return A list of Archives. + */ fun listArchives(count: Int = 1000, offset: Int = 0): List = client.listArchives(listCompositionsFilter(count, offset)) + /** + * Call this method to work with an existing archive. + * + * @param archiveId UUID of the archive to work with. + * + * @return An [ExistingArchive] object with methods to interact with the archive. + */ fun archive(archiveId: String): ExistingArchive = ExistingArchive(archiveId) + /** + * Class for working with an existing archive. + * + * @property id The archive ID. + */ inner class ExistingArchive internal constructor(id: String): ExistingResource(id) { fun info(): Archive = client.getArchive(id) @@ -129,13 +347,38 @@ class Video(private val client: VideoClient) { fun removeStream(streamId: String): Unit = client.removeArchiveStream(id, streamId) } + /** + * List all Broadcasts in the application. + * + * @param count (OPTIONAL) The number of Broadcasts to return (maximum 1000). + * @param offset (OPTIONAL) Index of the first Broadcast to return (used for pagination). + * + * @return A list of Broadcasts. + */ fun listBroadcasts(count: Int = 1000, offset: Int = 0): List = client.listBroadcasts(listCompositionsFilter(count, offset)) + /** + * Call this method to work with an existing Broadcast. + * + * @param broadcastId UUID of the Broadcast to work with. + * + * @return An [ExistingBroadcast] object with methods to interact with the Broadcast. + */ fun broadcast(broadcastId: String): ExistingBroadcast = ExistingBroadcast(broadcastId) + /** + * Class for working with an existing Broadcast. + * + * @property id The Broadcast ID. + */ inner class ExistingBroadcast internal constructor(id: String): ExistingResource(id) { + /** + * Retrieves the broadcast details. + * + * @return The broadcast metadata. + */ fun info(): Broadcast = client.getBroadcast(id) fun stop(): Broadcast = client.stopBroadcast(id) @@ -156,18 +399,43 @@ class Video(private val client: VideoClient) { fun removeStream(streamId: String): Unit = client.removeBroadcastStream(id, streamId) } - fun startRender(properties: RenderRequest.Builder.() -> Unit): RenderResponse = - client.startRender(RenderRequest.builder().apply(properties).build()) - + /** + * List all Experience Composers in the application. + * + * @param count (OPTIONAL) The number of Experience Composers to return (maximum 1000). + * @param offset (OPTIONAL) Index of the first Experience Composer to return (used for pagination). + * + * @return A list of Experience Composers. + */ fun listRenders(count: Int = 1000, offset: Int = 0): List = client.listRenders(ListStreamCompositionsRequest.builder().count(count).offset(offset).build()) + /** + * Call this method to work with an existing Experience Composer. + * + * @param renderId UUID of the Experience Composer to work with. + * + * @return An [ExistingRender] object with methods to interact with the Experience Composer. + */ fun render(renderId: String): ExistingRender = ExistingRender(renderId) + /** + * Class for working with an existing Experience Composer. + * + * @property id The render ID. + */ inner class ExistingRender internal constructor(id: String): ExistingResource(id) { + /** + * Retrieves the Experience Composer. + * + * @return The render details. + */ fun info(): RenderResponse = client.getRender(id) + /** + * Stops the Experience Composer. + */ fun stop(): Unit = client.stopRender(id) } } diff --git a/src/test/kotlin/com/vonage/client/kt/VideoTest.kt b/src/test/kotlin/com/vonage/client/kt/VideoTest.kt index 5fc5ef5..5c6fbc5 100644 --- a/src/test/kotlin/com/vonage/client/kt/VideoTest.kt +++ b/src/test/kotlin/com/vonage/client/kt/VideoTest.kt @@ -460,10 +460,9 @@ class VideoTest : AbstractTest() { ) ) - val response = client.connectToWebsocket { + val response = existingSession.connectToWebsocket { uri(websocketUri); headers(headers) - sessionId(sessionId); token(token) - streams(streamId, randomUuidStr) + streams(streamId, randomUuidStr); token(token) audioRate(Websocket.AudioRate.L16_16K) } assertNotNull(response) @@ -480,8 +479,8 @@ class VideoTest : AbstractTest() { expectedResponseParams = mapOf("id" to audioConnectorId) ) - val invocation = { client.connectToWebsocket { - uri(websocketUri); sessionId(sessionId); token(token) + val invocation = { existingSession.connectToWebsocket { + uri(websocketUri); token(token) }} val response = invocation() assertNotNull(response) @@ -576,11 +575,10 @@ class VideoTest : AbstractTest() { "streamId" to streamId ) ) - val response = client.sipDial { - sessionId(sessionId); token(token) + val response = existingSession.sipDial { uri(URI.create(sipUri), true) addHeaders(headers); secure(secure) - from(from); video(video) + from(from); video(video); token(token) observeForceMute(observeForceMute) username(userName); password(password) } @@ -599,7 +597,7 @@ class VideoTest : AbstractTest() { ), expectedResponseParams = mapOf("id" to sipCallId) ) - val invocation = { client.sipDial { + val invocation = { existingSession.sipDial { sessionId(sessionId); token(token); uri(URI.create(sipUri), false) } } val response = invocation() @@ -1139,9 +1137,8 @@ class VideoTest : AbstractTest() { expectedRequestParams = renderRequestMap, expectedResponseParams = mapOf() ) - val invocation = { client.startRender { - sessionId(sessionId); token(token) - url(mediaUrl); name(renderName) + val invocation = { existingSession.startRender { + token(token); url(mediaUrl); name(renderName) } } assertEqualsEmptyRender(invocation()) assertApiResponseException(renderBaseUrl, HttpMethod.POST, invocation) @@ -1156,7 +1153,7 @@ class VideoTest : AbstractTest() { ), expectedResponseParams = renderResponseMap ) - assertEqualsSampleRender(client.startRender { + assertEqualsSampleRender(existingSession.startRender { url(mediaUrl); maxDuration(maxDuration) resolution(Resolution.HD_LANDSCAPE) sessionId(sessionId); token(token); name(renderName)