diff --git a/CHANGES.md b/CHANGES.md index c20239fb..4969de7a 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,3 +1,7 @@ +# 7.24.0 + +* Updating Video API functionality with methods for Live Captions, Audio Connector, Experience Composer, and a `publisheronly` cleint token role. [#307](https://github.com/Vonage/vonage-ruby-sdk/pull/307) + # 7.23.0 * Minor updates to Verify v2. [#306](https://github.com/Vonage/vonage-ruby-sdk/pull/306) diff --git a/lib/vonage/keys.rb b/lib/vonage/keys.rb index 9934a5e5..862155bb 100644 --- a/lib/vonage/keys.rb +++ b/lib/vonage/keys.rb @@ -29,7 +29,12 @@ def camelcase(hash) 'screenshare_type', 'session_id', 'stream_mode', - 'archive_mode' + 'archive_mode', + 'language_code', + 'max_duration', + 'partial_captions', + 'status_callback_url', + 'audio_rate' ] hash.transform_keys do |k| if exceptions.include?(k.to_s) diff --git a/lib/vonage/version.rb b/lib/vonage/version.rb index dae97f76..4281258d 100644 --- a/lib/vonage/version.rb +++ b/lib/vonage/version.rb @@ -1,5 +1,5 @@ # typed: strong module Vonage - VERSION = '7.23.0' + VERSION = '7.24.0' end diff --git a/lib/vonage/video.rb b/lib/vonage/video.rb index 0cdae3c3..387bfc39 100644 --- a/lib/vonage/video.rb +++ b/lib/vonage/video.rb @@ -54,6 +54,9 @@ def create_session(**params) end def generate_client_token(session_id:, scope: 'session.connect', role: 'publisher', **params) + valid_roles = %w[publisher subscriber moderator publisheronly] + raise ArgumentError, "Invalid role: #{role}" unless valid_roles.include?(role) + claims = { application_id: @config.application_id, scope: scope, @@ -104,6 +107,22 @@ def streams @streams ||= Streams.new(@config) end + # @return [Captions] + # + def captions + @captions ||= Captions.new(@config) + end + + # @return [Renders] + # + def renders + @renders ||= Renders.new(@config) + end + # @return [WebSocket] + # + def web_socket + @web_socket ||= WebSocket.new(@config) + end end end diff --git a/lib/vonage/video/captions.rb b/lib/vonage/video/captions.rb new file mode 100644 index 00000000..187f3880 --- /dev/null +++ b/lib/vonage/video/captions.rb @@ -0,0 +1,67 @@ +# typed: true +# frozen_string_literal: true + +module Vonage + class Video::Captions < Namespace + include Keys + + self.authentication = BearerToken + + self.request_body = JSON + + self.host = :video_host + + # Start Live Captions for a Vonage Video stream + # + # @example + # response = client.video.captions.start( + # session_id: "12312312-3811-4726-b508-e41a0f96c68f", + # token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJp...", + # language_code: 'en-US', + # max_duration: 300, + # partial_captions: false, + # status_callback_url: 'https://example.com/captions/status' + # ) + # + # @params [required, String] :session_id The id of the session to start captions for + # + # @param [required, String] :token A valid Vonage Video token with role set to 'moderator' + # + # @params [optional, String] :language_code The BCP-47 code for a spoken language used on this call. The default value is "en-US" + # - Must be one of: 'en-US', 'en-AU', 'en-GB', 'zh-CN', 'fr-FR', 'fr-CA', 'de-DE', 'hi-IN', 'it-IT', 'ja-JP', 'ko-KR', 'pt-BR', 'th-TH' + # + # @param [optional, Integer] :max_duration The maximum duration for the audio captioning, in seconds. + # - The default value is 14,400 seconds (4 hours), the maximum duration allowed. + # - The minimum value for maxDuration is 300 (300 seconds, or 5 minutes). + # + # @param [optional, Boolean] :partial_captions Whether to enable this to faster captioning (true, the default) at the cost of some degree of inaccuracies. + # + # @param [optional, String] :status_callback_url The URL to send the status of the captions to. + # + # @return [Response] + # + # @see TODO: Add document link here + # + def start(session_id:, token:, **params) + request( + '/v2/project/' + @config.application_id + '/captions', + params: camelcase(params.merge({sessionId: session_id, token: token})), + type: Post) + end + + # Stop live captions for a session + # + # @example + # response = client.video.captions.stop(captions_id: "7c0580fc-6274-4de5-a66f-d0648e8d3ac3") + # + # @params [required, String] :captions_id ID of the connection used for captions + # + # @return [Response] + # + # @see TODO: Add document link here + # + def stop(captions_id:) + request('/v2/project/' + @config.application_id + '/captions/' + captions_id + '/stop', type: Post) + end + end +end diff --git a/lib/vonage/video/renders.rb b/lib/vonage/video/renders.rb new file mode 100644 index 00000000..d32fff27 --- /dev/null +++ b/lib/vonage/video/renders.rb @@ -0,0 +1,107 @@ +# typed: true +# frozen_string_literal: true + +module Vonage + class Video::Renders < Namespace + include Keys + + self.authentication = BearerToken + + self.request_body = JSON + + self.host = :video_host + + # Start an Experience Composer Render + # + # @example + # response = client.video.renders.start( + # session_id: "12312312-3811-4726-b508-e41a0f96c68f", + # token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJp...", + # url: 'https://example.com/', + # max_duration: 1800, + # resolution: '1280x720', + # properties: { + # name: 'foo' + # } + # ) + # + # @params [required, String] :session_id The session ID of the Vonage Video session you are working with. + # + # @param [required, String] :token A valid OpenTok JWT token with a Publisher role and (optionally) connection data to be associated with the output stream. + # + # @params [required, String] :url A publicly reachable URL controlled by the customer and capable of generating the content to be rendered without user intervention. + # + # @params [optional, Integer] :max_duration The maximum duration of the rendered video in seconds. + # - After this time, it is stopped automatically, if it is still running. + # - Min: 60 + # - Max: 3600 + # - Default: 3600 + # + # @params [optional, String] :resolution The resolution of the Experience Composer render. + # - Must be one of: '640x480', '480x640', '1280x720', '720x1280', '1080x1920', '1920x1080' + # + # @params [optional, Hash] :properties The initial configuration of Publisher properties for the composed output stream. + # @option properties [required, String] :name The name of the composed output stream which is published to the session. + # + # @return [Response] + # + # @see TODO: Add document link here + # + def start(session_id:, token:, url:, **params) + request( + '/v2/project/' + @config.application_id + '/render', + params: camelcase(params.merge({sessionId: session_id, token: token, url: url})), + type: Post) + end + + # Stop an Experience Composer render + # + # @example + # response = client.video.renders.stop(experience_composer_id: "1248e7070b81464c9789f46ad10e7764") + # + # @params [required, String] :experience_composer_id ID of the Experience Composer instance that you want to stop. + # + # @return [Response] + # + # @see TODO: Add document link here + # + def stop(experience_composer_id:) + request('/v2/project/' + @config.application_id + '/render/' + experience_composer_id, type: Delete) + end + + # Get information about an Experience Composer session + # + # @example + # response = client.video.renders.info(experience_composer_id: "1248e7070b81464c9789f46ad10e7764") + # + # @params [required, String] :experience_composer_id ID of the Experience Composer instance for which you are requesitng information. + # + # @return [Response] + # + # @see TODO: Add document link here + # + def info(experience_composer_id:) + request('/v2/project/' + @config.application_id + '/render/' + experience_composer_id) + end + + # List all Experience Composer renders in an application + # + # @example + # response = client.video.renders.list + # + # @params [optional, Integer] :offset Specify the index offset of the first experience composer. 0 is offset of the most recently started render. + # + # @params [optional, Integer] :count Limit the number of experience composers to be returned. + # + # @return [Video::Renders::ListResponse] + # + # @see TODO: Add document link here + # + def list(**params) + path = '/v2/project/' + @config.application_id + '/render' + path += "?#{Params.encode(camelcase(params))}" unless params.empty? + + request(path, response_class: ListResponse) + end + end +end diff --git a/lib/vonage/video/renders/list_response.rb b/lib/vonage/video/renders/list_response.rb new file mode 100644 index 00000000..119e3966 --- /dev/null +++ b/lib/vonage/video/renders/list_response.rb @@ -0,0 +1,11 @@ +# typed: true + +class Vonage::Video::Renders::ListResponse < Vonage::Response + include Enumerable + + def each + return enum_for(:each) unless block_given? + + @entity.items.each { |item| yield item } + end +end diff --git a/lib/vonage/video/web_socket.rb b/lib/vonage/video/web_socket.rb new file mode 100644 index 00000000..6d2fb07e --- /dev/null +++ b/lib/vonage/video/web_socket.rb @@ -0,0 +1,61 @@ +# typed: true +# frozen_string_literal: true + +module Vonage + class Video::WebSocket < Namespace + include Keys + + self.authentication = BearerToken + + self.request_body = JSON + + self.host = :video_host + + # Start an audio connector websocket connection + # + # @example + # response = client.video.web_socket.connect( + # session_id: "12312312-3811-4726-b508-e41a0f96c68f", + # token: "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJp...", + # websocket: { + # uri: 'wss://example.com/ws-endpoint', + # streams: ["8b732909-0a06-46a2-8ea8-074e64d43422"], + # headers: { property1: 'foo', property2: 'bar' }, + # audio_rate: 16000 + # } + # ) + # + # @params [required, String] :session_id The Vonage Video session ID that includes the Vonage Video streams you want to include in the WebSocket stream. + # - The Audio Connector feature is only supported in routed sessions + # + # @param [required, String] :token A valid Vonage Video token for the Audio Connector connection to the Vonage Video Session. + # - You can add additional data to the JWT to identify that the connection is the Audio Connector endpoint or for any other identifying data. + # + # @params [required, Hash] :websocket The WebSocket configuration for the Audio Connector connection. + # @option websocket [required, String] :uri A publicly reachable WebSocket URI to be used for the destination of the audio stream + # @option websocket [optional, String] :streams An array of stream IDs for the Vonage Video streams you want to include in the WebSocket audio. + # - If you omit this property, all streams in the session will be included. + # @option websocket [optional, Hash] :headers An object of key-value pairs of headers to be sent to your WebSocket server with each message, with a maximum length of 512 bytes. + # @option websocket [optional, Integer] :audio_rate A number representing the audio sampling rate in Hz + # - Must be one of: 8000, 16000 + # + # @return [Response] + # + # @see TODO: Add document link here + # + def connect(session_id:, token:, websocket:) + raise ArgumentError, 'websocket must be a Hash' unless websocket.is_a?(Hash) + raise ArgumentError, 'websocket must contain a uri' unless websocket.key?(:uri) + + request( + '/v2/project/' + @config.application_id + '/connect', + params: { + sessionId: session_id, + token: token, + websocket: camelcase(websocket) + }, + type: Post + ) + end + end +end diff --git a/test/vonage/test.rb b/test/vonage/test.rb index 16f5c560..bee82215 100644 --- a/test/vonage/test.rb +++ b/test/vonage/test.rb @@ -63,6 +63,10 @@ def sample_webhook_token "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1ODc0OTQ5NjIsImp0aSI6ImM1YmE4ZjI0LTFhMTQtNGMxMC1iZmRmLTNmYmU4Y2U1MTFiNSIsImlzcyI6IlZvbmFnZSIsInBheWxvYWRfaGFzaCI6ImQ2YzBlNzRiNTg1N2RmMjBlM2I3ZTUxYjMwYzBjMmE0MGVjNzNhNzc4NzliNmYwNzRkZGM3YTIzMTdkZDAzMWIiLCJhcGlfa2V5IjoiYTFiMmMzZCIsImFwcGxpY2F0aW9uX2lkIjoiYWFhYWFhYWEtYmJiYi1jY2NjLWRkZGQtMDEyMzQ1Njc4OWFiIn0.JQRKi1d0SQitmjPINfTWMpt3XZkGsLbD7EjCdXoNSbk" end + def sample_video_token + "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpYXQiOjE3MTMxNzc4NTAsImp0aSI6IjYzMmI5NmMwLTM0MzEtNGI5NS1iNmNiLWNjNGFjNDEzNDhkNSIsImV4cCI6MTcxMzE3ODc1MCwiYXBwbGljYXRpb25faWQiOiJlMzM2ZmUxOS01MWNhLTRiNTktYTczOS1jNzA3MWIzZjY5Y2MiLCJzY29wZSI6InNlc3Npb24uY29ubmVjdCIsInNlc3Npb25faWQiOiIxX01YNWxNek0yWm1VeE9TMDFNV05oTFRSaU5Ua3RZVGN6T1Mxak56QTNNV0l6WmpZNVkyTi1makUzTURJME9ETTVNakV6TVRaLVNVeFdLMnh4ZFUxR1VESTJlbFZWVkcxUE9FTnlUVzVSZm41LSIsInJvbGUiOiJtb2RlcmF0b3IiLCJpbml0aWFsX2xheW91dF9jbGFzc19saXN0IjoiIiwic3ViIjoidmlkZW8iLCJhY2wiOnsicGF0aHMiOnsiL3Nlc3Npb24vKioiOnt9fX19.kHj4lHKxWKurScu6LWm7z0G69WuLHyx-Vgf3CDlCYdRRAqJ--SRXPukAAGuA52wvou4wVStd6BWCiSUDxjNb6wG5li3Op6RDdpIiooJ6C1kVdUmbEgBLww76Vy34If0d99XbEZX13KW0XmN6oyrNSih3paanqYzjJcG9elKxnMXLhnTB8Mq6OQXVAiB4Qr6LCqdB57a1eU52Ak2-1F41p3LnzDaElFOepbCdFtIXiFxjd3r62JXiYMNxQCseT2RyPqcKQsFUSBV6k3GvY8ONece-ClSED_prXkChW1o3_7sRsfFqBaQiCGjw4vtIIE17S4atv4Z5rzBPLvUsqpUfog" + end + def sample_valid_signature_secret "ZYtdTtGV3BCFN7tWmOWr1md66XsquMggr4W2cTtXtcPgfnI0Xw" end diff --git a/test/vonage/video/captions_test.rb b/test/vonage/video/captions_test.rb new file mode 100644 index 00000000..6b1643ac --- /dev/null +++ b/test/vonage/video/captions_test.rb @@ -0,0 +1,62 @@ +# typed: false +require_relative '../test' + +class Vonage::Video::CaptionsTest < Vonage::Test + def captions + Vonage::Video::Captions.new(config) + end + + def uri + 'https://video.api.vonage.com/v2/project/' + application_id + '/captions' + end + + def test_start_method + request_params = {sessionId: video_session_id, token: sample_video_token} + stub_request(:post, uri).with(body: request_params).to_return(response) + + assert_kind_of Vonage::Response, captions.start(session_id: video_session_id, token: sample_video_token) + end + + def test_start_method_with_optional_params + request_params = { + sessionId: video_session_id, + token: sample_video_token, + languageCode: 'en-US', + maxDuration: 300, + partialCaptions: false, + statusCallbackUrl: 'https://send-status-to.me' + } + + stub_request(:post, uri).with(body: request_params).to_return(response) + + response = captions.start( + session_id: video_session_id, + token: sample_video_token, + language_code: 'en-US', + max_duration: 300, + partial_captions: false, + status_callback_url: 'https://send-status-to.me' + ) + + assert_kind_of Vonage::Response, response + end + + def test_start_method_without_session_id + assert_raises(ArgumentError) { captions.start(token: sample_video_token) } + end + + def test_start_method_without_token + assert_raises(ArgumentError) { captions.start(session_id: video_session_id) } + end + + def test_stop_method + captions_id = "7c0680fc-6274-4de5-a66f-d0648e8d3ac2" + stub_request(:post, uri + "/#{captions_id}/stop").to_return(response) + + assert_kind_of Vonage::Response, captions.stop(captions_id: captions_id) + end + + def test_stop_method_without_captions_id + assert_raises(ArgumentError) { captions.stop } + end +end diff --git a/test/vonage/video/renders_test.rb b/test/vonage/video/renders_test.rb new file mode 100644 index 00000000..fde6b7ba --- /dev/null +++ b/test/vonage/video/renders_test.rb @@ -0,0 +1,127 @@ +# typed: false +require_relative '../test' + +class Vonage::Video::RendersTest < Vonage::Test + def renders + Vonage::Video::Renders.new(config) + end + + def uri + 'https://video.api.vonage.com/v2/project/' + application_id + '/render' + end + + def experience_composer_id + "1248e7070b81464c9789f46ad10e7764" + end + + def test_start_method + request_params = { + sessionId: video_session_id, + token: sample_video_token, + url: 'https://example.com/' + } + + stub_request(:post, uri).with(body: request_params).to_return(response) + + response = renders.start( + session_id: video_session_id, + token: sample_video_token, + url: 'https://example.com/' + ) + + assert_kind_of Vonage::Response, response + end + + def test_start_method_with_optional_params + request_params = { + sessionId: video_session_id, + token: sample_video_token, + url: 'https://example.com/', + maxDuration: 1800, + resolution: '1280x720', + properties: { + name: 'foo' + } + } + + stub_request(:post, uri).with(body: request_params).to_return(response) + + response = renders.start( + session_id: video_session_id, + token: sample_video_token, + url: 'https://example.com/', + max_duration: 1800, + resolution: '1280x720', + properties: { + name: 'foo' + } + ) + + assert_kind_of Vonage::Response, response + end + + def test_start_method_without_session_id + assert_raises(ArgumentError) do + renders.start( + token: sample_video_token, + url: 'https://example.com/' + ) + end + end + + def test_start_method_without_token + assert_raises(ArgumentError) do + renders.start( + session_id: video_session_id, + url: 'https://example.com/' + ) + end + end + + def test_start_method_without_url + assert_raises(ArgumentError) do + renders.start( + session_id: video_session_id, + token: sample_video_token + ) + end + end + + def test_stop_method + stub_request(:delete, uri + "/#{experience_composer_id}").to_return(response) + + assert_kind_of Vonage::Response, renders.stop(experience_composer_id: experience_composer_id) + end + + def test_stop_method_without_experience_composer_id + assert_raises(ArgumentError) { renders.stop } + end + + def test_info_method + stub_request(:get, uri + "/#{experience_composer_id}").to_return(response) + + assert_kind_of Vonage::Response, renders.info(experience_composer_id: experience_composer_id) + end + + def test_info_method_without_experience_composer_id + assert_raises(ArgumentError) { renders.info } + end + + def test_list_method + stub_request(:get, uri).to_return(video_list_response) + + renders_list = renders.list + + assert_kind_of Vonage::Video::Renders::ListResponse, renders_list + renders_list.each { |render| assert_kind_of Vonage::Entity, render } + end + + def test_list_method_with_optional_params + stub_request(:get, uri + '?offset=10&count=20').to_return(video_list_response) + + renders_list = renders.list(offset: 10, count: 20) + + assert_kind_of Vonage::Video::Renders::ListResponse, renders_list + renders_list.each { |render| assert_kind_of Vonage::Entity, render } + end +end diff --git a/test/vonage/video/web_socket_test.rb b/test/vonage/video/web_socket_test.rb new file mode 100644 index 00000000..244de28d --- /dev/null +++ b/test/vonage/video/web_socket_test.rb @@ -0,0 +1,113 @@ +# typed: false +require_relative '../test' + +class Vonage::Video::WebSocketTest < Vonage::Test + def web_socket + Vonage::Video::WebSocket.new(config) + end + + def uri + 'https://video.api.vonage.com/v2/project/' + application_id + '/connect' + end + + def test_connect_method + request_params = { + sessionId: video_session_id, + token: sample_video_token, + websocket: { + uri: 'wss://example.com/ws-endpoint' + } + } + + stub_request(:post, uri).with(body: request_params).to_return(response) + + response = web_socket.connect( + session_id: video_session_id, + token: sample_video_token, + websocket: { + uri: 'wss://example.com/ws-endpoint' + } + ) + + assert_kind_of Vonage::Response, response + end + + def test_connect_method_with_optional_params + request_params = { + sessionId: video_session_id, + token: sample_video_token, + websocket: { + uri: 'wss://example.com/ws-endpoint', + streams: ['stream1', 'stream2'], + headers: { property1: 'foo', property2: 'bar' }, + audioRate: 16000 + } + } + + stub_request(:post, uri).with(body: request_params).to_return(response) + + response = web_socket.connect( + session_id: video_session_id, + token: sample_video_token, + websocket: { + uri: 'wss://example.com/ws-endpoint', + streams: ['stream1', 'stream2'], + headers: { property1: 'foo', property2: 'bar' }, + audio_rate: 16000 + } + ) + + assert_kind_of Vonage::Response, response + end + + def test_connect_method_without_session_id + assert_raises(ArgumentError) do + web_socket.connect( + token: sample_video_token, + websocket: { + uri: 'wss://example.com/ws-endpoint' + } + ) + end + end + + def test_connect_method_without_token + assert_raises(ArgumentError) do + web_socket.connect( + session_id: video_session_id, + websocket: { + uri: 'wss://example.com/ws-endpoint' + } + ) + end + end + + def test_connect_method_without_websocket + assert_raises(ArgumentError) do + web_socket.connect( + session_id: video_session_id, + token: sample_video_token + ) + end + end + + def test_connect_method_without_websocket_uri + assert_raises(ArgumentError) do + web_socket.connect( + session_id: video_session_id, + token: sample_video_token, + websocket: {} + ) + end + end + + def test_connect_method_with_invalid_websocket_data_type + assert_raises(ArgumentError) do + web_socket.connect( + session_id: video_session_id, + token: sample_video_token, + websocket: 'wss://example.com/ws-endpoint' + ) + end + end +end diff --git a/test/vonage/video_test.rb b/test/vonage/video_test.rb index 83377ec2..55283578 100644 --- a/test/vonage/video_test.rb +++ b/test/vonage/video_test.rb @@ -106,6 +106,42 @@ def test_generate_client_token assert_equal 'abc123', decoded_token_payload['session_id'] end + def test_generate_client_token_with_publisher_role + token = video.generate_client_token(session_id: 'abc123', role: 'publisher') + decoded_token_payload = decode_jwt_payload(token) + + assert_equal 'abc123', decoded_token_payload['session_id'] + assert_equal 'publisher', decoded_token_payload['role'] + end + + def test_generate_client_token_with_subscriber_role + token = video.generate_client_token(session_id: 'abc123', role: 'subscriber') + decoded_token_payload = decode_jwt_payload(token) + + assert_equal 'abc123', decoded_token_payload['session_id'] + assert_equal 'subscriber', decoded_token_payload['role'] + end + + def test_generate_client_token_with_moderator_role + token = video.generate_client_token(session_id: 'abc123', role: 'moderator') + decoded_token_payload = decode_jwt_payload(token) + + assert_equal 'abc123', decoded_token_payload['session_id'] + assert_equal 'moderator', decoded_token_payload['role'] + end + + def test_generate_client_token_with_publisheronly_role + token = video.generate_client_token(session_id: 'abc123', role: 'publisheronly') + decoded_token_payload = decode_jwt_payload(token) + + assert_equal 'abc123', decoded_token_payload['session_id'] + assert_equal 'publisheronly', decoded_token_payload['role'] + end + + def test_generate_client_token_with_invalid_role + assert_raises(ArgumentError) { video.generate_client_token(session_id: 'abc123', role: 'foo') } + end + def test_generate_client_token_with_custom_options expire_time = Time.now.to_i + 500 token = video.generate_client_token(session_id: 'abc123', role: 'moderator', initial_layout_class_list: ['foo', 'bar'], data: 'test', expire_time: expire_time) @@ -137,4 +173,16 @@ def test_moderation_method def test_signals_method assert_kind_of Vonage::Video::Signals, video.signals end + + def test_captions_method + assert_kind_of Vonage::Video::Captions, video.captions + end + + def test_renders_method + assert_kind_of Vonage::Video::Renders, video.renders + end + + def test_web_socket_method + assert_kind_of Vonage::Video::WebSocket, video.web_socket + end end